mirror of
https://github.com/eclipse-mosquitto/mosquitto.git
synced 2026-05-21 12:24:29 +02:00
Add authentication to http-api
This commit is contained in:
parent
4d296b2f78
commit
dd83c8e97a
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
1
test/broker/22-http-api-auth.pwfile
Normal file
1
test/broker/22-http-api-auth.pwfile
Normal file
|
|
@ -0,0 +1 @@
|
|||
user:$6$Ut1cUS9PG8+gC3vn$tOjCfSJJDe1Alu9HktxxyyzwN4+6mAMSWGRAF9gmMN8pzcGTPVEYYMAZpCEp96Oz2ZRRz5YKM6lPMf1tUbb6zA==
|
||||
80
test/broker/22-http-api-auth.py
Executable file
80
test/broker/22-http-api-auth.py
Executable file
|
|
@ -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)
|
||||
|
|
@ -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 != "<html></html>":
|
||||
raise ValueError(f"/ {payload}")
|
||||
raise ValueError(f"Error: / {payload}")
|
||||
|
||||
|
||||
rc = 0
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue