diff --git a/src/sdk/src/README.md b/sdk/src/README.md similarity index 100% rename from src/sdk/src/README.md rename to sdk/src/README.md diff --git a/src/sdk/src/apis/common/models.py b/sdk/src/apis/common/models.py similarity index 100% rename from src/sdk/src/apis/common/models.py rename to sdk/src/apis/common/models.py diff --git a/src/sdk/src/apis/common/utils.py b/sdk/src/apis/common/utils.py similarity index 100% rename from src/sdk/src/apis/common/utils.py rename to sdk/src/apis/common/utils.py diff --git a/src/sdk/src/apis/efeb/client.py b/sdk/src/apis/efeb/client.py similarity index 100% rename from src/sdk/src/apis/efeb/client.py rename to sdk/src/apis/efeb/client.py diff --git a/src/sdk/src/apis/efeb/constants.py b/sdk/src/apis/efeb/constants.py similarity index 100% rename from src/sdk/src/apis/efeb/constants.py rename to sdk/src/apis/efeb/constants.py diff --git a/src/sdk/src/apis/efeb/utils.py b/sdk/src/apis/efeb/utils.py similarity index 100% rename from src/sdk/src/apis/efeb/utils.py rename to sdk/src/apis/efeb/utils.py diff --git a/src/sdk/src/apis/hebe/__init__.py b/sdk/src/apis/hebe/__init__.py similarity index 100% rename from src/sdk/src/apis/hebe/__init__.py rename to sdk/src/apis/hebe/__init__.py diff --git a/src/sdk/src/apis/hebe/certificate.py b/sdk/src/apis/hebe/certificate.py similarity index 100% rename from src/sdk/src/apis/hebe/certificate.py rename to sdk/src/apis/hebe/certificate.py diff --git a/src/sdk/src/apis/hebe/client.py b/sdk/src/apis/hebe/client.py similarity index 100% rename from src/sdk/src/apis/hebe/client.py rename to sdk/src/apis/hebe/client.py diff --git a/src/sdk/src/apis/hebe/constants.py b/sdk/src/apis/hebe/constants.py similarity index 100% rename from src/sdk/src/apis/hebe/constants.py rename to sdk/src/apis/hebe/constants.py diff --git a/src/sdk/src/apis/hebe/exceptions.py b/sdk/src/apis/hebe/exceptions.py similarity index 100% rename from src/sdk/src/apis/hebe/exceptions.py rename to sdk/src/apis/hebe/exceptions.py diff --git a/src/sdk/src/apis/hebe/signer.py b/sdk/src/apis/hebe/signer.py similarity index 100% rename from src/sdk/src/apis/hebe/signer.py rename to sdk/src/apis/hebe/signer.py diff --git a/src/sdk/src/apis/hebe/student.py b/sdk/src/apis/hebe/student.py similarity index 100% rename from src/sdk/src/apis/hebe/student.py rename to sdk/src/apis/hebe/student.py diff --git a/src/sdk/src/apis/prometheus_web/client.py b/sdk/src/apis/prometheus_web/client.py similarity index 100% rename from src/sdk/src/apis/prometheus_web/client.py rename to sdk/src/apis/prometheus_web/client.py diff --git a/src/sdk/src/apis/prometheus_web/constants.py b/sdk/src/apis/prometheus_web/constants.py similarity index 100% rename from src/sdk/src/apis/prometheus_web/constants.py rename to sdk/src/apis/prometheus_web/constants.py diff --git a/src/sdk/src/apis/prometheus_web/exceptions.py b/sdk/src/apis/prometheus_web/exceptions.py similarity index 100% rename from src/sdk/src/apis/prometheus_web/exceptions.py rename to sdk/src/apis/prometheus_web/exceptions.py diff --git a/src/sdk/src/interfaces/core/interface.py b/sdk/src/interfaces/core/interface.py similarity index 100% rename from src/sdk/src/interfaces/core/interface.py rename to sdk/src/interfaces/core/interface.py diff --git a/src/sdk/src/interfaces/prometheus/context.py b/sdk/src/interfaces/prometheus/context.py similarity index 100% rename from src/sdk/src/interfaces/prometheus/context.py rename to sdk/src/interfaces/prometheus/context.py diff --git a/src/sdk/src/interfaces/prometheus/exceptions.py b/sdk/src/interfaces/prometheus/exceptions.py similarity index 100% rename from src/sdk/src/interfaces/prometheus/exceptions.py rename to sdk/src/interfaces/prometheus/exceptions.py diff --git a/src/sdk/src/interfaces/prometheus/interface.py b/sdk/src/interfaces/prometheus/interface.py similarity index 100% rename from src/sdk/src/interfaces/prometheus/interface.py rename to sdk/src/interfaces/prometheus/interface.py diff --git a/src/sdk/src/interfaces/prometheus/utils.py b/sdk/src/interfaces/prometheus/utils.py similarity index 100% rename from src/sdk/src/interfaces/prometheus/utils.py rename to sdk/src/interfaces/prometheus/utils.py diff --git a/src/sdk/src/models/exam.py b/sdk/src/models/exam.py similarity index 100% rename from src/sdk/src/models/exam.py rename to sdk/src/models/exam.py diff --git a/src/sdk/src/models/grade.py b/sdk/src/models/grade.py similarity index 100% rename from src/sdk/src/models/grade.py rename to sdk/src/models/grade.py diff --git a/src/sdk/src/models/note.py b/sdk/src/models/note.py similarity index 100% rename from src/sdk/src/models/note.py rename to sdk/src/models/note.py diff --git a/src/sdk/src/models/student.py b/sdk/src/models/student.py similarity index 100% rename from src/sdk/src/models/student.py rename to sdk/src/models/student.py diff --git a/src/impl/hebece/src/__init__.py b/src/impl/hebece/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/impl/hebece/src/api.py b/src/impl/hebece/src/api.py new file mode 100644 index 0000000..718f19e --- /dev/null +++ b/src/impl/hebece/src/api.py @@ -0,0 +1,152 @@ +from datetime import datetime +import requests +import json +from bs4 import BeautifulSoup +from impl.hebece.src.signer import * +from impl.hebece.src.utils import * +from impl.hebece.src.const import * + +session = requests.Session() +certificate, fingerprint, private_key = generate_key_pair() + +def getDebugInfo(data): + data = json.loads(data) + status = data.get("Status", {}) + code = status.get("Code") + message = status.get("Message") + return code, message + +def makeRequest(url): + digest, canonical_url, signature = get_signature_values(fingerprint, private_key, body=None, full_url=url, timestamp=datetime.now()) + + headers = makeHeader(signature, canonical_url) + + response = requests.get(url, headers=headers) + content = response.text + + dinfo = getDebugInfo(content) + return content, dinfo + +def APILogin(login, password): + + url = "https://eduvulcan.pl/" + response1 = session.get(url) + + url = "https://eduvulcan.pl/logowanie" + response2 = session.get(url) + + soup = BeautifulSoup(response2.text, 'html.parser') + token_input = soup.find('input', {'name': '__RequestVerificationToken'}) + token = {"__RequestVerificationToken": token_input['value']} + + cookies = {**response1.cookies.get_dict(), **response2.cookies.get_dict()} + cookies_str = "; ".join([f"{key}={value}" for key, value in cookies.items()]) + cookies_str += f"; __RequestVerificationToken={token_input['value']}" + + # Prometheus + url = "https://eduvulcan.pl/logowanie?ReturnUrl=%2fapi%2fap" + headers = makeLoginHeader(cookies_str) + + data = { + "Alias": login, + "Password": password, + "captchaUser": "", + "__RequestVerificationToken": token_input['value'], + } + + response = session.post(url, headers=headers, data=data) + content = response.text + cookie_jar = response.cookies.get_dict() + + try: + soup = BeautifulSoup(content, "html.parser") + input_element = soup.find("input", {"id": "ap"}) + value = input_element["value"] + parsed_json = json.loads(value) + + tokens = parsed_json.get("Tokens", []) + token = " ".join(tokens) + + return token + except TypeError: + pass + +def JWTLogin(token, debug=False): + tenant = get_tenant_from_jwt(token) + + url = f"https://lekcjaplus.vulcan.net.pl/{tenant}{JWT}" + + RequestId = getRandomIdentifier() + SelfIdentifier = getRandomIdentifier() + + Certificate = certificate + CertificateThumbprint = fingerprint + Tokens = token + + digest, canonical_url, signature = get_signature_values(fingerprint, private_key, body=None, full_url=url, timestamp=datetime.now()) + + headers = makeHeader(signature, canonical_url) + + timestamp = datetime.now() + date = getDate() + + body = { + "AppName": "DzienniczekPlus 3.0", + "AppVersion": "24.11.07 (G)", + "NotificationToken": None, + "API": 1, + "RequestId": str(RequestId), + "Timestamp": getTimestamp(), + "TimestampFormatted": str(date), + "Envelope": { + "OS": DEVICE_OS, + "Certificate": Certificate, + "CertificateType": "X509", + "DeviceModel": DEVICE, + "SelfIdentifier": str(SelfIdentifier), + "CertificateThumbprint": CertificateThumbprint, + "Tokens": [Tokens] + } + } + + body_json = json.dumps(body, indent=4) + + response = session.post(url, headers=headers, data=body_json) + content = response.text + + if debug: + dinfo = getDebugInfo(content) + return content, dinfo + + return content + +def HEBELogin(tenant, debug=False): + url = f"https://lekcjaplus.vulcan.net.pl/{tenant}{HEBE}?mode=2&lastSyncDate=1970-01-01%2001%3A00%3A00" + content, dinfo = makeRequest(url) + return content, dinfo + +def getLuckyNumber(tenant, schoolid, pupilid, constituentid, debug=False): + timestamp = datetime.now() + date = timestamp.strftime("%Y-%m-%d") + + url = f"https://lekcjaplus.vulcan.net.pl/{tenant}/{schoolid}{LUCKY}?pupilId={pupilid}&constituentId={constituentid}&day={date}" + content, dinfo = makeRequest(url) + return content, dinfo + +def getGrades(tenant, schoolid, pupilid, unitid, periodid, debug=False): + url = f"https://lekcjaplus.vulcan.net.pl/{tenant}/{schoolid}/api/mobile/grade/byPupil?unitId={unitid}&pupilId={pupilid}&periodId={periodid}&lastSyncDate=1970-01-01%2001%3A00%3A00&lastId=-2147483648&pageSize=500" + + content, dinfo = makeRequest(url) + return content, dinfo + +def getTimetable(tenant, schoolid, pupilid, start_date, end_date, debug=False): + url = f"https://lekcjaplus.vulcan.net.pl/{tenant}/{schoolid}/api/mobile/schedule/withchanges/byPupil?pupilId={pupilid}&dateFrom={start_date}&dateTo={end_date}&lastId=-2147483648&pageSize=500&lastSyncDate=1970-01-01%2001%3A00%3A00" + + content, dinfo = makeRequest(url) + return content, dinfo + +def getExams(tenant, schoolid, pupilid, start_date, end_date, debug=False): + url = f"https://lekcjaplus.vulcan.net.pl/{tenant}/{schoolid}/api/mobile/exam/byPupil?pupilId={pupilid}&dateFrom={start_date}&dateTo={end_date}&lastId=-2147483648&pageSize=500&lastSyncDate=1970-01-01%2001%3A00%3A00" + + content, dinfo = makeRequest(url) + return content, dinfo diff --git a/src/impl/hebece/src/const.py b/src/impl/hebece/src/const.py new file mode 100644 index 0000000..750d5cb --- /dev/null +++ b/src/impl/hebece/src/const.py @@ -0,0 +1,52 @@ +from impl.hebece.src.utils import * +# Endpoints + +JWT = "/api/mobile/register/jwt" +HEBE = "/api/mobile/register/hebe" +LUCKY = "/api/mobile/school/lucky" +GRADES = "/api/mobile/grade/byPupil" +TIMETABLE = "/api/mobile/schedule/withchanges/byPupil" +EXAMS = "/api/mobile/exam/byPupil" + +# Header +DEVICE = "SM-G935F" +DEVICE_OS = "Android" +APPVERSION = "24.11.07 (G)" + +def makeHeader(signature, canonical_url): + return { + "accept-encoding": "gzip", + "content-type": "application/json", + "host": "lekcjaplus.vulcan.net.pl", + "signature": signature, + "user-agent": "Dart/3.3 (dart:io)", + "vapi": "1", + "vcanonicalurl": canonical_url, + "vdate": getDate(), + "vos": DEVICE_OS, + "vversioncode": "640", + } + +def makeLoginHeader(cookies): + return { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "Accept-Encoding": "gzip, deflate, br, zstd", + "Accept-Language": "en-US,en;q=0.9", + "Cache-Control": "max-age=0", + "Connection": "keep-alive", + "Content-Type": "application/x-www-form-urlencoded", + "Cookie": cookies, + "Host": "eduvulcan.pl", + "Origin": "https://eduvulcan.pl", + "Referer": "https://eduvulcan.pl/logowanie?ReturnUrl=%2fapi%2fap", + "sec-ch-ua": "\"Chromium\";v=\"130\", \"Android WebView\";v=\"130\", \"Not?A_Brand\";v=\"99\"", + "sec-ch-ua-mobile": "?1", + "sec-ch-ua-platform": "\"Android\"", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-User": "?1", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Linux; Android 13; SM-G935F Build/TQ3A.230901.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.6723.107 Mobile Safari/537.36", + "X-Requested-With": "pl.edu.vulcan.hebe.ce", + } \ No newline at end of file diff --git a/src/impl/hebece/src/parser.py b/src/impl/hebece/src/parser.py new file mode 100644 index 0000000..18cc204 --- /dev/null +++ b/src/impl/hebece/src/parser.py @@ -0,0 +1,37 @@ +import requests +import json +import uuid +import hashlib +import sqlite3 +import os +import base64 +from impl.hebece.src.signer import * +from impl.hebece.src.utils import * +from impl.hebece.src.api import * +from datetime import datetime, timedelta +from bs4 import BeautifulSoup + +def getUserInfo(tenant): + content, dinfo = HEBELogin(tenant) + + data = json.loads(content) + envelope = data.get("Envelope", [])[0] + + pupil = envelope.get("Pupil", {}) + unit = envelope.get("Unit", {}) + links = envelope.get("Links", {}) + ConstituentUnit = envelope.get("ConstituentUnit", {}) + periods = envelope.get("Periods", []) + + + Name = pupil.get("FirstName", {}) + SecondName = pupil.get("SecondName", {}) + Surname = pupil.get("Surname", {}) + Class = envelope.get("ClassDisplay", {}) + PupilID = pupil.get("Id", {}) + SchoolID = links.get("Symbol", {}) + ConstituentID = ConstituentUnit.get("Id", {}) + UnitID = unit.get("Id", {}) + PeriodID = next((period.get('Id') for period in periods if period.get('Current')), None) + + return Name, SecondName, Surname, Class, PupilID, SchoolID, ConstituentID, UnitID, PeriodID \ No newline at end of file diff --git a/src/impl/hebece/src/signer.py b/src/impl/hebece/src/signer.py new file mode 100644 index 0000000..dfc9369 --- /dev/null +++ b/src/impl/hebece/src/signer.py @@ -0,0 +1,98 @@ +import cryptography +import re +import urllib +import base64 +from OpenSSL import crypto +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives.serialization import load_der_private_key +from cryptography.hazmat.backends import default_backend + + +def get_encoded_path(full_url): + path = re.search(r"(api/mobile/.+)", full_url) + if path is None: + raise ValueError( + "The URL does not seem correct (does not match `(api/mobile/.+)` regex)" + ) + return urllib.parse.quote(path[1], safe="").lower() + +def get_digest(body): + if not body: + return None + + m = hashlib.sha256() + m.update(bytes(body, "utf-8")) + return base64.b64encode(m.digest()).decode("utf-8") + +def get_headers_list(body, digest, canonical_url, timestamp): + sign_data = [ + ["vCanonicalUrl", canonical_url], + ["Digest", digest] if body else None, + ["vDate", timestamp.strftime("%a, %d %b %Y %H:%M:%S GMT")], + ] + + return ( + " ".join(item[0] for item in sign_data if item), + "".join(item[1] for item in sign_data if item), + ) + +def get_signature(data, private_key): + # Convert data to a string representatio + data_str = ( + json.dumps(data) + if isinstance(data, (dict, list)) + else str(data) + ) + + # Decode the base64 private key and load it + private_key_bytes = base64.b64decode(private_key) + pkcs8_key = load_der_private_key(private_key_bytes, password=None, backend=default_backend()) + + # Sign the data + signature = pkcs8_key.sign( + bytes(data_str, "utf-8"), + padding.PKCS1v15(), + hashes.SHA256() + ) + + # Encode the signature in base64 and return + return base64.b64encode(signature).decode("utf-8") + +def get_signature_values(fingerprint, private_key, body, full_url, timestamp): + canonical_url = get_encoded_path(full_url) + digest = get_digest(body) + headers, values = get_headers_list(body, digest, canonical_url, timestamp) + signature = get_signature(values, private_key) + + return ( + "SHA-256={}".format(digest) if digest else None, + canonical_url, + 'keyId="{}",headers="{}",algorithm="sha256withrsa",signature=Base64(SHA256withRSA({}))'.format( + fingerprint, headers, signature + ), + ) + +def pem_getraw(pem): + return pem.decode("utf-8").replace("\n", "").split("-----")[2] + +def generate_key_pair(): + pkcs8 = crypto.PKey() + pkcs8.generate_key(crypto.TYPE_RSA, 2048) + + x509 = crypto.X509() + x509.set_version(2) + x509.set_serial_number(1) + subject = x509.get_subject() + subject.CN = "APP_CERTIFICATE CA Certificate" + x509.set_issuer(subject) + x509.set_pubkey(pkcs8) + x509.sign(pkcs8, "sha256") + x509.gmtime_adj_notBefore(0) + x509.gmtime_adj_notAfter(20 * 365 * 24 * 60 * 60) + + certificate = pem_getraw(crypto.dump_certificate(crypto.FILETYPE_PEM, x509)) + fingerprint = x509.digest("sha1").decode("utf-8").replace(":", "").lower() + private_key = pem_getraw(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkcs8)) + + return certificate, fingerprint, private_key diff --git a/src/impl/hebece/src/utils.py b/src/impl/hebece/src/utils.py new file mode 100644 index 0000000..1f240fd --- /dev/null +++ b/src/impl/hebece/src/utils.py @@ -0,0 +1,54 @@ +import base64 +import json +import uuid +from datetime import datetime, timedelta + +def encodebase64(data): + return base64.b64encode(data.encode("utf-8")).decode("utf-8") + +def decodebase64(data): + return base64.b64decode(data.encode("utf-8")).decode("utf-8") + +def get_tenant_from_jwt(token): + try: + # Split the JWT into parts + header, payload, signature = token.split('.') + + # Decode the payload from Base64 + # Add padding + payload += '=' * (-len(payload) % 4) + decoded_payload = base64.urlsafe_b64decode(payload).decode('utf-8') + + # Parse the payload as JSON + payload_json = json.loads(decoded_payload) + + # Return the tenant + return payload_json.get('tenant') + except (ValueError, json.JSONDecodeError, KeyError) as e: + print(f"Error decoding JWT: {e}") + return None + +def getRandomIdentifier(): + ruuid = str(uuid.uuid4()) + + return ruuid + +def get_current_week(): + # Get today's date + today = datetime.today() + # Calculate the start of the week (Monday) + start_of_week = today - timedelta(days=today.weekday()) + # Calculate the end of the week (Sunday) + end_of_week = start_of_week + timedelta(days=6) + + # Return the dates as formatted strings + return start_of_week.strftime('%Y-%m-%d'), end_of_week.strftime('%Y-%m-%d') + +def getDate(): + return datetime.now().strftime("%a, %d %b %Y %H:%M:%S GMT") + +def getTimestamp(): + now = datetime.now() + Timestamp = now.timestamp() + + return Timestamp \ No newline at end of file diff --git a/src/test.py b/src/test.py new file mode 100644 index 0000000..8599afb --- /dev/null +++ b/src/test.py @@ -0,0 +1,37 @@ +from impl.hebece.src.api import * +from impl.hebece.src.parser import * +from impl.hebece.src.utils import * +from impl.hebece.src.signer import * + +if __name__ == '__main__': + today = datetime.today().strftime('%d-%m-%y') + start_date, end_date = get_current_week() + + token = APILogin(login = input("login: "),password = input("password: ")) + if not token: + print("You entered wrong login, password or VULCAN asked for captcha. Verify your login and password and try to log into eduVULCAN from your browser.") + input("Press Enter to exit...") + exit() + + tenant = get_tenant_from_jwt(token) + + content, dinfoJWT = JWTLogin(token, debug=True) + + content, dinfoHEBE = HEBELogin(tenant, debug=True) + + Name, SecondName, Surname, Class, PupilID, SchoolID, ConstituentID, UnitID, PeriodID = getUserInfo(tenant) + + content, dinfoLUCK = getLuckyNumber(tenant=tenant, schoolid=SchoolID, pupilid=PupilID, constituentid=ConstituentID, debug=True) + + content, dinfoGRADE = getGrades(tenant=tenant, schoolid=SchoolID, pupilid=PupilID, unitid=UnitID, periodid=PeriodID, debug=True) + + content, dinfoTIME = getTimetable(tenant=tenant, schoolid=SchoolID, pupilid=PupilID, start_date=start_date, end_date=end_date, debug=True) + + content, dinfoEXAM = getExams(tenant=tenant, schoolid=SchoolID, pupilid=PupilID, start_date=start_date, end_date=end_date, debug=True) + + print(f"\nJWT Status: {dinfoJWT[0]} {dinfoJWT[1]}") + print(f"HEBE Status: {dinfoHEBE[0]} {dinfoHEBE[1]}") + print(f"Lucky Number Status: {dinfoLUCK[0]} {dinfoLUCK[1]}") + print(f"Grades Status: {dinfoGRADE[0]} {dinfoGRADE[1]}") + print(f"Timetable Status: {dinfoTIME[0]} {dinfoTIME[1]}") + print(f"Exams Status: {dinfoEXAM[0]} {dinfoEXAM[1]}") \ No newline at end of file