diff --git a/compliant_api.py b/compliant_api.py index 6872fff..88e826a 100644 --- a/compliant_api.py +++ b/compliant_api.py @@ -14,6 +14,12 @@ def md5(input): m.update(bytes(input,encoding="utf-8")) return m.hexdigest() +def generate_key(ls): + key = "" + for i in range(64): + key += str(random.choice(list(range(10)) + list("abcdefghijklmnopqrstuvwxyz") + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ"))) + ls.append(key) + return key #def check_sig(keys): # try: @@ -26,15 +32,28 @@ def md5(input): -def handle(path,keys): -# log("API REQUEST") -# log(str(path)) -# for k in keys: -# log(str(k) + ": " + str(keys.getall(k))) +def handle(path,keys,headers,auth): + print("API request: " + str(path)) + print("Keys:") + for k in keys: + print("\t" + str(k) + ": " + str(keys.get(k))) + print("Headers:") + for h in headers: + print("\t" + str(h) + ": " + str(headers.get(h))) + print("Auth: " + str(auth)) - if path[0] == "audioscrobbler": - return handle_audioscrobbler(path[1:],keys) + try: + if path[0] in ["audioscrobbler","gnukebox","gnufm"]: + response = handle_audioscrobbler(path[1:],keys) + elif path[0] in ["listenbrainz","lbrnz"]: + response = handle_listenbrainz(path[1:],keys,headers) + else: + response = {"error_message":"Invalid scrobble protocol"} + except: + response = {"error_message":"Unknown API error"} + print("Response: " + str(response)) + return response # no need to save these on disk, clients can always request a new session mobile_sessions = [] @@ -46,13 +65,18 @@ def handle_audioscrobbler(path,keys): if keys.get("method") == "auth.getMobileSession": token = keys.get("authToken") user = keys.get("username") - for key in database.allAPIkeys(): - if md5(user + md5(key)) == token: - sessionkey = "" - for i in range(64): - sessionkey += str(random.choice(list(range(10)) + list("abcdefghijklmnopqrstuvwxyz") + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ"))) - mobile_sessions.append(sessionkey) + password = keys.get("password") + # either username and password + if user is not None and password is not None: + if password in database.allAPIkeys(): + sessionkey = generate_key(mobile_sessions) return {"session":{"key":sessionkey}} + # or username and token (deprecated by lastfm) + elif user is not None and token is not None: + for key in database.allAPIkeys(): + if md5(user + md5(key)) == token: + sessionkey = generate_key(mobile_sessions) + return {"session":{"key":sessionkey}} return {"error":4} @@ -77,3 +101,20 @@ def handle_audioscrobbler(path,keys): return {"scrobbles":{"@attr":{"ignored":0}}} return {"error":3} + + else: + return {"error_message":"API version not supported"} + + +def handle_listenbrainz(path,keys,headers): + + if path[0] == "1": + + if path[1] == "submit-listens": + + if headers.get("Authorization") is not None: + print(headers.get("Authorization")) + return {"wat":"wut"} + + else: + return {"error_message":"API version not supported"} diff --git a/database.py b/database.py index 5f896a3..e324aa3 100644 --- a/database.py +++ b/database.py @@ -1,6 +1,5 @@ # server -from bottle import Bottle, route, get, post, run, template, static_file, request, response, FormsDict -import waitress +from bottle import request, response, FormsDict # rest of the project from cleanup import * from utilities import * @@ -29,8 +28,6 @@ import urllib -dbserver = Bottle() - dblock = Lock() #global database lock SCROBBLES = [] # Format: tuple(track_ref,timestamp,saved) @@ -183,6 +180,47 @@ def getTrackID(artists,title): ######## +# silly patch to get old syntax working without dbserver + +# function to register all the functions to the real server +def register_subroutes(server,path): + for subpath in dbserver.handlers_get: + func = dbserver.handlers_get[subpath] + decorator = server.get(path + subpath) + decorator(func) + for subpath in dbserver.handlers_post: + func = dbserver.handlers_post[subpath] + decorator = server.post(path + subpath) + decorator(func) + + +# fake server +class FakeBottle: + def __init__(self): + self.handlers_get = {} + self.handlers_post = {} + + # these functions pretend that they're the bottle decorators, but only write + # down which functions asked for them so they can later report their names + # to the real bottle server + def get(self,path): + def register(func): + self.handlers_get[path] = func + return func + return register + def post(self,path): + def register(func): + self.handlers_post[path] = func + return func + return register + + def route(self,path): + return self.get(path) + + +dbserver = FakeBottle() + + @dbserver.route("/test") @@ -618,15 +656,14 @@ def post_scrobble(): # standard-compliant scrobbling methods @dbserver.post("/s/") -def sapi(path): - path = path.split("/") - keys = FormsDict.decode(request.forms) - return compliant_api.handle(path,keys) @dbserver.get("/s/") def sapi(path): path = path.split("/") - keys = FormsDict.decode(request.query) - return compliant_api.handle(path,keys) + path = list(filter(None,path)) + keys = FormsDict.decode(request.params) + headers = request.headers + auth = request.auth + return compliant_api.handle(path,keys,headers,auth) @@ -806,17 +843,14 @@ def search(): # Starts the server -def runserver(PORT): - log("Starting database server...") +def start_db(): + log("Starting database...") global lastsync lastsync = int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp()) build_db() - - loadAPIkeys() - - run(dbserver, host='::', port=PORT, server='waitress') - log("Database server reachable!") + #run(dbserver, host='::', port=PORT, server='waitress') + log("Database reachable!") def build_db(): diff --git a/server.py b/server.py index c0c27e0..c5e771e 100755 --- a/server.py +++ b/server.py @@ -28,7 +28,7 @@ from urllib.error import * #settings.config(files=["settings/default.ini","settings/settings.ini"]) #settings.update("settings/default.ini","settings/settings.ini") -MAIN_PORT, DATABASE_PORT = settings.get_settings("WEB_PORT","API_PORT") +MAIN_PORT = settings.get_settings("WEB_PORT") webserver = Bottle() @@ -68,10 +68,19 @@ def customerror(error): return html + +#@webserver.get("/api/") +#def api(pth): +# return database.handle_get(pth,request) + +#@webserver.post("/api/") +#def api_post(pth): +# return database.handle_post(pth,request) + # this is the fallback option. If you run this service behind a reverse proxy, it is recommended to rewrite /db/ requests to the port of the db server # e.g. location /db { rewrite ^/db(.*)$ $1 break; proxy_pass http://yoururl:12349; } -@webserver.get("/api/") +#@webserver.get("/api/") def database_get(pth): keys = FormsDict.decode(request.query) # The Dal★Shabet handler keystring = "?" @@ -88,13 +97,22 @@ def database_get(pth): response.status = e.code return -@webserver.post("/api/") +#@webserver.post("/api/") def database_post(pth): - response.set_header("Access-Control-Allow-Origin","*") + #print(request.headers) + #response.set_header("Access-Control-Allow-Origin","*") try: - proxyresponse = urllib.request.urlopen("http://[::1]:" + str(DATABASE_PORT) + "/" + pth,request.body) + proxyrequest = urllib.request.Request( + url="http://[::1]:" + str(DATABASE_PORT) + "/" + pth, + data=request.body, + headers=request.headers, + method="POST" + ) + proxyresponse = urllib.request.urlopen(proxyrequest) + contents = proxyresponse.read() response.status = proxyresponse.getcode() + response.headers = proxyresponse.headers response.content_type = "application/json" return contents except HTTPError as e: @@ -215,7 +233,9 @@ setproctitle.setproctitle("Maloja") ## start database server #_thread.start_new_thread(SourceFileLoader("database","database.py").load_module().runserver,(DATABASE_PORT,)) -_thread.start_new_thread(database.runserver,(DATABASE_PORT,)) +#_thread.start_new_thread(database.runserver,(DATABASE_PORT,)) +database.start_db() +database.register_subroutes(webserver,"/api") log("Starting up Maloja server...") run(webserver, host='::', port=MAIN_PORT, server='waitress') diff --git a/settings/default.ini b/settings/default.ini index e3c0ff6..67a11e2 100644 --- a/settings/default.ini +++ b/settings/default.ini @@ -1,7 +1,6 @@ [HTTP] WEB_PORT = 42010 -API_PORT = 42011 [Third Party Services]