diff --git a/server.py b/server.py
index e4aa235..13ad57d 100644
--- a/server.py
+++ b/server.py
@@ -4,6 +4,7 @@ import asyncio
import aiodns
import aiohttp
import validators
+import socket
from sanic import Sanic
from sanic.log import logger
@@ -48,6 +49,14 @@ def check_rate_limit(key, now):
RATE_LIMIT_DB[key] = time.time()
+async def check_port_is_open(ip, port):
+
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(2)
+ result = sock.connect_ex((ip, port))
+ return result == 0
+
+
async def query_dns(host, dns_entry_type):
loop = asyncio.get_event_loop()
dns_resolver = aiodns.DNSResolver(loop=loop)
@@ -62,7 +71,7 @@ async def query_dns(host, dns_entry_type):
logger.error("Unhandled error while resolving DNS entry")
-@app.route("/check/", methods=["POST"])
+@app.route("/check-http/", methods=["POST"])
async def check_http(request):
"""
This function received an HTTP request from a YunoHost instance while this
@@ -178,6 +187,78 @@ async def check_http(request):
return json_response({"status": "ok"})
+@app.route("/check-ports/", methods=["POST"])
+async def check_ports(request):
+ """
+ This function received an HTTP request from a YunoHost instance while this
+ server is hosted on our infrastructure. The request is expected to be a
+ POST request with a body like {"ports": [80,443,22,25]}
+
+ The general workflow is the following:
+
+ - grab the ip from the request
+ - check for ip based rate limit (see RATE_LIMIT_SECONDS value)
+ - get json from body and ports list from it
+ - check ports are opened or closed
+ - answer the list of opened / closed ports
+ """
+
+ # this is supposed to be a fast operation if run often enough
+ now = time.time()
+ clear_rate_limit_db(now)
+
+ # ############################################# #
+ # Validate request and extract the parameters #
+ # ############################################# #
+
+ ip = request.ip
+
+ check_rate_limit_ip = check_rate_limit(ip, now)
+ if check_rate_limit_ip:
+ return check_rate_limit_ip
+
+ try:
+ data = request.json
+ except InvalidUsage:
+ logger.info(f"Invalid json in request, body is : {request.body}")
+ return json_response({
+ "status": "error",
+ "code": "error_bad_json",
+ "content": "Invalid usage, body isn't proper json",
+ }, status=400)
+
+ def is_port_number(p):
+ return isinstance(p, int) and p > 0 and p < 65535
+
+ # Check "ports" exist in request and is a list of port
+ if not data or "ports" not in data:
+ logger.info(f"Unvalid request didn't specified a ports list (body is : {request.body}")
+ return json_response({
+ "status": "error",
+ "code": "error_no_ports_list",
+ "content": "Request must specify a list of ports to check",
+ }, status=400)
+ elif not isinstance(data["ports"], list) or any(not is_port_number(p) for p in data["ports"]) or len(data["ports"]) > 30 or data["ports"] == []:
+ logger.info(f"Invalid request, ports list is not an actual list of ports, or is too long : {request.body}")
+ return json_response({
+ "status": "error",
+ "code": "error_invalid_ports_list",
+ "content": "This is not an acceptable port list : ports must be between 0 and 65535 and at most 30 ports can be checked",
+ }, status=400)
+
+ ports = set(data["ports"]) # Keep only a set so that we get unique ports
+
+ # ############################################# #
+ # Run the actual check #
+ # ############################################# #
+
+ result = {}
+ for port in ports:
+ result[port] = await check_port_is_open(ip, port)
+
+ return json_response({"status": "ok", "ports": result})
+
+
@app.route("/")
async def main(request):
return html("You aren't really supposed to use this website using your browser.
It's a small server to check if a YunoHost instance can be reached by http before trying to instal a LE certificate.")