Implement nonce mechanism

This commit is contained in:
Alexandre Aubin 2019-07-29 21:17:34 +02:00
parent b593e6ba77
commit 9a68ffabc1

View file

@ -1,3 +1,4 @@
import re
import time import time
import asyncio import asyncio
import aiodns import aiodns
@ -65,8 +66,13 @@ async def query_dns(host, dns_entry_type):
async def check_http(request): async def check_http(request):
""" """
This function received an HTTP request from a YunoHost instance while this This function received an HTTP request from a YunoHost instance while this
server is hosted on our infrastructure. The expected request body is: server is hosted on our infrastructure. The request is expected to be a
{"domain": "domain-to-check.tld"} and the method POST POST request with a body like {"domain": "domain-to-check.tld",
"nonce": "1234567890abcdef" }
The nonce value is a single-use ID, and we will try to reach
http://domain.tld/.well-known/ynh-{nonce} which should return 200 if we
are indeed reaching the right server.
The general workflow is the following: The general workflow is the following:
@ -75,7 +81,7 @@ async def check_http(request):
- get json from body and domain from it - get json from body and domain from it
- check for domain based rate limit (see RATE_LIMIT_SECONDS value) - check for domain based rate limit (see RATE_LIMIT_SECONDS value)
- check domain is in valid format - check domain is in valid format
- now try to do an http request on the ip using the domain as target host - try to do an http request on the ip (using the domain as target host) for the page /.well-known/ynh-diagnosis/{nonce}
- answer saying if the domain can be reached - answer saying if the domain can be reached
""" """
@ -83,6 +89,10 @@ async def check_http(request):
now = time.time() now = time.time()
clear_rate_limit_db(now) clear_rate_limit_db(now)
# ############################################# #
# Validate request and extract the parameters #
# ############################################# #
ip = request.ip ip = request.ip
check_rate_limit_ip = check_rate_limit(ip, now) check_rate_limit_ip = check_rate_limit(ip, now)
@ -99,12 +109,12 @@ async def check_http(request):
"content": "Invalid usage, body isn't proper json", "content": "Invalid usage, body isn't proper json",
}, status=400) }, status=400)
if not data or "domain" not in data: if not data or "domain" not in data or "nonce" not in data:
logger.info(f"Unvalid request didn't specified a domain (body is : {request.body}") logger.info(f"Unvalid request didn't specified a domain and a nonce id (body is : {request.body}")
return json_response({ return json_response({
"status": "error", "status": "error",
"code": "error_no_domain", "code": "error_no_domain",
"content": "Request must specify a domain", "content": "Request must specify a domain and a nonce",
}, status=400) }, status=400)
domain = data["domain"] domain = data["domain"]
@ -121,14 +131,32 @@ async def check_http(request):
"content": "domain is not in the right format (do not include http:// or https://)", "content": "domain is not in the right format (do not include http:// or https://)",
}, status=400) }, status=400)
nonce = data["nonce"]
# nonce id is arbitrarily defined to be a
# 16-digit hexadecimal string
if not re.match(r"^[a-f0-9]{16}$", nonce):
logger.info(f"Invalid request, is not in the right format (nonce is : {nonce})")
return json_response({
"status": "error",
"code": "error_nonce_bad_format",
"content": "nonce is not in the right format (it should be a 16-digit hexadecimal string)",
}, status=400)
# ############################################# #
# Run the actual check #
# ############################################# #
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
try: try:
async with session.get("http://" + ip, url = "http://" + ip + "/.well-known/ynh-diagnosis/" + nonce
async with session.get(url,
headers={"Host": domain}, headers={"Host": domain},
timeout=aiohttp.ClientTimeout(total=30)) as response: timeout=aiohttp.ClientTimeout(total=30)) as response:
# XXX in the futur try to do a double check with the server to # XXX in the futur try to do a double check with the server to
# see if the correct content is get # see if the correct content is get
await response.text() await response.text()
assert response.status == 200
logger.info(f"Success when checking http access for {domain} asked by {ip}") logger.info(f"Success when checking http access for {domain} asked by {ip}")
# TODO various kind of errors # TODO various kind of errors
except aiohttp.client_exceptions.ClientConnectorError: except aiohttp.client_exceptions.ClientConnectorError: