Skip to content

Commit 27b058e

Browse files
committed
Simplify and fix http_server.py
It no longer handles basic authentication, instead it only listens on the loopback address. A reverse proxy like nginx should handle auth like this and https too. (Old auth was broken too due to trusting cookies.)
1 parent 80c8ce8 commit 27b058e

File tree

4 files changed

+79
-135
lines changed

4 files changed

+79
-135
lines changed

ioibot/http_server.py

Lines changed: 70 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,83 @@
1-
import base64
21
import logging
3-
import os
42

53
from aiohttp import web
6-
from aiohttp_session import SimpleCookieStorage, get_session, setup
7-
import asyncpg
8-
import bcrypt
9-
from dotenv import load_dotenv
10-
import pandas as pd
11-
import yaml
124

13-
from .config import Config
5+
from ioibot.storage import Storage
146

157
logger = logging.getLogger(__name__)
168
logger.setLevel(logging.DEBUG)
179

18-
load_dotenv()
19-
20-
uname = os.getenv('VOTING_USERNAME').strip()
21-
passw_hash = bytes(os.getenv('VOTING_PASSWORD').strip(), "utf-8")
22-
23-
# Middleware function to perform Basic Authentication
24-
async def basic_auth_middleware(app, handler):
25-
async def middleware(request):
26-
# Extract the Authorization header from the request
27-
auth_header = request.headers.get("Authorization")
28-
session = await get_session(request)
29-
30-
if auth_header:
31-
try:
32-
auth_type, encoded_credentials = auth_header.split(" ")
33-
if auth_type.lower() == "basic":
34-
decoded_credentials = base64.b64decode(encoded_credentials).decode("utf-8")
35-
username, password = decoded_credentials.split(":")
36-
username = username.strip()
37-
password = password.strip()
38-
password = bytes(password, "utf-8")
39-
if username == uname and bcrypt.hashpw(password, passw_hash) == passw_hash:
40-
logger.info(f"Authentication successful for {request.remote}")
41-
42-
session["authenticated"] = True
43-
return await handler(request)
44-
45-
except Exception as e:
46-
print(f"Authentication error: {e}")
47-
48-
# Authentication failed or no credentials provided, check session for authentication
49-
if session.get("authenticated"):
50-
logger.info(f"Authentication successful for {request.remote}")
51-
52-
return await handler(request)
10+
routes = web.RouteTableDef()
11+
store_key = web.AppKey("store", Storage)
12+
static_root = 'webpage' # TODO make this configurable
13+
14+
# return currently active poll result
15+
@routes.get("/api/polls")
16+
async def polls_active(req: web.Request):
17+
store = req.app[store_key]
18+
conn = store.conn
19+
teams = store.teams
20+
21+
poll_details = await conn.fetchrow(
22+
"SELECT poll_id, question, status, anonymous, multiple_choice FROM polls WHERE display")
23+
if poll_details is None:
24+
return web.json_response({})
25+
26+
votes = []
27+
[poll_id, question, status, anonymous, multiple_choice] = poll_details
28+
29+
poll_choices = await conn.fetch(
30+
"SELECT poll_choice_id, choice, marker FROM poll_choices WHERE poll_id = $1", poll_id)
31+
if not poll_choices:
32+
return web.HTTPInternalServerError()
33+
34+
choices = [{'choice_id': choice_id, 'choice': choice, 'marker': marker} for (choice_id, choice, marker) in poll_choices]
35+
36+
if anonymous:
37+
if status == 1:
38+
results = dict()
39+
for (poll_choice_id, _, _) in poll_choices:
40+
results[poll_choice_id] = 0
41+
42+
anonym_votes = await conn.fetch("SELECT poll_choice_id FROM poll_anonym_active_votes")
43+
for (vote,) in anonym_votes:
44+
results[vote] += 1
45+
vote_items = results.items()
46+
elif status == 2:
47+
vote_items = await conn.fetch("SELECT poll_choice_id, count FROM poll_anonym_votes WHERE poll_id = $1", poll_id)
5348
else:
54-
logger.info(f"Authentication failed for {request.remote}")
55-
56-
# Authentication failed, return a 401 Unauthorized response
57-
response = web.Response(status=401)
58-
response.headers["WWW-Authenticate"] = 'Basic realm="Secure Area"'
59-
return response
60-
61-
return middleware
62-
63-
64-
async def create_app(config: Config, conn: asyncpg.Pool):
49+
vote_items = []
50+
votes = [{'count': count, 'choice_id': choice} for (choice, count) in vote_items]
51+
else: # not anonymous
52+
vote_items = await conn.fetch("SELECT poll_choice_id, team_code, voted_by, voted_at FROM poll_votes WHERE poll_id = $1", poll_id)
53+
votes = [{'team_code': f"({team_code}) {teams.loc[teams['Code'] == team_code, ['Name']].values[0][0]}", 'voted_by': voted_by, 'voted_at': voted_at, 'choice_id': choice} for (choice, team_code, voted_by, voted_at) in vote_items]
54+
missing_teams = teams.loc[~teams['Code'].isin([team_code for (_, team_code, _, _) in vote_items]), ['Code', 'Name']]
55+
for _, row in missing_teams.iterrows():
56+
votes.append({'team_code': f"({row['Code']}) {row['Name']}", 'voted_by': None, 'voted_at': None, 'choice_id': None})
57+
58+
response = {
59+
'question': question,
60+
'choices': choices,
61+
'anonymous': anonymous,
62+
'multiple_choice': multiple_choice,
63+
'status': status,
64+
'votes': votes,
65+
}
66+
67+
return web.json_response(response)
68+
69+
# for development, should be handled by nginx
70+
@routes.get('/')
71+
async def get_index(req: web.Request):
72+
return web.FileResponse(f'{static_root}/index.html')
73+
routes.static('/', static_root)
74+
75+
async def run_webapp(store: Storage):
6576
app = web.Application()
66-
setup(app, SimpleCookieStorage(max_age=3600))
67-
app.middlewares.append(basic_auth_middleware)
68-
routes = web.RouteTableDef()
69-
teams_all = pd.read_csv(config.team_url)
70-
teams = teams_all[teams_all['Voting'] == 1]
71-
72-
73-
# website
74-
@routes.get("/polls")
75-
async def polls(request):
76-
return web.FileResponse("./webpage/index.html")
77-
78-
# return currently active poll result
79-
@routes.get("/polls/display")
80-
async def polls_active(request):
81-
poll_details = await conn.fetchrow(
82-
"SELECT poll_id, question, status, anonymous, multiple_choice FROM polls WHERE display = 1")
83-
if poll_details is None:
84-
return web.json_response({})
85-
86-
votes = []
87-
[poll_id, question, status, anonymous, multiple_choice] = poll_details
88-
89-
poll_choices = await conn.fetch(
90-
"SELECT poll_choice_id, choice, marker FROM poll_choices WHERE poll_id = $1", poll_id)
91-
if not poll_choices:
92-
return web.HTTPInternalServerError()
93-
94-
choices = [{'choice_id': choice_id, 'choice': choice, 'marker': marker} for (choice_id, choice, marker) in poll_choices]
95-
96-
if anonymous:
97-
if status == 1:
98-
results = dict()
99-
for (poll_choice_id, _, _) in poll_choices:
100-
results[poll_choice_id] = 0
101-
102-
anonym_votes = await conn.fetch("SELECT poll_choice_id FROM poll_anonym_active_votes")
103-
for (vote,) in anonym_votes:
104-
results[vote] += 1
105-
vote_items = results.items()
106-
elif status == 2:
107-
vote_items = await conn.fetch("SELECT poll_choice_id, count FROM poll_anonym_votes WHERE poll_id = $1", poll_id)
108-
else:
109-
vote_items = []
110-
votes = [{'count': count, 'choice_id': choice} for (choice, count) in vote_items]
111-
else: # not anonymous
112-
vote_items = await conn.fetch("SELECT poll_choice_id, team_code, voted_by, voted_at FROM poll_votes WHERE poll_id = $1", poll_id)
113-
votes = [{'team_code': f"({team_code}) {teams.loc[teams['Code'] == team_code, ['Name']].values[0][0]}", 'voted_by': voted_by, 'voted_at': voted_at, 'choice_id': choice} for (choice, team_code, voted_by, voted_at) in vote_items]
114-
missing_teams = teams.loc[~teams['Code'].isin([team_code for (_, team_code, _, _) in vote_items]), ['Code', 'Name']]
115-
for _, row in missing_teams.iterrows():
116-
votes.append({'team_code': f"({row['Code']}) {row['Name']}", 'voted_by': None, 'voted_at': None, 'choice_id': None})
117-
118-
response = {
119-
'question': question,
120-
'choices': choices,
121-
'anonymous': anonymous,
122-
'multiple_choice': multiple_choice,
123-
'status': status,
124-
'votes': votes,
125-
}
126-
127-
return web.json_response(response)
128-
129-
130-
app.router.add_routes(routes)
131-
app.router.add_static("/", "./")
132-
return app
133-
134-
async def run_webapp(config: Config, conn: asyncpg.Pool):
135-
app = await create_app(config, conn)
77+
app[store_key] = store
78+
app.add_routes(routes)
79+
13680
runner = web.AppRunner(app)
13781
await runner.setup()
138-
site = web.TCPSite(runner, "0.0.0.0", 9000)
82+
site = web.TCPSite(runner, '127.0.0.1', 9000) # TODO make this configurable
13983
await site.start()

ioibot/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ async def async_main():
7373
async with store.db_connect():
7474
await asyncio.gather(
7575
loop(config, client),
76-
run_webapp(config, store.conn))
76+
run_webapp(store))
7777

7878
async def loop(config, client):
7979
# Keep trying to reconnect on failure (with some time in-between)

webpage/index.html

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
<head>
55
<meta charset="UTF-8">
66
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
7-
<link rel="stylesheet" href="./webpage/style.css">
7+
<link rel="stylesheet" href="style.css">
88
<meta name="viewport" content="width=device-width, initial-scale=1.0">
99
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
1010
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
11-
<script type="text/javascript" src="./webpage/purify.min.js"></script>
11+
<script type="text/javascript" src="purify.min.js"></script>
1212
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
13-
<script src="./webpage/script.js"></script>
13+
<script src="script.js"></script>
1414

15-
<link rel="icon" href="./webpage/asset/favicon.svg" type="image/svg+xml">
15+
<link rel="icon" href="asset/favicon.svg" type="image/svg+xml">
1616
<title>Vote Result</title>
1717

1818
<script>
@@ -29,7 +29,7 @@
2929
<div>
3030
<div class="row justify-content-center pt-4 pb-5" id="header">
3131
<div class="col-12 col-lg-3 align-middle d-flex justify-content-center float-none float-lg-start">
32-
<img id="ioiLogo" src="./webpage/asset/ioi_logo.png">
32+
<img id="ioiLogo" src="asset/ioi_logo.png">
3333
</div>
3434
<h2 class="col-12 col-lg-5 text-break px-1 py-3 my-auto text-center align-middle h-100" id="question">No poll is
3535
currently active.</h2>
@@ -58,12 +58,12 @@ <h2 class="col-12 col-lg-5 text-break px-1 py-3 my-auto text-center align-middle
5858
</div>
5959
<div id="no-poll">
6060
<div class="my-auto align-middle p-5 w-100" style="height: 200px;">
61-
<img class="w-100 h-100" style="object-fit: contain;" src="./webpage/asset/ioi_logo.png">
61+
<img class="w-100 h-100" style="object-fit: contain;" src="asset/ioi_logo.png">
6262
</div>
6363
<div class="my-auto align-middle p-5 w-100">
6464
<h2 class="text-center">No poll is currently active.</h2>
6565
</div>
6666
</div>
6767
</div>
6868

69-
</html>
69+
</html>

webpage/script.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ function updateChart(status) {
7272
}
7373

7474
function fetchPollResult() {
75-
$.get("/polls/display", function(json_data) {
75+
$.get("/api/polls", function(json_data) {
7676

7777
const get_data = function() {
7878
if ($.isEmptyObject(json_data)) { return false; }

0 commit comments

Comments
 (0)