#!/usr/bin/env python # Startdate: 2021-06-17 # goals: # References: # for session_app: # https://code.tutsplus.com/tutorials/flask-authentication-with-ldap--cms-23101 # https://www.techlifediary.com/python-web-development-tutorial-using-flask-session-cookies/ # delete cookie https://stackoverflow.com/a/14386413/3569534 # timeout sessions https://stackoverflow.com/a/11785722/3569534 # future: https://code.tutsplus.com/tutorials/flask-authentication-with-ldap--cms-23101 # better timeout session: https://stackoverflow.com/a/49891626/3569534 # store "formdata" in session for changing the basic auth to form data for the ldap login https://stackoverflow.com/a/56904875/3569534 # modify url from urlparse https://stackoverflow.com/a/21629125/3569534 # preserve POST with code 307 https://stackoverflow.com/a/15480983/3569534 # for bws: # https://stackoverflow.com/questions/37259740/passing-variables-from-flask-to-javascript # https://stackoverflow.com/questions/43690222/overloading-less-than-in-python/45493522#45493522 # https://stackoverflow.com/questions/21787496/converting-epoch-time-with-milliseconds-to-datetime # for javascript: # https://javascript.info/xmlhttprequest # https://stackoverflow.com/questions/38370854/making-a-div-flash-just-once/38370873#38370873 # https://stackoverflow.com/questions/38370854/making-a-div-flash-just-once/38371229#38371229 # https://stackoverflow.com/questions/8674618/adding-options-to-select-with-javascript/8674667#8674667 # https://stackoverflow.com/questions/33758595/html-form-run-javascript-on-enter/33758792#33758792 # https://stackoverflow.com/questions/647282/is-there-an-onselect-event-or-equivalent-for-html-select/12404521#12404521 # get all options from select-options http://www.mredkj.com/tutorials/tutorial007.html # https://stackoverflow.com/questions/1628826/how-to-add-an-onchange-event-to-a-select-box-via-javascript/24829023#24829023 # https://stackoverflow.com/questions/8267857/how-to-wait-until-array-is-filled-asynchronous/8267923#8267923 # https://stackoverflow.com/questions/9713058/send-post-data-using-xmlhttprequest/9713078#9713078 # Improve: # Run: # FLASK_APP=bws.py FLASK_DEBUG=1 flask run --host 0.0.0.0 # Dependencies: # apt-get install python3-flask from flask import Flask, Response, redirect, url_for, render_template, request, _request_ctx_stack as stack, make_response, session, jsonify, escape #from flask_kerberos import init_kerberos, requires_authentication, _unauthorized, _forbidden, _gssapi_authenticate from functools import wraps from urllib.parse import urlparse import binascii, datetime, os, json # Custom libraries for Bookmark Web Service import session_ldap import bws_db app = Flask(__name__) if 'BWS_CONFIG' in os.environ: conf_file=os.environ['BWS_CONFIG'] else: conf_file="bws.cfg" app.config.from_pyfile(conf_file, silent=False) #app.config.from_object(__name__) if 'DEBUG' in app.config and app.config['DEBUG']: app.debug=True secret_key_value = os.urandom(24) secret_key_value_hex_encoded = binascii.hexlify(secret_key_value) app.config['SECRET_KEY'] = secret_key_value_hex_encoded app.permanent_session_lifetime=datetime.timedelta(minutes=app.config['SESSION_DURATION_MINUTES']) app.config['SESSION_TYPE']='filesystem' app.config['database'] = 'bws.sqlite1' ## WRAPPER FUNCTIONS def requires_session(function): ''' Requires a valid session, provided by cookie! ''' @wraps(function) def decorated(*args, **kwargs): if not session: return Response(f"Not logged in! Try logging in.",401) #return Response("requires session",401) else: if 'user' not in session: return Response("User is not in this session.",401) s_user = session['user'] s_groups = session['groups'] c_user = request.cookies.get('user') print(f"session user: {s_user}") print(f"cookie user: {c_user}") if session['user'] != c_user: return Response(f"Not logged in! Try logging in.",401) #return Response("Wrong user for this session!.",401) # otherwise, everything is good! return function(s_user, s_groups, *args,**kwargs) # catch-all return Response("requires session",401) return decorated # Goal: function that determines if loggedin_uid = b.uid or if loggedin_uid is in group admins # This adds a parameter, allowed, to the function call def requires_bid_access(function): """ Requires that loggedin_uid = b.uid or that loggedin_uid is in admin group """ @wraps(function) def decorated(*args, **kwargs): print(f"DEBUG(bws) requires_bid_access: kwargs={kwargs}") user, groups = args # magic blank = bws_db.Bookmark(app.config['database'],_from_db=True) loggedin_uid = bws_db.get_uid_from_any(app.config['database'],user) buid = -1 bid = kwargs['bid'] allowed = True # until we prove otherwise if "new" == bid: b = blank else: # get Bookmark object print(f"DEBUG(bws): will ask for bid {bid}.") b = bws_db.get_bookmark_by_id(app.config['database'],bid) buid = b.uid print(f"Got bookmark {b}") busername = "" # if not empty, then the loggedinuser is admin if loggedin_uid != buid and (-1 != buid and "-1" != buid): allowed = False if app.config['ADMIN_GROUP'] in groups: busername = bws_db.get_user_name_from_uid(app.config['database'],buid) # Then we allow it, with a warning allowed = True print(f"DEBUG(bws) requires_bid_access: loggedin_uid = {loggedin_uid} and bookmark uid = {buid}") # Yes, we are omitting **kwargs because to this decorator, kwargs is only bid which is already being passed somehow and we don't want to double-send it. #return function(loggedin_uid, busername, allowed, *args) # args is, [user,groups,] return function(*args,allowed,bid,loggedin_uid,busername,b) return decorated def requires_authn_ldap(function): ''' Require that the wrapped view function only be called by users authenticated with ldap. The view function will have the authenticated users principal passed to it as its first argument. :param function: flask view function :type function: function :returns: decorated function :rtype: function ''' @wraps(function) def decorated(*args, **kwargs): # formdata is in session if we are coming from login_basic() form = session.get('formdata', None) if form: #print(f"DEBUG(bws): requires_authn_ldap form={form}") session.pop('formdata') if 'username' in form: username = form['username'] if 'password' in form: pw = form['password'] else: # then we are coming from the form with POST data if 'username' not in request.form or 'password' not in request.form: return _unauthorized_ldap() username = request.form['username'] pw = request.form['password'] #print(f"DEBUG(bws): requires_authn_ldap with username={username} and pw={pw}") # learn dn of user this_uri = get_next_ldap_server(app) this_user = session_ldap.list_matching_users( server_uri=this_uri, bind_dn=app.config['LDAP_BIND_DN'], bind_pw=app.config['LDAP_BIND_PASSWORD'], user_base=app.config['LDAP_USER_BASE'], username=username, user_match_attrib=app.config['LDAP_USER_MATCH_ATTRIB'] ) # list_matching_users always returns list, so if it contains <> 1 we are in trouble if len(this_user) != 1: print(f"WARNING: cannot determine unique user for {app.config['LDAP_USER_MATCH_ATTRIB']}={username} which returned {this_user}") return _unauthorized_ldap() this_user = this_user[0] print(f"DEBUG(bws): requires_authn_ldap: found in ldap the username {this_user}") ll = ldap_login(this_user,pw) if ll: shortuser = session_ldap.get_ldap_username_attrib_from_dn( authenticated_user=ll, user_match_attrib=app.config['LDAP_USER_DISPLAY_ATTRIB'] ) groups = session_ldap.get_ldap_user_groups( connection=ll, user_dn=this_user, user_attrib_memberof=app.config['LDAP_USER_ATTRIB_MEMBEROF'], group_name_attrib=app.config['LDAP_GROUP_DISPLAY_ATTRIB'], group_base=app.config['LDAP_GROUP_BASE'] ) print(f"DEBUG(bws): user {shortuser} has groups {groups}") return function(shortuser, groups ,*args, **kwargs) else: return _unauthorized_ldap() return decorated def _unauthorized_ldap(): return Response(f'Unauthorized! Invalid ldap credentials... returning to login form', 401) def render_template_extra(templatefile,*args, **kwargs): """Render template with extra values passed, for nav and info""" s_user = session['user'] c_user = request.cookies.get('user') groups = session['groups'] cookie=request.cookies s_cn = "" if 'computername' in session: s_cn = session['computername'] s_t = "Bookmark Web Service" if 'title' in session: s_t = session['title'] #print(f"DEBUG(bws): render_template_extra title is \"{s_t}\"") return render_template(templatefile, # for nav settings_url = url_for("settings"), logout_url = url_for("logout"), # for info s_user=s_user, cookie = cookie, session_computername = s_cn, session_title = s_t, groups=groups, *args, **kwargs ) ## BARE FUNCTIONS def convert_timestamp_from_epoch_to_useful(ts): """ Convert epoch timestamps from varying specificity to a useful, to-the-seconds string without timezone, i.e., "YYYY-MM-DDTHH:MM:SS". """ out_ts = "" try: out_ts = datetime.datetime.fromtimestamp(ts/1.0).strftime('%Y-%m-%dT%H:%M:%S') except Exception as e: #print(f"DEBUG(bws) convert_timestamp: woops, {e}, trying 1000") try: out_ts = datetime.datetime.fromtimestamp(ts/1000.0).strftime('%Y-%m-%dT%H:%M:%S') except Exception as e: #print(f"DEBUG(bws) convert_timestamp: woops, {e}, trying 1000000") try: out_ts = datetime.datetime.fromtimestamp(ts/1000000.0).strftime('%Y-%m-%dT%H:%M:%S') except Exception as e: #print(f"DEBUG(bws) convert_timestamp: woops, {e}, just using \"{ts}\" directly)") out_ts = ts return out_ts ## ROUTE FUNCTIONS @app.route("/") def index(): if session: print("This user is already logged in!") return redirect(url_for("view")) return render_template('index.html') @app.route("/view/") @requires_session def view(user=None,groups=None): return render_template_extra('view.html') # This will be useful for setting the timestamp on the /edit/ page. @app.route("/internal/now/") def now(): return datetime.datetime.now().isoformat(timespec="seconds"),200 @app.route("/edit/",defaults={'bid':-1},methods=['GET','POST']) @app.route("/edit//",methods=['GET','POST']) @requires_session @requires_bid_access def edit(user=None,groups=None,allowed=False,bid="",loggedin_uid=-1,busername="",b=None): #print(f"DEBUG(bws) edit: locals()={locals()}") print(f"DEBUG(bws) edit: user={user}") print(f"DEBUG(bws) edit: groups={groups}") print(f"DEBUG(bws) edit: allowed={allowed}") print(f"DEBUG(bws) edit: bid={bid}") print(f"DEBUG(bws) edit: loggedin_uid={loggedin_uid}") print(f"DEBUG(bws) edit: busername={busername}") print(f"DEBUG(bws) edit: b={b}") # Common things for both POST and GET ts = datetime.datetime.now().isoformat(timespec='seconds'), if "" == bid or -1 == bid or "-1" == bid: try: return redirect(request.referer) except: return "",302 admin_message = "" if busername != "": # Then we allow it, with a warning admin_message = f"Using admin access to edit bookmark for user \"{busername}\"." if "GET" == request.method: if allowed: print(f"DEBUG: for rendering edit.html, aoint={b.always_open_in_new_tab}") return render_template_extra("edit.html", bid = b.bid, url = b.url, title = b.title, tags = b.tags, ts = b.timestamp, notes = b.notes, iid = b.iid, order_id = b.order_id, always_open_in_new_tab = b.always_open_in_new_tab, admin_message = admin_message, allowed = allowed ) else: # not allowed, from above logic return f"Invalid bookmark.

Returning momentarily...",401 elif "POST" == request.method: f = request.form # n is the new object dictionary, with the default values. Required fields are not listed here. Checkboxes are not listed here. n = { "title": "", "tags": [], "notes": "", "iid": -1, "timestamp": ts, "order_id": -1 } if 'draganddrop' in request.headers: if "new" != bid: return "Bad request",400 else: # process the draganddrop stuff here print(f"DEBUG(bws) edit/new/ d&d") #for i in f: # print(f"DEBUG(bws) edit/new/ d&d: Form[\"{i}\"]: \"{f[i]}\"") if 'type' not in f: return """Bad request: needs 'type' matching of these supported types: ['text/x-moz-place','text/x-moz-url','text/html']""",400 if 'string' not in f: return "Bad request: no contents of 'string'!",400 ftype = f['type'] fstring = f['string'] if "text/x-moz-place" == ftype: # item is a dict: title, id, itemGuid, instanceID, parent, parentGuid, dateAdded,lastModified,type,uri. newitem = json.loads(fstring) print(f"DEBUG(bws) edit/new/ d&d newitem={newitem}") try: ftitle = newitem['title'] except: ftitle = n['title'] try: furl = newitem['uri'] except: return "Invalid request! No uri in text/x-moz-place object.",400 try: ftags = newitem['tags'] except: ftags = n['tags'] new_ftags = [] for i in ftags: if str(i) != "": new_ftags.append(i) ftags = new_ftags try: fnotes = newitem['comment'] except: fnotes = n['notes'] fts = convert_timestamp_from_epoch_to_useful(newitem['dateAdded']) try: forder_id = newitem['id'] except: forder_id = 0 faoint = False try: fusername = f['username'] fuid = bws_db.get_uid_from_any(app.config['database'],fusername) except: fuid = loggedin_uid b = bws_db.Bookmark( source = app.config['database'], url = furl, title = ftitle, uid = fuid, tags = ftags, notes = fnotes, iid = 0, timestamp = fts, order_id = forder_id, always_open_in_new_tab = faoint ) if -1 == b.bid or "-1" == b.bid: return "Something failed on server side.",500 else: return f"Added bookmark id {b.bid}",201 # will get displayed in the drop box for 1.5 seconds by the javascript return "OK",200 elif "text/x-moz-url" == ftype: print(f"DEBUG(bws) still need support to parse text/x-moz-url \"{fstring}\"") return "OK",200 elif "text/html" == ftype: print(f"DEBUG(bws) still need support to parse text/html \"{fstring}\"") else: print(f"DEBUG(bws) e/n/ d&d: Unsupported type as of yet: \"{ftype}\" string \"{fstring}\"") return f"Unsupported type as of yet: \"{ftype}\" string \"{fstring}\"",400 else: # not drag-and-drop print(f"DEBUG(BWS): Got a POST for /edit/") for i in f: print(f"DEBUG(bws): Form[\"{i}\"]: \"{f[i]}\"") # validate required fields for word in ['url']: if word not in f: return f"Bad request: needs attribute \"{word}\".",400 # checkboxes are stupid, and will appear in the list as false, if the checkbox is checked. If the checkbox is unchecked, the attribute simply will be absent in the POST. try: ftitle = f['title'] except: ftitle = n['title'] try: ftags = f['tags'].split(','), except: ftags = n['tags'] if tuple == type(ftags): ftags=ftags[0] new_ftags = [] for i in ftags: if str(i) != "": new_ftags.append(i) ftags=new_ftags try: fnotes = f['notes'] except: fnotes = n['notes'] try: fts = f['timestamp'] except: fts = n['timestamp'] try: forder_id = f['order_id'] print(f"DEBUG(bws) edit: forder_id=f['order_id']={forder_id}") except: forder_id = n['order_id'] print(f"DEBUG(bws) edit: forder_id=n['order_id']={forder_id}") faoint = False if 'always_open_in_new_tab' in f: print(f"DEBUG(bws): always_open_in_new_tab was in the form, so setting it to True") faoint = True print(f"DEBUG(bws): ftags is {ftags}") if "new" == bid: if "" == fts: fts = n['timestamp'] print(f"DEBUG(bws): making new bookmark") b = bws_db.Bookmark( source = app.config['database'], url = f['url'], title = ftitle, uid = loggedin_uid, tags = ftags, notes = fnotes, iid = 0, timestamp = fts, order_id = forder_id, always_open_in_new_tab = faoint ) if -1 == b.bid or "-1" == b.bid: return "Something failed on server side.",500 else: #return f"The server returned bid {b.bid}",201 return f"Added bookmark id {b.bid}!

Returning momentarily...",201 else: if allowed: # POST, allowed, but not new, so just update. message = "" print(f"DEBUG(bws) edit: updating bookmark {b.bid}") if f['url'] != b.url: b.url = f['url'] message += "

  • url
  • " if ftitle != b.title: b.title = ftitle message += "
  • title
  • " if str(ftags) != str(b.tags): b.tags = ftags message += "
  • tags
  • " if fnotes != b.notes: b.notes = fnotes message += "
  • notes
  • " if str(fts) != str(b.timestamp): b.timestamp = fts message += "
  • timestamp
  • " if str(forder_id) != str(b.order_id): b.order_id = forder_id message += "
  • order_id
  • " if str(faoint) != str(b.always_open_in_new_tab): b.always_open_in_new_tab = faoint message += "
  • always_open_in_new_tab
  • " if "" != message: b.update() message = "Bookmark attributes updated:" message += f"Returning momentarily..." return message,200 else: # no changes #message += "No changes" message += f"Returning momentarily..." return message,200 else: # POST, not allowed return f"Bad request.

    Returning momentarily...",401 return f"Bad request.

    Returning momentarily...",400 # because html forms do not support http DELETE @app.route("/delete/",defaults={'bid':-1},methods=['POST']) @app.route("/delete//",methods=['GET','POST']) @requires_session @requires_bid_access def delete(user=None,groups=None,allowed=False,bid="",loggedin_uid=-1,busername="",b=None): print(f"DEBUG(bws) delete: got request to delete {bid} from loggedin_uid {user}, with allowed={allowed}") if allowed: text = str(b) response = b.delete() if 1 == response: return f"Deleted {escape(text)}.

    Returning momentarily...",200 else: return f"Got some other response, {response}.",500 else: return f"Bad request.

    Returning momentarily...",401 ############################# # xhr requests @app.route("/view/list//") @requires_session def view_list_user(user=None,groups=None,username=""): if "users" == username: if 'Accept' in request.headers and 'application/json' not in request.headers['Accept']: return "Only json is implemented for /view/list/users/",400 user_list = bws_db.get_all_users(app.config['database']) print(f"DEBUG(bws) view_list_user: sending json user_list") return jsonify(json.loads(json.dumps(user_list,cls=bws_db.BookmarkEncoder))) else: print(f"Request, user {user} in groups {groups}") if user == username: #print(f"User requesting his own bookmarks") pass elif app.config['ADMIN_GROUP'] in groups: #print(f"Admin user requesting bookmarks for user {username}") pass else: return Response(f"Wrong user",401) a = bws_db.get_all_bookmarks(app.config['database'],username) if 'Accept' in request.headers and 'application/json' in request.headers['Accept']: print(f"DEBUG(bws): Returning user {username} bookmarks as json") # I do not know how to jsonify with cls=bws_db.BookmarkEncoder, so I just used # this back-and-forth thing return jsonify( json.loads(json.dumps(a,cls=bws_db.BookmarkEncoder)), edit_url = url_for('edit') ) else: print(f"DEBUG(bws) view_list_user: Returning user {username} bookmarks as html") return render_template("view_list.html", bm_list = a ) @app.route("/view/set///", methods=['POST']) @requires_session def set_computername(user=None,groups=None,item="",value=""): print(f"DEBUG(bws): Setting session {session} item {item} to value \"{value}\".") if item in ['computername','title']: session[item] = value return "OK",200 else: return "bad item",400 ## LOGIN FUNCTIONS @app.route("/login/new") @app.route("/login/new/") def login_new(): """Force the user to authenticate again. This is not about creating a new user.""" return redirect(url_for("login", new="")) @app.route("/login/", methods=['POST','GET']) def login(user="None"): if request.method == "GET": if 'user' in session and request.cookies.get('user') == session['user'] and (not 'new' in request.args): return redirect(url_for("view")) auth_header = request.headers.get("Authorization") if auth_header: print(f"DEBUG(bws): auth_header provided, but kerberos was removed... WHAT DO I DO NOW?!") #if "negotiate" in auth_header: # # assume we are already trying to log in with kerberos # return redirect(url_for("login_kerberos")) # default, show login form return redirect(url_for("login_form")) elif request.method == "POST": if request.authorization: return redirect(url_for("login_basic"),code=307) return handle_login_ldap_from_non_ldap(request) def get_next_ldap_server(app): # on first ldap_login attempt, cache this lookup result: if 'LDAP_HOSTS' not in app.config: this_domain = urlparse(app.config['LDAP_URI']).hostname app.config['LDAP_HOSTS'] = session_ldap.list_ldap_servers_for_domain(this_domain) else: # rotate them! So every ldap_login attempt will use the next ldap server in the list. this_list = app.config['LDAP_HOSTS'] a = this_list[0] this_list.append(a) this_list.pop(0) app.config['LDAP_HOSTS'] = this_list # construct a new, full uri. this_netloc = app.config['LDAP_HOSTS'][0] up = urlparse(app.config['LDAP_URI']) if up.port: this_netloc += f":{up.port}" this_uri = up._replace(netloc=this_netloc).geturl() return this_uri def ldap_login(username,password): #print(f"DEBUG(bws): Trying user {username} with pw '{password}'") this_uri = get_next_ldap_server(app) # Perform the ldap interactions user = session_ldap.authenticated_user( server_uri=this_uri, user_dn=username, password=password ) if user: return user else: return False return False @app.route("/login/ldap", methods=['POST','GET']) @app.route("/login/ldap/", methods=['POST','GET']) @requires_authn_ldap def login_ldap(user,groups=[]): resp = Response(f'success with ldap') resp.set_cookie('type',"ldap") resp = login_generic(session,resp,user,groups) return resp def login_generic(session,resp,user,groups=[]): resp.set_cookie('user',user) end_time = datetime.datetime.now(datetime.timezone.utc) + app.permanent_session_lifetime end_time_str = datetime.datetime.strftime(end_time,"%FT%TZ") resp.set_cookie('timestamp',end_time_str) session.permanent = True session['user']=user session['end_time'] = end_time_str session['groups'] = groups session.modified = True return resp @app.route("/login/form", methods=['POST','GET']) @app.route("/login/form/", methods=['POST','GET']) def login_form(): if request.method == "GET": options = { "ldap": "ldap", "other": "other" } return render_template("login_form.html", login_url = url_for("login") ) #options=options else: # assume it is a POST return redirect(url_for("login_ldap"), code=307) def handle_login_ldap_from_non_ldap(request): # set default logintype for user logintype = "ldap" # redirect to whichever option was chosen in the drop-down if 'logintype' in request.form: logintype = request.form['logintype'] if "ldap" == logintype: return redirect(url_for("login_ldap"), code=307) else: return f"Authentication method {logintype} not supported yet.",400 @app.route("/logout") @app.route("/logout/") def logout(): resp = Response(f'logged out') # not documented but is found on the Internet in a few random places: session.clear() resp.set_cookie('user','',expires=0) resp.set_cookie('type','',expires=0) resp.set_cookie('session','',expires=0) resp.set_cookie('timestamp','',expires=0) return resp @app.route("/login/basic",methods=['POST','GET']) @app.route("/login/basic/",methods=['POST','GET']) def login_basic(): if not request.authorization: return Response(f"Please provide username and password.",401,{'WWW-Authenticate': 'Basic'}) if 'username' not in request.authorization: return Response(f"No username provided.",401) if 'password' not in request.authorization: return Response(f"No password provided.",401) username = request.authorization.username pw = request.authorization.password form={'username':username,'password':pw} session['formdata'] = form session.modified = True return redirect(url_for("login_ldap"),code=307) @app.route("/settings/", methods=['GET','POST']) @requires_session def settings(user,groups): print(f"DEBUG(bws): visit settings page as user {user}") print(f"DEBUG(bws): with groups {groups}") #if "admins" not in groups: #return Response(f'

    Not Found

    What you were looking for is just not there.

    Start over', 404) #return Response(f'

    Not Found

    The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.

    ', 404) # return Response(f'

    Not Authorized

    You are not authorized to access this page.

    ', 403) if False: a = False else: if request.method == "GET": s_cn = "" if 'computername' in session: s_cn = session['computername'] s_t = "Bookmark Web Service" if 'title' in session: s_t = session['title'] return render_template( 'settings.html', ldap_uri=app.config['LDAP_URI'], session_computername = s_cn, session_title = s_t ) elif request.method == "POST": form = request.form print(f"Form: {form}") message = "" if 'computername' not in form and 'title' not in form: return Response("Invalid input.", 400) else: s_cn = "" if 'computername' in session: s_cn = session['computername'] s_t = "" if 'title' in session: s_t = session['title'] if 'computername' in form and form['computername'] != s_cn: session['computername'] = form['computername'] message += "
  • computer name
  • " session.modified = True if 'title' in form and form['title'] != s_t: session['title'] = form['title'] message += "
  • Tab title
  • " session.modified = True if "" != message: message = "Settings updated:" #message += f"
    " pp = url_for("view") message += f"Returning momentarily..." message += f""" """ return Response(message, 200) ## This bumps the session lifetime to two minutes farther out from each web request with this session. #@app.before_request #def make_session_permanent(): # session.permanent = True # session['end_time'] = datetime.datetime.now()+app.permanent_session_lifetime if __name__ == '__main__': print("should listen to ",app.config['LISTEN_HOST']) app.run( host=app.config['LISTEN_HOST'], port=app.config['LISTEN_PORT'], debug=app.config['DEBUG'] )