app.py (16029B)
1from aiohttp import web, WSCloseCode 2from aiohttp_session import get_session, new_session 3from aiohttp_session.cookie_storage import EncryptedCookieStorage 4from base64 import urlsafe_b64decode 5from cryptography import fernet 6from datetime import datetime 7from hashlib import md5 8 9import aiohttp_session 10import aiohttp_session.redis_storage 11import aioredis 12import aiosqlite 13import asyncio 14import crypto 15import json 16import os 17import random 18import sys 19import traceback 20 21base_template = """ 22<html> 23<head> 24 <title>Fireworx</title> 25 <link rel="icon" type="image/x-icon" href="/static/favicon.png"> 26 <link rel="stylesheet" href="/static/style.css"> 27 <link rel="stylesheet" href="/static/glitch.css"> 28</head> 29{body} 30</html> 31""" 32 33main_template = base_template.format(body=""" 34<body class=mainpage> 35<div id=main> 36 {navbar} 37</div> 38<div id=canvas_overlay>Click here!</div> 39<canvas id=canvas></canvas> 40</body> 41<script src="static/firework.js"></script> 42<script src="static/content.js"></script> 43""") 44 45register_template = base_template.format(body=""" 46<body class=registerpage> 47 <meta id="pow_prefix" content="{pow_prefix}"> 48 <div id=main> 49 {navbar} 50 <h1>Register:</h1> 51 <p>Our cryptographically secure login system is based on challenge-response 52 using the digital signature algorithm (DSA).<br>In other words, 53 <i>military-grade</i> encryption and utterly undefeatable!! 😎</p> 54 <br> 55 <div id=registerform class=form> 56 <table> 57 <tbody> 58 <tr><th>Name:</th><td><input type=text name=username id=username></td></tr> 59 <tr><th>P:</th><td><input type=text name=p id=p></td></tr> 60 <tr><th>Q:</th><td><input type=text name=q id=q></td></tr> 61 <tr><th>G:</th><td><input type=text name=g id=g></td></tr> 62 <tr><th>X:</th><td><input type=text name=x id=x></td></tr> 63 <tr><th>Y:</th><td><input type=text name=y id=y></td></tr> 64 </tbody> 65 </table> 66 <div> 67 <a class=left onclick=gen_privkey()>Generate</a> 68 <a class=left onclick=copy_privkey()>Copy</a> 69 <a class=right onclick=do_register()>Register</a> 70 </div> 71 </div> 72 <br> 73 <p id=errorlog></p> 74 </div> 75</body> 76<script src="static/spark-md5.min.js"></script> 77<script src="static/content.js"></script> 78""") 79 80login_template = base_template.format(body=""" 81<body class=loginpage> 82 <div id=main> 83 {navbar} 84 <h1>Login:</h1> 85 <p>Our cryptographically secure login system is based on challenge-response 86 using the digital signature algorithm (DSA).<br>In other words, 87 <i>military-grade</i> encryption and utterly undefeatable!! 😎</p> 88 <br> 89 <div id=loginform class=form> 90 <table> 91 <tbody> 92 <tr><th>Name:</th><td><input type=text name=username id=username></td></tr> 93 <tr><th>Challenge:</th> 94 <td><input type=text name=challenge id=challenge 95 value="..." readonly></td></tr> 96 <tr><th>Signature:</th> 97 <td><input type=text name=signature id=signature></td></tr> 98 </tbody> 99 </table> 100 <div> 101 <a class=right onclick=do_login()>Login</a> 102 </div> 103 </div> 104 <br> 105 <p id=errorlog></p> 106 </div> 107</body> 108<script src="static/content.js"></script> 109""") 110 111profile_template = base_template.format(body=""" 112<body class=profilepage> 113 <div id=main> 114 {navbar} 115 <div class=container> 116 <div id=proplist> 117 <h2>Properties:</h2> 118 {proplist} 119 </div> 120 <div id=eventlog> 121 <h2>Events:</h2> 122 {eventlog} 123 </div> 124 </div> 125 </div> 126</body> 127""") 128 129inspire_template = base_template.format(body=""" 130<body class=quotepage> 131 <div id=main> 132 {navbar} 133 <div id=quote> 134 <p>{quote}</p> 135 - V 136 </div> 137 </div> 138</body> 139""") 140 141inspire_navbar_html = """ 142<div id="glitch"> 143 <a href="/inspire"> 144 <span class="glitch__color glitch__color--red">Inspire</span> 145 <span class="glitch__color glitch__color--blue">Inspire</span> 146 <span class="glitch__color glitch__color--green">Inspire</span> 147 <span class="glitch__color glitch__main">Inspire</span> 148 <span class="glitch__line glitch__line--first"></span> 149 <span class="glitch__line glitch__line--second"></span> 150 </a> 151</div> 152""" 153 154navbar_nouser_template = """ 155<div id=navbar> 156<div class=left><a href=/>Home</a></div> 157<div class=right><a href=/register>Register</a></div> 158<div class=right><a href=/login>Login</a></div> 159</div> 160""" 161 162navbar_user_template = """ 163<div id=navbar> 164<div class=left><a href=/>Home</a></div> 165{inspire} 166<div class=right><a href=/logout>Logout</a></div> 167<div class=right><a href=/profile>{username}</a></div> 168</div> 169""" 170 171quotes = [ 172 "The service is a symbol, as is the act of exploiting it. Symbols are " 173 "given power by the people. Alone, a symbol is meaningless, but with " 174 "enough people, exploiting a service can change the world.", 175 "Every exploit is special.", 176 "Users shouldn't be afraid of their services. Services should be afraid " 177 "of their users.", 178 "She used to tell me that god was in the code.", 179 "Your pretty service took so long to build, now, with a snap of " 180 "history's fingers, down it goes.", 181 "Challenge authors use unsafe code to teach safe code.", 182 "You wear cat ears for so long, you forget who you were underneath them.", 183 "Every time I've seen this code change, it's always been for the worse.", 184 "I remember our teacher telling us that writing unsafe code was an " 185 "adolescent phase people outgrew. Sara did. I didn't.", 186 "Strength through Unity, Unity through good code!", 187 "Bugs will plunge this country back into chaos and I will not let that happen.", 188 "There's no flesh or blood within this code to kill. There's only an idea. " 189 "Ideas are bulletproof." 190] 191 192sockets = [] 193 194def log(*args, **kwargs): 195 print(*args, **kwargs, file=sys.stderr) 196 197def html_table(entries, header="top"): 198 html = "<table>" 199 for y, row in enumerate(entries): 200 html += "<tr>" 201 for x, val in enumerate(row): 202 if y == 0 and header == "top" or x == 0 and header == "left": 203 html += "<th>" + str(val) + "</th>" 204 else: 205 html += "<td>" + str(val) + "</td>" 206 html += "</tr>" 207 html += "</table>" 208 return html 209 210def html_response(html): 211 return web.Response(text=html, content_type="text/html") 212 213def gen_navbar(session): 214 if "username" in session: 215 if random.randint(0, 5) == 0: 216 inspire = inspire_navbar_html 217 else: 218 inspire = "" 219 return navbar_user_template.format( 220 inspire=inspire, username=session["username"]) 221 else: 222 return navbar_nouser_template 223 224async def handle_main(request): 225 session = await get_session(request) 226 navbar = gen_navbar(session) 227 html = main_template.format(navbar=navbar) 228 return html_response(html) 229 230async def handle_ws(request): 231 ws = web.WebSocketResponse() 232 await ws.prepare(request) 233 sockets.append(ws) 234 235 try: 236 async for msg in ws: 237 continue 238 finally: 239 sockets.remove(ws) 240 await ws.close() 241 242 return ws 243 244async def handle_register(request): 245 session = await get_session(request) 246 247 if request.method == "GET": 248 navbar = gen_navbar(session) 249 session["pow_prefix"] = "".join(random.choices("0123456789abcdef", k=5)) 250 html = register_template.format(navbar=navbar, 251 pow_prefix=session["pow_prefix"]) 252 return html_response(html) 253 254 if "userid" in session: 255 return web.Response(status=400, text="Already logged in") 256 257 try: 258 params = await request.post() 259 username = params["username"] 260 p = int(params["p"]) 261 q = int(params["q"]) 262 g = int(params["g"]) 263 x = int(params["x"]) 264 y = int(params["y"]) 265 except KeyError: 266 return web.Response(status=400, text="Missing params") 267 except ValueError: 268 return web.Response(status=400, text="Invalid params") 269 270 username = params["username"] 271 if len(username) < 4: 272 return web.Response(status=400, text="Username too short") 273 274 privkey = crypto.DSAKey(p, q, g, x, y) 275 276 try: 277 sql = "INSERT INTO users (name,p,q,g,x,y) values (?,?,?,?,?,?)" 278 vals = [str(v) for v in privkey.vals()] 279 res = await db.execute_insert(sql, (username, *vals)) 280 userid = res[0] 281 await db.commit() 282 except Exception as e: 283 traceback.print_exc() 284 return web.Response(status=400, text="Registration failed (database)") 285 286 session["username"] = username 287 session["userid"] = userid 288 289 return web.Response(status=200, text="OK") 290 291async def handle_login(request): 292 session = await get_session(request) 293 294 if request.method == "GET": 295 navbar = gen_navbar(session) 296 html = login_template.format(navbar=navbar) 297 return html_response(html) 298 299 if "userid" in session: 300 return web.Response(status=400, text="Already logged in") 301 302 try: 303 params = await request.post() 304 username = params["username"] 305 challenge = int(params["challenge"]) 306 r,s = params["signature"].split(",") 307 signature = (int(r), int(s)) 308 except KeyError as e: 309 return web.Response(status=400, text="Missing param " + str(e)) 310 except ValueError: 311 if "," not in params["signature"]: 312 return web.Response(status=400, text="Signature format: r,s") 313 else: 314 return web.Response(status=400, text="Invalid integers") 315 316 if len(username) < 4: 317 return web.Response(status=400, text="Username too short") 318 319 if "challenge" not in session: 320 return web.Response(status=400, text="Challenge not set") 321 322 if challenge != session["challenge"]: 323 return web.Response(status=400, text="Expired challenge") 324 session.pop("challenge") 325 326 sql = "SELECT id,p,q,g,x,y FROM users WHERE name = ?" 327 async with db.execute(sql, (username,)) as cursor: 328 row = await cursor.fetchone() 329 if row is None: 330 return web.Response(status=400, text="No such user") 331 userid = row[0] 332 privkey = crypto.DSAKey(*[int(v) for v in row[1:]]) 333 pubkey = privkey.pubkey() 334 335 try: 336 assert pubkey.verify(challenge, signature) 337 except Exception as e: 338 if not isinstance(e, AssertionError): 339 trace = traceback.format_exc() 340 log(f"Login signature verify failed:\n{trace}") 341 text = "Verify failed! Expected signature:\n" 342 try: 343 r,s = privkey.sign(challenge) 344 text += f"{r},{s}" 345 except: 346 trace = traceback.format_exc() 347 log(f"Generating correct signature failed:\n{trace}") 348 text += "SIGN FAILED" 349 return web.Response(status=400, text=text) 350 351 session["username"] = username 352 session["userid"] = userid 353 354 return web.Response(status=200, text="OK") 355 356async def handle_logout(request): 357 session = await get_session(request) 358 session.invalidate() 359 return web.HTTPFound("/") 360 361async def handle_inspire(request): 362 session = await get_session(request) 363 navbar = gen_navbar(session) 364 quote = random.choice(quotes) 365 html = inspire_template.format(navbar=navbar, quote=quote) 366 return html_response(html=html) 367 368async def handle_profile(request): 369 session = await get_session(request) 370 try: 371 username = request.match_info["username"] 372 except: 373 if "username" not in session: 374 return web.HTTPFound("/login") 375 username = session["username"] 376 377 sql = "SELECT p,q,g,x,y FROM users WHERE name = ?" 378 async with db.execute(sql, (username,)) as cursor: 379 row = await cursor.fetchone() 380 if row is None: 381 return web.Response(status=400, text="Missing info (database)") 382 privkey = crypto.DSAKey(*[int(v) for v in row]) 383 384 data = [("name", username),] 385 if "username" in session: 386 data += privkey.dict().items() 387 else: 388 data += privkey.pubkey().dict().items() 389 proplist = html_table(data, header="left") 390 391 data = [("time", "x", "y", "wish")] 392 if "username" in session: 393 userid = session["userid"] 394 sql = "SELECT x,y,time,wish FROM events WHERE userid = ?" 395 async with db.execute(sql, (userid,)) as cursor: 396 async for row in cursor: 397 x = f"{row[0]:.3f}" 398 y = f"{row[1]:.3f}" 399 time, wish = row[2:] 400 data.append((time, x, y, wish)) 401 eventlog = html_table(data, header="top") 402 403 navbar = gen_navbar(session) 404 html = profile_template.format(navbar=navbar, 405 proplist=proplist, eventlog=eventlog) 406 return html_response(html) 407 408async def handle_genkey(request): 409 session = await get_session(request) 410 411 if "pow_prefix" not in session: 412 return web.Response(status=400, text="Invalid session") 413 414 try: 415 work = request.query["pow"] 416 pow_prefix = request.query["pow_prefix"] 417 except KeyError: 418 return web.Response(status=400, text="Missing param " + str(e)) 419 420 if pow_prefix != session["pow_prefix"]: 421 return web.Response(status=400, text="Expired pow prefix") 422 423 md5hash = md5(work.encode()).hexdigest() 424 if not md5hash.startswith(session["pow_prefix"]): 425 return web.Response(status=400, text="Bad proof of work") 426 427 try: 428 privkey = crypto.DSAKey.gen() 429 except Exception as e: 430 return web.Response(status=400, text="Private key generation failed") 431 432 data = {k:str(v) for k,v in privkey.dict().items()} 433 return web.Response(status=200, text=json.dumps(data)) 434 435async def handle_challenge(request): 436 session = await get_session(request) 437 438 session["challenge"] = crypto.gen_challenge() 439 return web.Response(text=str(session["challenge"])) 440 441async def handle_launch(request): 442 session = await get_session(request) 443 444 if "userid" not in session: 445 return web.Response(status=400, text="Not logged in") 446 userid = session["userid"] 447 448 try: 449 params = await request.post() 450 x = float(params["x"]) 451 y = float(params["y"]) 452 wish = params["wish"] if "wish" in params else "" 453 except (KeyError, ValueError): 454 return web.Response(status=400, text="Missing / invalid params") 455 456 time = datetime.strftime(datetime.now(), "%H:%M:%S") 457 sql = "INSERT INTO events (userid,time,wish,x,y) values (?,?,?,?,?)" 458 await db.execute(sql, (userid, time, wish, x, y)) 459 await db.commit() 460 461 event = { 462 "type": "firework", 463 "x": x, 464 "y": y 465 } 466 467 for ws in sockets: 468 await ws.send_str(json.dumps(event)) 469 470 return web.Response(status=200) 471 472async def create_runner(): 473 app = web.Application() 474 redis_host = os.getenv("REDIS_HOST") 475 redis_port = os.getenv("REDIS_PORT") 476 redis = await aioredis.from_url(f"redis://{redis_host}:{redis_port}") 477 storage = aiohttp_session.redis_storage.RedisStorage(redis) 478 aiohttp_session.setup(app, storage) 479 app.add_routes([ 480 web.get('/', handle_main), 481 web.get('/ws', handle_ws), 482 web.get("/register", handle_register), 483 web.post("/register", handle_register), 484 web.get("/login", handle_login), 485 web.post("/login", handle_login), 486 web.get("/logout", handle_logout), 487 web.get("/inspire", handle_inspire), 488 web.get("/profile", handle_profile), 489 web.get("/profile/{username}", handle_profile), 490 web.get("/genkey", handle_genkey), 491 web.get("/challenge", handle_challenge), 492 web.post("/launch", handle_launch), 493 web.static("/static", "static") 494 ]) 495 return web.AppRunner(app) 496 497async def main(): 498 global db 499 db = await aiosqlite.connect("data/db.sqlite") 500 await db.execute("PRAGMA foreign_keys = ON") 501 runner = await create_runner() 502 await runner.setup() 503 site = web.TCPSite(runner, "0.0.0.0", 1812) 504 await site.start() 505 506if __name__ == "__main__": 507 loop = asyncio.new_event_loop() 508 asyncio.set_event_loop(loop) 509 loop.run_until_complete(main()) 510 loop.run_forever() 511