cic-internal-integration/apps/cic-ussd/cic_ussd/runnable/daemons/cic_user_ussd_server.py

227 lines
8.4 KiB
Python
Raw Normal View History

2021-04-30 10:15:03 +02:00
"""This module handles requests originating from the ussd service provider.
2021-02-06 16:13:47 +01:00
"""
2021-04-30 10:15:03 +02:00
2021-02-06 16:13:47 +01:00
# standard imports
import json
import logging
2021-05-11 12:58:00 +02:00
from urllib.parse import parse_qs
2021-02-06 16:13:47 +01:00
# third-party imports
2021-04-30 10:15:03 +02:00
import celery
import i18n
import redis
from chainlib.chain import ChainSpec
2021-04-30 10:15:03 +02:00
from confini import Config
2021-02-06 16:13:47 +01:00
# local imports
from cic_ussd.chain import Chain
2021-02-06 16:13:47 +01:00
from cic_ussd.db import dsn_from_config
from cic_ussd.db.models.base import SessionBase
from cic_ussd.encoder import PasswordEncoder
2021-05-01 16:14:20 +02:00
from cic_ussd.error import InitializationError
2021-02-06 16:13:47 +01:00
from cic_ussd.files.local_files import create_local_file_data_stores, json_file_parser
from cic_ussd.menu.ussd_menu import UssdMenu
from cic_ussd.metadata.signer import Signer
from cic_ussd.metadata.base import Metadata
2021-02-06 16:13:47 +01:00
from cic_ussd.operations import (define_response_with_content,
process_menu_interaction_requests,
define_multilingual_responses)
from cic_ussd.phone_number import process_phone_number, Support, E164Format
2021-05-01 16:14:20 +02:00
from cic_ussd.processor import get_default_token_data
from cic_ussd.redis import cache_data, create_cached_data_key, InMemoryStore
2021-02-06 16:13:47 +01:00
from cic_ussd.requests import (get_request_endpoint,
2021-04-30 10:15:03 +02:00
get_request_method)
from cic_ussd.runnable.server_base import exportable_parser, logg
2021-02-06 16:13:47 +01:00
from cic_ussd.session.ussd_session import UssdSession as InMemoryUssdSession
from cic_ussd.state_machine import UssdStateMachine
2021-05-11 12:58:00 +02:00
from cic_ussd.validator import check_ip, check_request_content_length, validate_phone_number, validate_presence
2021-02-06 16:13:47 +01:00
2021-04-30 10:15:03 +02:00
args = exportable_parser.parse_args()
2021-02-06 16:13:47 +01:00
# define log levels
if args.vv:
logging.getLogger().setLevel(logging.DEBUG)
elif args.v:
logging.getLogger().setLevel(logging.INFO)
2021-04-16 22:24:07 +02:00
# parse config
2021-07-15 00:54:48 +02:00
config = Config(args.c, env_prefix=args.env_prefix)
2021-04-16 22:24:07 +02:00
config.process()
config.censor('PASSWORD', 'DATABASE')
logg.debug('config loaded from {}:\n{}'.format(args.c, config))
2021-02-06 16:13:47 +01:00
2021-04-30 10:15:03 +02:00
# set up db
data_source_name = dsn_from_config(config)
SessionBase.connect(data_source_name,
pool_size=int(config.get('DATABASE_POOL_SIZE')),
debug=config.true('DATABASE_DEBUG'))
2021-02-06 16:13:47 +01:00
# set up translations
i18n.load_path.append(config.get('APP_LOCALE_PATH'))
i18n.set('fallback', config.get('APP_LOCALE_FALLBACK'))
# set Fernet key
PasswordEncoder.set_key(config.get('APP_PASSWORD_PEPPER'))
# create in-memory databases
ussd_menu_db = create_local_file_data_stores(file_location=config.get('USSD_MENU_FILE'),
table_name='ussd_menu')
UssdMenu.ussd_menu_db = ussd_menu_db
# define universal redis cache access
InMemoryStore.cache = redis.StrictRedis(host=config.get('REDIS_HOSTNAME'),
port=config.get('REDIS_PORT'),
password=config.get('REDIS_PASSWORD'),
db=config.get('REDIS_DATABASE'),
decode_responses=True)
InMemoryUssdSession.redis_cache = InMemoryStore.cache
# define metadata URL
Metadata.base_url = config.get('CIC_META_URL')
# define signer values
2021-04-06 19:53:38 +02:00
export_dir = config.get('PGP_EXPORT_DIR')
if export_dir:
validate_presence(path=export_dir)
Signer.gpg_path = export_dir
2021-03-05 17:28:07 +01:00
Signer.gpg_passphrase = config.get('PGP_PASSPHRASE')
2021-04-06 19:53:38 +02:00
key_file_path = f"{config.get('PGP_KEYS_PATH')}{config.get('PGP_PRIVATE_KEYS')}"
if key_file_path:
validate_presence(path=key_file_path)
Signer.key_file_path = key_file_path
2021-02-06 16:13:47 +01:00
# initialize celery app
celery.Celery(backend=config.get('CELERY_RESULT_URL'), broker=config.get('CELERY_BROKER_URL'))
# load states and transitions data
states = json_file_parser(filepath=config.get('STATEMACHINE_STATES'))
transitions = json_file_parser(filepath=config.get('STATEMACHINE_TRANSITIONS'))
chain_spec = ChainSpec(
common_name=config.get('CIC_COMMON_NAME'),
engine=config.get('CIC_ENGINE'),
network_id=config.get('CIC_NETWORK_ID')
)
Chain.spec = chain_spec
2021-02-06 16:13:47 +01:00
UssdStateMachine.states = states
UssdStateMachine.transitions = transitions
2021-05-01 16:14:20 +02:00
# retrieve default token data
default_token_data = get_default_token_data()
chain_str = Chain.spec.__str__()
# cache default token for re-usability
if default_token_data:
cache_key = create_cached_data_key(
identifier=chain_str.encode('utf-8'),
salt=':cic.default_token_data'
)
cache_data(key=cache_key, data=json.dumps(default_token_data))
else:
raise InitializationError(f'Default token data for: {chain_str} not found.')
2021-02-06 16:13:47 +01:00
2021-05-11 12:58:00 +02:00
valid_service_codes = config.get('APP_SERVICE_CODE').split(",")
E164Format.region = config.get('PHONE_NUMBER_REGION')
2021-06-07 10:02:03 +02:00
Support.phone_number = config.get('APP_SUPPORT_PHONE_NUMBER')
2021-05-11 12:58:00 +02:00
2021-02-06 16:13:47 +01:00
def application(env, start_response):
"""Loads python code for application to be accessible over web server
:param env: Object containing server and request information
:type env: dict
:param start_response: Callable to define responses.
:type start_response: any
2021-04-30 10:15:03 +02:00
:return: a list containing a bytes representation of the response object
:rtype: list
2021-02-06 16:13:47 +01:00
"""
# define headers
errors_headers = [('Content-Type', 'text/plain'), ('Content-Length', '0')]
headers = [('Content-Type', 'text/plain')]
2021-06-29 12:49:25 +02:00
# create session for the life time of http request
session = SessionBase.create_session()
2021-02-06 16:13:47 +01:00
if get_request_method(env=env) == 'POST' and get_request_endpoint(env=env) == '/':
2021-05-11 12:58:00 +02:00
if env.get('CONTENT_TYPE') != 'application/x-www-form-urlencoded':
2021-06-23 06:29:38 +02:00
start_response('405 Urlencoded, please', errors_headers)
2021-05-11 12:58:00 +02:00
return []
post_data = env.get('wsgi.input').read()
post_data = post_data.decode('utf-8')
try:
post_data = parse_qs(post_data)
except TypeError:
start_response('400 Size matters', errors_headers)
return []
service_code = post_data.get('serviceCode')[0]
phone_number = post_data.get('phoneNumber')[0]
external_session_id = post_data.get('sessionId')[0]
2021-02-06 16:13:47 +01:00
2021-05-11 12:58:00 +02:00
try:
user_input = post_data.get('text')[0]
except TypeError:
user_input = ""
2021-02-06 16:13:47 +01:00
# add validation for phone number
if phone_number:
phone_number = process_phone_number(phone_number=phone_number, region=E164Format.region)
2021-02-06 16:13:47 +01:00
# validate ip address
if not check_ip(config=config, env=env):
start_response('403 Sneaky, sneaky', errors_headers)
return []
# validate content length
if not check_request_content_length(config=config, env=env):
start_response('400 Size matters', errors_headers)
return []
# validate service code
2021-05-11 12:58:00 +02:00
if service_code not in valid_service_codes:
2021-02-06 16:13:47 +01:00
response = define_multilingual_responses(
key='ussd.kenya.invalid_service_code',
locales=['en', 'sw'],
prefix='END',
2021-05-11 12:58:00 +02:00
valid_service_code=valid_service_codes[0])
response_bytes, headers = define_response_with_content(headers=headers, response=response)
start_response('200 OK', headers)
2021-02-06 16:13:47 +01:00
return [response_bytes]
# validate phone number
if not validate_phone_number(phone_number):
logg.error('invalid phone number {}'.format(phone_number))
2021-02-06 16:13:47 +01:00
start_response('400 Invalid phone number format', errors_headers)
return []
logg.debug('session {} started for {}'.format(external_session_id, phone_number))
2021-02-06 16:13:47 +01:00
# handle menu interaction requests
chain_str = chain_spec.__str__()
response = process_menu_interaction_requests(chain_str=chain_str,
2021-02-06 16:13:47 +01:00
external_session_id=external_session_id,
phone_number=phone_number,
queue=args.q,
service_code=service_code,
2021-06-29 12:49:25 +02:00
session=session,
2021-02-06 16:13:47 +01:00
user_input=user_input)
response_bytes, headers = define_response_with_content(headers=headers, response=response)
start_response('200 OK,', headers)
2021-06-29 12:49:25 +02:00
session.commit()
session.close()
2021-02-06 16:13:47 +01:00
return [response_bytes]
2021-05-11 12:58:00 +02:00
else:
2021-06-23 06:29:38 +02:00
logg.error('invalid query {}'.format(env))
for r in env:
logg.debug('{}: {}'.format(r, env))
2021-06-29 12:49:25 +02:00
session.close()
2021-05-11 12:58:00 +02:00
start_response('405 Play by the rules', errors_headers)
return []