initial commit
This commit is contained in:
169
src/capport/api/__init__.py
Normal file
169
src/capport/api/__init__.py
Normal file
@@ -0,0 +1,169 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import logging
|
||||
import typing
|
||||
import uuid
|
||||
|
||||
import capport.database
|
||||
import capport.comm.hub
|
||||
import capport.comm.message
|
||||
import capport.utils.cli
|
||||
import capport.utils.ipneigh
|
||||
import quart
|
||||
import quart_trio
|
||||
import trio
|
||||
from capport import cptypes
|
||||
from capport.config import Config
|
||||
|
||||
|
||||
app = quart_trio.QuartTrio(__name__)
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
config: typing.Optional[Config] = None
|
||||
hub: typing.Optional[capport.comm.hub.Hub] = None
|
||||
hub_app: typing.Optional[ApiHubApp] = None
|
||||
nc: typing.Optional[capport.utils.ipneigh.NeighborController] = None
|
||||
|
||||
|
||||
def get_client_ip() -> cptypes.IPAddress:
|
||||
try:
|
||||
addr = ipaddress.ip_address(quart.request.remote_addr)
|
||||
except ValueError as e:
|
||||
_logger.warning(f'Invalid client address {quart.request.remote_addr!r}: {e}')
|
||||
quart.abort(500, 'Invalid client address')
|
||||
if addr.is_loopback:
|
||||
forw_addr_headers = quart.request.headers.getlist('X-Forwarded-For')
|
||||
if len(forw_addr_headers) == 1:
|
||||
try:
|
||||
return ipaddress.ip_address(forw_addr_headers[0])
|
||||
except ValueError as e:
|
||||
_logger.warning(f'Invalid forwarded client address {forw_addr_headers!r} (from {addr}): {e}')
|
||||
quart.abort(500, 'Invalid client address')
|
||||
elif forw_addr_headers:
|
||||
_logger.warning(f'Multiple forwarded client addresses {forw_addr_headers!r} (from {addr})')
|
||||
quart.abort(500, 'Invalid client address')
|
||||
return addr
|
||||
|
||||
|
||||
async def get_client_mac_if_present(address: typing.Optional[cptypes.IPAddress]=None) -> typing.Optional[cptypes.MacAddress]:
|
||||
assert nc # for mypy
|
||||
if not address:
|
||||
address = get_client_ip()
|
||||
return await nc.get_neighbor_mac(address)
|
||||
|
||||
|
||||
async def get_client_mac(address: typing.Optional[cptypes.IPAddress]=None) -> cptypes.MacAddress:
|
||||
mac = await get_client_mac_if_present(address)
|
||||
if mac is None:
|
||||
_logger.warning(f"Couldn't find MAC addresss for {address}")
|
||||
quart.abort(404, 'Unknown client')
|
||||
return mac
|
||||
|
||||
|
||||
class ApiHubApp(capport.comm.hub.HubApplication):
|
||||
async def mac_states_changed(self, *, from_peer_id: uuid.UUID, pending_updates: capport.database.PendingUpdates) -> None:
|
||||
# TODO: support websocket notification updates to clients?
|
||||
pass
|
||||
|
||||
|
||||
async def user_login(address: cptypes.IPAddress, mac: cptypes.MacAddress) -> None:
|
||||
assert config # for mypy
|
||||
assert hub # for mypy
|
||||
pu = capport.database.PendingUpdates()
|
||||
try:
|
||||
hub.database.login(mac, config.session_timeout, pending_updates=pu)
|
||||
except capport.database.NotReadyYet as e:
|
||||
quart.abort(500, str(e))
|
||||
|
||||
if pu.macs:
|
||||
_logger.info(f'User {mac} (with IP {address}) logged in')
|
||||
for msg in pu.serialize():
|
||||
await hub.broadcast(msg)
|
||||
|
||||
|
||||
async def user_logout(mac: cptypes.MacAddress) -> None:
|
||||
assert hub # for mypy
|
||||
pu = capport.database.PendingUpdates()
|
||||
try:
|
||||
hub.database.logout(mac, pending_updates=pu)
|
||||
except capport.database.NotReadyYet as e:
|
||||
quart.abort(500, str(e))
|
||||
if pu.macs:
|
||||
_logger.info(f'User {mac} logged out')
|
||||
for msg in pu.serialize():
|
||||
await hub.broadcast(msg)
|
||||
|
||||
|
||||
async def user_lookup() -> cptypes.MacPublicState:
|
||||
assert hub # for mypy
|
||||
address = get_client_ip()
|
||||
mac = await get_client_mac_if_present(address)
|
||||
if not mac:
|
||||
return cptypes.MacPublicState.from_missing_mac(address)
|
||||
else:
|
||||
return hub.database.lookup(address, mac)
|
||||
|
||||
|
||||
async def _run_hub(*, task_status=trio.TASK_STATUS_IGNORED) -> None:
|
||||
global hub
|
||||
global hub_app
|
||||
global nc
|
||||
assert config # for mypy
|
||||
try:
|
||||
async with capport.utils.ipneigh.connect() as mync:
|
||||
nc = mync
|
||||
_logger.info("Running hub for API")
|
||||
myapp = ApiHubApp()
|
||||
myhub = capport.comm.hub.Hub(config=config, app=myapp)
|
||||
hub = myhub
|
||||
hub_app = myapp
|
||||
await myhub.run(task_status=task_status)
|
||||
finally:
|
||||
hub = None
|
||||
hub_app = None
|
||||
nc = None
|
||||
_logger.info("Done running hub for API")
|
||||
await app.shutdown()
|
||||
|
||||
|
||||
@app.before_serving
|
||||
async def init():
|
||||
global config
|
||||
config = Config.load()
|
||||
capport.utils.cli.init_logger(config)
|
||||
await app.nursery.start(_run_hub)
|
||||
|
||||
|
||||
# @app.route('/all')
|
||||
# async def route_all():
|
||||
# return hub_app.database.as_json()
|
||||
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
async def index():
|
||||
state = await user_lookup()
|
||||
return await quart.render_template('index.html', state=state)
|
||||
|
||||
|
||||
@app.route('/login', methods=['POST'])
|
||||
async def login():
|
||||
address = get_client_ip()
|
||||
mac = await get_client_mac(address)
|
||||
await user_login(address, mac)
|
||||
return quart.redirect('/', code=303)
|
||||
|
||||
|
||||
@app.route('/logout', methods=['POST'])
|
||||
async def logout():
|
||||
mac = await get_client_mac()
|
||||
await user_logout(mac)
|
||||
return quart.redirect('/', code=303)
|
||||
|
||||
|
||||
@app.route('/api/captive-portal', methods=['GET'])
|
||||
# RFC 8908: https://datatracker.ietf.org/doc/html/rfc8908
|
||||
async def captive_api():
|
||||
state = await user_lookup()
|
||||
return state.to_rfc8908(config)
|
22
src/capport/api/templates/index.html
Normal file
22
src/capport/api/templates/index.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Captive Portal Universität Stuttgart</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
<body>
|
||||
{% if not state.mac %}
|
||||
It seems you're accessing this site from outside the network this captive portal is running for.
|
||||
{% elif state.captive %}
|
||||
To get access to the internet please accept our usage guidelines by clicking this button:
|
||||
<form method="POST" action="/login"><button type="submit">Accept</button></form>
|
||||
{% else %}
|
||||
You already accepted out conditions and are currently granted access to the internet:
|
||||
<form method="POST" action="/login"><button type="submit">Renew session</button></form>
|
||||
<form method="POST" action="/logout"><button type="submit">Close session</button></form>
|
||||
<br>
|
||||
Your current session will last for {{ state.allowed_remaining }} seconds.
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user