diff --git a/src/http_api.c b/src/http_api.c index 35de2953..8867671e 100644 --- a/src/http_api.c +++ b/src/http_api.c @@ -340,6 +340,21 @@ static enum MHD_Result http_api__process_api(struct MHD_Connection *connection, } +static int check_basic_auth(struct mosquitto__listener *listener, struct MHD_Connection *connection) +{ + struct mosquitto context = {0}; + int rc; + + context.listener = listener; + + context.username = MHD_basic_auth_get_username_password (connection, &context.password); + rc = mosquitto_basic_auth(&context); + MHD_free(context.username); + MHD_free(context.password); + + return rc; +} + static enum MHD_Result http_api_handler(void *cls, struct MHD_Connection *connection, const char *url, const char *method, const char *version, const char *upload_data, size_t *upload_data_size, void **con_cls) @@ -358,6 +373,13 @@ static enum MHD_Result http_api_handler(void *cls, struct MHD_Connection MHD_destroy_response(response); return ret; } + + if(check_basic_auth(listener, connection) != MOSQ_ERR_SUCCESS){ + char *buf = "Not authorised\n"; + struct MHD_Response *response = MHD_create_response_from_buffer(strlen(buf), (void *)buf, MHD_RESPMEM_MUST_COPY); + return MHD_queue_basic_auth_fail_response(connection, "Mosquitto API", response); + } + if(!strncasecmp(url, "/api/", strlen("/api/"))){ return http_api__process_api(connection, url); }else{ @@ -406,7 +428,6 @@ int http_api__start(struct mosquitto__listener *listener) return MOSQ_ERR_NOMEM; } } - listener->security_options->allow_anonymous = true; listener->protocol = mp_http_api; bind_address = listener->host; diff --git a/test/broker/22-http-api-api.py b/test/broker/22-http-api-api.py index 6e2039ce..b60097de 100755 --- a/test/broker/22-http-api-api.py +++ b/test/broker/22-http-api-api.py @@ -7,6 +7,7 @@ import re def write_config(filename, mqtt_port, ws_port, http_port): with open(filename, 'w') as f: + f.write(f"allow_anonymous true\n") f.write(f"listener {mqtt_port}\n") f.write(f"listener 0 {mqtt_port}.sock\n") @@ -138,10 +139,10 @@ try: http_conn.request("GET", "/api/v1/version") response = http_conn.getresponse() if response.status != 200: - raise ValueError(f"/api/v1/version {response.status}") + raise ValueError(f"Error: /api/v1/version {response.status}") payload = response.read().decode('utf-8') if not re.match(r'^\d+\.\d+\.\d+$', payload): - raise ValueError(f"/api/v1/version\n{payload}") + raise ValueError(f"Error: /api/v1/version\n{payload}") rc = 0 diff --git a/test/broker/22-http-api-auth.pwfile b/test/broker/22-http-api-auth.pwfile new file mode 100644 index 00000000..fd4ac0a2 --- /dev/null +++ b/test/broker/22-http-api-auth.pwfile @@ -0,0 +1 @@ +user:$6$Ut1cUS9PG8+gC3vn$tOjCfSJJDe1Alu9HktxxyyzwN4+6mAMSWGRAF9gmMN8pzcGTPVEYYMAZpCEp96Oz2ZRRz5YKM6lPMf1tUbb6zA== diff --git a/test/broker/22-http-api-auth.py b/test/broker/22-http-api-auth.py new file mode 100755 index 00000000..9eb947e2 --- /dev/null +++ b/test/broker/22-http-api-auth.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 + +from mosq_test_helper import * +import base64 +import http.client +import json +import re + +def write_config(filename, mqtt_port, http_port): + with open(filename, 'w') as f: + f.write(f"listener {mqtt_port}\n") + + f.write(f"listener {http_port}\n") + f.write("protocol http_api\n") + f.write(f"plugin {mosq_test.get_build_root()}/plugins/password-file/mosquitto_password_file.so\n") + f.write("plugin_opt_password_file %s/%s\n" % (Path(__file__).resolve().parent, filename.replace('.conf', '.pwfile'))) + +mqtt_port, http_port = mosq_test.get_port(2) +conf_file = os.path.basename(__file__).replace('.py', '.conf') +write_config(conf_file, mqtt_port, http_port) + +broker = mosq_test.start_broker(filename=os.path.basename(__file__), use_conf=True, port=mqtt_port) + +rc = 1 + +try: + http_conn = http.client.HTTPConnection(f"localhost:{http_port}") + + # No auth + http_conn.request("GET", "/api/v1/version") + response = http_conn.getresponse() + if response.status != 401: + raise ValueError(f"Error: /api/v1/version {response.status}") + payload = response.read().decode('utf-8') + if payload != "Not authorised\n": + raise ValueError(f"Error: {payload}") + + # Bad auth + credentials = "user:invalid" + encoded_credentials = base64.b64encode(credentials.encode()).decode() + headers = { + "Authorization": f"Basic {encoded_credentials}" + } + http_conn.request("GET", "/api/v1/version", headers=headers) + response = http_conn.getresponse() + if response.status != 401: + raise ValueError(f"Error: /api/v1/version {response.status}") + payload = response.read().decode('utf-8') + if payload != "Not authorised\n": + raise ValueError(f"Error: {payload}") + + # Good auth + credentials = "user:password" + encoded_credentials = base64.b64encode(credentials.encode()).decode() + headers = { + "Authorization": f"Basic {encoded_credentials}" + } + http_conn.request("GET", "/api/v1/version", headers=headers) + response = http_conn.getresponse() + if response.status != 200: + raise ValueError(f"Error: /api/v1/version {response.status}") + + rc = 0 +except mosq_test.TestError: + pass +except Exception as e: + print(e) +finally: + os.remove(conf_file) + broker.terminate() + if mosq_test.wait_for_subprocess(broker): + print("broker not terminated") + if rc == 0: rc=1 + (stdo, stde) = broker.communicate() + if rc != 0: + print(stde.decode('utf-8')) + rc = 1 + + +exit(rc) diff --git a/test/broker/22-http-api-file.py b/test/broker/22-http-api-file.py index 7e13eb64..0c74b367 100755 --- a/test/broker/22-http-api-file.py +++ b/test/broker/22-http-api-file.py @@ -6,6 +6,7 @@ import json def write_config(filename, mqtt_port, http_port): with open(filename, 'w') as f: + f.write("allow_anonymous true\n") f.write(f"listener {mqtt_port}\n") f.write(f"listener {http_port} 127.0.0.1\n") f.write("protocol http_api\n") @@ -31,40 +32,40 @@ try: http_conn.request("POST", "/post") response = http_conn.getresponse() if response.status != 405: - raise ValueError(f"/post {response.status}") + raise ValueError(f"Error: /post {response.status}") # Bad request http_conn.request("PUT", "/put") response = http_conn.getresponse() if response.status != 405: - raise ValueError(f"/put {response.status}") + raise ValueError(f"Error: /put {response.status}") # Missing file http_conn.request("GET", "/missing") response = http_conn.getresponse() if response.status != 404: - raise ValueError(f"/api/missing {response.status}") + raise ValueError(f"Error: /api/missing {response.status}") # File not in dir http_conn.request("GET", "../../../../../../../../etc/passwd") response = http_conn.getresponse() if response.status != 404: - raise ValueError(f"../../../../../../../../etc/passwd {response.status}") + raise ValueError(f"Error: ../../../../../../../../etc/passwd {response.status}") # Present file http_conn.request("GET", "/index.html") response = http_conn.getresponse() if response.status != 200: - raise ValueError(f"/index.html {response.status}") + raise ValueError(f"Error: /index.html {response.status}") # Root http_conn.request("GET", "/") response = http_conn.getresponse() if response.status != 200: - raise ValueError(f"/ {response.status}") + raise ValueError(f"Error: / {response.status}") payload = response.read().decode('utf-8') if payload != "": - raise ValueError(f"/ {payload}") + raise ValueError(f"Error: / {payload}") rc = 0 diff --git a/test/broker/22-http-api-tls.py b/test/broker/22-http-api-tls.py index de5df6d7..2491cb82 100755 --- a/test/broker/22-http-api-tls.py +++ b/test/broker/22-http-api-tls.py @@ -7,6 +7,7 @@ import ssl def write_config(filename, mqtt_port, http_port): with open(filename, 'w') as f: + f.write("allow_anonymous true\n") f.write(f"listener {mqtt_port}\n") f.write(f"listener 0 {mqtt_port}.sock\n") f.write(f"certfile {ssl_dir}/server.crt\n") @@ -35,19 +36,19 @@ try: http_conn.request("POST", "/api/badrequest") response = http_conn.getresponse() if response.status != 405: - raise ValueError(f"/api/badrequest {response.status}") + raise ValueError(f"Error: /api/badrequest {response.status}") # Missing API http_conn.request("GET", "/api/missing") response = http_conn.getresponse() if response.status != 404: - raise ValueError(f"/api/missing {response.status}") + raise ValueError(f"Error: /api/missing {response.status}") # Listeners API http_conn.request("GET", "/api/v1/listeners") response = http_conn.getresponse() if response.status != 200: - raise ValueError(f"/api/v1/listeners {response.status}") + raise ValueError(f"Error: /api/v1/listeners {response.status}") payload = json.loads(response.read().decode('utf-8')) expected_payload = { "listeners": [{ @@ -71,13 +72,13 @@ try: }] } if payload != expected_payload: - raise ValueError(f"/api/v1/listeners payload {payload}") + raise ValueError(f"Error: /api/v1/listeners payload {payload}") # systree API http_conn.request("GET", "/api/v1/systree") response = http_conn.getresponse() if response.status != 200: - raise ValueError(f"/api/v1/systree {response.status}") + raise ValueError(f"Error: /api/v1/systree {response.status}") payload = json.loads(response.read().decode('utf-8')) for topic in [ @@ -127,7 +128,7 @@ try: '$SYS/broker/uptime': -1 } if payload != expected_payload: - raise ValueError(f"/api/v1/systree payload\n{payload}\n{expected_payload}") + raise ValueError(f"Error: /api/v1/systree payload\n{payload}\n{expected_payload}") rc = 0 except mosq_test.TestError: diff --git a/test/broker/Makefile b/test/broker/Makefile index 02e25c8a..a3ca621a 100644 --- a/test/broker/Makefile +++ b/test/broker/Makefile @@ -352,6 +352,7 @@ endif 22: ./22-http-api-api.py + ./22-http-api-auth.py ./22-http-api-file.py ./22-http-api-tls.py diff --git a/test/broker/test.py b/test/broker/test.py index b6031d75..3552f6f1 100755 --- a/test/broker/test.py +++ b/test/broker/test.py @@ -294,7 +294,8 @@ tests = [ (1, './21-proxy-v2-ssl-require-tls-failure.py'), (1, './21-proxy-v2-ssl-require-tls-success.py'), - (2, './22-http-api-api.py'), + (3, './22-http-api-api.py'), + (2, './22-http-api-auth.py'), (2, './22-http-api-file.py'), (2, './22-http-api-tls.py'),