"""TIES4080 pääteohjaus 3: laskuri sessiolla ja kirjautuminen. Sessioon tallennettu tieto säilyy pyynnöstä toiseen. Flaskin oletussessio kulkee selaimen evästeessä allekirjoitettuna - se ei siis ole salattu, joten sessioon ei laiteta mitään arkaluontoista. """ import os from functools import wraps from flask import Flask, flash, redirect, render_template, request, session, url_for from werkzeug.security import check_password_hash app = Flask(__name__) # Salainen avain allekirjoittaa sessioevästeen. Sen tietävä voi väärentää # session, joten avainta ei kirjoiteta lähdekoodiin eikä versionhallintaan. # users.jyu.fi-palvelimella ei voi asettaa omia ympäristömuuttujia, joten avain # luetaan tiedostosta, joka on www-hakemiston ulkopuolella samassa kansiossa # tietokannan kanssa. # # Tiedosto luodaan kerran komennoilla: # python3 -c "import secrets; print(secrets.token_hex(32))" > ~/public_html/hidden/avain.txt # chmod 600 ~/public_html/hidden/avain.txt AVAINTIEDOSTO = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "hidden", "avain.txt") def lue_salainen_avain(polku): try: with open(polku, encoding="UTF-8") as tiedosto: avain = tiedosto.read().strip() except OSError as virhe: raise RuntimeError( f"Salaista avainta ei saada luettua tiedostosta {polku}. " "Luo tiedosto komennolla: " "python3 -c \"import secrets; print(secrets.token_hex(32))\" > avain.txt" ) from virhe if not avain: raise RuntimeError(f"Avaintiedosto {polku} on tyhjä.") return avain app.secret_key = lue_salainen_avain(AVAINTIEDOSTO) app.config.update( SESSION_COOKIE_SAMESITE="Lax", # torjuu tavallisimmat CSRF-tapaukset SESSION_COOKIE_SECURE=True, # eväste vain https-yhteydellä SESSION_COOKIE_HTTPONLY=True, # ei näy javascriptille (oletus) ) TUNNUS = "ties4080" # Tarkiste salasanalle "testi". Selkokielistä salasanaa ei koskaan tallenneta # eikä vertailla. generate_password_hash arpoo jokaiselle salasanalle oman # suolan ja käyttää tarkoituksella hidasta algoritmia. TARKISTE = ("scrypt:32768:8:1$kS80LL7mVNyr7pwB$ed17bfdc0021bfc34d0fc2d3db0709" "760d77d29ca3d9560734aa14e30031eb3d31fc1d020155745330b9df3c780fbc" "4f3677a0e0f9f49410a52541afd73928b4") def auth(f): """Dekoraattori, joka vaatii kirjautumisen ja ohjaa tarvittaessa lomakkeelle.""" @wraps(f) def kaaritty(*args, **kwargs): if "kirjautunut" not in session: return redirect(url_for("kirjautumislomake"), code=303) return f(*args, **kwargs) return kaaritty @app.get("/") def etusivu(): return redirect(url_for("laskuri"), code=303) # -------------------------------------------------------------------------- # Kirjautuminen # -------------------------------------------------------------------------- @app.get("/kirjaudu") def kirjautumislomake(): return render_template("kirjaudu.html") @app.post("/kirjaudu") def kirjaudu(): tunnus = request.form.get("tunnus", "").strip() salasana = request.form.get("salasana", "") if tunnus != TUNNUS or not check_password_hash(TARKISTE, salasana): flash("Tunnus tai salasana on väärin.") return render_template("kirjaudu.html"), 401 # Vanha sessio tyhjennetään ennen uuden avaamista. Muuten hyökkääjä voi # asettaa uhrin selaimeen tuntemansa sessiotunnisteen ja päästä sisään # sen avulla (session fixation). session.clear() session["kirjautunut"] = tunnus return redirect(url_for("laskuri"), code=303) @app.post("/kirjaudu-ulos") def kirjaudu_ulos(): session.clear() return redirect(url_for("kirjautumislomake"), code=303) # -------------------------------------------------------------------------- # Laskuri # -------------------------------------------------------------------------- @app.get("/laskuri") @auth def laskuri(): # get palauttaa oletusarvon, jos sessiomuuttujaa ei vielä ole. # Poikkeuskäsittelyä ei tarvita. return render_template("laskuri.html", summa=session.get("laskuri", 0)) @app.post("/laskuri") @auth def laske(): luku = request.form.get("luku", 0, type=int) session["laskuri"] = session.get("laskuri", 0) + luku # POST/Redirect/GET: sivun päivittäminen ei lisää samaa lukua uudelleen return redirect(url_for("laskuri"), code=303) @app.post("/nollaa") @auth def nollaa(): session.pop("laskuri", None) return redirect(url_for("laskuri"), code=303) if __name__ == "__main__": app.run(debug=True)