Skip to content
This repository has been archived by the owner on Nov 28, 2020. It is now read-only.

Leaderboard protection #10

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 88 additions & 17 deletions server/flask_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,24 @@
from random import randrange
from urllib.parse import parse_qs

from flask import Flask, jsonify, request, send_from_directory
from flask import Flask, jsonify, request, send_from_directory, session
from sqlalchemy import create_engine, text

import gui
import typing_test

app = Flask(__name__, static_url_path="", static_folder="static")
app.config["SECRET_KEY"] = os.urandom(32)

MIN_PLAYERS = 2
MAX_PLAYERS = 4
QUEUE_TIMEOUT = timedelta(seconds=1)
MAX_WAIT = timedelta(seconds=5)


WPM_THRESHOLD = 10


if __name__ == "__main__":
engine = create_engine("mysql://localhost/cats")
else:
Expand All @@ -30,16 +34,14 @@
statement = text(
"""CREATE TABLE IF NOT EXISTS leaderboard (
username varchar(128),
wpm integer,
PRIMARY KEY (`username`)
wpm integer
);"""
)
conn.execute(statement)
statement = text(
"""CREATE TABLE IF NOT EXISTS memeboard (
username varchar(128),
wpm integer,
PRIMARY KEY (`username`)
wpm integer
);"""
)
conn.execute(statement)
Expand All @@ -57,12 +59,16 @@ class State:

State = State()

used_wpm_nonces = {}

def get_from_gui(f, request):
return f(parse_qs(request.get_data().decode("ascii")))

def passthrough(path):
def decorator(f):
@app.route(path, methods=["POST"], endpoint=f.__name__)
def decorated():
return jsonify(f(parse_qs(request.get_data().decode("ascii"))))
return jsonify(get_from_gui(f, request))

return decorated
return decorator
Expand All @@ -73,8 +79,34 @@ def index():
return send_from_directory("static", "index.html")


passthrough("/request_paragraph")(gui.request_paragraph)
passthrough("/analyze")(gui.compute_accuracy)
@app.route("/request_paragraph", methods=["POST"])
def request_paragraph():
paragraph = get_from_gui(gui.request_paragraph, request)
session["paragraph"] = paragraph
return paragraph


@app.route("/analyze", methods=["POST"])
def analyze():
analysis = get_from_gui(gui.compute_accuracy, request)
claimed = request.form.get("promptedText")
typed = request.form.get("typedText")

if claimed == session.get("paragraph") and 'start' not in session:
session["start"] = time.time()
session.pop("end", None)
session.pop("wpm_nonce", None)

if claimed == session.get("paragraph") and typed == claimed and 'end' not in session:
session["end"] = time.time()
wpm_nonce = os.urandom(32)
while wpm_nonce in used_wpm_nonces:
wpm_nonce = os.urandom(32)
session["wpm_nonce"] = wpm_nonce

return analysis


passthrough("/autocorrect")(gui.autocorrect)
passthrough("/fastest_words")(lambda x: gui.fastest_words(x, lambda targets: [State.progress[target] for target in targets["targets[]"]]))

Expand Down Expand Up @@ -121,6 +153,7 @@ def request_match():
len(State.queue) >= MIN_PLAYERS:
# start game!
curr_text = gui.request_paragraph(None)
session["paragraph"] = curr_text
game_id = get_id()

for player in State.queue:
Expand Down Expand Up @@ -172,12 +205,9 @@ def record_progress(id, progress, updated):
prompt = State.game_data[game_id]["text"]
wpm = len(prompt) / (time.time() - State.progress[id][0][1]) * 60 / 5

if wpm > 200:
return ""

with engine.connect() as conn:
conn.execute("INSERT INTO leaderboard (username, wpm) VALUES (?, ?)", ["<student playing locally>", wpm])
return ""
return "", 204


@app.route("/request_progress", methods=["POST"])
Expand All @@ -197,16 +227,57 @@ def request_all_progress():

@app.route("/record_wpm", methods=["POST"])
def record_name():
if "username" not in request.form:
return jsonify(
{
"error": "No username present",
}
), 400
elif len(request.form["username"]) > 30:
return jsonify(
{
"error": "Username too long",
}
), 400

if "wpm" not in request.form:
return jsonify(
{
"error": "No WPM present",
}
), 400

if "start" not in session or "end" not in session or "paragraph" not in session or "wpm_nonce" not in session:
return jsonify(
{
"error": "Unable to calculate real WPM from session",
}
), 400

if session["wpm_nonce"] in used_wpm_nonces:
return jsonify(
{
"error": "Nonce already used"
}
), 400

username = request.form.get("username")
wpm = float(request.form.get("wpm"))
confirm_string = request.form.get("confirm")

if len(username) > 30 or wpm > 200 or confirm_string != "If you want to mess around, send requests to /record_meme! Leave this endpoint for legit submissions please. Don't be a jerk and ruin this for everyone, thanks!":
return record_meme()
real_wpm = typing_test.wpm(session["paragraph"], session["end"] - session["start"])

used_wpm_nonces[session["wpm_nonce"]] = session["wpm_nonce"]

if abs(wpm - real_wpm) > WPM_THRESHOLD:
return jsonify(
{
"error": "Invalid WPM",
}
), 400

with engine.connect() as conn:
conn.execute("INSERT INTO leaderboard (username, wpm) VALUES (%s, %s)", [username, wpm])
return ""
return "", 204


@app.route("/record_meme", methods=["POST"])
Expand All @@ -216,7 +287,7 @@ def record_meme():

with engine.connect() as conn:
conn.execute("INSERT INTO memeboard (username, wpm) VALUES (%s, %s)", [username, wpm])
return ""
return "", 204


@app.route("/wpm_threshold", methods=["POST"])
Expand Down
6 changes: 4 additions & 2 deletions server/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,10 @@ def compute_accuracy(data):
typed_text = data.get("typedText", [""])[0]
start_time = float(data["startTime"][0])
end_time = float(data["endTime"][0])
return [typing_test.wpm(typed_text, end_time - start_time),
typing_test.accuracy(typed_text, prompted_text)]
return {
"wpm": typing_test.wpm(typed_text, end_time - start_time),
"accuracy": typing_test.accuracy(typed_text, prompted_text),
}


def similar(w, v, n):
Expand Down
7 changes: 2 additions & 5 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ class App extends Component {
endTime: this.getCurrTime(),
}).done((data) => {
this.setState({
wpm: data[0].toFixed(1),
accuracy: data[1].toFixed(1),
wpm: data.wpm.toFixed(1),
accuracy: data.accuracy.toFixed(1),
currTime: this.getCurrTime(),
});
});
Expand Down Expand Up @@ -287,9 +287,6 @@ class App extends Component {
$.post("/record_wpm", {
username,
wpm: this.state.wpm,
confirm: "If you want to mess around, send requests"
+ " to /record_meme! Leave this endpoint for legit submissions please. Don't be a"
+ " jerk and ruin this for everyone, thanks!",
});
this.hideUsernameEntry();
};
Expand Down
1 change: 1 addition & 0 deletions src/NamePrompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default function NamePrompt({ show, onHide, onSubmit }) {
id="exampleInputEmail1"
aria-describedby="emailHelp"
placeholder="Enter username"
maxlength="30"
/>
<small id="emailHelp" className="form-text text-muted">
Please don't name yourself anything inappropriate!
Expand Down