Newer
Older
#!/usr/bin/python
# -*- coding: UTF-8 -*-
from cStringIO import StringIO
from datetime import datetime
from email.mime.text import MIMEText
from threading import Thread
import functools
import hashlib
import json
from PIL import Image
from flask import render_template, send_from_directory
from flask import send_file
from flask import session
from flask import url_for
from flask_compress import Compress
from pyotp import random_base32
from werkzeug import abort, redirect
import gnupg
import psycopg2
import pytz
from functions import pbkdf2, AESCipher
from functions import random_data
from functions import render_jinja_html
from functions import rotate_image_upon_exif
from functions import float_or_null

Marco De Donno
committed
Image.MAX_IMAGE_PIXELS = 1 * 1024 * 1024 * 1024
################################################################################
debug = os.environ.get( "DEBUG", False )
baseurl = os.environ.get( "BASEURL", "" )
################################################################################

Marco De Donno
committed
# Decorators

Marco De Donno
committed
def session_field_required( field, value ):
def decorator( func ):
@functools.wraps( func )
def wrapper_login_required( *args, **kwargs ):
if not field in session:
return redirect( url_for( "login" ) )
elif not session.get( field ) == value:
return redirect( url_for( "login" ) )
return func( *args, **kwargs )
return wrapper_login_required
return decorator
def login_required( func ):
@functools.wraps( func )
def wrapper_login_required( *args, **kwargs ):
if not session.get( 'logged', False ) :
session[ 'after' ] = request.full_path
return redirect( url_for( "login", after = True ) )
return func( *args, **kwargs )
return wrapper_login_required
def referer_required( func ):
@functools.wraps( func )
def wrapper_login_required( *args, **kwargs ):
if not request.headers.get( "Referer", False ):
return "referrer needed", 404
return func( *args, **kwargs )
return wrapper_login_required
def admin_required( func ):
@functools.wraps( func )
def wrapper_login_required( *args, **kwargs ):
if not session.get( 'logged', False ) or not session.get( 'account_type', None ) == 1:
return redirect( url_for( "login" ) )
return func( *args, **kwargs )
return wrapper_login_required

Marco De Donno
committed
def redis_cache( ttl = 3600 ):
def decorator( func ):
@functools.wraps( func )
def wrapper_cache( *args, **kwargs ):
lst = []
lst.append( func.__name__ )
lst.extend( args )
index = "_".join( lst )
index = hashlib.sha256( index ).hexdigest()
d = config.redis_shared.get( index )
if d != None:
buff = StringIO()
buff.write( base64.b64decode( d ) )
buff.seek( 0 )

Marco De Donno
committed
else:
d = func( *args, **kwargs )
buff = StringIO()

Marco De Donno
committed
buff.seek( 0 )
d_cached = base64.b64encode( buff.getvalue() )
config.redis_shared.set( index, d_cached, ex = ttl )
return d
return wrapper_cache
return decorator
################################################################################
# Generic routing
def ping():
return "pong"
################################################################################
# App serving
@app.route( baseurl + '/app/<path>' )
def send_app_files( path ):
return send_from_directory( 'app', path )
@app.route( baseurl + '/static/<path>' )
def send_static_files( path ):
return send_from_directory( 'static', path )
################################################################################
# Sessions
@app.before_request
def renew_session():
session.permanent = True
app.permanent_session_lifetime = timedelta( seconds = config.session_timeout )
@app.route( baseurl + '/logout' )
def logout():
session.clear()
return redirect( url_for( 'home' ) )
@app.route( baseurl + '/login' )
def login( after = False ):
if after:
after = session.get( "after" )
session.clear()
session[ 'after' ] = after
else:
session.clear()
session[ 'need_to_check' ] = [ 'password' ]
session[ 'logged' ] = False
return render_template(
"login.html",
baseurl = baseurl,
js = config.cdnjs,
css = config.cdncss
)
@app.route( baseurl + '/do_login', methods = [ 'POST' ] )
def do_login():
need_to_check = session.get( "need_to_check", [ 'password' ] )
try:
current_check = need_to_check[ 0 ]
except:
current_check = None
############################################################################
if current_check == "password":
q = config.db.query( 'SELECT * FROM users WHERE username = %s', ( request.form.get( "username" ), ) )
user = q.fetchone()
if user == None:
return jsonify( {
'error': False,
'logged': False
} )
form_password = request.form.get( "password", None )
if form_password == None or not pbkdf2( form_password, user[ 'password' ] ):
return jsonify( {
'error': False,
'logged': False,
} )
elif not user[ 'active' ]:
return jsonify( {
'error': False,
'logged': False,
'message': 'Your account is not activated. Please contact an administrator.'
} )
else:
session[ 'session_id' ] = str( uuid4() )
session[ 'username' ] = user[ 'username' ]
session[ 'user_id' ] = user[ 'id' ]
session[ 'password_check' ] = True
session[ 'need_to_check' ].remove( current_check )
session[ 'password' ] = pbkdf2( form_password, "AES256", 50000 )
if user[ 'must_use_totp' ]:
session[ 'need_to_check' ].append( 'totp' )
if user[ 'must_use_securitykey' ]:
session[ 'need_to_check' ].append( 'securitykey' )
q = config.db.query( 'SELECT username, totp FROM users WHERE username = %s', ( session[ 'username' ], ) )
user = q.fetchone()
if not pyotp.TOTP( user[ 'totp' ] ).verify( request.form[ "totp" ], valid_window = 1 ):
return jsonify( {
'error': False,
'logged': False,
'message': 'Wrong TOTP'
session[ 'need_to_check' ].remove( current_check )
if len( session[ 'need_to_check' ] ) == 0 and session.get( "password_check", False ):
for key in [ 'process', 'need_to_check', 'password_check' ]:

Marco De Donno
committed
if key in session:
session.pop( key )
q = config.db.query( 'SELECT type FROM users WHERE username = %s', ( session[ 'username' ], ) )
user = q.fetchone()
session[ 'account_type' ] = user[ 'type' ]
if session.get( 'after', False ):
return jsonify( {
'error': False,
'logged': True,
'redirect': session.get( 'after' )
} )
else:
return jsonify( {
'error': False,
'logged': True,
} )
'next_step': session[ 'need_to_check' ][ 0 ]
################################################################################
# Reset
@app.route( baseurl + '/reset_password' )
def password_reset():
session.clear()
session[ 'process' ] = "request_password_reset"
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
return render_template(
"password_reset.html",
baseurl = baseurl,
js = config.cdnjs,
css = config.cdncss
)
@app.route( baseurl + '/do_reset_password', methods = [ 'POST' ] )
def do_password_reset():
email = request.form.get( "email", None )
Thread( target = do_password_reset_thread, args = ( email, ) ).start()
return jsonify( {
'error': False,
'message': 'OK'
} )
def do_password_reset_thread( email ):
q = config.db.query( 'SELECT id, username, email FROM users' )
users = q.fetchall()
for user in users:
if not user[ 'email' ].startswith( "pbkdf2$" ):
continue
elif pbkdf2( email, user[ 'email' ] ):
id = hashlib.sha512( random_data( 100 ) ).hexdigest()
####################################################################
data = {
'process': 'password_reset',
'process_id': id,
'user_id': user[ 'id' ]
}
data = json.dumps( data )
data = base64.b64encode( data )
config.redis_shared.set( "reset_" + id, data, ex = 24 * 3600 )
####################################################################
email_content = render_jinja_html(
"templates/email", "reset.html",
id = id,
url = config.domain + baseurl + "/reset_password_stage2"
)
msg = MIMEText( email_content, "html" )
msg[ 'Subject' ] = 'ICNML - User password reset'
msg[ 'From' ] = config.sender
msg[ 'To' ] = email
s = smtplib.SMTP( config.smtpserver )
s.sendmail( config.sender, [ email ], msg.as_string() )
s.quit()
break
else:
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
@app.route( baseurl + '/reset_password_stage2/<id>', methods = [ 'GET', 'POST' ] )
def password_reset_stage2( id ):
id = str( id )
data = config.redis_shared.get( "reset_" + id )
if data != None:
data = base64.b64decode( data )
data = json.loads( data )
password = request.form.get( "password", None )
userid = data.get( "user_id", None )
if password != None:
password = pbkdf2( password, random_data( 50 ), 50000 )
config.db.query( "UPDATE users SET password = %s WHERE id = %s", ( password, userid ) )
config.db.commit()
config.redis_shared.delete( "reset_" + id )
return jsonify( {
'error': False,
'password_updated': True
} )
else:
return render_template(
"password_reset_stage2.html",
baseurl = baseurl,
id = id,
js = config.cdnjs,
css = config.cdncss
)
else:
return jsonify( {
'error': True,
'message': 'Reset procedure not found/expired'
} )
################################################################################
@app.route( baseurl + '/u2f/admin' )
@login_required
return render_template(
baseurl = baseurl,
js = config.cdnjs,
css = config.cdncss,
keys = do_u2f_get_list_of_keys( all = True )
def do_u2f_get_list_of_keys( uid = None, all = False ):
user_id = session.get( "user_id", uid )
sql = "SELECT id, key_name as name, created_on, last_usage, usage_counter, active FROM webauthn WHERE user_id = %s"
if not all:
sql += " AND active = true"
sql += " ORDER BY sign_count DESC"
q = config.db.query( sql, ( user_id, ) )
keys = q.fetchall()
data = []
for key in keys:
data.append( dict( key ) )
return data
@app.route( baseurl + '/u2f/begin_activate', methods = [ 'POST' ] )
@login_required
session[ 'key_name' ] = request.form.get( "key_name", None )
username = session.get( "username" )
challenge = random_base32( 64 )
ukey = random_base32( 64 )
session[ 'challenge' ] = challenge
session[ 'register_ukey' ] = ukey
make_credential_options = webauthn.WebAuthnMakeCredentialOptions(
challenge, config.rp_name, config.RP_ID,
ukey, username, username,
None
)
return jsonify( make_credential_options.registration_dict )
@app.route( baseurl + '/u2f/verify', methods = [ 'POST' ] )
@login_required
challenge = session[ 'challenge' ]
user_id = session[ 'user_id' ]
key_name = session.get( "key_name", None )
ukey = session[ 'register_ukey' ]
response = request.form
webauthn_registration_response = webauthn.WebAuthnRegistrationResponse(
config.RP_ID,
config.ORIGIN,
response,
challenge
)
try:
webauthn_credential = webauthn_registration_response.verify()
except Exception as e:
'error': True,
'message': 'Registration failed. Error: {}'.format( e )
config.db.query(
"""
INSERT INTO webauthn
( user_id, key_name, ukey, credential_id, pub_key, sign_count )
VALUES ( %s, %s, %s, %s, %s, %s )
""",
(
user_id, key_name,
ukey, webauthn_credential.credential_id,
webauthn_credential.public_key, webauthn_credential.sign_count,
)
)
config.db.commit()
return jsonify( {
'success': 'User successfully registered.'
} )
################################################################################
@app.route( baseurl + '/u2f/delete', methods = [ 'POST' ] )
@login_required
key_id = request.form.get( "key_id", False )
key_name = request.form.get( "key_name", False )
userid = session[ 'user_id' ]
try:
config.db.query( "DELETE FROM webauthn WHERE id = %s AND key_name = %s AND user_id = %s", ( key_id, key_name, userid, ) )
config.db.commit()
return jsonify( {
'error': False
} )
except Exception as e:
return jsonify( {
'error': True
} );
@app.route( baseurl + '/u2f/disable', methods = [ 'POST' ] )
@login_required
key_id = request.form.get( "key_id", False )
key_name = request.form.get( "key_name", False )
userid = session[ 'user_id' ]
try:
config.db.query( "UPDATE webauthn SET active = False WHERE id = %s AND key_name = %s AND user_id = %s", ( key_id, key_name, userid, ) )
config.db.commit()
return jsonify( {
'error': False
} )
except Exception as e:
return jsonify( {
'error': True
} );
@app.route( baseurl + '/u2f/enable', methods = [ 'POST' ] )
@login_required
key_id = request.form.get( "key_id", False )
key_name = request.form.get( "key_name", False )
userid = session[ 'user_id' ]
try:
config.db.query( "UPDATE webauthn SET active = True WHERE id = %s AND key_name = %s AND user_id = %s", ( key_id, key_name, userid, ) )
config.db.commit()
return jsonify( {
'error': False
} )
except Exception as e:
return jsonify( {
'error': True
} );
@app.route( baseurl + '/u2f/begin_assertion' )
def u2f_begin_assertion():
user_id = session.get( "user_id" )
if 'challenge' in session:
del session[ 'challenge' ]
challenge = random_base32( 64 )
session[ 'challenge' ] = challenge
q = config.db.query( "SELECT * FROM webauthn WHERE user_id = %s AND active = true", ( user_id, ) )
key_list = q.fetchall()
credential_id_list = []
for key in key_list:
credential_id_list.append( {
'type': 'public-key',
'id': key[ 'credential_id' ],
'transports': [ 'usb', 'nfc', 'ble', 'internal' ]
} )
assertion_dict = {
'challenge': challenge,
'timeout': 60000,
'allowCredentials': credential_id_list,
'rpId': config.RP_ID,
}
return jsonify( {
'error': False,
'data': assertion_dict
@app.route( baseurl + '/u2f/verify_assertion', methods = [ 'POST' ] )
def verify_assertion():
challenge = session.get( 'challenge' )
assertion_response = request.form
credential_id = assertion_response.get( 'id' )
q = config.db.query( "SELECT * FROM webauthn WHERE credential_id = %s", ( credential_id, ) )
user = q.fetchone()
webauthn_user = webauthn.WebAuthnUser(
None, session[ 'username' ], None, None,
user[ 'credential_id' ], user[ 'pub_key' ], user[ 'sign_count' ], config.RP_ID
)
webauthn_assertion_response = webauthn.WebAuthnAssertionResponse(
webauthn_user,
assertion_response,
challenge,
config.ORIGIN,
uv_required = False
)
try:
sign_count = webauthn_assertion_response.verify()
except Exception as e:
return jsonify( {
'error': True,
'message': 'Assertion failed. Error: {}'.format( e )
} )
dt = datetime.now( pytz.timezone( 'Europe/Zurich' ) )
q = config.db.query( "UPDATE webauthn SET sign_count = %s, last_usage = %s, usage_counter = usage_counter + 1 WHERE credential_id = %s", ( sign_count, dt, credential_id, ) )
session[ 'need_to_check' ].remove( "securitykey" )
do_login()
return jsonify( {
'error': False
} )
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
################################################################################
# New user
@app.route( baseurl + '/signin' )
def new_user():
q = config.db.query( "SELECT id, name FROM account_type WHERE can_singin = true" )
r = q.fetchall()
account_type = []
for rr in r:
account_type.append( dict( rr ) )
return render_template(
"signin.html",
baseurl = baseurl,
list_account_type = account_type,
js = config.cdnjs,
css = config.cdncss
)
@app.route( baseurl + '/do_signin', methods = [ 'POST' ] )
def add_account_request_to_db():
try:
first_name = request.form[ 'first_name' ]
last_name = request.form[ 'last_name' ]
email = request.form[ 'email' ]
account_type = request.form[ 'account_type' ]
uuid = str( uuid4() )
config.db.query(
"""
INSERT INTO signin_requests
( first_name, last_name, email, account_type, uuid )
VALUES ( %s, %s, %s, %s, %s )
""",
( first_name, last_name, email, account_type, uuid )
)
config.db.commit()
return jsonify( {
'error': False,
'uuid': uuid
} )
except:
return jsonify( {
'error': True
} )
@app.route( baseurl + '/validate_signin' )
@admin_required
def validate_signin():
q = config.db.query( """
SELECT signin_requests.*, account_type.name as account_type
FROM signin_requests
LEFT JOIN account_type ON signin_requests.account_type = account_type.id
WHERE signin_requests.status = 'pending'
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
""" )
r = q.fetchall()
users = []
for rr in r:
users.append( dict( rr ) )
return render_template(
"validate_signin.html",
baseurl = baseurl,
users = users,
js = config.cdnjs,
css = config.cdncss
)
@app.route( baseurl + '/do_validate_signin', methods = [ 'POST' ] )
@admin_required
def do_validate_signin():
request_id = request.form.get( "id" )
q = config.db.query( 'SELECT * FROM signin_requests WHERE id = %s', ( request_id, ) )
s = q.fetchone()
s = dict( s )
r = {}
r[ 'user' ] = s
r[ 'user' ][ 'request_time' ] = str( r[ 'user' ][ 'request_time' ] )
r[ 'user' ][ 'validation_time' ] = str( r[ 'user' ][ 'validation_time' ] )
r[ 'acceptance' ] = {}
r[ 'acceptance' ][ 'username' ] = session[ 'username' ]
r[ 'acceptance' ][ 'time' ] = str( datetime.now() )
j = json.dumps( r )
challenge = base64.b64encode( j )
challenge = challenge.replace( "=", "" )
session[ 'validation_user_challenge' ] = challenge
############################################################################
user_id = session[ 'user_id' ]
q = config.db.query( "SELECT * FROM webauthn WHERE user_id = %s ORDER BY last_usage DESC LIMIT 1", ( user_id, ) )
key = q.fetchone()
webauthn_user = webauthn.WebAuthnUser(
key[ 'ukey' ], session[ 'username' ], session[ 'username' ], None,
key[ 'credential_id' ], key[ 'pub_key' ], key[ 'sign_count' ], config.RP_ID
)
webauthn_assertion_options = webauthn.WebAuthnAssertionOptions( webauthn_user, challenge )
return jsonify( {
'error': False,
'data': webauthn_assertion_options.assertion_dict
} )
@app.route( baseurl + '/do_validate_signin_2', methods = [ 'POST' ] )
@admin_required
def do_validate_signin_2():
challenge = session.get( 'validation_user_challenge' )
assertion_response = request.form
assertion_response_s = base64.b64encode( json.dumps( assertion_response ) )
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
credential_id = assertion_response.get( 'id' )
q = config.db.query( "SELECT * FROM webauthn WHERE credential_id = %s", ( credential_id, ) )
user = q.fetchone()
webauthn_user = webauthn.WebAuthnUser(
user[ 'ukey' ], session[ 'username' ], session[ 'username' ], None,
user[ 'credential_id' ], user[ 'pub_key' ], user[ 'sign_count' ], "icnml.unil.ch"
)
webauthn_assertion_response = webauthn.WebAuthnAssertionResponse(
webauthn_user,
assertion_response,
challenge,
config.ORIGIN,
uv_required = False
)
try:
sign_count = webauthn_assertion_response.verify()
except Exception as e:
return jsonify( {
'error': True,
'message': 'Assertion failed. Error: {}'.format( e )
} )
############################################################################
if len( challenge ) % 4 != 0:
challenge += "=" * ( 4 - ( len( challenge ) % 4 ) )
newuser = base64.b64decode( challenge )
newuser = json.loads( newuser )
user_type = newuser[ 'user' ][ 'account_type' ]
email = newuser[ 'user' ][ 'email' ]
email_hash = pbkdf2( email, random_data( 100 ), 50000 )
request_uuid = newuser[ 'user' ][ 'uuid' ]

Marco De Donno
committed
request_id = newuser[ 'user' ][ 'id' ]
username_id = newuser[ 'user' ][ 'username_id' ]
n = config.db.query( "SELECT name FROM account_type WHERE id = %s", ( user_type, ) )
n = n.fetchone()
n = n[ 0 ]

Marco De Donno
committed
username = n + "_" + str( username_id )
username = username.lower()
try:
config.db.query( "UPDATE signin_requests SET validation_time = now(), assertion_response = %s, status = 'validated'", ( assertion_response_s, ) )
config.db.query( "INSERT INTO users ( username, email, type ) VALUES ( %s, %s, %s )", ( username, email_hash, user_type ) )
config.db.commit()

Marco De Donno
committed
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
except:
return jsonify( {
'error': True,
'message': 'Can not insert into database.'
} )
############################################################################
email_content = render_jinja_html(
"templates/email", "signin.html",
username = username,
url = "https://icnml.unil.ch/config/" + newuser[ 'user' ][ 'uuid' ]
)
msg = MIMEText( email_content, "html" )
msg[ 'Subject' ] = 'ICNML - Login information'
msg[ 'From' ] = config.sender
msg[ 'To' ] = email
s = smtplib.SMTP( config.smtpserver )
s.sendmail( config.sender, [ email ], msg.as_string() )
s.quit()
############################################################################
return jsonify( {
'error': False
} )
@app.route( baseurl + '/config/<uuid>' )
def config_new_user( uuid ):
session.clear()
q = config.db.query( "SELECT email FROM signin_requests WHERE uuid = %s", ( uuid, ) )
r = q.fetchone()
try:
email = r[ 'email' ]
session[ 'signin_user_validation_email' ] = email
session[ 'signin_user_validation_uuid' ] = uuid
return render_template(
"user_config.html",
baseurl = baseurl,
js = config.cdnjs,
css = config.cdncss,
session_timeout = config.session_timeout
)
except:
return redirect( url_for( 'home' ) )
@app.route( baseurl + '/do_config', methods = [ 'POST' ] )
def do_config_new_user():
email = session[ 'signin_user_validation_email' ]
uuid = session[ 'signin_user_validation_uuid' ]
username = request.form.get( "username" )
password = request.form.get( "password" )
session[ 'username' ] = username
############################################################################
q = config.db.query( "SELECT count(*) FROM signin_requests WHERE uuid = %s AND email = %s", ( uuid, email, ) )
r = q.fetchone()
r = r[ 0 ]
if r == 0:
return jsonify( {
'error': True,
'message': "no signin request"
} )
q = config.db.query( "SELECT * FROM users WHERE username = %s", ( username, ) )
user = q.fetchone()
if user == None:
return jsonify( {
'error': True,
'message': "no user"
} )
elif not password.startswith( "pbkdf2" ):
return jsonify( {
'error': True,
'message': "password not in the correct format"
} )
elif not pbkdf2( email, user[ 'email' ] ):
return jsonify( {
'error': True,
'message': "email not corresponding to the request form"
} )
elif user.get( 'password', None ) != None:
return jsonify( {
'error': True,
'message': "password already set"
} )
############################################################################
password = pbkdf2( password, random_data( 65 ), 50000 )
q = config.db.query( "UPDATE users SET password = %s WHERE username = %s", ( password, username, ) )
config.db.commit()
session[ 'username' ] = username
############################################################################
return jsonify( {
'error': False
} )
@app.route( baseurl + '/totp_help' )
def totp_help():
return render_template(
"totp_help.html",
baseurl = baseurl,
js = config.cdnjs,
css = config.cdncss,
)
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
################################################################################
# QR Code generation
def renew_secret():
secret = random_base32( 40 )
session[ 'secret' ] = secret
return secret
def get_secret():
secret = session.get( "secret", None )
if secret == None:
secret = renew_secret()
return secret
@app.route( baseurl + '/set_secret' )
def set_secret():
config.db.query( "UPDATE users SET totp = %s WHERE username = %s", ( session[ 'secret' ], session[ 'username' ], ) )
config.db.commit()
return jsonify( {
'error': False
} )
@app.route( baseurl + '/secret' )
def request_secret():
get_secret()
return jsonify( {
'error': False,
'secret': session[ 'secret' ]
} )
@app.route( baseurl + '/new_secret' )
@login_required
def request_renew_secret():
renew_secret()
return jsonify( {
'error': False,
'secret': session[ 'secret' ]
} )
@app.route( baseurl + '/qrcode' )
def send_qrcode():
if 'username' in session:
qrcode_value = 'otpauth://totp/ICNML%20' + session[ 'username' ] + '?secret=' + get_secret()
else:
qrcode_value = 'otpauth://totp/ICNML?secret=' + get_secret()
img = qrcode.make( qrcode_value )
temp = StringIO()
img.save( temp, format = "png" )
temp.seek( 0 )
return send_file( temp, mimetype = 'image/png' )
@app.route( baseurl + '/user_qrcode' )
@login_required
return render_template(
"qrcode.html",
baseurl = baseurl,
secret = get_secret(),
js = config.cdnjs,
css = config.cdncss
)
################################################################################
# Data decryption
def do_decrypt( data ):
return AESCipher( session[ 'password' ] ).decrypt( data )
def do_encrypt( data ):
return AESCipher( session[ 'password' ] ).encrypt( data )
################################################################################
# File upload
@app.route( baseurl + '/upload', methods = [ 'GET', 'POST' ] )
@login_required
def upload_file():
upload_type = request.form.get( "upload_type", None )
if upload_type == None:
return jsonify( {
'error': True,
'msg': 'Must specify a file type to upload a file'
} )
if request.method == 'POST':
if 'file' not in request.files:
return jsonify( { 'error': True, 'msg': 'No file in the POST request' } )
elif 'upload_id' not in request.form:
return jsonify( { 'error': True, 'msg': 'No upload_id' } )
else:
try:
upload_id = request.form.get( "upload_id" )
sql = "SELECT id FROM submissions WHERE uuid = %s"
r = config.db.query( sql, ( upload_id, ) )
upload_id = r.fetchone()[ 'id' ]
except:
return jsonify( {
'error': True
} )
file = request.files[ 'file' ]
filename = do_encrypt( file.filename )
file_uuid = str( uuid4() )
fp = StringIO()
file.save( fp )
file_size = fp.tell()
fp.seek( 0 )
if upload_type in [ 'latent', 'tenprint_card_front', 'tenprint_card_back' ]:
img = Image.open( fp )
img_format = img.format
width, height = img.size
res = int( img.info[ 'dpi' ][ 0 ] )
img = rotate_image_upon_exif( img )
buff = StringIO()
img.save( buff, format = img_format )
buff.seek( 0 )
file_data = buff.getvalue()
if upload_type in [ 'tenprint_card_front', 'tenprint_card_back' ]:
create_thumbnail( file_uuid, img )
else:
file_data = fp.getvalue()
file_data = base64.b64encode( file_data )
if upload_type == "consent_form":
file_data = gpg.encrypt( file_data, config.gpg_key )
file_data = str( file_data )
upload_type_id = {
'consent_form': 0,
'tenprint_card_front': 1,
'tenprint_card_back': 2,
'latent': 3
sql = "INSERT INTO files ( folder, creator, filename, type, format, size, width, height, resolution, uuid, data ) VALUES ( %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s )"
data = ( upload_id, session[ 'user_id' ], filename, upload_type_id, img_format, file_size, width, height, res, file_uuid, file_data )
config.db.query( sql, data )
config.db.commit()
return jsonify( {
'error': False,
'filename': filename,
'filesize': file_size,
'uuid': file_uuid
} )
else:
return abort( 403 )
################################################################################
return render_template(
baseurl = baseurl,
js = config.cdnjs,
css = config.cdncss,
session_timeout = config.session_timeout
)
@app.route( baseurl + '/submission/do_new', methods = [ "GET", "POST" ] )
email = request.form.get( "email", False )
if email:
# Check for duplicate base upon the email data
sql = "SELECT id, email_hash FROM submissions WHERE submitter_id = %s"
r = config.db.query( sql, ( session[ 'user_id' ], ) )
for case in r.fetchall():
if pbkdf2( email, case[ 'email_hash' ] ):
return jsonify( {
'error': True,
'msg': "Email already used"
} )
break
email_aes = do_encrypt( email )
email_hash = pbkdf2( email, random_data( 50 ), 50000 )
upload_nickname = request.form.get( "upload_nickname", None )
upload_nickname = do_encrypt( upload_nickname )
submitter_id = session[ 'user_id' ]
status = "pending"
sql = "INSERT INTO submissions ( uuid, email_aes, email_hash, nickname, status, submitter_id ) VALUES ( %s, %s, %s, %s, %s, %s ) RETURNING id"
data = ( id, email_aes, email_hash, upload_nickname, status, submitter_id )
config.db.commit()
return jsonify( {
'error': False,
'id': id
} )
else:
return jsonify( {
'error': True,
'msg': "Email not provided"
} )
@app.route( baseurl + '/submission/<id>/update' )
@login_required
try:
sql = "SELECT email_aes as email, nickname, created_time FROM submissions WHERE submitter_id = %s AND uuid = %s"
r = config.db.query( sql, ( session[ 'user_id' ], id ) )
user = r.fetchone()
for key in [ 'email', 'nickname' ]:
user[ key ] = do_decrypt( user[ key ] )
return render_template(
baseurl = baseurl,
js = config.cdnjs,
css = config.cdncss,
session_timeout = config.session_timeout,
upload_id = id,
**user
)
except:
return jsonify( {
'error': True,
'msg': "Case not found"
} )

Marco De Donno
committed
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
@app.route( baseurl + '/submission/<id>/update/nickname', methods = [ 'POST' ] )
@login_required
def submission_update_nickname( id ):
nickname = request.form.get( "nickname", None )
if nickname != None and len( nickname ) != 0:
try:
nickname = do_encrypt( nickname )
sql = "UPDATE submissions SET nickname = %s WHERE uuid = %s"
config.db.query( sql, ( nickname, id, ) )
config.db.commit()
return jsonify( {
'error': False
} )
except:
return jsonify( {
'error': True,
'message': "DB error"
} )
else:
return jsonify( {
'error': True,
'message': "No new nickname in the POST request"
} )
sql = "SELECT * FROM submissions WHERE submitter_id = %s ORDER BY created_time DESC"
r = config.db.query( sql, ( session[ 'user_id' ], ) )
q = r.fetchall()
donors = []
for donor in q:
donors.append( {
'id': donor.get( "id", None ),
'email': do_decrypt( donor.get( "email_aes", None ) ),
'nickname': do_decrypt( donor.get( "nickname", None ) ),
'uuid': donor.get( "uuid", None )
} )
return render_template(
baseurl = baseurl,
js = config.cdnjs,
css = config.cdncss,
session_timeout = config.session_timeout,
@app.route( baseurl + '/submission/<id>/latent/list' )
@login_required
sql = "SELECT id FROM submissions WHERE uuid = %s AND submitter_id = %s"
r = config.db.query( sql, ( id, session[ 'user_id' ], ) )
case_id = r.fetchone()[ 'id' ]
sql = "SELECT uuid, filename, size, creation_time FROM files WHERE folder = %s AND type = 3"
r = config.db.query( sql, ( case_id, ) )
files = r.fetchall()
for i, v in enumerate( files ):
v[ 'filename' ] = do_decrypt( v[ 'filename' ] )
v[ 'size' ] = round( ( float( v[ 'size' ] ) / ( 1024 * 1024 ) ) * 100 ) / 100
return render_template(
baseurl = baseurl,
js = config.cdnjs,
css = config.cdncss,
session_timeout = config.session_timeout,
submission_id = id,
files = files
)
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
@app.route( baseurl + '/submission/<id>/latent/<lid>' )
@login_required
def submission_latent( id, lid ):
sql = "SELECT id FROM submissions WHERE uuid = %s"
r = config.db.query( sql, ( id, ) )
id = r.fetchone()[ 'id' ]
sql = """
SELECT
files.uuid, files.filename, files.format, files.resolution, files.width, files.height, files.size, files.creation_time, files.type
FROM files
WHERE
folder = %s AND
files.uuid = %s
"""
r = config.db.query( sql, ( id, lid, ) )
latent = r.fetchone()
latent[ 'size' ] = round( 100 * float( latent[ 'size' ] ) / ( 1024 * 1024 ) ) / 100
latent[ 'filename' ] = do_decrypt( latent[ 'filename' ] )
return render_template(
"submission/latent.html",
baseurl = baseurl,
js = config.cdnjs,
css = config.cdncss,
session_timeout = config.session_timeout,
submission_id = id,
latent = latent
)
################################################################################
# Image processing
@app.route( baseurl + '/image/info/<id>' )
@login_required
def img_info( id ):
d = do_img_info( id )
if d != None:
return jsonify( d )
else:
return abort( 404 )
def do_img_info( id ):
sql = "SELECT size, width, height, resolution, format FROM files WHERE uuid = %s"
r = config.db.query( sql, ( id, ) )
d = r.fetchone()
if d != None:
return dict( d )
else:
return None
@app.route( baseurl + '/image/preview/<id>' )
@referer_required
@login_required
def img_preview( id ):
sql = "SELECT size, data FROM thumbnails WHERE uuid = %s"
r = config.db.query( sql, ( id, ) )
img = r.fetchone()
if img == None:
sql = "SELECT size, data FROM files WHERE uuid = %s"
r = config.db.query( sql, ( id, ) )
img = r.fetchone()
do_thumbnail = True
else:
do_thumbnail = False
if img != None:
img = base64.b64decode( img[ 'data' ] )
buff = StringIO()
buff.write( img )
buff.seek( 0 )
img = Image.open( buff )
if do_thumbnail:
img.thumbnail( ( 300, 300 ) )
buff = StringIO()
img.save( buff, format = 'PNG' )
buff.seek( 0 )
return send_file( buff, mimetype = "image/png" )
else:
return abort( 404 )
def create_thumbnail( file_uuid, img ):
img.thumbnail( ( 1000, 1000 ) )
width, height = img.size
file_format = img.format
buff = StringIO()
img.save( buff, format = img.format )
img_size = buff.tell()
buff.seek( 0 )
img_data = buff.getvalue()
img_data = base64.b64encode( img_data )
sql = "INSERT INTO thumbnails ( uuid, width, height, size, format, data ) VALUES ( %s, %s, %s, %s, %s, %s )"
data = ( file_uuid, width, height, img_size, img.format, img_data, )
config.db.query( sql, data )
config.db.commit()
return

Marco De Donno
committed
################################################################################

Marco De Donno
committed
@app.route( baseurl + '/submission/<id>/tenprint/list' )

Marco De Donno
committed
@login_required

Marco De Donno
committed
sql = "SELECT id FROM submissions WHERE uuid = %s"
r = config.db.query( sql, ( id, ) )
submission_id = r.fetchone()[ 'id' ]
sql = """
SELECT
id, filename, uuid, type, creation_time
FROM files
WHERE folder = %s AND ( type = 1 OR type = 2 )
ORDER BY creation_time DESC
"""

Marco De Donno
committed
r = config.db.query( sql, ( submission_id, ) )
q = r.fetchall()
tenprint_cards = {
'1': [],
'2': []
}

Marco De Donno
committed
for tenprint in q:
tenprint_cards[ str( tenprint[ 'type' ] ) ].append( {

Marco De Donno
committed
'id': tenprint.get( "id", None ),
'filename': do_decrypt( tenprint.get( "filename", None ) ),
'uuid': tenprint.get( "uuid", None ),
'type': tenprint.get( "type", None )

Marco De Donno
committed
} )
return render_template(

Marco De Donno
committed
baseurl = baseurl,
js = config.cdnjs,
css = config.cdncss,
session_timeout = config.session_timeout,
tenprint_cards_front = tenprint_cards[ '1' ],
tenprint_cards_back = tenprint_cards[ '2' ],

Marco De Donno
committed
submission_id = id
)
@app.route( baseurl + '/submission/<id>/tenprint/<tid>' )

Marco De Donno
committed
@login_required

Marco De Donno
committed
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
sql = "SELECT id FROM submissions WHERE uuid = %s"
r = config.db.query( sql, ( id, ) )
id = r.fetchone()[ 'id' ]
sql = """
SELECT
files.uuid, files.filename, files.format, files.resolution, files.width, files.height, files.size, files.creation_time, files.type,
file_template.template
FROM files
JOIN file_template ON files.uuid = file_template.file
WHERE
folder = %s AND
files.uuid = %s
"""
r = config.db.query( sql, ( id, tid, ) )
file = r.fetchone()
file[ 'size' ] = round( 100 * float( file[ 'size' ] ) / ( 1024 * 1024 ) ) / 100
file[ 'filename' ] = do_decrypt( file[ 'filename' ] )
if file[ 'type' ] == 1:
t = 'front'
elif file[ 'type' ] == 2:
t = 'back'
############################################################################
sql = 'SELECT width, height, image_resolution FROM tenprint_cards WHERE id = %s LIMIT 1'
r = config.db.query( sql, ( file[ 'template' ], ) )
tmp = r.fetchone()
card_info = {
'width': int( round( float( tmp[ 'width' ] ) / 2.54 * tmp[ 'image_resolution' ] ) ),
'height': int( round( float( tmp[ 'height' ] ) / 2.54 * tmp[ 'image_resolution' ] ) ),
'width_cm': tmp[ 'width' ],
'height_cm': tmp[ 'height' ]
}
############################################################################
sql = """
SELECT
tenprint_zones.pc, tl_x, tl_y, br_x, br_y, angle, pc.name
FROM tenprint_zones
JOIN tenprint_zones_location ON tenprint_zones.pc = tenprint_zones_location.pc
JOIN pc ON tenprint_zones.pc = pc.id
WHERE
card = %s AND
tenprint_zones_location.side = %s
ORDER BY pc
"""
r = config.db.query( sql, ( file[ 'template' ], t, ) ).fetchall()
zones = []
for pc, tl_x, tl_y, br_x, br_y, angle, pc_name in r:
tl_x = float_or_null( tl_x )
tl_y = float_or_null( tl_y )
br_x = float_or_null( br_x )
br_y = float_or_null( br_y )
zones.append( {
"pc": pc,
"tl_x": tl_x,
"tl_y": tl_y,
"br_x": br_x,
"br_y": br_y,
"angle": angle,
"pc_name": pc_name
} )
datacolumns = [ 'tl_x', 'tl_y', 'br_x', 'br_y', 'angle' ]
############################################################################
sql = 'SELECT width, height, resolution FROM files WHERE uuid = %s LIMIT 1'
r = config.db.query( sql, ( tid, ) )
img_info = r.fetchone()
svg_hw_factor = float( img_info[ 'width' ] ) / float( img_info[ 'height' ] )
return render_template(

Marco De Donno
committed
baseurl = baseurl,
js = config.cdnjs,
css = config.cdncss,
session_timeout = config.session_timeout,
upload_id = id,
tenprint_id = tid,
file = file,
t = t,
card_id = file[ 'uuid' ],
card_info = card_info,
img_info = img_info,
svg_hw_factor = svg_hw_factor,
zones = zones,
datacolumns = datacolumns
)
################################################################################
# Tenprint templates
@app.route( baseurl + '/tenprint_template/<id>/<t>' )
@login_required
def tp_template( id, t ):
if t in [ 'front', 'back' ]:
sql = """SELECT
tenprint_zones.pc, tl_x, tl_y, br_x, br_y, angle, pc.name
FROM tenprint_zones
JOIN tenprint_zones_location ON tenprint_zones.pc = tenprint_zones_location.pc
JOIN pc ON tenprint_zones.pc = pc.id
WHERE card = %s AND tenprint_zones_location.side = %s ORDER BY pc
"""
r = config.db.query( sql, ( id, t, ) ).fetchall()
zones = []
for pc, tl_x, tl_y, br_x, br_y, angle, pc_name in r:
tl_x = float_or_null( tl_x )
tl_y = float_or_null( tl_y )
br_x = float_or_null( br_x )
br_y = float_or_null( br_y )
zones.append( {
"pc": pc,
"tl_x": tl_x,
"tl_y": tl_y,
"br_x": br_x,
"br_y": br_y,
"angle": angle,
"pc_name": pc_name
datacolumns = [ 'tl_x', 'tl_y', 'br_x', 'br_y', 'angle' ]

Marco De Donno
committed
sql = 'SELECT id, country_code, width, height, size_display, image_' + t + '_width, image_' + t + '_height, image_resolution FROM tenprint_cards WHERE id = %s LIMIT 1'
r = config.db.query( sql, ( id, ) )
img_info = r.fetchone()
card_info = {
'width': int( round( float( img_info[ 'width' ] ) / 2.54 * img_info[ 'image_resolution' ] ) ),
'height': int( round( float( img_info[ 'height' ] ) / 2.54 * img_info[ 'image_resolution' ] ) ),
}

Marco De Donno
committed
svg_hw_factor = float( img_info[ 'image_' + t + '_width' ] ) / float( img_info[ 'image_' + t + '_height' ] )
baseurl = baseurl,
admin = int( session[ 'account_type' ] ) == 1,
js = config.cdnjs,
css = config.cdncss,
session_timeout = config.session_timeout,
account_type = session.get( "account_type", None ),
zones = zones,
img_info = img_info,
card_info = card_info,

Marco De Donno
committed
svg_hw_factor = svg_hw_factor,

Marco De Donno
committed
t = t,
datacolumns = datacolumns,
)
else:
return abort( 403 )
@app.route( baseurl + '/tenprint_template/image/<id>/<t>' )
@referer_required
@login_required
def tp_template_img( t, id ):
if t in [ 'front', 'back' ]:
sql = "SELECT image_front, image_back FROM tenprint_cards WHERE id = %s LIMIT 1"
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
r = config.db.query( sql, ( id, ) )
img = r.fetchone()
if img != None:
img = base64.b64decode( img[ 'image_' + t ] )
buff = StringIO()
buff.write( img )
buff.seek( 0 )
img = Image.open( buff )
img.thumbnail( ( 1000, 1000 ) )
buff2 = StringIO()
img.save( buff2, format = 'PNG' )
buff2.seek( 0 )
return send_file( buff2, mimetype = "image/png" )
else:
return abort( 403 )

Marco De Donno
committed
@app.route( baseurl + '/tenprint_template/zones/update/<card>', methods = [ "GET", "POST" ] )
@login_required

Marco De Donno
committed
def update_zone_coordinates( card ):
card = int( card )
data = request.form.get( "data" )

Marco De Donno
committed
if data != None:
data = json.loads( data )
for pc, value in data.iteritems():
pc = int( pc )
for coordinate, v in value.iteritems():
sql = "UPDATE tenprint_zones SET " + coordinate + " = %s WHERE card = %s AND pc = %s"
data = ( v, card, pc, )
config.db.query( sql, data )

Marco De Donno
committed
config.db.commit()
return jsonify( {
'error': False
} )
else:
return abort( 403 )
################################################################################
# Home page
@app.route( baseurl + '/' )
@login_required
return render_template(
"index.html",
baseurl = baseurl,
admin = int( session[ 'account_type' ] ) == 1,
js = config.cdnjs,
css = config.cdncss,
session_timeout = config.session_timeout,
account_type = session.get( "account_type", None )
################################################################################
# Main startup
if __name__ == '__main__':
gpg = gnupg.GPG()
for file in os.listdir( config.keys_folder ):
with open( config.keys_folder + "/" + file, "r" ) as fp:
gpg.import_keys( fp.read() )
app.run( debug = debug, host = "0.0.0.0", threaded = True )