def process( app ): query = app.query response = app.response session = app.session source_id = int( query.parms["id"] ) mode = query.parms["mode"] if "mode" in query.parms else "convert" if app.user.can_read( source_id ): source_obj = files.File( app, object_id=source_id ) if re.match( r"^video/.*", source_obj.media_type ): new_poster_offset = float(query.parms["poster_offset"]) if "poster_offset" in query.parms else None new_poster_id = int(query.parms["poster_id"]) if "poster_id" in query.parms else None if new_poster_id and app.user.can_write( source_id ) and app.user.can_read( new_poster_id ): new_poster_obj = files.File( app, object_id=new_poster_id ) else: new_poster_obj = None # zur Bestimmung der Größen und Bitraten der Alternativobjekte identifizieren wir zunächst das Orignalobjekt: source_size = source_obj.get_size() source_meta = source_obj.identify() source_width = int( source_meta["mplayer"]["id"]["video"]["width"] ) source_rate = round(source_size*8/float(source_meta["mplayer"]["id"]["length"])/1000) results = [] class ConversionDescription: def __init__( self, role, width, media_type, rate=0, condition=lambda x: True ): self.role = role self.width = width self.media_type = media_type self.rate = rate self.condition = condition def applies( self ): return self.condition(self) def __eq__( self, other ): # rate is not part of equality, because this is used to compare conversion candidates with already existing substitutes # and bitrate is deemed to specific for that purpose; we just assume rates are ok/equal for a given size return ( (self.role, self.width, self.media_type, self.applies()) == (other.role, other.width, other.media_type, other.applies()) ) def __str__( self ): return( "(role=%s, width=%dpx, media_type=%s, rate=%dk, applies=%s)" % (self.role, self.width, self.media_type, self.rate, str(self.applies())) ) # we want a poster image, an mp4-substitute in source_width for non-mp4 sources and a scaled down mp4-substitute for big sources: missing_conversions = [ ConversionDescription( role="poster", width=min(1280, source_width), media_type="image/jpeg" ), ConversionDescription( role="compatible", width=source_width, media_type="video/mp4", rate=source_rate, condition = lambda self: source_obj.media_type != "video/mp4" ), ConversionDescription( role="compatible", width=min(1280, source_width), media_type="video/mp4", rate=min(2000, source_rate), condition = lambda self: (self.width < (source_width*0.8)) and (self.rate < (source_rate*0.8)) ), ] c = app.db.cursor() # Hier müssen wir zunächst prüfen ob das angefragte Objekt selbst schon ein Substitute-Objekt ist, ... c.execute( """select original_id from substitutes where substitute_id=?""", [source_obj.id] ) if c.fetchone(): # ... denn für Substitute-Objekte sollten keine weiteren Substitute-Objekte generiert werden. missing_conversions = [] c.execute( """select s.substitute_id, s.type, s.size, s.priority, sobj.id from substitutes s left join objects sobj on sobj.id=s.substitute_id where s.original_id=?""", [source_obj.id] ) for row in c: substitute = { "substitute_id" : int(row[0]), "type" : row[1], "size" : int(row[2]), "priority" : int(row[3]) } sobj_id = row[4] if sobj_id==None: # Zombie-Substitutes bereinigen (FIXME: sollte das von DBObject.delete() erledigt werden?): del_c = app.db.cursor() del_c.execute( """delete from substitutes where substitute_id=?""", [substitute["substitute_id"]] ) else: substitute_obj = db_object.DBObject( app, object_id=substitute["substitute_id"] ) conversion = ConversionDescription( role=substitute["type"], width=substitute["size"], media_type=substitute_obj.media_type ) if conversion in missing_conversions: if substitute["type"]=="poster" and (new_poster_offset or new_poster_obj): # bestehendes Poster-Substitute entfernen, da es neu definiert werden soll: del_c = app.db.cursor() del_c.execute( """delete from substitutes where original_id=? and substitute_id=?""", [source_obj.id,substitute_obj.id] ) else: missing_conversions.remove( conversion ) results.append( substitute ) else: results.append( substitute ) error_list = [] if mode == "convert": # Alle fehlende Objekte sofort ohne Daten anlegen, um Mehrfachkonvertierung zu vermeiden: new_objects = [] for conversion in [x for x in missing_conversions if x.applies()]: # Privilege-Escalation damit nicht nur der Eigentümer des Target-Objekts diesen Code ausführen kann: app_old_user = app.user app.user = user.get_admin_user(app) existing_object_id = None if conversion.role=="poster" and new_poster_obj: existing_object_id = new_poster_obj.id new_obj = files.File( app, object_id=existing_object_id, parent_id=source_obj.id, media_type=conversion.media_type ) if not existing_object_id: new_obj.conversion = conversion; new_objects.append( new_obj ) substitute = { "substitute_id" : new_obj.id, "type" : conversion.role, "size" : conversion.width, "priority" : None } results.append( substitute ) app.user = app_old_user c = app.db.cursor() c.execute( """insert into substitutes (original_id, substitute_id, type, size) values(?,?,?,?)""", [source_obj.id, new_obj.id, conversion.role, conversion.width] ) # Konvertierungsvorgänge für angelegte Objekte durchführen: for new_obj in new_objects: # this may take a long time, so wie have to commit first: app.db.commit() base_type, sub_type = new_obj.media_type.split("/") new_tmp_name = new_obj.storage_path+".tmp."+sub_type if( re.match(r"^video/.*", new_obj.media_type) ): # Konvertierung mit geänderter Breite bei Erhaltug des Seitenverhältnisses: # http://stackoverflow.com/questions/8218363/maintaining-ffmpeg-aspect-ratio p = subprocess.Popen( ["ffmpeg", "-y", "-i", source_obj.storage_path, "-vf", "scale=%d:trunc(ow/a/2)*2" % (new_obj.conversion.width), "-r", "25", "-b", "%dk" % new_obj.conversion.rate, "-qmin", "0", "-strict", "-2", new_tmp_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) elif( new_obj.conversion.role=="poster" and new_obj.media_type == "image/jpeg" ): # Vorschaubild bei Zeitindex 3s extrahieren: p = subprocess.Popen( ["ffmpeg", "-y", "-i", source_obj.storage_path, "-vf", "scale=%d:trunc(ow/a/2)*2" % (new_obj.conversion.width), "-ss", str(new_poster_offset if new_poster_offset else 3), "-vframes", "1", new_tmp_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) else: raise NotImplementedError( "missing operation for conversion: %s" % (str(new_obj.conversion)) ) stdout, stderr = p.communicate() if p.returncode!=0: try: # FIXME: Löschenfunktion nach DBObject ausmodularisieren und Dateibereinigung nach files.File: # Privilege-Escalation damit nicht nur der Eigentümer des Target-Objekts diesen Code ausführen kann: app_old_user = app.user app.user = user.get_admin_user(app) db_object.DBObject.delete_in( app, [new_obj.id] ) app.user = app_old_user os.remove( new_tmp_name ) c = app.db.cursor() c.execute( """delete from substitutes where original_id=? and substitute_id=?""", [source_obj.id, new_obj.id] ) results = [x for x in results if x["substitute_id"]!=new_obj.id] except Exception as e: error_list.append( e ) errmsg = stderr.decode().split("\n")[-1] error_list.append( errors.InternalProgramError(errmsg) ) else: os.rename( new_tmp_name, new_obj.storage_path ) # Fehlerbehandlung: if error_list: msg = "" for error in error_list: if msg: msg += "; " msg += str(error) raise errors.InternalProgramError( msg ) else: for result in results: result["substitute_object"] = get_module.get(app, object_ids=[result["substitute_id"]]) response.output = json.dumps( {"succeeded": True, "substitutes": results} ) elif re.match( r"^audio/.*", source_obj.media_type ): # TODO: add safe conversions results = [] response.output = json.dumps( {"succeeded": True, "substitutes": results} ) else: raise NotImplementedError( "unsupported media type: "+source_obj.media_type ) else: raise errors.PrivilegeError()
def process( app ): """Registrierungsanfrage prüfen (Eindeutigkeit des Nutzernamens, syntaktische Korrektheit der Email-Adresse, korrekte Doppelangabe und kryptographische Mindestanforderung des Passwortes) und ggf. vorläufiges Nutzerobjekt anlegen und Bestätigungsanfrage im E-Mail-Ausgang speichern, die einen Link auf dieses Modul mit einer zusätzlichen E-Mail-Session-ID (msid) enthält. Die msid wurde zuvor, genau wie der eindeutige Schlüssel des vorläufigen Nutzerobjektes, serverseitig im Session-Parametersatz von sid hinterlegt. Die Freischaltung der Registrierung sollte nur erfolgen, wenn dieses Modul über den Bestätigungslink aufgerufen wurde (msid vorhanden) und URL-msid sowie serverseitig hinterlegte Session-msid übereinstimmen. Falls der msid-Vergleich fehlschlägt könnte dem Nutzer angeboten werden die E-Mail-Verifizierung via Knopfdruck mit neuer msid zu wiederholen, da es sein kann, dass die ursprüngliche Registrierung versehentlich in einem anderen Browser erfolgte, als der spätere Abruf des E-Mail-Links (Standardbrowser). Hierzu müsste msid als reguläre Session-Kopie von sid angelegt worden und genau wie sid mit dem vorläufigen Nutzerobjekt verknüpft worden sein, um als gültige Fallback-Session fungieren zu können. Alternativ könnnte man einfach sid und msid in den E-Mail-Link aufnehmen. Beide Fehlertoleranz-Ansätze sind verwundbar für Lauschangriffe auf den E-Mail-Kanal.""" query = app.query response = app.response session = app.session if "msid" in query.parms: confirmation_session = application.Session( app, query.parms["msid"] ) #if "registration_sid" in confirmation_session.parms \ #and confirmation_session.parms["registration_sid"] == session.sid \ if "registration_user_id" in confirmation_session.parms: user_id = int( confirmation_session.parms["registration_user_id"] ) usr = user.User( app=app, user_id=user_id ) # Nutzer Lese-/Schreib-Zugriff auf sein eigenes Nutzerobjekt geben: usr.grant_read( user_id ) usr.grant_write( user_id ) confirmation_session.parms=[] confirmation_session.store() response.output = json.dumps( {"succeeded" : True} ) try: send_registration_notification( app=app, usr=usr ) except Exception as e: for line in traceback.format_exception( Exception, e, e.__traceback__ ): app.log( line ) return else: try: send_registration_failed_notification( app=app, confirmation_session=confirmation_session ) except Exception as e: for line in traceback.format_exception( Exception, e, e.__traceback__ ): app.log( line ) raise Exception( "Registration confirmation failed" ) elif "nick" in query.parms and "password" in query.parms \ and "email" in query.parms: nick = query.parms["nick"] password = query.parms["password"] email = query.parms["email"] if "reconfirm" in query.parms: # Bei gültigen Login-Daten (keine Ausnahme in check_login), # Neuauslösung der Email-Bestätigung an eine ggf. geänderte # Adresse erlauben: usr = login.check_login( app ) app_old_user = app.user app.user = user.get_admin_user(app) usr.update( email=email ) app.user = app_old_user send_confirmation_request( app=app, user_id=usr.id ) response.output = json.dumps( {"succeeded" : True} ) return else: app_old_user = app.user app.user = user.get_admin_user(app) usr = user.User( app=app, parent_id=1, nick=nick, plain_password=password, email=email ) app.user = app_old_user # FIXME: Falls das Versenden der E-Mail hier sofort fehlschlägt, # müssen wir user.create entweder zurückrollen oder den # fehlgeschlagenen Zustellungsversuch zwischenspeichern, sodass # er vom Administrator oder einem Cronjob später nochmal ausgelöst # werden kann: send_confirmation_request( app=app, user_id=usr.id ) response.output = json.dumps( {"succeeded" : True} ) return else: raise Exception( "Missing parameters" )
def process( app ): query = app.query response = app.response session = app.session if "msid" in query.parms: confirmation_session = application.Session( app, query.parms["msid"] ) if "password_recovery_sid" in confirmation_session.parms \ and "password_recovery_user_id" in confirmation_session.parms: user_id = int( confirmation_session.parms["password_recovery_user_id"] ) if "nick" in query.parms and "new_password" in query.parms \ and confirmation_session.parms["password_recovery_sid"] == session.sid: # Asking the user requesting password recovery for his own nick might add a thin line # of additional security against the registered email account being controlled by an attacker, # as long as the nick is not disclosed in the recovery request confirmation mail. usr = user.User( app=app, user_id=user_id ) usr_status = usr.status() if( usr_status["login"]["nick"]!=query.parms["nick"] ): try: send_password_recovery_failed_notification( app=app, confirmation_session=confirmation_session ) except Exception as e: for line in traceback.format_exception( Exception, e, e.__traceback__ ): app.log( line ) raise Exception( "Password recovery failed" ) # We have to escalate to admin privileges for the password change procedure to not require old password validation app_old_user = app.user app.user = user.get_admin_user( app ) usr.update( new_password=query.parms["new_password"] ) app.user = app_old_user # Purge confirmation session confirmation_session.parms=[] confirmation_session.store() response.output = json.dumps( {"succeeded" : True} ) try: send_password_recovery_notification( app=app, usr=usr ) except Exception as e: for line in traceback.format_exception( Exception, e, e.__traceback__ ): app.log( line ) return elif "cancel" in query.parms \ and user_id == app.user.id: # Purge confirmation session confirmation_session.parms=[] confirmation_session.store() response.output = json.dumps( {"succeeded" : True} ) try: send_password_recovery_cancel_notification( app=app, usr=usr ) except Exception as e: for line in traceback.format_exception( Exception, e, e.__traceback__ ): app.log( line ) return else: try: send_password_recovery_failed_notification( app=app, confirmation_session=confirmation_session ) except Exception as e: for line in traceback.format_exception( Exception, e, e.__traceback__ ): app.log( line ) raise Exception( "Password recovery failed" ) else: try: send_password_recovery_failed_notification( app=app, confirmation_session=confirmation_session ) except Exception as e: for line in traceback.format_exception( Exception, e, e.__traceback__ ): app.log( line ) raise Exception( "Password recovery failed" ) elif "nick" in query.parms and "email" in query.parms: nick = query.parms["nick"] email = query.parms["email"] c = app.db.cursor() c.execute( """select object_id from users where nick=? and email=?""", [nick, email] ) result = c.fetchone() if result==None: raise Exception( "Invalid user name or email address" ) else: user_id = int( result[0] ) app_old_user = app.user app.user = user.get_admin_user( app ) usr = user.User( app, user_id=user_id ) app.user = app_old_user send_confirmation_request( app=app, user_id=usr.id ) response.output = json.dumps( {"succeeded" : True} ) else: raise Exception( "Missing parameters" )
def process(app): query = app.query response = app.response session = app.session target_id = int(query.parms["id"]) mode = query.parms["mode"] if "mode" in query.parms else "convert" if app.user.can_read(target_id): target_obj = files.File(app, object_id=target_id) if re.match(r"^video/.*", target_obj.media_type): new_poster_offset = float(query.parms["poster_offset"]) if "poster_offset" in query.parms else None new_poster_id = int(query.parms["poster_id"]) if "poster_id" in query.parms else None if new_poster_id and app.user.can_write(target_id) and app.user.can_read(new_poster_id): new_poster_obj = files.File(app, object_id=new_poster_id) else: new_poster_obj = None # Wir brauchen min. webm (Firefox, Chrome) und mp4 (Safari, IE, Chrome) für eine halbwegs gute # Client-Abdeckung. # Direkte Kindobjekte danach durchsuchen: results = [] missing_conversions = [ ("poster", 480, "image/jpeg"), ("compatible", 480, "video/mp4"), ("compatible", 480, "video/webm"), ] c = app.db.cursor() # Hier müssen wir zunächst prüfen ob das angefragte Objekt selbst schon ein Substitute-Objekt ist, ... c.execute("""select original_id from substitutes where substitute_id=?""", [target_obj.id]) if c.fetchone(): # ... denn für Substitute-Objekte sollten keine weiteren Substitute-Objekte generiert werden. missing_conversions = [] c.execute( """select s.substitute_id, s.type, s.size, s.priority, sobj.id from substitutes s left join objects sobj on sobj.id=s.substitute_id where s.original_id=?""", [target_obj.id], ) for row in c: substitute = { "substitute_id": int(row[0]), "type": row[1], "size": int(row[2]), "priority": int(row[3]), } sobj_id = row[4] if sobj_id == None: # Zombie-Substitutes bereinigen (FIXME: sollte das von DBObject.delete() erledigt werden?): del_c = app.db.cursor() del_c.execute("""delete from substitutes where substitute_id=?""", [substitute["substitute_id"]]) app.db.commit() else: substitute_obj = db_object.DBObject(app, object_id=substitute["substitute_id"]) conversion = (substitute["type"], substitute["size"], substitute_obj.media_type) if conversion in missing_conversions: if substitute["type"] == "poster" and (new_poster_offset or new_poster_obj): # bestehendes Poster-Substitute entfernen, da es neu definieter werden soll: del_c = app.db.cursor() del_c.execute( """delete from substitutes where original_id=? and substitute_id=?""", [target_obj.id, substitute_obj.id], ) app.db.commit() else: missing_conversions.remove(conversion) results.append(substitute) error_list = [] if mode == "convert": # Alle fehlende Objekte sofort ohne Daten anlegen, um Mehrfachkonvertierung zu vermeiden: new_objects = [] for conversion in missing_conversions: conversion_type, conversion_size, new_media_type = conversion # Privilege-Escalation damit nicht nur der Eigentümer des Target-Objekts diesen Code ausführen kann: app_old_user = app.user app.user = user.get_admin_user(app) existing_object_id = None if conversion_type == "poster" and new_poster_obj: existing_object_id = new_poster_obj.id new_obj = files.File( app, object_id=existing_object_id, parent_id=target_obj.id, media_type=new_media_type ) if not existing_object_id: new_obj.conversion = conversion new_objects.append(new_obj) substitute = { "substitute_id": new_obj.id, "type": conversion_type, "size": conversion_size, "priority": None, } results.append(substitute) app.user = app_old_user c = app.db.cursor() c.execute( """insert into substitutes (original_id, substitute_id, type, size) values(?,?,?,?)""", [target_obj.id, new_obj.id, conversion_type, conversion_size], ) app.db.commit() # Konvertierungsvorgänge für angelegte Objekte durchführen: for new_obj in new_objects: conversion_type, conversion_size, ignored = new_obj.conversion base_type, sub_type = new_obj.media_type.split("/") new_tmp_name = new_obj.storage_path + ".tmp." + sub_type if re.match(r"^video/.*", new_obj.media_type): # Konvertierung mit konservativer Breite von 480px bei Erhaltug des Seitenverhältnisses: # http://stackoverflow.com/questions/8218363/maintaining-ffmpeg-aspect-ratio p = subprocess.Popen( [ "ffmpeg", "-y", "-i", target_obj.storage_path, "-vf", "scale=%d:trunc(ow/a/2)*2" % (conversion_size), "-r", "25", "-b", "1000k", "-qmin", "0", "-strict", "-2", new_tmp_name, ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) elif conversion_type == "poster" and new_obj.media_type == "image/jpeg": # Vorschaubild bei Zeitindex 3s extrahieren (TODO: Mit beliebigem Zeitindex einstellbar machen?): p = subprocess.Popen( [ "ffmpeg", "-y", "-i", target_obj.storage_path, "-vf", "scale=%d:trunc(ow/a/2)*2" % (conversion_size), "-ss", str(new_poster_offset if new_poster_offset else 3), "-vframes", "1", new_tmp_name, ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) else: raise NotImplementedError("missing operation for conversion: " + str(new_obj.conversion)) stdout, stderr = p.communicate() if p.returncode != 0: try: # FIXME: Löschenfunktion nach DBObject ausmodularisieren und Dateibereinigung nach files.File: # Privilege-Escalation damit nicht nur der Eigentümer des Target-Objekts diesen Code ausführen kann: app_old_user = app.user app.user = user.get_admin_user(app) delete_module.delete_in(app, [new_obj.id]) app.user = app_old_user os.remove(new_tmp_name) c = app.db.cursor() c.execute( """delete from substitutes where original_id=? and substitute_id=?""", [target_obj.id, new_obj.id], ) app.db.commit() results = [x for x in results if x["substitute_id"] != new_obj.id] except Exception as e: error_list.append(e) errmsg = stderr.decode().split("\n")[-1] error_list.append(errors.InternalProgramError(errmsg)) else: os.rename(new_tmp_name, new_obj.storage_path) # Fehlerbehandlung: if error_list: msg = "" for error in error_list: if msg: msg += "; " msg += str(error) raise errors.InternalProgramError(msg) else: for result in results: result["substitute_object"] = get_module.get(app, object_ids=[result["substitute_id"]]) response.output = json.dumps({"succeeded": True, "substitutes": results}) elif re.match(r"^audio/.*", target_obj.media_type): raise NotImplementedError("unsupported media type: " + target_obj.media_type) else: raise NotImplementedError("unsupported media type: " + target_obj.media_type) else: raise errors.PrivilegeError()