From 64e342e01e9e51f394aebc6fedf9dff321904538 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Sat, 22 Feb 2020 22:46:30 +0100 Subject: [PATCH 01/54] Added two routes: GET /readings and GET /login Login returns the login.html page, while /readings returns the readings-index.html with a readings template variable containing the unencrypted messages. --- Database.md | 3 +++ main.py | 36 +++++++++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Database.md b/Database.md index 6c96c5f9..d5003e4d 100644 --- a/Database.md +++ b/Database.md @@ -107,6 +107,8 @@ Public and Private PGP Key (GnuPG): , "last_name" : "" , "expiration" : "" , "feed" : [""] +, "readings" : [""] +, "writings" : [""] , "old_keys" : [{ "expiration" : "" , "public_key" :"" , "private_key" : "" @@ -133,6 +135,7 @@ A post is just signed by the author , "ciphertext" : "" }] , "hash" : "" +, "draft" : } ``` If in "to" there is `"all"` then the body isn't encrypted diff --git a/main.py b/main.py index 71e7f9ea..734d8694 100644 --- a/main.py +++ b/main.py @@ -1,11 +1,17 @@ #!/usr/bin/env python3 from Server import Elections, Patches, Users -from flask import Flask, request, render_template, send_file +from Server.Crypto import Storage +from flask import Flask, request, render_template +import pymongo +from pymongo import MongoClient import json, os from Crypto.Hash import SHA3_256 -app = Flask(__name__, static_url_path="/static", static_folder="output") +app = Flask( __name__ + , static_url_path="/static" + , static_folder="output/static" + , template_folder="output") app.secret_key = os.environ["SECRET_KEY"] # Errors @@ -15,7 +21,31 @@ @app.route("/", methods=["GET"]) def index(): - return send_file("output/index.html") + if session.get("username"): + return render_template("index.html") + else: + return render_template("login.html") + +@app.route("/login", methods=["GET"]) +def login(): + return render_template("login.html") + +@app.route("/readings", methods=["GET"]) +def readings(): + if session.get("username"): + client = MongoClient() + db = client.demnet + messages = db.messages + users = db.users + readings = users.find_one({ "username" : session["username" ] })["readings"] + readings = [messages.find_one({ "hash" : reading }) for reading in readings] + readings = [{ key : reading[key] if key != "body" else Storage.encrypt(reading["body"]) for key in reading } for reading in readings] + + return render_template("readings-index.html", readings=readings) + + + + @app.route("/login",methods=["POST"]) def login(): From bf741b4e314afc90c70081847ea9fdff600146b2 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Sun, 23 Feb 2020 10:24:48 +0100 Subject: [PATCH 02/54] Simplified messages: We only allow public posts from now on. --- Database.md | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/Database.md b/Database.md index d5003e4d..16e5d78b 100644 --- a/Database.md +++ b/Database.md @@ -117,23 +117,16 @@ Public and Private PGP Key (GnuPG): ``` # messages -Anybody can send end-to-end encypted messages to anyone. -A message, that is addressed to `all` is a post. -The process of encrypting and decrypting a message -from Alice to Bob is like this: - - Alice's Message -> Alice's Private Key -> Bob's Public Key -> Send -> Bob's Private Key -> Alice's Public Key - -A post is just signed by the author - - Alice's Message -> Alice's Private Key -> Published - +For now, there can be only one kind of messages: +Public posts. +These messages can be read and are addressed to anyone. ```json { "from" : "" -, "to" : [ "" ] -, "body" : [{ "recipient_name" : "" - , "ciphertext" : "" - }] +, "to" : "all" +, "body" : { "title" : "" , "draft" : <true if message is a draft and not yet ready to be send.> } From 7f6e5e76eafc47ceabd7e04be009ae5a7711ac3b Mon Sep 17 00:00:00 2001 From: Joris Gutjahr <joris.gutjahr@protonmail.com> Date: Sun, 23 Feb 2020 11:51:20 +0100 Subject: [PATCH 03/54] GET /read Route implemented. --- Database.md | 1 - main.py | 36 ++++++++++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/Database.md b/Database.md index 16e5d78b..9c32e090 100644 --- a/Database.md +++ b/Database.md @@ -126,7 +126,6 @@ These messages can be read and are addressed to anyone. , "body" : { "title" : "<title of the post" , "content" : "<markdown content of the post." } -} , "hash" : "<SHA256 of a dict of all the other fields in the message, used as unique and secure identifier>" , "draft" : <true if message is a draft and not yet ready to be send.> } diff --git a/main.py b/main.py index 734d8694..d2188507 100644 --- a/main.py +++ b/main.py @@ -15,9 +15,10 @@ app.secret_key = os.environ["SECRET_KEY"] # Errors -ok = 0 -invalidData = 1 -invalidContext = 2 +ok = 0 +invalidData = 1 +invalidContext = 2 +notLoggedIn = 3 @app.route("/", methods=["GET"]) def index(): @@ -30,21 +31,40 @@ def index(): def login(): return render_template("login.html") +""" +Returns the readings-index.html template +with template argument: + readings : List[Tuple[title,hash of reading]] + +""" @app.route("/readings", methods=["GET"]) def readings(): if session.get("username"): client = MongoClient() db = client.demnet messages = db.messages - users = db.users readings = users.find_one({ "username" : session["username" ] })["readings"] readings = [messages.find_one({ "hash" : reading }) for reading in readings] - readings = [{ key : reading[key] if key != "body" else Storage.encrypt(reading["body"]) for key in reading } for reading in readings] + readings = [(reading["body"]["title"], reading["hash"]) for reading in readings] return render_template("readings-index.html", readings=readings) - - - + else: + return notLoggedIn +""" +Returns the reading.html template +with argument: + reading, the full message. +""" +@app.route("/read/<reading_hash>", methods=["GET"]) +def read(reading_hash): + if session.get("username"): + client = MongoClient() + db = client.demnet + messages = db.messages + reading = messages.find_one({ "hash" : reading_hash }) + return render_template("reading.html", reading=reading) + else: + return notLoggedIn @app.route("/login",methods=["POST"]) From 292e60203a6b3d2092136f7c0f440d3e6b5dce92 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr <joris.gutjahr@protonmail.com> Date: Sun, 23 Feb 2020 14:16:48 +0100 Subject: [PATCH 04/54] Added Feed creation to GET / route: If the user is logged in, returns feed sorted by most recent uploads as dict of "title" and "hash". So you can use /read/<hash> to get to reading that upload. --- main.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index d2188507..174df9a7 100644 --- a/main.py +++ b/main.py @@ -20,10 +20,26 @@ invalidContext = 2 notLoggedIn = 3 +""" +Returns either the login.html +or feed. +If the user is logged in, returns feed +sorted by most recent uploads as +dict of "title" and "hash". +So you can use /read/<hash> to get to reading that upload. +""" @app.route("/", methods=["GET"]) def index(): if session.get("username"): - return render_template("index.html") + client = MongoClient() + db = client.demnet + messages = db.messages + messages = messages.find().sort('upload_time', pymongo.DESCENDING) + messages_ = [] + for message in messages: + messages_.append({ "title" : message["title"], "hash" : message["hash"] } } ) + + return render_template("index.html", messages=messages_) else: return render_template("login.html") From 3c9c3dc0e74cde12371dcae55c74cafa7040eab9 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr <joris.gutjahr@protonmail.com> Date: Mon, 24 Feb 2020 14:41:57 +0100 Subject: [PATCH 05/54] Added upload_time as a simple increasing integer on every new message. --- Server/Users.py | 9 +++------ main.py | 4 ++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Server/Users.py b/Server/Users.py index ba7349f3..e250aba7 100644 --- a/Server/Users.py +++ b/Server/Users.py @@ -129,9 +129,7 @@ def encrypt(message,keys): """ Publishing a message works in these steps: -1. Encrypt/Sign the message -2. Publish it in demnet.messages -3. Add a notification to the recipient's feed. +1. Publish it in demnet.messages Parameters: - *message* message document with unencrypted body - *keys* private and public keys of the author @@ -140,13 +138,12 @@ def encrypt(message,keys): False if not """ def publish(message, keys): - # 1. Encrypt/Sign the message - body = encrypt(message, keys) if body != False: # 2. Publish it in demnet.messages + messages = db.messages message['body'] = body message['hash'] = SHA256.new(json.dumps(body).encode('utf-8')).hexdigest() - messages = db.messages + message['upload_time'] = messages.find().sort("upload_time", pymongo.DESCENDING).limit(1)[0] + 1 # Some inconsistent (race condition) is possible, but not critical. messages.insert_one(message) # 3. Add a notification to the recipient's feed diff --git a/main.py b/main.py index 174df9a7..f9082c92 100644 --- a/main.py +++ b/main.py @@ -130,8 +130,8 @@ def vote(): @app.route("/message", methods=["POST"]) def message(): author = session.get("username") - recipients = json.loads(request.values.get('to')) - body = request.values.get('body') + recipients = "all" + body = json.loads(request.values.get('body')) keys = session.get("keys") passphrase = session.get("passphrase") From e7998ec4aba9b9c42fd72672483d41bc17678c2f Mon Sep 17 00:00:00 2001 From: Joris Gutjahr <joris.gutjahr@protonmail.com> Date: Mon, 24 Feb 2020 21:55:32 +0100 Subject: [PATCH 06/54] United GET /login and POST / into a single function - Which fixes a bug. --- main.py | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/main.py b/main.py index f9082c92..d5275c9a 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 from Server import Elections, Patches, Users -from Server.Crypto import Storage from flask import Flask, request, render_template import pymongo from pymongo import MongoClient @@ -37,15 +36,34 @@ def index(): messages = messages.find().sort('upload_time', pymongo.DESCENDING) messages_ = [] for message in messages: - messages_.append({ "title" : message["title"], "hash" : message["hash"] } } ) + messages_.append({ "title" : message["title"], "hash" : message["hash"] }) return render_template("index.html", messages=messages_) else: - return render_template("login.html") + return redirect("/login") -@app.route("/login", methods=["GET"]) +@app.route("/login", methods=["GET", "POST"]) def login(): - return render_template("login.html") + if request.method == "GET": + return render_template("login.html") + elif request.method == "POST": + username = request.values.get("username") + password = request.values.get("password") + + if not session.get("keys") and username and password: + sha3_256 = SHA3_256.new() + sha3_256.update(password.encode('utf-8')) + passphrase = sha3_256.hexdigest() + keys = Users.login(username,passphrase) + if not keys: + return invalidData + else: + session["keys"] = keys + session["SHA3-256_passphrase"] = passphrase + session["username"] = username + return ok + else: + return invalidContext """ Returns the readings-index.html template @@ -85,23 +103,7 @@ def read(reading_hash): @app.route("/login",methods=["POST"]) def login(): - username = request.values.get("username") - password = request.values.get("password") - - if not session.get("keys") and username and password: - sha3_256 = SHA3_256.new() - sha3_256.update(password.encode('utf-8')) - passphrase = sha3_256.hexdigest() - keys = Users.login(username,passphrase) - if not keys: - return invalidData - else: - session["keys"] = keys - session["SHA3-256_passphrase"] = passphrase - session["username"] = username - return ok - else: - return invalidContext + ################################################################### ############################ CRITICAL ############################# From 62f35910522432d1673e8986e3bf221e7fa3284e Mon Sep 17 00:00:00 2001 From: Joris Gutjahr <joris.gutjahr@protonmail.com> Date: Mon, 24 Feb 2020 21:56:39 +0100 Subject: [PATCH 07/54] Fixed syntax bug. --- main.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/main.py b/main.py index d5275c9a..d84e0be7 100644 --- a/main.py +++ b/main.py @@ -100,11 +100,6 @@ def read(reading_hash): else: return notLoggedIn - -@app.route("/login",methods=["POST"]) -def login(): - - ################################################################### ############################ CRITICAL ############################# ################################################################### From 0326b8d572391b8a695d942825ef0ec77c400728 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr <joris.gutjahr@protonmail.com> Date: Mon, 24 Feb 2020 21:58:16 +0100 Subject: [PATCH 08/54] Import session --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index d84e0be7..f4b81003 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 from Server import Elections, Patches, Users -from flask import Flask, request, render_template +from flask import Flask, request, render_template, session import pymongo from pymongo import MongoClient import json, os From a8a070aa1dfab2f8f95dfd23e301ea4623205ca6 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr <joris.gutjahr@protonmail.com> Date: Mon, 24 Feb 2020 21:59:04 +0100 Subject: [PATCH 09/54] Imported redirect. --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index f4b81003..976b0476 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 from Server import Elections, Patches, Users -from flask import Flask, request, render_template, session +from flask import Flask, request, render_template, session, redirect import pymongo from pymongo import MongoClient import json, os From e371f9c8d868d6df177ac87cfca737d583883f68 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr <joris.gutjahr@protonmail.com> Date: Mon, 24 Feb 2020 22:00:13 +0100 Subject: [PATCH 10/54] Turned response codes into strings. --- main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index 976b0476..f10282c7 100644 --- a/main.py +++ b/main.py @@ -14,10 +14,10 @@ app.secret_key = os.environ["SECRET_KEY"] # Errors -ok = 0 -invalidData = 1 -invalidContext = 2 -notLoggedIn = 3 +ok = "0" +invalidData = "1" +invalidContext = "2" +notLoggedIn = "3" """ Returns either the login.html From b68d2ad59359a92116457258df5a29e5dfd8290a Mon Sep 17 00:00:00 2001 From: Joris Gutjahr <joris.gutjahr@protonmail.com> Date: Mon, 24 Feb 2020 22:01:39 +0100 Subject: [PATCH 11/54] If invalid username or password, redirect to login. --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index f10282c7..0e887b98 100644 --- a/main.py +++ b/main.py @@ -63,7 +63,7 @@ def login(): session["username"] = username return ok else: - return invalidContext + return redirect("/login") """ Returns the readings-index.html template From 55a431b1edce77ad84d543f3b6f63888343b9e9e Mon Sep 17 00:00:00 2001 From: Joris Gutjahr <joris.gutjahr@protonmail.com> Date: Tue, 25 Feb 2020 14:53:09 +0100 Subject: [PATCH 12/54] Using try except else to catch errors. --- main.py | 69 +++++++++++++++++++++++++++++++-------------------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/main.py b/main.py index 0e887b98..97676169 100644 --- a/main.py +++ b/main.py @@ -14,10 +14,11 @@ app.secret_key = os.environ["SECRET_KEY"] # Errors -ok = "0" -invalidData = "1" -invalidContext = "2" -notLoggedIn = "3" +ok = "0" +catch_all_error = "1" +invalid_data = "2" +invalid_context = "3" +not_logged_in = "4" """ Returns either the login.html @@ -106,39 +107,45 @@ def read(reading_hash): @app.route("/vote", methods=["POST"]) def vote(): - # Stop Logging Temporarily to anything but errors - app.logger.setLevel(100) # Higher then **CRITICAL** logs must be send, for them to be logged - - username = session.get("username") - election = request.values.get('election') - vote = request.values.get('vote') - - if username and election and vote: - Elections.vote(election, vote, username) # After this function is called, nobody has any knowledge of the association between user and vote. - - app.logger.setLevel(0) # The crucial unnoticable part has past. - # Not even the client is notified, if there was anything wrong, except if they get a timeout. - return ok + try: + app.logger.setLevel(100) + + username = session.get("username") + election = request.values.get('election') + vote = request.values.get('vote') + + if username and election and vote: + # After this function is called, nobody has any knowledge of the association between user and vote. + Elections.vote(election, vote, username) + + app.logger.setLevel(0) # The crucial unnoticable part has past. + # Not even the client is notified, if there was anything wrong, except if they get a timeout. + return ok + except Exception as e: + raise e + finally: + return ok ################################################################### -############################ CRITICAL OVER ######################## +############################ /CRITICAL ############################ ################################################################### @app.route("/message", methods=["POST"]) def message(): - author = session.get("username") - recipients = "all" - body = json.loads(request.values.get('body')) - keys = session.get("keys") - passphrase = session.get("passphrase") - - if author and recipients and body and keys: - message = { "body" : body - , "to" : recipients - , "from" : author - } + try: + author = session["username"] + body = json.loads(request.values["body"]) + keys = session["keys"] + + message = { "body" : body + , "from" : author + } + Users.publish( message, keys ) - return ok + except KeyError: + return invalidData + invalidContext + except: + return catch_all_error else: - return invalidData + return ok From 4bfc7e192a7a6696f98a1469da3e15d374b95504 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr <joris.gutjahr@protonmail.com> Date: Tue, 25 Feb 2020 15:01:35 +0100 Subject: [PATCH 13/54] Removed restrictions on /read. Added try catch error handleing.. --- main.py | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/main.py b/main.py index 97676169..0189462d 100644 --- a/main.py +++ b/main.py @@ -84,7 +84,7 @@ def readings(): return render_template("readings-index.html", readings=readings) else: - return notLoggedIn + return not_logged_in """ Returns the reading.html template with argument: @@ -92,14 +92,11 @@ def readings(): """ @app.route("/read/<reading_hash>", methods=["GET"]) def read(reading_hash): - if session.get("username"): - client = MongoClient() - db = client.demnet - messages = db.messages - reading = messages.find_one({ "hash" : reading_hash }) - return render_template("reading.html", reading=reading) - else: - return notLoggedIn + client = MongoClient() + db = client.demnet + messages = db.messages + reading = messages.find_one({ "hash" : reading_hash }) + return render_template("reading.html", reading=reading) ################################################################### ############################ CRITICAL ############################# @@ -110,20 +107,15 @@ def vote(): try: app.logger.setLevel(100) - username = session.get("username") - election = request.values.get('election') - vote = request.values.get('vote') - - if username and election and vote: - # After this function is called, nobody has any knowledge of the association between user and vote. - Elections.vote(election, vote, username) + username = session["username"] + election = request.values['election'] + vote = request.values['vote'] - app.logger.setLevel(0) # The crucial unnoticable part has past. - # Not even the client is notified, if there was anything wrong, except if they get a timeout. - return ok + Elections.vote(election, vote, username) + app.logger.setLevel(0) except Exception as e: raise e - finally: + else: return ok ################################################################### From fdf9ba5aa6976e7d026bef4c5160a90f7c7b3168 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr <joris.gutjahr@protonmail.com> Date: Tue, 25 Feb 2020 15:35:36 +0100 Subject: [PATCH 14/54] =?UTF-8?q?Made=20Feed=20publicly=20accessible=20and?= =?UTF-8?q?=20used=20try=20=E2=80=A6=20catch=20=E2=80=A6=20else.=20Added?= =?UTF-8?q?=20errors.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 77 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 45 insertions(+), 32 deletions(-) diff --git a/main.py b/main.py index 0189462d..f7c5a9ba 100644 --- a/main.py +++ b/main.py @@ -14,11 +14,12 @@ app.secret_key = os.environ["SECRET_KEY"] # Errors -ok = "0" -catch_all_error = "1" -invalid_data = "2" -invalid_context = "3" -not_logged_in = "4" +ok = "0" +error_for_unknown_reason = "1" +error_but_not = "2" +invalid_data = "3" +invalid_context = "4" +not_logged_in = "5" """ Returns either the login.html @@ -30,7 +31,7 @@ """ @app.route("/", methods=["GET"]) def index(): - if session.get("username"): + try: client = MongoClient() db = client.demnet messages = db.messages @@ -39,32 +40,40 @@ def index(): for message in messages: messages_.append({ "title" : message["title"], "hash" : message["hash"] }) - return render_template("index.html", messages=messages_) + response = render_template ( "index.html" + , messages = messages_ + , logged_in = session.get("username") != None + ) + + except: + return error_for_unknown_reason else: - return redirect("/login") + return response + @app.route("/login", methods=["GET", "POST"]) def login(): - if request.method == "GET": - return render_template("login.html") - elif request.method == "POST": - username = request.values.get("username") - password = request.values.get("password") - - if not session.get("keys") and username and password: - sha3_256 = SHA3_256.new() + try: + if request.method == "GET": + return render_template("login.html") + elif request.method == "POST": + username = request.values["username"] + password = request.values["password"] + + sha3_256 = SHA3_256.new() sha3_256.update(password.encode('utf-8')) - passphrase = sha3_256.hexdigest() - keys = Users.login(username,passphrase) - if not keys: - return invalidData - else: - session["keys"] = keys - session["SHA3-256_passphrase"] = passphrase - session["username"] = username - return ok - else: - return redirect("/login") + passphrase = sha3_256.hexdigest() + keys = Users.login( username, passphrase ) + session["keys"] = keys + session["username"] = username + response = redirect("/") + + except KeyError: + return invalid_data + except: + return error_for_unknown_reason + else: + return response """ Returns the readings-index.html template @@ -74,17 +83,21 @@ def login(): """ @app.route("/readings", methods=["GET"]) def readings(): - if session.get("username"): + try: client = MongoClient() db = client.demnet messages = db.messages - readings = users.find_one({ "username" : session["username" ] })["readings"] + readings = users.find_one({ "username" : session["username"] })["readings"] readings = [messages.find_one({ "hash" : reading }) for reading in readings] readings = [(reading["body"]["title"], reading["hash"]) for reading in readings] - - return render_template("readings-index.html", readings=readings) - else: + response = render_template("readings-index.html", readings=readings) + except KeyError: return not_logged_in + except: + return error_but_not + not_logged_in + else: + return response + """ Returns the reading.html template with argument: From 3a97a09f0a6ccdac9e08405e6cf723e289eb6826 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr <joris.gutjahr@protonmail.com> Date: Tue, 25 Feb 2020 16:21:11 +0100 Subject: [PATCH 15/54] Made errors into a dictionary, that can easily be transformed using dict operations. This is used to enable a debug mode, where instead of numeric error codes, strings are used. --- main.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/main.py b/main.py index f7c5a9ba..0874ddb6 100644 --- a/main.py +++ b/main.py @@ -14,12 +14,17 @@ app.secret_key = os.environ["SECRET_KEY"] # Errors -ok = "0" -error_for_unknown_reason = "1" -error_but_not = "2" -invalid_data = "3" -invalid_context = "4" -not_logged_in = "5" +debug = os.environ.get("DEBUG") +errors = { "OK" : "0" + , "error_for_unknown_reason" : "1" + , "error_but_not" : "2" + , "invalid_data" : "3" + , "invalid_context" : "4" + , "not_logged_in" : "5" + } + +errors = { key : errors[key] if not debug else key for key in errors } + """ Returns either the login.html @@ -36,12 +41,14 @@ def index(): db = client.demnet messages = db.messages messages = messages.find().sort('upload_time', pymongo.DESCENDING) - messages_ = [] - for message in messages: - messages_.append({ "title" : message["title"], "hash" : message["hash"] }) + messages = list(map( lambda m: { "title" : m["title"] + , "hash" : m["hash"] } + , list(messages) + ) + ) response = render_template ( "index.html" - , messages = messages_ + , messages = messages , logged_in = session.get("username") != None ) From 5d8bc7c869a54574d9dde82e1691cda692536edf Mon Sep 17 00:00:00 2001 From: Joris Gutjahr <joris.gutjahr@protonmail.com> Date: Tue, 25 Feb 2020 16:26:55 +0100 Subject: [PATCH 16/54] Using errors dictionary. --- main.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/main.py b/main.py index 0874ddb6..66796c7e 100644 --- a/main.py +++ b/main.py @@ -53,7 +53,7 @@ def index(): ) except: - return error_for_unknown_reason + return errors["error_for_unknown_reason"] else: return response @@ -76,9 +76,9 @@ def login(): response = redirect("/") except KeyError: - return invalid_data + return errors["invalid_data"] except: - return error_for_unknown_reason + return errors["error_for_unknown_reason"] else: return response @@ -99,9 +99,9 @@ def readings(): readings = [(reading["body"]["title"], reading["hash"]) for reading in readings] response = render_template("readings-index.html", readings=readings) except KeyError: - return not_logged_in + return errors["not_logged_in"] except: - return error_but_not + not_logged_in + return errors["error_but_not"] + errors["not_logged_in"] else: return response @@ -112,11 +112,16 @@ def readings(): """ @app.route("/read/<reading_hash>", methods=["GET"]) def read(reading_hash): - client = MongoClient() - db = client.demnet - messages = db.messages - reading = messages.find_one({ "hash" : reading_hash }) - return render_template("reading.html", reading=reading) + try: + client = MongoClient() + db = client.demnet + messages = db.messages + reading = messages.find_one({ "hash" : reading_hash }) + response = render_template("reading.html", reading=reading) + except: + return errors["error_for_unknown_reason"] + else: + return response ################################################################### ############################ CRITICAL ############################# @@ -127,16 +132,20 @@ def vote(): try: app.logger.setLevel(100) + if session.get("username"): + raise "invalid_context" username = session["username"] election = request.values['election'] vote = request.values['vote'] Elections.vote(election, vote, username) app.logger.setLevel(0) - except Exception as e: - raise e + except "invalid_context": + return errors["invalid_context"] + except KeyError: + return errors["invalid_data"] else: - return ok + return errors["OK"] ################################################################### ############################ /CRITICAL ############################ From ec7a4586f8fc931fe94436a778971bc728d0492c Mon Sep 17 00:00:00 2001 From: Joris Gutjahr <joris.gutjahr@protonmail.com> Date: Tue, 25 Feb 2020 16:31:43 +0100 Subject: [PATCH 17/54] Consistent use of errors and good raising of errors. --- main.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 66796c7e..68f6f70d 100644 --- a/main.py +++ b/main.py @@ -154,6 +154,9 @@ def vote(): @app.route("/message", methods=["POST"]) def message(): try: + if not session.get("username") or not session.get("keys"): + raise "invalid_context" + author = session["username"] body = json.loads(request.values["body"]) keys = session["keys"] @@ -165,8 +168,10 @@ def message(): Users.publish( message, keys ) except KeyError: - return invalidData + invalidContext + return errors["invalidData"] + except "invalid_context": + return errors["invalid_context"] except: - return catch_all_error + return errors["error_for_unknown_reason"] else: - return ok + return errors["OK"] From 0b2a94dc244649efbdaf2d362c7ce8d3c3fc3b41 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr <joris.gutjahr@protonmail.com> Date: Tue, 25 Feb 2020 17:01:14 +0100 Subject: [PATCH 18/54] Using sorting functions in python instead of mongodb ones. And raising exceptions if the error is unknown. --- main.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/main.py b/main.py index 68f6f70d..18f6854c 100644 --- a/main.py +++ b/main.py @@ -40,8 +40,8 @@ def index(): client = MongoClient() db = client.demnet messages = db.messages - messages = messages.find().sort('upload_time', pymongo.DESCENDING) - messages = list(map( lambda m: { "title" : m["title"] + messages = list(messages.find()).sort(key=lambda m: m["upload_time"], reverse=True) + messages = list(map( lambda m: { "title" : m["body"]["title"] , "hash" : m["hash"] } , list(messages) ) @@ -52,8 +52,8 @@ def index(): , logged_in = session.get("username") != None ) - except: - return errors["error_for_unknown_reason"] + except Exception as e: + raise e else: return response @@ -77,8 +77,8 @@ def login(): except KeyError: return errors["invalid_data"] - except: - return errors["error_for_unknown_reason"] + except Exception as e: + raise e else: return response @@ -118,8 +118,8 @@ def read(reading_hash): messages = db.messages reading = messages.find_one({ "hash" : reading_hash }) response = render_template("reading.html", reading=reading) - except: - return errors["error_for_unknown_reason"] + except Exception as e: + raise e else: return response @@ -171,7 +171,7 @@ def message(): return errors["invalidData"] except "invalid_context": return errors["invalid_context"] - except: - return errors["error_for_unknown_reason"] + except Exception as e: + raise e else: return errors["OK"] From ff488366a6778f84d99a07f261f0bf08bf4f95ca Mon Sep 17 00:00:00 2001 From: Joris Gutjahr <joris.gutjahr@protonmail.com> Date: Tue, 25 Feb 2020 18:10:31 +0100 Subject: [PATCH 19/54] global client and collection objects. Moved output/home.html -> output/index.html and output/home.css -> output/static/index.css --- main.py | 33 +- output/home.html | 24 - output/index.html | 10345 +----------------------- output/{home.css => static/index.css} | 0 4 files changed, 38 insertions(+), 10364 deletions(-) delete mode 100644 output/home.html rename output/{home.css => static/index.css} (100%) diff --git a/main.py b/main.py index 18f6854c..37f8cb0a 100644 --- a/main.py +++ b/main.py @@ -13,6 +13,10 @@ , template_folder="output") app.secret_key = os.environ["SECRET_KEY"] +client = MongoClient() +db = client.demnet +messages = db.messages +users = db.users # Errors debug = os.environ.get("DEBUG") errors = { "OK" : "0" @@ -37,18 +41,15 @@ @app.route("/", methods=["GET"]) def index(): try: - client = MongoClient() - db = client.demnet - messages = db.messages - messages = list(messages.find()).sort(key=lambda m: m["upload_time"], reverse=True) - messages = list(map( lambda m: { "title" : m["body"]["title"] - , "hash" : m["hash"] } - , list(messages) - ) - ) - + sorted_messages = sorted(list(messages.find({})),key=lambda m: m["upload_time"], reverse=True) + sorted_messages = list(map( lambda m: { "title" : m["body"]["title"] + , "hash" : m["hash"] } + , sorted_messages + ) + ) + print(sorted_messages) response = render_template ( "index.html" - , messages = messages + , messages = sorted_messages , logged_in = session.get("username") != None ) @@ -91,17 +92,14 @@ def login(): @app.route("/readings", methods=["GET"]) def readings(): try: - client = MongoClient() - db = client.demnet - messages = db.messages readings = users.find_one({ "username" : session["username"] })["readings"] readings = [messages.find_one({ "hash" : reading }) for reading in readings] readings = [(reading["body"]["title"], reading["hash"]) for reading in readings] response = render_template("readings-index.html", readings=readings) except KeyError: return errors["not_logged_in"] - except: - return errors["error_but_not"] + errors["not_logged_in"] + except Exception as e: + raise e else: return response @@ -113,9 +111,6 @@ def readings(): @app.route("/read/<reading_hash>", methods=["GET"]) def read(reading_hash): try: - client = MongoClient() - db = client.demnet - messages = db.messages reading = messages.find_one({ "hash" : reading_hash }) response = render_template("reading.html", reading=reading) except Exception as e: diff --git a/output/home.html b/output/home.html deleted file mode 100644 index 4dc1a01e..00000000 --- a/output/home.html +++ /dev/null @@ -1,24 +0,0 @@ -<!DOCTYPE html> -<html lang="en" dir="ltr"> - <head> - <meta charset="utf-8"> - <title>DemNet - - - -
-

| Die post und vorschläge anderer personen die am meisten gelikt wurden |

-
- - - - - - - - diff --git a/output/index.html b/output/index.html index 9c331473..59cbf4b0 100644 --- a/output/index.html +++ b/output/index.html @@ -1,10321 +1,24 @@ - - - - - Main - - - - - -

-
-
-
-
-
\ No newline at end of file
+
+
+  
+    
+    DemNet
+    
+  
+  
+     
+

| Die post und vorschläge anderer personen die am meisten gelikt wurden |

+
+ + + + + + + + diff --git a/output/home.css b/output/static/index.css similarity index 100% rename from output/home.css rename to output/static/index.css From c5f92b79c2667e0b6ce87219e5c63be315d57130 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Tue, 25 Feb 2020 18:11:36 +0100 Subject: [PATCH 20/54] Removed no longer needed parts of Users.publish --- Server/Users.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Server/Users.py b/Server/Users.py index e250aba7..8fec0e62 100644 --- a/Server/Users.py +++ b/Server/Users.py @@ -12,6 +12,7 @@ from Crypto.Hash import SHA256 from Crypto.Cipher import AES from pymongo import MongoClient +import pymongo import datetime, sys, json client = MongoClient() @@ -137,19 +138,13 @@ def encrypt(message,keys): True if successfull False if not """ -def publish(message, keys): - if body != False: +def publish(message): + if message['body'] != False: # 2. Publish it in demnet.messages messages = db.messages - message['body'] = body - message['hash'] = SHA256.new(json.dumps(body).encode('utf-8')).hexdigest() - message['upload_time'] = messages.find().sort("upload_time", pymongo.DESCENDING).limit(1)[0] + 1 # Some inconsistent (race condition) is possible, but not critical. + message['hash'] = SHA256.new(json.dumps(message['body']).encode('utf-8')).hexdigest() + message['upload_time'] = messages.find().sort("upload_time", pymongo.DESCENDING).limit(1)[0]["upload_time"] + 1 # Some inconsistent (race condition) is possible, but not critical. messages.insert_one(message) - # 3. Add a notification to the recipient's feed - for recipient_name in message['to']: - users.update_one({ "username" : recipient_name } - , { "$push" : { "feed" : message["hash"] } } - ) return True return False From cdd7240dc4fe0d561ddcccf6dac74b79cde0dcc5 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Tue, 25 Feb 2020 18:34:39 +0100 Subject: [PATCH 21/54] Added writings route. --- main.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/main.py b/main.py index 37f8cb0a..77a9ff0b 100644 --- a/main.py +++ b/main.py @@ -17,6 +17,7 @@ db = client.demnet messages = db.messages users = db.users + # Errors debug = os.environ.get("DEBUG") errors = { "OK" : "0" @@ -30,14 +31,7 @@ errors = { key : errors[key] if not debug else key for key in errors } -""" -Returns either the login.html -or feed. -If the user is logged in, returns feed -sorted by most recent uploads as -dict of "title" and "hash". -So you can use /read/ to get to reading that upload. -""" + @app.route("/", methods=["GET"]) def index(): try: @@ -83,12 +77,7 @@ def login(): else: return response -""" -Returns the readings-index.html template -with template argument: - readings : List[Tuple[title,hash of reading]] -""" @app.route("/readings", methods=["GET"]) def readings(): try: @@ -103,11 +92,20 @@ def readings(): else: return response -""" -Returns the reading.html template -with argument: - reading, the full message. -""" +@app.route("/writings", methods=["GET"]) +def writings(): + try: + writings = users.find_one({ "username" : session["username"] })["writings"] + writings = [messages.find_one({ "hash" : writing }) for writing in writings] + writings = [(writing["body"]["title"],writing["hash"]) for writing in writings] + response = render_template("writings-index.html", writings=writings) + except KeyError: + return errors["not_logged_in"] + except Exception as e: + raise e + else: + return response + @app.route("/read/", methods=["GET"]) def read(reading_hash): try: From 001e1c1a6df9c1348a58ca8bcc003d459883e01e Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Wed, 26 Feb 2020 09:08:21 +0100 Subject: [PATCH 22/54] GET /vote GET /write/ Implemented. --- main.py | 55 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/main.py b/main.py index 77a9ff0b..0469df7f 100644 --- a/main.py +++ b/main.py @@ -17,6 +17,7 @@ db = client.demnet messages = db.messages users = db.users +elections = db.elections # Errors debug = os.environ.get("DEBUG") @@ -26,6 +27,7 @@ , "invalid_data" : "3" , "invalid_context" : "4" , "not_logged_in" : "5" + , "invalid_user" : "6" } errors = { key : errors[key] if not debug else key for key in errors } @@ -35,13 +37,15 @@ @app.route("/", methods=["GET"]) def index(): try: - sorted_messages = sorted(list(messages.find({})),key=lambda m: m["upload_time"], reverse=True) + messages_count = 10 + sorted_messages = list(messages.find({})) + upload_time_cut = max(sorted_messages,key=lambda m: m["upload_time"])["upload_time"] - messages_count + sorted_messages = list(filter(lambda m: m["upload_time"] >= upload_time_cut, sorted_messages)) sorted_messages = list(map( lambda m: { "title" : m["body"]["title"] , "hash" : m["hash"] } , sorted_messages ) ) - print(sorted_messages) response = render_template ( "index.html" , messages = sorted_messages , logged_in = session.get("username") != None @@ -110,35 +114,56 @@ def writings(): def read(reading_hash): try: reading = messages.find_one({ "hash" : reading_hash }) - response = render_template("reading.html", reading=reading) + response = render_template("read.html", reading=reading) except Exception as e: raise e else: return response +@app.route("/write/", methods=["GET"]) +def write(): + try: + writing = messages.find_one({ "hash" : writing_hash }) + + if session["username"] == writing["author"]: + response = render_template("write.html", writing=writing) + else: + response = errors["invalid_user"] + except KeyError: + return errors["invalid_context"] + else: + return response + ################################################################### ############################ CRITICAL ############################# ################################################################### -@app.route("/vote", methods=["POST"]) +@app.route("/vote", methods=["POST", "GET"]) def vote(): try: - app.logger.setLevel(100) - - if session.get("username"): - raise "invalid_context" - username = session["username"] - election = request.values['election'] - vote = request.values['vote'] - - Elections.vote(election, vote, username) - app.logger.setLevel(0) + if request.method == "GET": + election = request.values['hash'] + election = elections.find_one({ "hash" : election }) + election = { key : election[key] for key in election if (key == "title" or key == "options") } + response = render_template("vote.html", election=election) + else: + app.logger.setLevel(100) + + if session.get("username"): + raise "invalid_context" + username = session["username"] + hash = request.values['hash'] + vote = request.values['vote'] + + Elections.vote(election, vote, username) + app.logger.setLevel(0) + response = errors["OK"] except "invalid_context": return errors["invalid_context"] except KeyError: return errors["invalid_data"] else: - return errors["OK"] + return response ################################################################### ############################ /CRITICAL ############################ From 6212e56a44922d4da235030cda76d7c6171d143d Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Wed, 26 Feb 2020 09:16:39 +0100 Subject: [PATCH 23/54] Adding Exception classes. Handling TypError in GET /vote. --- main.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 0469df7f..56f39cbe 100644 --- a/main.py +++ b/main.py @@ -32,6 +32,10 @@ errors = { key : errors[key] if not debug else key for key in errors } +class Error_for_unknown_reason (Exception): pass +class Invalid_Context (Exception): pass +class Invalid_Data (Exception): pass +class Invalid_User (Exception): pass @app.route("/", methods=["GET"]) @@ -149,8 +153,8 @@ def vote(): else: app.logger.setLevel(100) - if session.get("username"): - raise "invalid_context" + if not session.get("username"): + raise Invalid_Context() username = session["username"] hash = request.values['hash'] vote = request.values['vote'] @@ -158,10 +162,14 @@ def vote(): Elections.vote(election, vote, username) app.logger.setLevel(0) response = errors["OK"] - except "invalid_context": + except Invalid_Context: return errors["invalid_context"] except KeyError: return errors["invalid_data"] + except TypeError: + return errors["invalid_data"] + except Exception as e: + raise e else: return response From d9aeb2e47c0cf8aec2bcb6e311a7e911db047b91 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Wed, 26 Feb 2020 15:11:25 +0100 Subject: [PATCH 24/54] Simple implementation of read.html and index.html - The data is included in {{}} or {%%} and {{}} does automatic XSS protection on the server. This will have to be automized, improved and the XSS prevention moved into the browser. --- output/index.html | 6 +++--- output/read.html | 11 +++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 output/read.html diff --git a/output/index.html b/output/index.html index 59cbf4b0..8d0f9712 100644 --- a/output/index.html +++ b/output/index.html @@ -7,11 +7,11 @@
-

| Die post und vorschläge anderer personen die am meisten gelikt wurden |

+ {% for message in messages %} +

{{ message.title }}

+ {% endfor %}
- -
  • Feed
  • Vote
  • diff --git a/output/read.html b/output/read.html new file mode 100644 index 00000000..65b4156e --- /dev/null +++ b/output/read.html @@ -0,0 +1,11 @@ + + + + + DemNet - {{ reading.body.title }} + + +

    {{ reading.body.title }}

    +

    {{ reading.body.content }}

    + + From 965c311147ecdfe2206e808d06246b187905699e Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Wed, 26 Feb 2020 17:07:58 +0100 Subject: [PATCH 25/54] Trying to implement register() function and changing date format to iso. Using datetime.date.fromisoformat and isoformat. --- main.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 56f39cbe..8fa2ea85 100644 --- a/main.py +++ b/main.py @@ -5,7 +5,8 @@ import pymongo from pymongo import MongoClient import json, os -from Crypto.Hash import SHA3_256 +from Crypto.Hash import SHA3_256, SHA256 +from Crypto.PublicKey import RSA app = Flask( __name__ , static_url_path="/static" @@ -201,3 +202,28 @@ def message(): raise e else: return errors["OK"] + +# REGISTRATION + +def register(username : str + ,id_token : str + , passwords : List[str] + , first_name : str + , last_name : str + ): + id = SHA256.new(id_token.encode("utf-8")).hexdigest() + passwords = [SHA256.new(password.encode("utf-8")) for password in passwords] + keys = RSA.generate(2048) + private_keys = [keys.export_key(passphrase=password) for password in passwords] + public_key = keys.publickey().export_key() + user = { "username" : username + , "id" : id + , "passwords" : passwords + , "first_name" : first_name + , "last_name" : last_name + , "public_key" : public_key + , "private_keys" : private_keys + , "readings" : [] + , "writings" : [] + , "expiration" : + } From 4baa2aface770e219d6b0ecf3faf95cbbc2552a0 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Wed, 26 Feb 2020 17:08:41 +0100 Subject: [PATCH 26/54] Trying to implement register() function and changing date format to iso. Using datetime.date.fromisoformat and isoformat. --- Database.md | 8 +++++--- Server/Users.py | 7 ++++--- main.py | 28 +++++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/Database.md b/Database.md index 9c32e090..df0a6d30 100644 --- a/Database.md +++ b/Database.md @@ -97,11 +97,13 @@ A Law Document looks like this: ``` # users -For every Registered user there is a -Public and Private PGP Key (GnuPG): +For every Registered user there is a private and public key. +The private key is encrypted with each of the users three passwords. +This makes it possible to use private keys with each of them ```json { "public_key" : "" -, "private_key" : "" +, "private_keys" : [""] +, "passwords" : [""] , "username" : "" , "first_name" : "" , "last_name" : "" diff --git a/Server/Users.py b/Server/Users.py index 8fec0e62..792f85e3 100644 --- a/Server/Users.py +++ b/Server/Users.py @@ -32,7 +32,7 @@ def login(username, passphrase): if keys.publickey().export_key(format="PEM") != user["public_key"]: users.update_one({ "username" : username }, { "$set" : { "public_key" : keys.publickey().export_key(format="PEM") } } ) - if datetime.datetime.strptime(user["expiration"], "%m/%d/%Y") > datetime.datetime.now(): + if datetime.date.fromisoformat(user["expiration"]) > datetime.datetime.now(): new_keys = RSA.generate(2048) new_expiration = datetime.timedelta (weeks=104 @@ -44,8 +44,9 @@ def login(username, passphrase): ,microseconds=0 ) + datetime.datetime.now() - private_key = new_keys.export_key(format="PEM", passphrase=passphrase) - public_key = new_keys.publickey().export_key(format="PEM") + new_expiration = new_expiration.isoformat() + private_key = new_keys.export_key(format="PEM", passphrase=passphrase) + public_key = new_keys.publickey().export_key(format="PEM") users.update_one( { "username" : username } diff --git a/main.py b/main.py index 56f39cbe..8fa2ea85 100644 --- a/main.py +++ b/main.py @@ -5,7 +5,8 @@ import pymongo from pymongo import MongoClient import json, os -from Crypto.Hash import SHA3_256 +from Crypto.Hash import SHA3_256, SHA256 +from Crypto.PublicKey import RSA app = Flask( __name__ , static_url_path="/static" @@ -201,3 +202,28 @@ def message(): raise e else: return errors["OK"] + +# REGISTRATION + +def register(username : str + ,id_token : str + , passwords : List[str] + , first_name : str + , last_name : str + ): + id = SHA256.new(id_token.encode("utf-8")).hexdigest() + passwords = [SHA256.new(password.encode("utf-8")) for password in passwords] + keys = RSA.generate(2048) + private_keys = [keys.export_key(passphrase=password) for password in passwords] + public_key = keys.publickey().export_key() + user = { "username" : username + , "id" : id + , "passwords" : passwords + , "first_name" : first_name + , "last_name" : last_name + , "public_key" : public_key + , "private_keys" : private_keys + , "readings" : [] + , "writings" : [] + , "expiration" : + } From b2d080a2024c4bb15b234d7ec47d65fbb159775d Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Wed, 26 Feb 2020 18:08:37 +0100 Subject: [PATCH 27/54] Added implementation of Cryptographic functions, encrypt, sign and verify in main.py. --- main.py | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/main.py b/main.py index 8fa2ea85..ba5c2921 100644 --- a/main.py +++ b/main.py @@ -7,6 +7,7 @@ import json, os from Crypto.Hash import SHA3_256, SHA256 from Crypto.PublicKey import RSA +from typing import List app = Flask( __name__ , static_url_path="/static" @@ -205,11 +206,11 @@ def message(): # REGISTRATION -def register(username : str - ,id_token : str - , passwords : List[str] - , first_name : str - , last_name : str +def register(username : str + ,id_token : str + , passwords : List[str] + , first_name : str + , last_name : str ): id = SHA256.new(id_token.encode("utf-8")).hexdigest() passwords = [SHA256.new(password.encode("utf-8")) for password in passwords] @@ -225,5 +226,53 @@ def register(username : str , "private_keys" : private_keys , "readings" : [] , "writings" : [] - , "expiration" : + , "expiration" : (datetime.timedelta (weeks=104 + ,days=0 + ,hours=0 + ,minutes=0 + ,seconds=0 + ,milliseconds=0 + ,microseconds=0 + ) + datetime.datetime.now()).isoformat() } + +# CRYPTOGRAPHY +from Crypto.Cipher import AES, PKCS1_OAEP +from Crypto.Random import get_random_bytes +from Crypto.Signature import pkcs1_15 + +"""Encrypt string with AES Key and encrypt them with recipients public key. +Returns : (Signed encryption key, AES Nonce, tag, ciphertext) +""" +def encrypt(message : str, recipient_keys : RSA.RsaKey): + + # Create and encrypt AES Key + aes_session_key = get_random_bytes(16) + cipher_rsa = PKCS1_OAEP.new(recipient_keys) + enc_session_key = cipher_rsa.encrypt(aes_session_key) + + # Encrypt using previously generated AES Key. + cipher_aes = AES.new(aes_session_key, AES.MODE_EAX) + ciphertext, tag = cipher_aes.encrypt_and_digest(message.encode("utf-8")) + + return (enc_session_key, cipher_aes.nonce, tag, ciphertext) + +"""Return signed hash of a string with private key. +Returns : (signature : bytes, hash : SHA256) +""" +def sign(message : str, author_key : RSA.RsaKey): + hash = SHA256.new(message.encode("utf-8")).hexdigest() + signature = pkcs1_15.new(author_key).sign(hash) + return (signature, hash) + +"""Verify a message signed by sign(). +""" +def verify(signature : bytes, hash : SHA256, author_public_key : RSA.RsaKey): + try: + pkcs1_15.new(author_public_key).verify(hash, signature) + except ValueError: + return False + except Exception as e: + raise e + else: + return True From 8faff0100d7af41998358536b8336a85e7833f5b Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Wed, 26 Feb 2020 19:08:02 +0100 Subject: [PATCH 28/54] Exception handeling in Crypto. --- main.py | 84 ++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 50 insertions(+), 34 deletions(-) diff --git a/main.py b/main.py index ba5c2921..74466157 100644 --- a/main.py +++ b/main.py @@ -38,7 +38,7 @@ class Error_for_unknown_reason (Exception): pass class Invalid_Context (Exception): pass class Invalid_Data (Exception): pass class Invalid_User (Exception): pass - +class Already_Registered (Exception): pass @app.route("/", methods=["GET"]) def index(): @@ -212,29 +212,41 @@ def register(username : str , first_name : str , last_name : str ): - id = SHA256.new(id_token.encode("utf-8")).hexdigest() - passwords = [SHA256.new(password.encode("utf-8")) for password in passwords] - keys = RSA.generate(2048) - private_keys = [keys.export_key(passphrase=password) for password in passwords] - public_key = keys.publickey().export_key() - user = { "username" : username - , "id" : id - , "passwords" : passwords - , "first_name" : first_name - , "last_name" : last_name - , "public_key" : public_key - , "private_keys" : private_keys - , "readings" : [] - , "writings" : [] - , "expiration" : (datetime.timedelta (weeks=104 - ,days=0 - ,hours=0 - ,minutes=0 - ,seconds=0 - ,milliseconds=0 - ,microseconds=0 - ) + datetime.datetime.now()).isoformat() - } + try: + id = SHA256.new(id_token.encode("utf-8")).hexdigest() + passwords = [SHA256.new(password.encode("utf-8")) for password in passwords] + keys = RSA.generate(2048) + private_keys = [keys.export_key(passphrase=password) for password in passwords] + public_key = keys.publickey().export_key() + user = { "username" : username + , "id" : id + , "passwords" : passwords + , "first_name" : first_name + , "last_name" : last_name + , "public_key" : public_key + , "private_keys" : private_keys + , "readings" : [] + , "writings" : [] + , "expiration" : (datetime.timedelta (weeks=104 + ,days=0 + ,hours=0 + ,minutes=0 + ,seconds=0 + ,milliseconds=0 + ,microseconds=0 + ) + datetime.datetime.now()).isoformat() + } + + if users.find_one({ "id" : id }): + raise Already_Registered + + except Exception as e: + raise e + else: + return True + + + # CRYPTOGRAPHY from Crypto.Cipher import AES, PKCS1_OAEP @@ -245,18 +257,22 @@ def register(username : str Returns : (Signed encryption key, AES Nonce, tag, ciphertext) """ def encrypt(message : str, recipient_keys : RSA.RsaKey): + try: + # Create and encrypt AES Key + aes_session_key = get_random_bytes(16) + cipher_rsa = PKCS1_OAEP.new(recipient_keys) + enc_session_key = cipher_rsa.encrypt(aes_session_key) + + # Encrypt using previously generated AES Key. + cipher_aes = AES.new(aes_session_key, AES.MODE_EAX) + ciphertext, tag = cipher_aes.encrypt_and_digest(message.encode("utf-8")) + except Exception as e: + raise e + else: + return (enc_session_key, cipher_aes.nonce, tag, ciphertext) - # Create and encrypt AES Key - aes_session_key = get_random_bytes(16) - cipher_rsa = PKCS1_OAEP.new(recipient_keys) - enc_session_key = cipher_rsa.encrypt(aes_session_key) - - # Encrypt using previously generated AES Key. - cipher_aes = AES.new(aes_session_key, AES.MODE_EAX) - ciphertext, tag = cipher_aes.encrypt_and_digest(message.encode("utf-8")) - - return (enc_session_key, cipher_aes.nonce, tag, ciphertext) +"""Decrypt """Return signed hash of a string with private key. Returns : (signature : bytes, hash : SHA256) """ From 40fcc8e8603f2d7807440dc1ec87293c1086657e Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Wed, 26 Feb 2020 19:35:05 +0100 Subject: [PATCH 29/54] Added inverse function to encrypt, decrypt. --- main.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 74466157..369fde17 100644 --- a/main.py +++ b/main.py @@ -272,7 +272,29 @@ def encrypt(message : str, recipient_keys : RSA.RsaKey): return (enc_session_key, cipher_aes.nonce, tag, ciphertext) -"""Decrypt +"""Inverse of encrypt and takes the results of encrypt and the private key of the recipient. +""" +def decrypt (recipients_private_key : RSA.RsaKey + , enc_session_key : bytes + , nonce : bytes + , tag : bytes + , ciphertext : bytes + ): + try: + rsa_cipher = PKCS1_OAEP.new(recipients_private_key) + aes_session_key = cipher_rsa.decrypt(enc_session_key) + + cipher_aes = AES.new(aes_session_key, AES.MODE_EAX , nonce=nonce) + message = cipher_aes.decrypt_and_verify(ciphertext, tag) + + + except ValueError: + return False + except Exception as e: + raise e + else: + return message + """Return signed hash of a string with private key. Returns : (signature : bytes, hash : SHA256) """ From 19c53ce0b4b9e1af90ae0cdeaf2fef501fd618a6 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Wed, 26 Feb 2020 19:52:21 +0100 Subject: [PATCH 30/54] Added GET /register and POST /register to create a new user, which is only allowed as a user with username "joris". --- main.py | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index 369fde17..be37bcda 100644 --- a/main.py +++ b/main.py @@ -30,6 +30,7 @@ , "invalid_context" : "4" , "not_logged_in" : "5" , "invalid_user" : "6" + , "already_registered" : "7" } errors = { key : errors[key] if not debug else key for key in errors } @@ -206,8 +207,33 @@ def message(): # REGISTRATION -def register(username : str - ,id_token : str +@app.route("/register", methods=["GET","POST"]) +def register_route(): + try: + if request.method == "GET": + response = render_template("register.html") + else: + if session["username"] != "joris": + raise Invalid_Context + else: + username = request.values["username"] + id = request.values["id"] + passwords = json.loads(request.values["passwords"]) + first_name = request.values["first_name"] + last_name = request.values["last_name"] + response = register(username, id, passwords, first_name, last_name) + response = errors["OK"] if response else errors["error_for_unknown_reason"] + except Invalid_Context: + return errors["invalid_context"] + except Already_Registered: + return errors["already_registered"] + except Exception as e: + raise e + else: + return response + +def register( username : str + , id_token : str , passwords : List[str] , first_name : str , last_name : str @@ -237,9 +263,10 @@ def register(username : str ) + datetime.datetime.now()).isoformat() } - if users.find_one({ "id" : id }): + if users.find_one({ "id" : id }) or users.find_one({ "username" : username }): raise Already_Registered - + else: + users.insert_one(user) except Exception as e: raise e else: From 9d9b82e4df94db680e3c4c62635e64b6ce92ca4d Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Wed, 26 Feb 2020 20:51:06 +0100 Subject: [PATCH 31/54] Converting Hash objects to strings. --- main.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/main.py b/main.py index be37bcda..f5b417da 100644 --- a/main.py +++ b/main.py @@ -5,9 +5,10 @@ import pymongo from pymongo import MongoClient import json, os -from Crypto.Hash import SHA3_256, SHA256 +from Crypto.Hash import SHA256 from Crypto.PublicKey import RSA from typing import List +import datetime app = Flask( __name__ , static_url_path="/static" @@ -239,18 +240,13 @@ def register( username : str , last_name : str ): try: - id = SHA256.new(id_token.encode("utf-8")).hexdigest() - passwords = [SHA256.new(password.encode("utf-8")) for password in passwords] - keys = RSA.generate(2048) - private_keys = [keys.export_key(passphrase=password) for password in passwords] - public_key = keys.publickey().export_key() + id = SHA256.new(id_token.encode("utf-8")).hexdigest().encode("utf-8") + passwords = [SHA256.new(password.encode("utf-8")).hexdigest() for password in passwords] user = { "username" : username , "id" : id , "passwords" : passwords , "first_name" : first_name , "last_name" : last_name - , "public_key" : public_key - , "private_keys" : private_keys , "readings" : [] , "writings" : [] , "expiration" : (datetime.timedelta (weeks=104 From 3d22ca69c99f96d4a7ccb701d8c629a58180c925 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Wed, 26 Feb 2020 21:19:27 +0100 Subject: [PATCH 32/54] Improved output/login.html. Removed all encryption and keys for the alpha phase. --- main.py | 22 ++++++++++++++-------- output/login.html | 29 ++++++++++++++--------------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/main.py b/main.py index f5b417da..7b68d920 100644 --- a/main.py +++ b/main.py @@ -32,6 +32,8 @@ , "not_logged_in" : "5" , "invalid_user" : "6" , "already_registered" : "7" + , "no_user_with_that_name" : "8" + , "invalid_password" : "9" } errors = { key : errors[key] if not debug else key for key in errors } @@ -72,18 +74,22 @@ def login(): return render_template("login.html") elif request.method == "POST": username = request.values["username"] - password = request.values["password"] - - sha3_256 = SHA3_256.new() - sha3_256.update(password.encode('utf-8')) - passphrase = sha3_256.hexdigest() - keys = Users.login( username, passphrase ) - session["keys"] = keys - session["username"] = username + password = SHA256.new(request.values["password"].encode("utf-8")).hexdigest() + + user = users.find_one({ "username" : username }) + if user: + if password in user["passwords"]: + session["username"] = username + else: + raise Invalid_Data("invalid_password") + else: + raise Invalid_Data("no_user_with_that_name") response = redirect("/") except KeyError: return errors["invalid_data"] + except Invalid_Data as e: + return errors[e.args[0]] except Exception as e: raise e else: diff --git a/output/login.html b/output/login.html index dd9f161a..0ec83edc 100644 --- a/output/login.html +++ b/output/login.html @@ -4,23 +4,22 @@ DemNet - + -
    -
    + +

    DemNet

    Log in for your world

    - Username:
    -
    -
    - Password:
    -
    - -
    - - + + + + + + + +
    - - - + + + From 8c2e749473bd1a5fa19ee35d8b89c13da43c1d34 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Wed, 26 Feb 2020 22:11:55 +0100 Subject: [PATCH 33/54] New Error subclass. --- main.py | 56 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/main.py b/main.py index 7b68d920..274f5eb8 100644 --- a/main.py +++ b/main.py @@ -34,15 +34,15 @@ , "already_registered" : "7" , "no_user_with_that_name" : "8" , "invalid_password" : "9" + , "already_voted" : "A" } errors = { key : errors[key] if not debug else key for key in errors } -class Error_for_unknown_reason (Exception): pass -class Invalid_Context (Exception): pass -class Invalid_Data (Exception): pass -class Invalid_User (Exception): pass -class Already_Registered (Exception): pass +class Error (Exception): + def status(self): + return errors[self.args[0]] + @app.route("/", methods=["GET"]) def index(): @@ -81,15 +81,15 @@ def login(): if password in user["passwords"]: session["username"] = username else: - raise Invalid_Data("invalid_password") + raise Error("invalid_password") else: raise Invalid_Data("no_user_with_that_name") response = redirect("/") except KeyError: return errors["invalid_data"] - except Invalid_Data as e: - return errors[e.args[0]] + except Error as e: + return e.status() except Exception as e: raise e else: @@ -142,7 +142,9 @@ def write(): if session["username"] == writing["author"]: response = render_template("write.html", writing=writing) else: - response = errors["invalid_user"] + raise Error("invalid_user") + except Error as e: + return e.status() except KeyError: return errors["invalid_context"] else: @@ -164,16 +166,26 @@ def vote(): app.logger.setLevel(100) if not session.get("username"): - raise Invalid_Context() + raise Error("invalid_context") username = session["username"] hash = request.values['hash'] vote = request.values['vote'] - Elections.vote(election, vote, username) + # Don't make or use encryption for voting **yet** + election = elections.find_one({ "hash" : hash }) + if election: + if username not in election["participants"]: + election["participants"].append(username) + election["votes"].append(vote) + else: + raise Error("already_voted") + else: + raise Error("invalid_context") + app.logger.setLevel(0) response = errors["OK"] - except Invalid_Context: - return errors["invalid_context"] + except Error as e: + return e.status() except KeyError: return errors["invalid_data"] except TypeError: @@ -191,7 +203,7 @@ def vote(): def message(): try: if not session.get("username") or not session.get("keys"): - raise "invalid_context" + raise Error("invalid_context") author = session["username"] body = json.loads(request.values["body"]) @@ -204,9 +216,9 @@ def message(): Users.publish( message, keys ) except KeyError: - return errors["invalidData"] - except "invalid_context": - return errors["invalid_context"] + return errors["invalid_data"] + except Error as e: + return e.status() except Exception as e: raise e else: @@ -221,7 +233,7 @@ def register_route(): response = render_template("register.html") else: if session["username"] != "joris": - raise Invalid_Context + raise Error("invalid_context") else: username = request.values["username"] id = request.values["id"] @@ -230,10 +242,8 @@ def register_route(): last_name = request.values["last_name"] response = register(username, id, passwords, first_name, last_name) response = errors["OK"] if response else errors["error_for_unknown_reason"] - except Invalid_Context: - return errors["invalid_context"] - except Already_Registered: - return errors["already_registered"] + except Error as e: + return e.status() except Exception as e: raise e else: @@ -266,7 +276,7 @@ def register( username : str } if users.find_one({ "id" : id }) or users.find_one({ "username" : username }): - raise Already_Registered + raise Error("already_registered") else: users.insert_one(user) except Exception as e: From e1c3e517bdcc114ea7f0678cc1cc20ca80ee9081 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Wed, 26 Feb 2020 22:16:38 +0100 Subject: [PATCH 34/54] Implementing voting. --- main.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 274f5eb8..4a2c3528 100644 --- a/main.py +++ b/main.py @@ -169,14 +169,16 @@ def vote(): raise Error("invalid_context") username = session["username"] hash = request.values['hash'] - vote = request.values['vote'] + vote = json.loads(request.values['vote']) # Don't make or use encryption for voting **yet** election = elections.find_one({ "hash" : hash }) if election: if username not in election["participants"]: - election["participants"].append(username) - election["votes"].append(vote) + elections.update({ "$push" : { "participants" : username + , "votes" : vote + } + }) else: raise Error("already_voted") else: From 8bd011dd7f8695871f8b5f420c708f75e056d0c6 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Wed, 26 Feb 2020 22:19:06 +0100 Subject: [PATCH 35/54] Unifying error catching in vote(). --- main.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/main.py b/main.py index 4a2c3528..f2879c5d 100644 --- a/main.py +++ b/main.py @@ -188,9 +188,7 @@ def vote(): response = errors["OK"] except Error as e: return e.status() - except KeyError: - return errors["invalid_data"] - except TypeError: + except (KeyError, TypeError): return errors["invalid_data"] except Exception as e: raise e From 80c8ffd1f4f9bba4bd4028bd9b82e141eb4b40e6 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Thu, 27 Feb 2020 09:21:09 +0100 Subject: [PATCH 36/54] Added writing hash to write() function as parameter. --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index f2879c5d..eabc0cb1 100644 --- a/main.py +++ b/main.py @@ -135,7 +135,7 @@ def read(reading_hash): return response @app.route("/write/", methods=["GET"]) -def write(): +def write(writing_hash): try: writing = messages.find_one({ "hash" : writing_hash }) From 85e7626c1c64a8a7f1144d448932b9dc8ef160d9 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Thu, 27 Feb 2020 16:48:38 +0100 Subject: [PATCH 37/54] GET /write and POST /write implemented --- main.py | 46 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index eabc0cb1..6f74a361 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,18 @@ #!/usr/bin/env python3 +# OWN MODULES from Server import Elections, Patches, Users +# FLASK from flask import Flask, request, render_template, session, redirect +# MONGODB import pymongo from pymongo import MongoClient -import json, os +# PYCRYPTODOME from Crypto.Hash import SHA256 from Crypto.PublicKey import RSA +# UITILS from typing import List -import datetime +import json, os, datetime, random app = Flask( __name__ , static_url_path="/static" @@ -128,12 +132,48 @@ def writings(): def read(reading_hash): try: reading = messages.find_one({ "hash" : reading_hash }) - response = render_template("read.html", reading=reading) + if reading["draft"]: + raise Error("invalid_context") + else: + response = render_template("read.html", reading=reading) + except Error as e: + return e.status() except Exception as e: raise e else: return response +@app.route("/write", methods=["GET","POST"]) +def write_new(): + try: + if request.method =="GET": + response = render_template("write.html", writing={}) + else: + title = request.values["title"] + content = request.values["content"] + author = session["username"] + user_author = users.find_one({ "username" : author }) + hash = f"{author}#{str(random.randint(0,99999)).zfill(5)}" #temporary, while files is still changing + message = { "body" : { "title" : title + , "content" : content + } + , "from" : author + , "hash" : hash + , "draft" : True + } + messages.insert_one(message) + response = errors["OK"] + except KeyError: + return errors["invalid_user"] + except Error as e: + return e.status() + except Exception as e: + raise e + else: + return response + + + @app.route("/write/", methods=["GET"]) def write(writing_hash): try: From 20ea2970504c83f56f899f84eef81dafd6820e0e Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Thu, 27 Feb 2020 19:34:29 +0100 Subject: [PATCH 38/54] POST /write added to make it possible to store what has been written. --- main.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/main.py b/main.py index 6f74a361..e9012ca4 100644 --- a/main.py +++ b/main.py @@ -157,7 +157,7 @@ def write_new(): message = { "body" : { "title" : title , "content" : content } - , "from" : author + , "from" : author , "hash" : hash , "draft" : True } @@ -174,15 +174,30 @@ def write_new(): -@app.route("/write/", methods=["GET"]) +@app.route("/write/", methods=["GET", "POST"]) def write(writing_hash): try: - writing = messages.find_one({ "hash" : writing_hash }) + if request.method == "GET": + writing = messages.find_one({ "hash" : writing_hash }) - if session["username"] == writing["author"]: - response = render_template("write.html", writing=writing) + if session["username"] == writing["author"]: + response = render_template("write.html", writing=writing) + else: + raise Error("invalid_user") else: - raise Error("invalid_user") + hash = request.values["hash"] + message = messages.find_one({"hash" : hash}) + message["title"] = request.values["title"] + message["content"] = request.values["content"] + publish = request.values["publish"] == "1" + + if publish: + message["hash"] = SHA256.new(json.dumps(message).encode("utf-8")).hexdigest() + message["draft"] = not publish + messages.replace_one({ "hash" : hash }, message) + else: + messages.update_one({ "hash" : hash }, message) + except Error as e: return e.status() except KeyError: From 61dc5f135f2c126de68a52e10e7dbbd594889a72 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Thu, 27 Feb 2020 19:42:35 +0100 Subject: [PATCH 39/54] redirecting to GET /write/ from POST /write, which means once you saved your message once, you'll be redirected to that messsages id: /write/ --- main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index e9012ca4..2d5bc841 100644 --- a/main.py +++ b/main.py @@ -161,8 +161,9 @@ def write_new(): , "hash" : hash , "draft" : True } + messages.insert_one(message) - response = errors["OK"] + response = redirect(f"/write/{hash}") except KeyError: return errors["invalid_user"] except Error as e: From 73a1f67309d6016926d3ba51f72454dded71dcf6 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Thu, 27 Feb 2020 21:06:57 +0100 Subject: [PATCH 40/54] Defined the three step life of a vote. 0. Proposing the vote 1. Collection options 2. Voting And implemented first route for the first step of the process, GET and POST /voting/propose. --- main.py | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/main.py b/main.py index 2d5bc841..436d58d5 100644 --- a/main.py +++ b/main.py @@ -206,6 +206,72 @@ def write(writing_hash): else: return response +""" +# The three stage process of an election. +The last step of an election is clear, the vote. +But before that can happen two other steps have to prepare the vote. + +0. Proposing the vote +Here a problem is explained. +This can be in form of a simple title +or a long study, containing reasoning, +technical details and a concret case for why this problem needs addressing. + +1. Collecting options +After the problem has been detailed, everybody is free to create a solution +and propose it as an option. + +2. Voting +Now every user is held to vote on the issue and select +the best solution to the problem. + +The first stage can take as long as it wants. +One can research an issue for years and deliver an extremely +detailed proposal for the issue, but once it is detailed and +officially proposed, the solutions must be proposed over a one +month period. +It is thus a good advice to anyone proposing a +problem to have published their results long enough beforehand, +so a solution can be developed. + +After the one month period, voting opens and for 2 weeks voters +can cast their votes. + +In conclusion: + +0. Proposing the vote : as much time as it needs +1. Collecting options : 4 weeks +2. Voting : 2 weeks +""" + +@app.route("/voting/propose", methods=["GET", "POST"]) +def propose_vote(): + try: + if request.method == "GET": + response = render_template("propose_vote.html") + else: + if not session.get("username"): + raise Errors("not_logged_in") + else: + title = request.values["title"] + description = request.values["description"] + vote = { "title" : title + , "description" : description + , "stage" : 0 + , "author" : + } + vote["id"] = SHA256.new(json.dumps(vote).encode("utf-8")).hexdigest() + elections.insert_one(vote) + response = redirect(f"/voting/option/{vote["id"]}") + + except Error as e: + return e.status() + except KeyError: + return errors["invalid_data"] + except Exception as e: + raise e + else: + return response ################################################################### ############################ CRITICAL ############################# ################################################################### From 4c7505a9258b056f5727db47f210b90099be255f Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Thu, 27 Feb 2020 21:13:43 +0100 Subject: [PATCH 41/54] Added route /voting/option/ to create an option to a vote. But I am going to stop now. --- main.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/main.py b/main.py index 436d58d5..7749bf4f 100644 --- a/main.py +++ b/main.py @@ -272,6 +272,15 @@ def propose_vote(): raise e else: return response + +@app.route("/voting/option/", methods=["GET,POST"]) +def propose_option(): + try: + pass + except Exception as e: + raise + else: + pass ################################################################### ############################ CRITICAL ############################# ################################################################### From acb9e972070f9152ca044757463a094dafd82be6 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Fri, 28 Feb 2020 07:28:07 +0100 Subject: [PATCH 42/54] Made Feed only list none-drafts. --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 7749bf4f..89564fbd 100644 --- a/main.py +++ b/main.py @@ -52,7 +52,7 @@ def status(self): def index(): try: messages_count = 10 - sorted_messages = list(messages.find({})) + sorted_messages = list(messages.find({ "draft" : False })) upload_time_cut = max(sorted_messages,key=lambda m: m["upload_time"])["upload_time"] - messages_count sorted_messages = list(filter(lambda m: m["upload_time"] >= upload_time_cut, sorted_messages)) sorted_messages = list(map( lambda m: { "title" : m["body"]["title"] From 9eab180e36d88e9c3f3e9bcd102a8da43715b417 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Fri, 28 Feb 2020 07:32:47 +0100 Subject: [PATCH 43/54] Implementing part of voting/option/ --- main.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 89564fbd..f7741de7 100644 --- a/main.py +++ b/main.py @@ -276,7 +276,22 @@ def propose_vote(): @app.route("/voting/option/", methods=["GET,POST"]) def propose_option(): try: - pass + if request.method == "GET": + response = render_template("propose_option.html") + else: + if not session.get("author"): + raise Error("not_logged_in") + else: + type_of_option = request.values["type"] + author = session["username"] + + if type_of_option == "text": + title = request.values["title"] + description = request.values["description"] + proposal = { "additions" : request.values["additions"] + } + else: + except Exception as e: raise else: From 9c9b248f0aa26dae91b996e5d5de5caef4f0acb8 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Fri, 28 Feb 2020 14:50:47 +0100 Subject: [PATCH 44/54] Cut losses and don't use dictionaries as arguments, if all the fields in the dictionary could also be arguments of themself. Also, don't write more comment than code. --- Server/Patches.py | 71 ++++++++++------------------------------------- 1 file changed, 14 insertions(+), 57 deletions(-) diff --git a/Server/Patches.py b/Server/Patches.py index 00a9ce89..0089cfa3 100644 --- a/Server/Patches.py +++ b/Server/Patches.py @@ -2,70 +2,27 @@ import subprocess, os, json from Crypto.Hash import SHA256 from pymongo import MongoClient - -""" -Env Variables: -$PATCHES : Folder with the Git Repositories of the Patches (format: patcher-patch/) -$ORIGIN_REPOSITORY : Location of the Original Git Repository, from which patches are cloned from -and to whom patches are pushed after being approved. - -create a patch: -- clone the origin_repository, as stored in the $ORIGIN_REPOSITORY envvar. -- create an entry in the collection patches on the demnet database. - -This patch needs the following options: - -- The name of the patcher, that can either be the username in demnet -or a pseudonyme chosen by a non-user, which will then be noted in a separate -variable called "is_user". - -- The name of the patch, that should be descriptive (hopefully) - -- The simple_description of the patcher's intent. -This is useful for users, if the patcher wants to hold an election before -starting development. These users won't often be interested in the technical -details, for whom this description should suffice. - -- The technical_description is for all developers trying to understand -the patcher's intent on a deeper level. - -- As part of the technical_description, the patcher can also reference issues, -bug reports and generally link to ressources, detailing his intention. -This is separatly done in the "references" field. -The Patcher should put a most conclusive list here -and reference only some of it in the descriptions. - -- The Patcher may not want to start developing, waste hours and hours of time, -and see that the users don't even have an interest their patch. -All patches have to pass an election eventually, but if the patcher wants to, -they can hold an election before starting to develop, just to see if there -is any interest. If the election is lost, the patcher will either have to delete -the patch or modify it substantially. If they want to hold such an election, -"hold_pre_election" must be set to True. - -- After the Patch has been closed and the Patch Repository deleted, the -patch entry "closed" = True - -After creating the patch, -a SHA256 of all the data is created and -returned as the reference to the patch. -This is also put into one field called "sha256". -(That this isn't included in the calculation of the hash is self-evident.) -""" -def create(patcher,patch_name, options): +from typing import List + +def create ( patcher : str + , patch_name : str + , is_user : bool + , simple_description : str + , technical_description : str + , references : List[str] + ): client = MongoClient() db = client.demnet patches = db.patches patch = { "patcher" : patcher - , "is_user" : options['is_user'] + , "is_user" : is_user , "name" : patch_name - , "simple_description" : options['simple_description'] - , "technical_description" : options['technical_description'] - , "hold_pre_election" : options['hold_pre_election'] - , "references" : options['references'] + , "simple_description" : simple_description + , "technical_description" : technical_description + , "references" : references , "closed" : False } - patch['hash'] = SHA256.new(data=json.dumps(patch).encode('utf-8')).hexdigest() + patch['hash'] = SHA256.new().update(json.dumps(patch)).encode('utf-8')).hexdigest() patch_id = patches.insert_one(patch).inserted_id subprocess.run(['bash', 'patch.sh', 'create', os.environ["ORIGIN_REPOSITORY"], patcher, patch_name]) From 6e771ff9b13168a62bbcf61486102e1b324e34a1 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Fri, 28 Feb 2020 14:53:52 +0100 Subject: [PATCH 45/54] Fixed mistake in calling SHA256.new(), providing data as positional argument, although it was a keyword argument. --- main.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/main.py b/main.py index f7741de7..1f01ac71 100644 --- a/main.py +++ b/main.py @@ -78,7 +78,7 @@ def login(): return render_template("login.html") elif request.method == "POST": username = request.values["username"] - password = SHA256.new(request.values["password"].encode("utf-8")).hexdigest() + password = SHA256.new().update(request.values["password"].encode("utf-8")).hexdigest() user = users.find_one({ "username" : username }) if user: @@ -193,7 +193,7 @@ def write(writing_hash): publish = request.values["publish"] == "1" if publish: - message["hash"] = SHA256.new(json.dumps(message).encode("utf-8")).hexdigest() + message["hash"] = SHA256.new().update(json.dumps(message).encode("utf-8")).hexdigest() message["draft"] = not publish messages.replace_one({ "hash" : hash }, message) else: @@ -260,7 +260,7 @@ def propose_vote(): , "stage" : 0 , "author" : } - vote["id"] = SHA256.new(json.dumps(vote).encode("utf-8")).hexdigest() + vote["id"] = SHA256.new().update(json.dumps(vote)).encode("utf-8")).hexdigest() elections.insert_one(vote) response = redirect(f"/voting/option/{vote["id"]}") @@ -402,8 +402,8 @@ def register( username : str , last_name : str ): try: - id = SHA256.new(id_token.encode("utf-8")).hexdigest().encode("utf-8") - passwords = [SHA256.new(password.encode("utf-8")).hexdigest() for password in passwords] + id = SHA256.new().update(id_token.encode("utf-8")).hexdigest().encode("utf-8") + passwords = [SHA256.new().update(password.encode("utf-8")).hexdigest() for password in passwords] user = { "username" : username , "id" : id , "passwords" : passwords @@ -481,12 +481,11 @@ def decrypt (recipients_private_key : RSA.RsaKey return message """Return signed hash of a string with private key. -Returns : (signature : bytes, hash : SHA256) """ def sign(message : str, author_key : RSA.RsaKey): - hash = SHA256.new(message.encode("utf-8")).hexdigest() + hash = SHA256.new().update(message.encode("utf-8")).hexdigest() signature = pkcs1_15.new(author_key).sign(hash) - return (signature, hash) + return (signature : bytes, hash : SHA256) """Verify a message signed by sign(). """ From f05cb3f8fbbb11ee162d1f170ed75c3c85bd8110 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Fri, 28 Feb 2020 17:30:20 +0100 Subject: [PATCH 46/54] Created type checks function and module. --- Server/Type_checks.py | 46 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 Server/Type_checks.py diff --git a/Server/Type_checks.py b/Server/Type_checks.py new file mode 100644 index 00000000..4732c139 --- /dev/null +++ b/Server/Type_checks.py @@ -0,0 +1,46 @@ +from typing import Dict, TypeVar +class Type_Check_Error (Exception): + def __init__(self,data_name, msg): + self.data_name = data_name + self.msg = msg + +Dict_Values = TypeVar("Dict_Values") +def check_for_type(dictionary_name : str, dictionary : Dict[str, Dict_Values]): + try: + if dictionary_name == "additions": + assert check_for_type(dictionary, { "title" : "" + , "book" : "" + , "articles" : [] + } + ) + elif dictionary == "amendments": + assert check_for_type(dictionary, { "book" : "" + , "law" : "" + , "articles" : [] + } + ) + elif dictionary == "repeals": + assert check_for_type(dictionary, { "books" : "" + , "laws" : [] + }) + else: + raise Type_Check_Error(dictionary_name, "unknown dictionary name") + except AssertionError as assertion_error: + raise Type_Check_Error(dictionary_name, assertion_error.args[0]) + except Exception as e: + raise e + else: + return True + +def check_dictionary_for_type ( dictionary : Dict[str, Dict_Values] + , types : Dict[str, Dict_Values] + ): + try: + tests = [(type(types[key]) == type(dictionary[key]), key) for key in types] + test_failed = False in filter(lambda t: t[0], tests) + assert not test_failed, f"Type Check Failed: {list(map(lambda t: t[1], filter(lambda t: t[0] == False, tests)))}" + + except Exception as e: + raise e + else: + return True From 22b4eda7a54992c113716757f8acb0519a70b4a5 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Fri, 28 Feb 2020 19:22:03 +0100 Subject: [PATCH 47/54] Type checking of input to /voting/option/ --- main.py | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/main.py b/main.py index 1f01ac71..4e584d16 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # OWN MODULES -from Server import Elections, Patches, Users +from Server import Elections, Patches, Users, Type_checks # FLASK from flask import Flask, request, render_template, session, redirect # MONGODB @@ -257,8 +257,8 @@ def propose_vote(): description = request.values["description"] vote = { "title" : title , "description" : description - , "stage" : 0 - , "author" : + , "stage" : 1 + , "author" : session["username"] } vote["id"] = SHA256.new().update(json.dumps(vote)).encode("utf-8")).hexdigest() elections.insert_one(vote) @@ -274,7 +274,7 @@ def propose_vote(): return response @app.route("/voting/option/", methods=["GET,POST"]) -def propose_option(): +def propose_option(vote_id): try: if request.method == "GET": response = render_template("propose_option.html") @@ -284,18 +284,37 @@ def propose_option(): else: type_of_option = request.values["type"] author = session["username"] - - if type_of_option == "text": - title = request.values["title"] - description = request.values["description"] - proposal = { "additions" : request.values["additions"] + election = elections.find_one({ "id" : vote_id, "stage" : 1 }) + if not election: + raise Error("invalid_data") + elif type_of_option == "text": + title = request.values["title"] + description = request.values["description"] + additions = json.loads(request.values["additions"]) + amendments = json.loads(request.values["amendments"]) + repeals = json.loads(request.values["repeals"]) + Type_checks.check_dictionary_for_type("additions", additions) + Type_checks.check_dictionary_for_type("amendments", amendments) + Type_checks.check_dictionary_for_type("repeals", repeals) + proposal = { "title" : request.values["title"] + , "description" : request.values["description"] + , "additions" : additions + , "amendments" : amendments + , "repeals" : repeals } + else: + pass + except json.JSONDecodeError as json_error: + # See parsing of the request.values for probable cause. + return f"{errors["invalid_data"]} { str(json_error) if os.environ.get("DEBUG") else "JSON Error"}" + except Type_Check_Error as type_error: + return errors["invalid_data"] + type_error.data_name except Exception as e: - raise + raise e else: - pass + return response ################################################################### ############################ CRITICAL ############################# ################################################################### From e81b54a7ccc294828e96dcbc7146d6db3519910b Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Fri, 28 Feb 2020 19:22:59 +0100 Subject: [PATCH 48/54] Changed check_dictionary_for_type to check_for_type. --- main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 4e584d16..1526957a 100644 --- a/main.py +++ b/main.py @@ -293,9 +293,9 @@ def propose_option(vote_id): additions = json.loads(request.values["additions"]) amendments = json.loads(request.values["amendments"]) repeals = json.loads(request.values["repeals"]) - Type_checks.check_dictionary_for_type("additions", additions) - Type_checks.check_dictionary_for_type("amendments", amendments) - Type_checks.check_dictionary_for_type("repeals", repeals) + Type_checks.check_for_type("additions", additions) + Type_checks.check_for_type("amendments", amendments) + Type_checks.check_for_type("repeals", repeals) proposal = { "title" : request.values["title"] , "description" : request.values["description"] , "additions" : additions From 65ce13192718ace671d462ba808ba2f015965718 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Fri, 28 Feb 2020 19:58:14 +0100 Subject: [PATCH 49/54] Put assertions in Type_checks.py into a loop. --- Server/Type_checks.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/Server/Type_checks.py b/Server/Type_checks.py index 4732c139..36501ee8 100644 --- a/Server/Type_checks.py +++ b/Server/Type_checks.py @@ -8,22 +8,25 @@ def __init__(self,data_name, msg): def check_for_type(dictionary_name : str, dictionary : Dict[str, Dict_Values]): try: if dictionary_name == "additions": - assert check_for_type(dictionary, { "title" : "" - , "book" : "" - , "articles" : [] - } + for addition in dictionary: + assert check_for_type(addition, { "title" : "" + , "book" : "" + , "articles" : [] + } ) elif dictionary == "amendments": - assert check_for_type(dictionary, { "book" : "" - , "law" : "" - , "articles" : [] - } - ) + for amendment in dictionary: + assert check_for_type(amendment, { "book" : "" + , "law" : "" + , "articles" : [] + } + ) elif dictionary == "repeals": - assert check_for_type(dictionary, { "books" : "" - , "laws" : [] - }) - else: + for repeal in repeals: + assert check_for_type(repael, { "books" : [] + , "laws" : [] + }) + else: raise Type_Check_Error(dictionary_name, "unknown dictionary name") except AssertionError as assertion_error: raise Type_Check_Error(dictionary_name, assertion_error.args[0]) From 89a8300125bdc4c175980ab0e075885053afd92a Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Fri, 28 Feb 2020 20:05:40 +0100 Subject: [PATCH 50/54] Pushing the newly created option to the options field in the election data structure in MongoDB in /voting/option/ --- main.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index 1526957a..43ab0e09 100644 --- a/main.py +++ b/main.py @@ -259,6 +259,7 @@ def propose_vote(): , "description" : description , "stage" : 1 , "author" : session["username"] + , "options" : [] } vote["id"] = SHA256.new().update(json.dumps(vote)).encode("utf-8")).hexdigest() elections.insert_one(vote) @@ -285,26 +286,36 @@ def propose_option(vote_id): type_of_option = request.values["type"] author = session["username"] election = elections.find_one({ "id" : vote_id, "stage" : 1 }) + + title = request.values["title"] + description = request.values["description"] + if not election: raise Error("invalid_data") - elif type_of_option == "text": - title = request.values["title"] - description = request.values["description"] + elif type_of_option == "0": + + additions = json.loads(request.values["additions"]) amendments = json.loads(request.values["amendments"]) repeals = json.loads(request.values["repeals"]) + Type_checks.check_for_type("additions", additions) Type_checks.check_for_type("amendments", amendments) Type_checks.check_for_type("repeals", repeals) - proposal = { "title" : request.values["title"] + + option = { "title" : request.values["title"] , "description" : request.values["description"] , "additions" : additions , "amendments" : amendments , "repeals" : repeals + , "kind" : 0 } + elections.update_one({ "id" : vote_id, "stage" : 1 }, { "$push" : { "options" : option}}) else: - pass + patcher = request.values["patcher"] + patch_name = request.values["patch_name"] + os.exists("") except json.JSONDecodeError as json_error: # See parsing of the request.values for probable cause. From 869ed56c295af304a92de2e5eb6e03731f2d5f2c Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Fri, 28 Feb 2020 20:23:36 +0100 Subject: [PATCH 51/54] path_of_patch to create the proper path to PATCH directory and check if it exists. --- Server/Patches.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Server/Patches.py b/Server/Patches.py index 0089cfa3..0f1bf90a 100644 --- a/Server/Patches.py +++ b/Server/Patches.py @@ -58,3 +58,7 @@ def close(patcher, patch_name, id, merge=False): return True else: return False + +def path_of_patch(patcher, patch_name): + path = f"{os.environ["PATCHES"]}/{patcher}-{patch_name}" + return path if os.path.isdir(path) else False From ff4cdb5acc63c6559c29980c2c9522cae96fc313 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Fri, 28 Feb 2020 20:24:58 +0100 Subject: [PATCH 52/54] Made check of existence in path_of_patch optional. --- Server/Patches.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Server/Patches.py b/Server/Patches.py index 0f1bf90a..61e5fbfe 100644 --- a/Server/Patches.py +++ b/Server/Patches.py @@ -47,7 +47,7 @@ def close(patcher, patch_name, id, merge=False): , "closed" : False } ) - repo_path = f"{os.environ['PATCHES']}/{patcher}-{patch_name}" + repo_path = path_of_patch(patcher, patch_name, check_existence=False) if patch and os.path.isdir(repo_path): if merge: merge(patcher, patch_name) @@ -59,6 +59,9 @@ def close(patcher, patch_name, id, merge=False): else: return False -def path_of_patch(patcher, patch_name): +def path_of_patch(patcher, patch_name, check_existence=True): path = f"{os.environ["PATCHES"]}/{patcher}-{patch_name}" - return path if os.path.isdir(path) else False + if check_existence: + return path if os.path.isdir(path) else False + else: + return path From a4f57a4928fbe814ea1446033e5dee6044649d24 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Fri, 28 Feb 2020 20:50:02 +0100 Subject: [PATCH 53/54] /voting/option/ can now take a patch as an option and uses the Patches module. Also created a function to set a Patch directory to readonly. --- Server/Patches.py | 8 ++++++++ main.py | 28 ++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/Server/Patches.py b/Server/Patches.py index 61e5fbfe..66054107 100644 --- a/Server/Patches.py +++ b/Server/Patches.py @@ -32,6 +32,14 @@ def create ( patcher : str def merge(patcher, patch_name): subprocess.run(['bash', 'patch.sh', 'merge', patcher, patch_name]) +def lock(patcher, patch_name): + path_of_patch = path_of_patch(patcher,patch_name) + if not path_of_patch: + return False + else: + os.chmod(path_of_patch, 0444) # readonly to all + return True + """ Closing a patch means: Deleting the Git Repo. diff --git a/main.py b/main.py index 43ab0e09..70b6bfa9 100644 --- a/main.py +++ b/main.py @@ -25,6 +25,7 @@ messages = db.messages users = db.users elections = db.elections +patches = db.patches # Errors debug = os.environ.get("DEBUG") @@ -313,15 +314,34 @@ def propose_option(vote_id): elections.update_one({ "id" : vote_id, "stage" : 1 }, { "$push" : { "options" : option}}) else: - patcher = request.values["patcher"] - patch_name = request.values["patch_name"] - os.exists("") - + patcher = request.values["patcher"] + patch_name = request.values["patch_name"] + comment = request.values["comment"] + path_of_patch = Patches.path_of_patch(patcher, patch_name) + + if path_of_patch and patcher == session["username"]: + Patches.lock(patcher, patch_name) + patch = patches.find_one({ "patcher" : patcher, "patch_name" : patch_name }) + patch_hash = patch["hash"] + option = { "patch" : patch_hash + , "comment" : comment + } + elections.update_one({ "id" : vote_id, "stage" : 1 }, { "$push" : { "options" : option}}) + + elif not path_of_patch: + raise Error("invalid_data") + else: + raise Error("invalid_user") + except json.JSONDecodeError as json_error: # See parsing of the request.values for probable cause. return f"{errors["invalid_data"]} { str(json_error) if os.environ.get("DEBUG") else "JSON Error"}" except Type_Check_Error as type_error: return errors["invalid_data"] + type_error.data_name + except Error as e: + return e.status() + except KeyError: + return Error("invalid_data").status() except Exception as e: raise e else: From dfd3ef080d30ca1cc6ab89f0323ff8773b7cc458 Mon Sep 17 00:00:00 2001 From: Joris Gutjahr Date: Sat, 7 Mar 2020 18:43:08 +0100 Subject: [PATCH 54/54] Stop. This is going to be merged and later continued with a simpler attitude. --- main.py | 11 +++++++---- requirements.txt | 4 ++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index 70b6bfa9..562aeeef 100644 --- a/main.py +++ b/main.py @@ -264,8 +264,10 @@ def propose_vote(): } vote["id"] = SHA256.new().update(json.dumps(vote)).encode("utf-8")).hexdigest() elections.insert_one(vote) - response = redirect(f"/voting/option/{vote["id"]}") + # Initate third stage after four weeks + + response = redirect(f"/voting/option/{vote["id"]}") except Error as e: return e.status() except KeyError: @@ -323,8 +325,8 @@ def propose_option(vote_id): Patches.lock(patcher, patch_name) patch = patches.find_one({ "patcher" : patcher, "patch_name" : patch_name }) patch_hash = patch["hash"] - option = { "patch" : patch_hash - , "comment" : comment + option = { "patch" : patch_hash + , "comment" : comment } elections.update_one({ "id" : vote_id, "stage" : 1 }, { "$push" : { "options" : option}}) @@ -332,7 +334,7 @@ def propose_option(vote_id): raise Error("invalid_data") else: raise Error("invalid_user") - + except json.JSONDecodeError as json_error: # See parsing of the request.values for probable cause. return f"{errors["invalid_data"]} { str(json_error) if os.environ.get("DEBUG") else "JSON Error"}" @@ -346,6 +348,7 @@ def propose_option(vote_id): raise e else: return response + ################################################################### ############################ CRITICAL ############################# ################################################################### diff --git a/requirements.txt b/requirements.txt index fa7d88f4..40c75c5b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +APScheduler==3.6.3 Click==7.0 Flask==1.1.1 gunicorn==20.0.4 @@ -6,4 +7,7 @@ Jinja2==2.10.3 MarkupSafe==1.1.1 pycryptodome==3.9.4 pymongo==3.10.1 +pytz==2019.3 +six==1.14.0 +tzlocal==2.0.0 Werkzeug==0.16.0