11from flask import Flask , render_template , jsonify , request , session , abort , redirect
2+ from werkzeug .security import generate_password_hash , check_password_hash
23from flask_socketio import SocketIO , send , emit
3- import secrets , time , socket , hashlib , json , logging , sqlite3
4+ import secrets , time , socket , hashlib , json , logging , sqlite3 , functools
45
56
7+ # -------- SETUP -------- #
8+
69# App + WebSocket
710app = Flask (__name__ .split ("." )[0 ])
811app .config ["SECRET_KEY" ] = secrets .token_hex (16 )
@@ -16,7 +19,7 @@ def startLogger():
1619 if not log .handlers :
1720 handler = logging .FileHandler ("static/app.log" , encoding = "utf-8" )
1821 streamHandler = logging .StreamHandler ()
19- fmt = "{asctime} [{levelname}] -- {message}"
22+ fmt = "{asctime} [{levelname}]: \n {message}"
2023 formatter = logging .Formatter (fmt , style = "{" )
2124 handler .setFormatter (formatter )
2225 streamHandler .setFormatter (formatter )
@@ -78,33 +81,65 @@ def coloredText(stuff, colorcode):
7881 json .dump (data , file )
7982 log .exception ("Error loading config.json" )
8083
81-
84+ # Check if logged in decorator
85+ def logged_in (route ):
86+ @functools .wraps (route )
87+ def wrapper_logged_in (* args , ** kwargs ):
88+ # for now just worry about the decorator
89+ with sqlite3 .connect ("myop.db" ) as c :
90+ cur = c .cursor ()
91+ if session .get ("user" ):
92+ cur .execute ("SELECT * FROM users WHERE callsign=?" , (session .get ("username" )))
93+ if not session .get ("username" ) in cur .fetchall ():
94+ log .debug ("user redirected to login" )
95+ return redirect ("/login" )
96+ else :
97+ log .debug ("user passed" )
98+ return route (* args , ** kwargs )
99+ else :
100+ log .info ("user passed without login" )
101+ return route (* args , ** kwargs )
102+ return wrapper_logged_in
103+
104+ # csrf checking
105+ def needs_csrf (route ):
106+ @functools .wraps (route )
107+ def wrapper_csrf (* args , ** kwargs ):
108+ if "csrf" in session :
109+ return route (* args , ** kwargs )
110+ else :
111+ session ["csrf" ] = secrets .token_hex (16 )
112+ return route (* args , ** kwargs )
113+ return wrapper_csrf
114+
115+
116+ # ------------ APP ------------- #
82117
83118# Misc. routes
84119@app .route ("/" )
120+ @logged_in
85121def main ():
86122 with open ("static/config.json" , "r" ) as f :
87123 title = json .load (f )["title" ]
88124 return render_template ("main.html" , title = title )
89125
90126@app .route ("/log" )
127+ @logged_in
91128def showlog ():
92129 with open ("static/app.log" , "r" ) as log :
93130 return log .read ().encode ("utf-8" ), 200
94131
95132
96133# ======== Control Panel ======== #
97- # Can add an admin login page at a later time.
98134
99135@app .route ("/control" , methods = ['GET' ])
136+ @logged_in
137+ @needs_csrf
100138def control ():
101- # check user.logged-in logic, for later
102- # return redirect("/login"), 301
103- if "csrf" not in session :
104- session ["csrf" ] = secrets .token_hex (16 )
105139 return render_template ("control.html" , csrf = session ["csrf" ])
106140
107141@app .route ("/controlapi" , methods = ['GET' , 'POST' ])
142+ @logged_in
108143def controlapi ():
109144 if request .method == "GET" :
110145 with open ('static/config.json' , "r" ) as file :
@@ -130,24 +165,52 @@ def controlapi():
130165
131166
132167# The actual login page
133- # @app.route("/login")
134- # def login():
135- # return render_template("login.html")
168+ # Pro tip: no @logged_in (for obvious reasons)
169+ @app .route ("/login" , methods = ["GET" , "POST" ])
170+ @needs_csrf
171+ def login ():
172+ if request .method == "GET" :
173+ return render_template ("login.html" , username = session .get ("username" ) or "" , csrf = session ["csrf" ])
174+ elif request .method == "POST" :
175+ u = request .form
176+ if ("csrf" or "username" or "password" )not in u : abort (403 )
177+ if session ["csrf" ] != u ["csrf" ]: abort (403 )
178+ log .debug ("login form passed basic qualifications" )
179+ with sqlite3 .connect ("myop.db" ) as c :
180+ cur = c .cursor ()
181+ c .row_factory = sqlite3 .Row
182+ cur .execute ("SELECT callsign, pwdhash FROM users WHERE (callsign = ?)" , (u ["username" ].lower (),))
183+ row = cur .fetchone ()
184+ if row is None :
185+ log .warning (f"someone tried to log in, but username didn't exist\n username: { u ["username" ]} " )
186+ abort (403 )
187+ if (row ) and (check_password_hash (row ["pwdhash" ], u ["password" ])):
188+ session ["user" ] = row ["callsign" ]
189+ log .info ("user logged in!" )
190+ return redirect ("/" , code = 301 )
191+ else :
192+ log .warning (f"someone failed a login!\n username: { u ["username" ]} \n password: { u ["password" ]} " )
193+ abort (403 )
194+ elif request .method == "DELETE" :
195+ session ["user" ] = None
196+ return redirect ("/login" , code = 301 )
197+
136198
137199
138200
139201# ======== Bulletins ======== #
140202
141203@app .route ("/bulletins" , methods = ['GET' ])
204+ @logged_in
205+ @needs_csrf
142206def bulletins ():
143207 with open ("static/config.json" , "r" ) as f :
144208 config = json .load (f )
145209 if not config .get ("services" ).get ("bulletins" ): return render_template ("disabled.html" ), 403
146- if "csrf" not in session :
147- session ["csrf" ] = secrets .token_hex (16 )
148210 return render_template ("bulletins.html" , csrf = session ["csrf" ], uname = "None" )
149211
150212@app .route ("/bulletinsapi" , methods = ["GET" ])
213+ @logged_in
151214def bulletinsapiget ():
152215 with sqlite3 .connect ("myop.db" ) as c :
153216 cur = c .cursor ()
@@ -167,6 +230,7 @@ def bulletinsapiget():
167230 return jsonify (data ), 200
168231
169232@app .route ("/bulletinsapi" , methods = ["POST" ])
233+ @logged_in
170234def bulletinsapipost ():
171235 posted = request .form .get ("csrf" )
172236 stored = session .get ("csrf" )
@@ -189,6 +253,7 @@ def bulletinsapipost():
189253 return redirect ("/bulletins" ), 301
190254
191255@app .route ("/bulletinsapi" , methods = ['DELETE' ])
256+ @logged_in
192257def bulletinsapidelete ():
193258 with sqlite3 .connect ("myop.db" ) as c :
194259 cur = c .cursor ()
@@ -201,6 +266,7 @@ def bulletinsapidelete():
201266# ======== Chat Stuff ========== #
202267
203268@app .route ("/chat" , methods = ['GET' ])
269+ @logged_in
204270def chat ():
205271 with open ("static/config.json" , "r" ) as f :
206272 config = json .load (f )
0 commit comments