########################################################################
# 
# passApp.py
#
# This Python/Flask application provides secure coreBuild option key
# generation. By default, the application will provide an HTTPS server
# on port 8085. It uses Google's sign-in authentication to confirm a
# users identity. This will allow access to anyone with a valid Google account
# (even non-Trimble). To further restrict access, a list of authorized
# users is available on the server, the server code (python below) gets
# an encrypted token that the Google login creates. Embedded in that
# token is the users email address. On each request for option codes,
# the python code gets the token via a cookie, it then uses a Google
# python library to (1) confirm the token is valid (they expire after an
# hour) (2) extracts the user's email address. If the token is valid, we
# know the user has securely logged into Google, and we have proof of 
# identity. We test the email from the token against the access list,
# we only generate an option key if the user is on the list.
#
# The HTML/Javascript used for the site is overkill, it is capable of
# building very complex dashboards. It was used as we are already using
# the code for more sophisticated systems, and it was quick to drop the 
# option code into this framework.
#
# This code reads certificate information for the HTTPS server, those
# files need to be available otherwise the server will not start! They
# were generated by IS for higgs.eng.trimble.com
#
# Copyright Trimble Inc 2020-2025
########################################################################

import sys
import os
import time
import datetime
import signal
import json
import logging
import subprocess
from logging.handlers import RotatingFileHandler
from flask import Flask, render_template_string, make_response, Response, request, jsonify, send_from_directory
from markupsafe import Markup


import pdb

from google.oauth2 import id_token
from google.auth.transport import requests

from okws import Okws

from urllib.parse import unquote

enableServerLogs = True
# To run from a fresh Anaconda 3.X python install:
# conda install -c conda-forge flask-caching
# conda install -c conda-forge flask-compress

enableOkwsDebuggingOutput = True
# Set to True to enable OKWS debugging output to the console.

isProduction = False
# If False, this is a Staging server that can generate Arrowhead Subcription
# and/or SPOK Staging keys depending on the user's access level.
# If set to True below based on the port number during startup, this server
# can produce SPOK Production keys if the user is authorized.

# Makes sure that the static data doesn't get reloaded 
try:
  from flask_caching import Cache
  cacheConfig = {
    "DEBUG": True,          # some Flask specific configs
    "CACHE_TYPE": "simple", # Flask-Caching related configs
    "CACHE_DEFAULT_TIMEOUT": 300
  }
  cacheSupported = True
except:
  cacheSupported = False


# Flask compress isn't loaded on all systems, only install when it is
# available. It will gzip data over HTTP. The client doesn't need to
# download a lot of data from the server, so this will not have much
# impact. However, it is good practice to reduce the bandwidth
# requirements
try:
  # This is not installed as standard by the anaconda python
  # installation. To install:
  from flask_compress import Compress
  CompressSupported = True
except:
  CompressSupported = False

try:
  from urllib.request import urlopen
except ImportError:
  from urllib2 import urlopen


app = Flask(__name__)

if(CompressSupported == True):
  # Print to the terminal as the log hasn't been setup yet
  print("HTTP Compression Installed")
  Compress(app)

if(cacheSupported):
  print("HTTP Cache Installed")
  cache = Cache(app,config=cacheConfig)

# This intercepts the web requests, it allows us to filter and
# provides a central place for logging
@app.before_request
def limit_remote_addr():
  if(request.remote_addr == '10.1.187.20'):
    # Block Trimble's scanner which messes up the logs
    return "", 403
  else:
    if(enableServerLogs == True):
      app.logger.info("%s %s" % (request.remote_addr, request.url))

# If we get a 404 (page not found) log an error
@app.errorhandler(404)
def handle404(error):
  if(enableServerLogs == True):
    app.logger.error("%s %s" % (request.remote_addr, request.url))
  return "", 404

def addAllowOrigins(response):
  response.headers.add('Access-Control-Allow-Origin', '*')
  #response.headers.add('Access-Control-Allow-Origin', '*.trimble.com')
  #response.headers.add('Access-Control-Allow-Origin', '*.trimblecorp.net')
  return response

@app.route("/")
def main():
  response = index()
  return addAllowOrigins( response )

@app.route('/favicon.ico')
def favicon():
  response = send_from_directory(os.path.join(app.root_path, 'static'),
                             'favicon.ico', mimetype='image/vnd.microsoft.icon')
  return addAllowOrigins( response )

# Provide a way for InTech to get a list of all options generated. For now
# this has no password protection, it is "security by obscurity"
@app.route("/InTech.csv")
def InTech():
  outbuff = ''
  with open('OptionLog.txt','r') as fid:
    for line in fid:
      tokens = line.split(',')
      if(tokens[2] == 'InTech'):
        outbuff += line
  return Response(outbuff,mimetype='text/csv')

@app.route("/Ag.csv")
def Ag():
  outbuff = ''
  with open('OptionLog.txt','r') as fid:
    for line in fid:
      tokens = line.split(',')
      if(tokens[2] == 'Ag'):
        outbuff += line
  return Response(outbuff,mimetype='text/csv')

# This is called automatially with the Google identit token after the user
# has authenticated by selecting a Google account. If the user is authorized
# for access, the XML returned indicates that the user is authenticated and
# includes flags to indicate whether authorization includes Arrowhead
# Subscription key generation ("subs"), SPOK key generation ("spok"), or
# SPOK key generation with the ability to create SN-changing keys ("spoksn").
@app.route("/authToken.cgi")
def authToken():
  result = None

  try:
    if(request.cookies.get('GToken')):
      cookie = request.cookies.get('GToken')
      
      idinfo = id_token.verify_oauth2_token( cookie,
                                             requests.Request(),
                                             CLIENT_ID )
      if idinfo['iss'] not in ['accounts.google.com',
                               'https://accounts.google.com']:
        raise ValueError('Wrong issuer.')

      (loggedInApp, email, picture, group,
       allowedTypes, allowedModels) = getAuth( idinfo )

      print( "OK loggedInApp: %s allowedTypes: %s" % (
             loggedInApp, allowedTypes ))

      prefix = "prd" if isProduction else "stg"

      subs = 'FALSE'
      if     "%s.sub" % ( prefix ) in allowedTypes \
         and not isProduction:
        subs = 'TRUE'

      spok = 'FALSE'
      if "%s.spok" % ( prefix ) in allowedTypes:
        spok = 'TRUE'

      spokSn = 'FALSE'
      if "%s.spok+sn" % ( prefix ) in allowedTypes:
        spokSn = 'TRUE'

      loggedIn = 'FALSE'
      if loggedInApp and len(allowedTypes) > 0:
        loggedIn = 'TRUE'
      else:
        subs   = 'FALSE'
        spok   = 'FALSE'
        spokSn = 'FALSE'

      result = "<data><auth>%s</auth><subs>%s</subs><spok>%s</spok><spoksn>%s</spoksn></data>" % (
               loggedIn, subs, spok, spokSn )

    else:
      result = "<data><warn>Google cookie error.</warn></data>"

    print( "result: %s" % ( result ) )

  except:
    result = "<data><warn>Invalid Google ID token.</warn></data>"

  response = Response(result, mimetype='text/xml')
  return addAllowOrigins( response )

# This is called with parameters when the user requests generation of
# an option key. Make sure the user is on the authorized list, if they
# are generate and return the option key.
@app.route("/optionKey.cgi")
def optionKey():
  (loggedInGoogle, loggedInApp, email, picture, group, \
   allowedTypes, allowedModels) = tokenLogin(request)

  if(loggedInGoogle == False):
    result = "<data><warn>Not logged in via Google</warn></data>"
  elif(loggedInApp):
    GotSerial = False
    SerialNum = ''

    receiverTypes = []
    receiverNames = []

    optTokens = request.args['opts'].split()
    print(optTokens)

    # Make sure there's a serial number, otherwise optionKey generates
    # a universal key. This is a risk (we don't want a key for RTX that
    # can be installed in any receiver to leak out of Trimble). Therefore,
    # prevent the generation of univeral keys.
    for i in range(len(optTokens)):
      if(optTokens[i].startswith('-s')):
        GotSerial = True
        SerialNum = optTokens[i][2:]
        break

    siteType = 'prd' if isProduction else 'stg'

    if(GotSerial):
      keyTypeStr = '%s.legacy' % ( siteType )
      keyCrypto   = 'none'
      spokKeyReason = ''

      command = "./optionKey " + request.args['opts'] + " 2>/dev/null"
      ret = subprocess.check_output(command, shell=True).decode().rstrip()

      # Create a SPOK key from legacy options using cm_licKeys.
      if '2' == request.args['keyType']:
        keyTypeStr = '%s.spok' % ( siteType )
        serial = request.args['serial']

        # The newSerial and hwIdToken values are required when setting
        # the serial number.
        newSerial     = None
        hwIdToken     = None
        newSerialType = None
        if 'newSerial' in request.args.keys():
          newSerial = request.args['newSerial']
        if 'hwIdToken' in request.args.keys():
          hwIdToken = request.args['hwIdToken']
        if 'newSerialType' in request.args.keys():
          newSerialType = request.args['newSerialType']

        if not None == newSerial:
          keyTypeStr = '%s.spok+sn' % ( siteType )

        keyCrypto  = 'okws-production' if isProduction else 'okws-staging'
        okws = Okws(Okws.SPOK_PRODUCTION if isProduction else Okws.SPOK_STAGING)
        okws.debug = enableOkwsDebuggingOutput

        # If this is a Production server, 
        if isProduction:
          for allowed in allowedModels:
            receiverNames.append( allowed['name'] )
            receiverTypes.append( allowed['type'] )
          spokKeyReason = unquote( request.args['spokKeyReason'] )

        ret = okws.getSpok( email, serial, ret,
                            newSerial = newSerial,
                            hwIdToken = hwIdToken,
                            newSerialType = newSerialType,
                            receiverTypes = receiverTypes )
        print("okws.getSpok(): ret: %s" % ( ret ))
        #print("OKWS Return: %s" % ( ret ))

        # Obsolete engineering key generation...
        # keyCrypto  = 'eng'
        # # Generate time string as yyyymmddThhmmss.fff, including milliseconds.
        # utc = datetime.datetime.utcnow().strftime( "%Y%m%dT%H%M%S.%f" )[:-3]
        # cmd = './cm_licKeys -v3 -encrypt'
        # cmd += ' -csv="CT=%s,LT=PO,SN=%s,FW=5.60,OP=%s"' % (utc, serial, ret)
        # cmd += ' 2>/dev/null'

        # ret = subprocess.check_output(cmd, shell=True).decode().rstrip()

      # Create a Subscription key from legacy options using cm_licKeys.
      # TODO: Make this more explicit.
      if '3' == request.args['keyType']:
        keyTypeStr = '%s.sub' % ( siteType )
        keyCrypto   = 'eng'
        serial = request.args['serial']

        duration = int( request.args['duration'] )
        # Generate time string as yyyymmddThhmmss.fff, including milliseconds.
        utc = datetime.datetime.utcnow()
        utcStr = utc.strftime( "%Y%m%dT%H%M%S.%f" )[:-3]
        expire = utc + datetime.timedelta( minutes = duration )
        expireStr = expire.strftime( "%Y%m%dT%Hjsdf%M%S" )

        okws = Okws(Okws.SPOK_PRODUCTION if isProduction else Okws.SPOK_STAGING)
        okws.debug = enableOkwsDebuggingOutput
        ret = okws.getSub( email, serial, ret, expire, [] )

        # Obsolete: Engineering key generation.
        #cmd = './cm_licKeys -v2 -encrypt'
        #cmd += ' -csv="CT=%s,LT=TL,ET=%s,SN=%s,FW=5.54,OP=%s"' % ( 
        #       utcStr, expireStr, serial, ret )
        #cmd += ' 2>/dev/null'
        #
        #ret = subprocess.check_output(cmd, shell=True).decode().rstrip()



      # Handle creation of a SMOK key.
      if '4' == request.args['keyType']:
        keyTypeStr  = '%s.smok' % ( siteType )
        keyCrypto   = 'smok'
        serial      = request.args['serial']

        firmwareWarranty =""        # Format: "YYYY-MM-DD"
        downgradeLimit   =""        # Format: "566"
        securityToken    =""
        unlock           = False
        unbrick          = False
        if 'smokFwYear' in request.args.keys() and 'smokFwMonth' in request.args.keys():
          firmwareWarranty = "%04d-%02d-01" % ( int(request.args['smokFwYear']), int(request.args['smokFwMonth']) )
 
        if 'smokFwDowngrade' in request.args.keys():
          downgradeLimit = str( request.args['smokFwDowngrade'] )

        if 'smokUnlockHwId' in request.args.keys():
          securityToken = request.args['smokUnlockHwId']
          unlock        = True

        if 'smokUnbrickHwId' in request.args.keys():
          securityToken = request.args['smokUnbrickHwId']
          unbrick       = True

        okws = Okws( Okws.SMOK_STAGING )
        ret = okws.getSmok( email, serial, firmwareWarranty, downgradeLimit,
                            securityToken, unlock, unbrick )

      # Return the key if this user is authorized to create this type of key.
      if None == ret:
        if keyCrypto == 'okws-production':
          result = "<data><warn>OKWS Production Key Generation Failed</warn>"
        elif keyCrypto == 'okws-staging':
          result = "<data><warn>OKWS Staging Key Generation Failed</warn>"
        elif keyCrypto == 'eng':
          result = "<data><warn>Eng. Key Generation Failed</warn>"
        elif keyCrypto == 'smok':
          result = "<data><warn>SMOK Key Generation Failed</warn>"
        else:
          result = "<data><warn>Key Generation Failed</warn>"

      elif keyTypeStr in allowedTypes:
        receiverList = ''
        if len(receiverNames) > 0:
          receiverNames.sort()
          receiverList = '<warn>Key restricted to these models:'
          for i in range(len(receiverNames)):
            name = receiverNames[i]
            if i > 0:
              receiverList += ','
            receiverList += ' ' + name
          receiverList += '</warn>'

        # Do not return a SPOK or Subscription key if no options have
        # been specified.
        print('len(optTokens) %s' % ( len(optTokens) ))

        keyType = request.args['keyType']
        if(len(optTokens) == 1) and not '1' == keyType and not '4' == keyType:
          result = "<data><key></key>" + receiverList

        if(len(optTokens) == 1) and not '1' == keyType and not '4' == keyType:
          result = "<data><key></key>"
        else:
          result = "<data><key>" + ret + "</key>" + receiverList

        # Warn if the key type is not SMOK and no options are specified.
        if(len(optTokens) == 1 and not '4' == request.args['keyType']):
          result += "<warn>No option selected</warn>"

      elif ret == None:
        result = "<data><warn>Key generation failed!</warn>"

      else:
        result = "<data><warn>Not authorized (type: %s)</warn>" % ( keyTypeStr )

      result += "</data>"
      # Fields:
      #   0: date/time string as "yyyy-mm-ddThh:mm:ss"
      #   1: email address
      #   2: group assocation
      #   3: address of remote client requesting the key
      #   4: serial number
      #   5: generated options key
      #   6: optionKey command parameters
      #   7: keyTypeStr: 'legacy', 'spok', or 'sub'
      #   8: keyCrypto: 'none' or 'eng'
      #   9: spokKeyReason: Reason entered in production.
      fidOption.write(  "%s,%s,%s,%s,%s,%s,'%s',%s,%s" 
                      % (getDateStr(),email,group,request.remote_addr, SerialNum,ret,request.args['opts'],keyTypeStr,keyCrypto) )
      if isProduction:
        fidOption.write( ',Reason="%s"' % ( spokKeyReason ) )

      fidOption.write( '\n' )
      fidOption.flush()
    else:
      result = "<data><warn>Universal keys not allowed - need a SN</warn></data>"
  else:
    result = "<data><warn>" + email + " has a valid google account, but is not authorized for this site - if you have a valid business need, contact stuart_riley@trimble.com to be added to the access list</warn></data>"

  print(result)
  response = Response(result,mimetype='text/xml')
  return addAllowOrigins( response )


# This is not used by the standard interface. It is there as a 
# quick test to be able to check the list of users and make sure
# the formatting is correct
@app.route("/users.json")
def users():
  with open("users.json",'r') as f: 
    users = json.load(f)
  jsonData = jsonify(users)
  return jsonData

# Helper function to return the data and time as a string for debug
def getDateStr():
  now = datetime.datetime.now() 
  date_time = now.strftime("%Y-%m-%dT%H:%M:%S")
  return(date_time)

def getAuth( idinfo ):
  loggedInApp   = False
  email         = None
  picture       = None
  group         = None
  allowedTypes  = []
  allowedModels = []

  # Get a list of authorized users - we read this everytime so we can
  # update the user list without having to restart the program
  with open("users.json",'r') as f: 
    users = json.load(f)
    
  group = None
  user  = None
  email = idinfo['email'].lower()
  for i in range(len(users)):
    if( email == users[i]['email'].lower()):
      user  = users[i]
      group = user['group']
      break

  if None == group:
    print( "ERROR User %s not found." % ( email ) )
    # The user is not found in the users.json file.
    fidUserError.write("%s,%s,UnknownUser\n" % (getDateStr(),email) )
    fidUserError.flush()

  else:

    print( "OK check %s authorization." % ( email ) )
    # Load authorizaton info - Load every time so that the JSON file can
    # bu updated without having to restart the program.
    with open("auth.json",'r') as f: 
      authorizations = json.load(f)

    if not group in authorizations['groups'].keys():
      print( "ERROR %s not in %s" %
             ( group, str( authorizations['groups'] ) ) )
    else:
      auths     = authorizations['auths']
      role      = authorizations['groups'][group]['role']

      stgAuth = authorizations['roles'][role]['stg_auth']
      prdAuth = authorizations['roles'][role]['prd_auth']

      # If specified, collect the set of allowed model names and model indices
      # into allowedModels. By default, allowedModels is '[]', and keys apply
      # to any model receiver.
      model_sets = authorizations['model_sets']
      models     = authorizations['models']

      types      = authorizations['groups'][group]['types']
      # Override the group definition of 'types' if specified for the user.
      if 'types' in user.keys():
        types = user['types']

      allowedModels = []
      usedModels = []
      for s in types:
        print("type: %s" % ( s ))
        if s in model_sets.keys():
          for name in model_sets[s]:
            if not name in usedModels:
              if name in models.keys():
                usedModels.append( name )
                allowedModels.append( {'name': name, 'type': models[name] } )
              else:
                pass        # TODO: Log error.
        elif s in models.keys():
          allowedModels.append( {'name': s, 'type': models[s] } )

        else:
          pass        # TODO: Log error.

      # If an authorization level is associated with the user,
      # it supersedes the level defined for the group. This allows
      # either a reduction or increase in the authorization level.
      if 'stg_auth' in user.keys():
        stgAuth = user['stg_auth']
      if 'prd_auth' in user.keys():
        prdAuth = user['prd_auth']

      print("stg_auth: %s prd_auth: %s" % ( stgAuth, prdAuth ))

      # The authorization is only valid if the specified level is matched.
      matched = False
      stgTypes = []
      iAuth = itemIndex( auths, stgAuth )
      if iAuth > 0:                       # Process only if g.t. 'none'.
        for auth in auths[1:]:            # Skip 'none'.
          print('Add stg.%s' % ( auth ))
          stgTypes.append( '%s.%s' % ( 'stg', auth ) )
          if auth == stgAuth:
            matched = True
            break

      # Ignore if the level does not match a defined level.
      if not matched:
        # TODO: Report this error.
        stgTypes = []

      matched = False
      prdTypes = []
      iAuth = itemIndex( auths, prdAuth )

      if iAuth > 0:                       # Process only if g.t. 'none'.
        for auth in auths[1:]:            # Skip 'none'.
          print('Add prd.%s' % ( auth ))
          prdTypes.append( '%s.%s' % ( 'prd', auth ) )
          if auth == prdAuth:
            matched = True
            break

      # Ignore if the level does not match a defined level.
      if not matched:
        # TODO: Report this error.
        prdTypes = []

      if isProduction and len(prdTypes) > 0:
        loggedInApp = True
        allowedTypes = prdTypes

      elif not isProduction and len(stgTypes) > 0:
        loggedInApp = True
        allowedTypes = stgTypes

      print("allowedTypes: %s" % ( allowedTypes ))

      if not loggedInApp:
        # The user authenticated, but was unable to login as they
        # aren't on the authorized list. Make a note of this.
        fidUserError.write("%s,%s,NotAuthorized-%s\n" % (
                           getDateStr(),
                           email,
                           "Production" if isProduction else "Staging"))
        fidUserError.flush()
        
  print("allowedModels: %s" % ( str(allowedModels) ))
  return(loggedInApp, email, picture, group, allowedTypes, allowedModels)

# Return the index of 'item' in 'items', or -1 if it 'item' is not found.
def itemIndex( items, item ):
  try:
    idx = items.index( item )
    return idx
  except ValueError:
    return -1

# Get the token from the cookie and check if it is valid. If it is valid, check if the
# user is on our access list.
def tokenLogin(request):
  loggedInGoogle = False
  loggedInApp    = False
  picture = None
  email   = None
  group   = None
  allowedTypes  = []
  allowedModels = []
  try:
    if(request.cookies.get('GToken')):
      cookie = request.cookies.get('GToken')
      
      idinfo = id_token.verify_oauth2_token(cookie, requests.Request(), CLIENT_ID)

      # Output some diagnostics
      print(getDateStr(),"Computer time",time.time(),
                         " expires = ",idinfo['exp'],
                         "user = ",idinfo['email'])

      if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
        raise ValueError('Wrong issuer.')
      else:
        loggedInGoogle = True
    
      if( loggedInGoogle ):
        (loggedInApp, email, picture, group,
         allowedTypes, allowedModels) = getAuth( idinfo )

  except ValueError:
    print("Invalid Token")
    ( loggedInGoogle, loggedInApp, email, picture, group,
      allowedTypes, allowedModels ) = ( False, False, None, None, [], [] )
 
  return( loggedInGoogle, loggedInApp, email, picture, group,
          allowedTypes, allowedModels )

@app.route("/index.html")
def index():
  webStr =  "{% extends 'menu.html' %}"
  # We are just inserting the client ID into the HTML - look for {{client_id}} in templates/menu.html
  html = render_template_string( Markup(webStr),
                                 client_id=CLIENT_ID,
                                 client_mode=CLIENT_MODE,
                                 client_mode_header=CLIENT_MODE_HEADER )
  response = make_response( html )
  return addAllowOrigins( response )

def signal_handler(signal, frame):
  print('You pressed Ctrl+C!')
  print("Exiting")
  fidUserError.close()
  fidOption.close()


  sys.exit(0)

modeHeader_staging = "Mode: Test/Staging"
modeHeader_production = "Mode: Production"

if __name__ == "__main__":
  # With no arguments will run on port 8085, this is the user facing 
  # service. For testing we have a client ID for 8087
  if(len(sys.argv) == 2):
    webPort = int(sys.argv[1])
  else:
    webPort = 8085

  if not webPort in [8085, 8087, 8095, 8097]:
    print("We only have client IDs for ports 8085, 8087, 8095, and 8097")
    sys.exit(0)

  # These keys were generated via the Google API console. They are only valid on 
  # higgs for ports 8085 and 8087
  if(webPort == 8085):
    isProduction = False

    # Stuart's original client ID.
    #CLIENT_ID = "961159168875-1lnajhslvknorkps4rkp961bgl5e9ohu.apps.googleusercontent.com"
    # Oauth 2.0 Client ID from the Google Project "gnsstech-corebuild-security".
    CLIENT_ID =   "607027746270-4ca15t2s17iag2nepr3tttmtok0f982q" \
                + ".apps.googleusercontent.com"
    CLIENT_MODE = "staging"
    CLIENT_MODE_HEADER = modeHeader_staging

  elif(webPort == 8087):
    isProduction = False
    # Stuart's original client ID.
    #CLIENT_ID = "961159168875-68tbl5pcidvckon4a8kdmnrb63d2ljrg.apps.googleusercontent.com"
    # Oauth 2.0 Client ID from the Google Project "gnsstech-corebuild-security".
    CLIENT_ID =   "607027746270-1c6g4fov3jm4iki1unl2trn9umbkpil3" \
                + ".apps.googleusercontent.com"
    CLIENT_MODE = "staging"
    CLIENT_MODE_HEADER = modeHeader_staging

  elif(webPort == 8095):
    isProduction = True
    # Oauth 2.0 Client ID from the Google Project "gnsstech-corebuild-security".
    CLIENT_ID =   "607027746270-5kv2u7tqtt0ne278hmd3npff48i0iq31" \
                + ".apps.googleusercontent.com"
    CLIENT_MODE = "production"
    CLIENT_MODE_HEADER = modeHeader_production

  elif(webPort == 8097):
    isProduction = True
    # Oauth 2.0 Client ID from the Google Project "gnsstech-corebuild-security".
    CLIENT_ID =   "607027746270-bhm5odagnbn0q85noh5d6sokjn864np5" \
                + ".apps.googleusercontent.com"
    CLIENT_MODE = "production"
    CLIENT_MODE_HEADER = modeHeader_production

  else:
    scriptName = os.path.basename(__file__)
    print("Usage: python %s PORT" % ( scriptName ))
    sys.exit(1)


  fidUserError = open("LoginErr.txt","a")
  if isProduction:
    fidOption = open("OptionLog-production.txt","a")
  else:
    fidOption = open("OptionLog.txt","a")

  signal.signal(signal.SIGINT, signal_handler)
  print("Setup CTRL-C handler")
  
  try:
    app.run(host='0.0.0.0',port=webPort,ssl_context=('secrets/higgs.public.crt',
                                                     'secrets/higgs.private.key'),
                                                     threaded=True)
  except Exception as e:
    print("Exception: %s" % ( str(e) ))
    sys.exit(1)
