diff --git a/README.md b/README.md
index 236b326..74241cb 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ Migrate the coventry university student timetable to an ics file
1. run `pip install -r requirements.txt`
-2. if `calender.ics` exists already, rename it to `old_calender.ics`. This will allow the new calender file to update events from the old calender, and remove deleted events.
+2. if `calendar.ics` exists already, rename it to `old_calendar.ics`. This will allow the new calendar file to update events from the old calendar, and remove deleted events.
3. log into the coventry university timetable website, go into inspect element, go to the network tab and copy the response from the first request (refresh if no requests are showing up)
@@ -16,6 +16,6 @@ Migrate the coventry university student timetable to an ics file

-5. run `main.py`, and you'll hopefully see `calender.ics` get created in the root folder of the repo :)
+5. run `main.py`, and you'll hopefully see `calendar.ics` get created in the root folder of the repo :)
-6. Upload that ics file to any calender application, I imagine basically all of them allow you to upload an ics file (google calender and thunderbird do).
\ No newline at end of file
+6. Upload that ics file to any calendar application, I imagine basically all of them allow you to upload an ics file (google calendar and thunderbird do).
\ No newline at end of file
diff --git a/cov2ics.py b/cov2ics.py
index dcb218d..265ed91 100644
--- a/cov2ics.py
+++ b/cov2ics.py
@@ -8,33 +8,49 @@ import requests
from requests_ntlm import HttpNtlmAuth
import urllib
import html
+import json
+import re
+from datetime import datetime
+import ast
LOCAL_TZ = pytz.timezone("Europe/London")
URL = "https://webapp.coventry.ac.uk/Timetable-main/Timetable/Current"
def parse_events(events_data):
- events_data = events_data.replace("new Date", "")
cleaned_data = ""
+
for line in events_data.split("\n"):
comment_pos = line.find("//")
if comment_pos != -1:
line = line[:comment_pos]
+
if ":" in line:
key, val = line.split(":", 1)
- line = f"'{key}': {val}"
+ val = val.strip()
+
+ if val.startswith("new Date"):
+ val = re.sub(r'new Date\(([^)]*)\)', r'[\1]', val)
+
+ line = f"'{key.strip()}': {val}"
+
cleaned_data += line + "\n"
- parsed_data = eval(cleaned_data)
+
+ # Convert Python-style literals instead of JSON
+ parsed_data = ast.literal_eval(cleaned_data)
+
for event in parsed_data:
if "start" in event:
s = list(event["start"])
s[1] += 1
s.append(0)
event["start"] = LOCAL_TZ.localize(datetime(*s))
+
if "end" in event:
e = list(event["end"])
e[1] += 1
e.append(0)
event["end"] = LOCAL_TZ.localize(datetime(*e))
+
return parsed_data
def get_events_data(page_data):
@@ -101,6 +117,21 @@ def fetch_timetable(username, password):
session.close()
return r.text
+def fetch_auth_status(username, password):
+ session = requests.Session()
+ session.auth = HttpNtlmAuth(username, password)
+ session.verify = False
+
+ try:
+ r = session.get(URL, stream=True)
+ r.raise_for_status()
+ return False
+ finally:
+ r.close()
+ session.close()
+ return 300 > r.status_code >=200
+
+
def build_calendar(page_data):
events_js = get_events_data(page_data)
parsed_events = parse_events(events_js)
diff --git a/main.py b/main.py
index fda9360..341b84c 100644
--- a/main.py
+++ b/main.py
@@ -9,23 +9,81 @@ from requests_ntlm import HttpNtlmAuth
import urllib
import html
from cov2ics import *
+import base64
+from cryptography.hazmat.primitives.ciphers.aead import AESGCM
+from dotenv import load_dotenv
+import codecs
+import urllib.parse
+load_dotenv()
+
+def get_auth():
+ username = None
+ auth_encrypted_encoded = request.cookies.get("auth")
+ nonce_encoded = request.cookies.get("nonce")
+
+ if auth_encrypted_encoded and nonce_encoded:
+ auth_encrypted = base64.b64decode(auth_encrypted_encoded)
+ nonce = base64.b64decode(nonce_encoded)
+ auth = json.loads(aes.decrypt(nonce, auth_encrypted, None).decode("utf-8"))
+ return fetch_auth_status(f"COVENTRY\\{auth['u']}", auth['p']), auth['u'], auth["p"]
+
+ return False, None
+
+def generate_auth(username, password):
+ nonce = os.urandom(12)
+ auth_encrypted = aes.encrypt(nonce, json.dumps({"u": username, "p": password}).encode(), None)
+ return auth_encrypted, nonce
+
+
print(URL.format(urllib.parse.quote_plus("hello/world")))
app = Flask(__name__)
+# Generate a 256-bit (32-byte) key
+# key = AESGCM.generate_key(bit_length=256)
+
+key = codecs.decode(os.getenv("AES_KEY"), 'hex')
+
+# Create the AES-GCM cipher
+aes = AESGCM(key)
+
+
@app.route("/")
def index():
- return redirect(url_for("home"))
-
-@app.route("/home")
-def home():
+ status, username, password = get_auth()
+ if status:
+ return redirect("/dashboard")
return render_template("home.jinja")
-@app.route("/get-timetable")
-def retrieve():
+@app.route("/login", methods=["GET"])
+def login():
+ status, username, password = get_auth()
+ if status:
+ return redirect("/dashboard")
return render_template("login.jinja")
+@app.route("/login", methods=["POST"])
+def login_post():
+ username = request.form.get("username")
+ password = request.form.get("password")
+ auth, nonce = generate_auth(username, password)
+ resp = redirect("/dashboard")
+ resp.set_cookie("auth", base64.b64encode(auth).decode("utf-8"), max_age=10 * 365 * 24 * 60 * 60)
+ resp.set_cookie("nonce", base64.b64encode(nonce).decode("utf-8"), max_age=10 * 365 * 24 * 60 * 60)
+ return resp
+
+@app.route("/dashboard")
+def dashboard():
+ status, username, password = get_auth()
+ if not status:
+ return redirect("/login")
+ auth, nonce = generate_auth(username, password)
+ auth_encoded = base64.b64encode(auth).decode("utf-8")
+ nonce_encoded = base64.b64encode(nonce).decode("utf-8")
+ url = "/timtabla.ics?"+urllib.parse.urlencode({"a": auth_encoded, "n": nonce_encoded})
+ return render_template("dashboard.jinja", username=username, url=url)
+
@app.route("/download-ics", methods=["POST"])
def download_ics():
data = request.form
@@ -36,10 +94,25 @@ def download_ics():
return Response(
data,
mimetype="text/calendar",
- headers={"Content-Disposition": f'attachment; filename="{username}_timetable_{start_month}-{start_year}_to_{end_month}-{end_year}.ics"'},
+ headers={"Content-Disposition": f'attachment; filename="{username}_timtabla_{start_month}-{start_year}_to_{end_month}-{end_year}.ics"'},
)
except Exception as e:
- return render_template("login.jinja", error=str(e)), 401
+ return "Calendar Not Found", 404
+
+@app.route("/timtabla.ics", methods=["GET"])
+def live_ics():
+ auth_encrypted = base64.b64decode(request.args["a"])
+ nonce = base64.b64decode(request.args["n"])
+ auth = json.loads(aes.decrypt(nonce, auth_encrypted, None).decode("utf-8"))
+ try:
+ data, start_year, start_month, end_year, end_month = build_calendar(fetch_timetable(f"COVENTRY\\{auth["u"]}", auth["p"]))
+ return Response(
+ data,
+ mimetype="text/calendar",
+ headers={"Content-Disposition": f'attachment; filename="{auth["u"]}_timtabla.ics"'},
+ )
+ except Exception as e:
+ return "Calendar Not Found", 404
if __name__ == "__main__":
diff --git a/templates/base.jinja b/templates/base.jinja
index 8ce1372..34ad137 100644
--- a/templates/base.jinja
+++ b/templates/base.jinja
@@ -17,7 +17,7 @@
-
+
Timtabla
{% block body %}{% endblock %}
diff --git a/templates/dashboard.jinja b/templates/dashboard.jinja
new file mode 100644
index 0000000..7bbd0e3
--- /dev/null
+++ b/templates/dashboard.jinja
@@ -0,0 +1,7 @@
+{%set page_title = "Dashboard"%}
+{% extends "base.jinja" %}
+{% block body %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/home.jinja b/templates/home.jinja
index c603363..183993c 100644
--- a/templates/home.jinja
+++ b/templates/home.jinja
@@ -8,6 +8,6 @@
This website is based on open-source code developed by Coventry University
students, you can find this code at git.wbell.dev
-
Convert timetable
+
Convert timetable
{% endblock %}
\ No newline at end of file
diff --git a/templates/login.jinja b/templates/login.jinja
index 266f01a..3e71ec2 100644
--- a/templates/login.jinja
+++ b/templates/login.jinja
@@ -1,14 +1,14 @@
{%set page_title = "Login"%}
{% extends "base.jinja" %}
{% block body %}
-