def add_signature_api(): """add backend signature to transaction""" payload = request.get_json(silent=True) try: user_id, auth_token = extract_headers(request) print('calling /user/add-signature for user_id %s ' % user_id) id = payload.get('id', None) sender_address = payload.get('sender_address', None) recipient_address = payload.get('recipient_address', None) amount = payload.get('amount', None) transaction = payload.get('transaction', None) validation_token = payload.get('validation-token', None) print( '### adding signature with validation token = %s and transaction:%s' % (validation_token, transaction)) if None in (user_id, id, sender_address, recipient_address, amount, transaction, validation_token): log.error('failed input checks on /user/submit_transaction') raise InvalidUsage('bad-request') except Exception as e: print('exception in /user/submit_transaction e=%s' % e) raise InvalidUsage('bad-request') if not utils.is_valid_client(user_id, validation_token): increment_metric('add-signature-invalid-token') raise jsonify(status='denied', reason='invalid token') auth_status = authorize(user_id) if auth_status != 'authorized': return jsonify(status='denied', reason=auth_status) tx = add_signature(id, sender_address, recipient_address, int(amount), transaction) return jsonify(status='ok', tx=tx)
def post_backup_restore(): """restore the user to the one with the previous private address this api is protected by the following means: - a phone number can only restore if a previous back was performed - a phone number can only restore to a previously owned address """ user_id, auth_token = extract_headers(request) #TODO consider adding this if it doesn't break anything #if config.AUTH_TOKEN_ENFORCED and not validate_auth_token(user_id, auth_token): # abort(403) # try: payload = request.get_json(silent=True) address = payload.get('address', None) if address is None: raise InvalidUsage('bad-request') except Exception as e: print(e) raise InvalidUsage('bad-request') else: user_id = restore_user_by_address(user_id, address) if user_id: increment_metric('restore-success') return jsonify(status='ok', user_id=user_id) else: increment_metric('restore-failure') raise InvalidUsage('cant restore user')
def send_kin_with_payment_service(public_address, amount, memo=None): """send kins to an address using the payment service""" # sanity: if public_address in (None, ''): log.error('cant send kin to address: %s' % public_address) return False, None if amount is None or amount < 1: log.error('cant send kin amount: %s' % amount) return False, None print('sending kin to address: %s' % public_address) headers = { 'X-REQUEST-ID': str(random.randint(1, 1000000)) } # doesn't actually matter payment_payload = { 'id': memo, 'amount': amount, 'app_id': 'TIPC', 'recipient_address': public_address, 'callback': "%s/payments/callback" % config.API_SERVER_URL } try: res = requests.post('%s/payments' % config.PAYMENT_SERVICE_URL, headers=headers, json=payment_payload) res.raise_for_status() except Exception as e: increment_metric('send_kin_error') print( 'caught exception sending %s kin to address %s using the payment service' % (amount, public_address)) print(e)
def report_p2p_tx_api(): """endpoint used by the client to report successful p2p txs""" if not config.P2P_TRANSFERS_ENABLED: # this api is disabled, clients should not have asked for it print('/user/transaction/p2p/add api is disabled by server config') raise InvalidUsage('api-disabled') payload = request.get_json(silent=True) try: # TODO Should we verify the tx against the blockchain? # TODO this api needs to be secured with auth token sender_id, auth_token = extract_headers(request) tx_hash = payload.get('tx_hash', None) destination_address = payload.get('destination_address', None) amount = payload.get('amount', None) if None in (tx_hash, sender_id, destination_address, amount): raise InvalidUsage('invalid params') except Exception as e: print('exception: %s' % e) raise InvalidUsage('bad-request') res, tx_dict = add_p2p_tx(tx_hash, sender_id, destination_address, amount) if res: # send back the dict with the tx details increment_metric('p2p-tx-added') return jsonify(status='ok', tx=tx_dict) else: raise InvalidUsage('failed to add p2ptx')
def create_account(public_address, initial_kin_amount): """create an account for the given public address""" #TODO all repeating logic? print('creating account with balance:%s' % initial_kin_amount) try: return app.kin_account.create_account( public_address, starting_balance=initial_kin_amount, fee=0) except Exception as e: increment_metric('create_account_error') print('caught exception creating account for address %s' % public_address) print(e)
def skip_picture_endpoint(): """advances current_picture_index""" if not config.DEBUG: limit_to_localhost() try: payload = request.get_json(silent=True) skip_by = payload.get('skip_by', 1) except Exception as e: print(e) raise InvalidUsage('bad-request') else: skip_picture_wait(skip_by) increment_metric('skip-picture-wait') return jsonify(status='ok')
def ack_auth_token_api(): """endpoint used by clients to ack the auth-token they received""" payload = request.get_json(silent=True) try: user_id, auth_token = extract_headers(request) token = payload.get('token', None) if None in (user_id, token): raise InvalidUsage('bad-request: invalid input') except Exception as e: print(e) raise InvalidUsage('bad-request') if ack_auth_token(user_id, token): increment_metric('auth-token-acked') return jsonify(status='ok') else: return jsonify(status='error', reason='wrong-token'), status.HTTP_400_BAD_REQUEST
def create_user(user_id, os_type, device_model, push_token, time_zone, device_id, app_ver, package_id): """create a new user and commit to the database. should only fail if the user_id is duplicate""" def parse_timezone(tz): """convert -02:00 to -2 or set reasonable default""" try: return int(tz[:(tz.find(':'))]) except Exception as e: log.error('failed to parse timezone: %s. using default. e: %s' % (tz, e)) return int(DEFAULT_TIME_ZONE) is_new_user = False try: user = get_user(user_id) log.info('user %s already exists, updating data' % user_id) except Exception as e: user = User() is_new_user = True user.user_id = user_id user.os_type = os_type user.device_model = device_model[:DEVICE_MODEL_MAX_SIZE] user.push_token = push_token if push_token is not None else user.push_token user.time_zone = parse_timezone(time_zone) user.device_id = device_id user.auth_token = uuid4() if not user.auth_token else user.auth_token user.package_id = package_id db.session.add(user) db.session.commit() if is_new_user: user_app_data = UserAppData() user_app_data.user_id = user_id user_app_data.app_ver = app_ver db.session.add(user_app_data) db.session.commit() # get/create an auth token for this user get_token_obj_by_user_id(user_id) else: increment_metric('reregister') return is_new_user
def extract_tx_payment_data(tx_hash): """ensures that the given tx_hash is a valid payment tx, and return a dict with the memo, amount and to_address""" if tx_hash is None: raise InvalidUsage('invalid params') # get the tx_hash data. this might take a second, # so retry while 'Resource Missing' is recevied count = 0 tx_data = None while count < config.STELLAR_TIMEOUT_SEC: try: tx_data = app.kin_sdk.get_transaction_data(tx_hash) except kin.KinErrors.ResourceNotFoundError: count = count + 1 sleep(1) else: break if tx_data is None: print('could not get tx_data for tx_hash: %s. waited %s seconds' % (tx_hash, count)) increment_metric('tx_data_timeout') return False, {} # get the simple op: op = tx_data.operation # verify op type from kin.transactions import OperationTypes if op.type != OperationTypes.PAYMENT: print('unexpected type: %s' % op.type) return False, {} # assemble the result dict data = { 'memo': tx_data.memo, 'amount': op.amount, 'to_address': op.destination } return True, data
def authorize(user_id): if config.AUTH_TOKEN_ENFORCED and not is_user_authenticated(user_id): print( 'user %s is not authenticated. rejecting results submission request' % user_id) increment_metric('rejected-on-auth') return 'auth-failed' if config.PHONE_VERIFICATION_REQUIRED and not is_user_phone_verified( user_id): print('blocking user (%s) results - didnt pass phone_verification' % user_id) return 'user_phone_not_verified' if is_userid_blacklisted(user_id): print('blocked user_id %s from booking goods - user_id blacklisted' % user_id) return 'denied' return 'authorized'
def email_backup_endpoint(): """generates an email with the user's backup details and sends it""" user_id, auth_token = extract_headers(request) if config.AUTH_TOKEN_ENFORCED and not validate_auth_token( user_id, auth_token): print( 'received a bad auth token from user_id %s: %s. ignoring for now' % (user_id, auth_token)) if config.AUTH_TOKEN_ENFORCED and not is_user_authenticated(user_id): abort(403) try: payload = request.get_json(silent=True) to_address = payload.get('to_address', None) enc_key = payload.get('enc_key', None) if None in (to_address, enc_key): raise InvalidUsage('bad-request') # TODO validate email address is legit except Exception as e: print(e) raise InvalidUsage('bad-request') #get_template from db, generate email and send with ses from .models.email_template import EMAIL_TEMPLATE_BACKUP_NAG_1 template_dict = get_email_template_by_type(EMAIL_TEMPLATE_BACKUP_NAG_1) if not template_dict: raise InternalError('cant fetch email template') from .send_email import send_mail_with_qr_attachment try: res = send_mail_with_qr_attachment(template_dict['sent_from'], [to_address], template_dict['title'], template_dict['body'], enc_key) print('email result: %s' % res) increment_metric('backup-email-sent-success') except Exception as e: log.error('failed to sent backup email to %s. e:%s' % (to_address, e)) increment_metric('backup-email-sent-failure') #TODO handle errors return jsonify(status='ok')
def send_kin(public_address, amount, memo=None): """send kins to an address""" # sanity: if public_address in (None, ''): log.error('cant send kin to address: %s' % public_address) return False, None if amount is None or amount < 1: log.error('cant send kin amount: %s' % amount) return False, None print('sending kin to address: %s' % public_address) try: return app.kin_account.send_kin(public_address, amount, fee=0, memo_text=memo) except Exception as e: increment_metric('send_kin_error') print('caught exception sending %s kin to address %s' % (amount, public_address)) print(e)
def limit_to_acl(return_bool=False): """aborts unauthorized requests for sensitive APIs (nginx specific). allow on DEBUG the optional 'return_bool' flag governs whether the function aborts the request (default) or just returns a boolean. """ source_ip = request.headers.get('X-Forwarded-For', None) if not source_ip: print('missing expected header') if return_bool: return False increment_metric('not-in-acl') abort(403) if not is_in_acl(source_ip): if return_bool: return False print('%s is not in ACL, rejecting' % source_ip) increment_metric('not-in-acl') abort(403) if return_bool: return True
def add_signature(id, sender_address, recipient_address, amount, transaction): """add backend signature to transaction""" print('adding whitelisted signature for transaction from %s to: %s' % (sender_address, recipient_address)) headers = { 'X-REQUEST-ID': str(random.randint(1, 1000000)) } # doesn't actually matter payment_payload = { 'id': id, 'sender_address': sender_address, 'recipient_address': recipient_address, 'amount': amount, 'transaction': transaction, 'app_id': 'TIPC', 'network_id': config.STELLAR_NETWORK } try: print('posting %s/tx/whitelist payload: %s' % (config.PAYMENT_SERVICE_URL, payment_payload)) result = requests.post('%s/tx/whitelist' % config.PAYMENT_SERVICE_URL, headers=headers, json=payment_payload) result.raise_for_status() print('result %s ' % result.text) print('result %s ' % result.content) tx_json = json.loads(result.content.decode("utf-8")) print('tx_json %s ' % tx_json) print('returning tx= %s ' % tx_json['tx']) return tx_json['tx'] except Exception as e: increment_metric('whitelist_error') print('caught exception while whitelisting transaction from %s to %s' % (sender_address, recipient_address)) print(e)
def set_user_phone_number_endpoint(): """get the firebase id token and extract the phone number from it""" payload = request.get_json(silent=True) try: user_id, auth_token = extract_headers(request) token = payload.get('token', None) unverified_phone_number = payload.get('phone_number', None) # only used in tests if None in (user_id, token): raise InvalidUsage('bad-request') except Exception as e: print(e) raise InvalidUsage('bad-request') if not config.DEBUG: print('extracting verified phone number fom firebase id token...') verified_number = extract_phone_number_from_firebase_id_token(token) if verified_number is None: print('bad id-token: %s' % token) return jsonify(status='error', reason='bad_token'), status.HTTP_404_NOT_FOUND # reject blacklisted phone prefixes for prefix in app.blocked_phone_prefixes: if verified_number.find(prefix) == 0: os_type = get_user_os_type(user_id) print( 'found blocked phone prefix (%s) in verified phone number (%s), userid (%s), OS (%s): aborting' % (prefix, verified_number, user_id, os_type)) abort(403) phone = verified_number else: #DEBUG # for tests, you can use the unverified number if no token was given if token: phone = extract_phone_number_from_firebase_id_token(token) if not phone: print('using un-verified phone number in debug') phone = unverified_phone_number.strip().replace('-', '') if not phone: print('could not extract phone in debug') return jsonify(status='error', reason='no_phone_number') # limit the number of registrations a single phone number can do, unless they come from the ACL if not limit_to_acl( return_bool=True) and count_registrations_for_phone_number( phone) > int(config.MAX_NUM_REGISTRATIONS_PER_NUMBER) - 1: print( 'rejecting registration from user_id %s and phone number %s - too many re-registrations' % (user_id, phone)) increment_metric("reject-too-many_registrations") abort(403) print('updating phone number for user %s' % user_id) set_user_phone_number(user_id, phone) increment_metric('user-phone-verified') # return success and the backup hint, if they exist hints = get_backup_hints(user_id) if config.DEBUG: print('restore hints for user_id, phone: %s: %s: %s' % (user_id, phone, hints)) return jsonify(status='ok', hints=hints)
def onboard_user(): """creates a wallet for the user and deposits some xlms there""" # input sanity try: user_id, auth_token = extract_headers(request) public_address = request.get_json(silent=True).get( 'public_address', None) if None in (public_address, user_id): raise InvalidUsage('bad-request') except Exception as e: raise InvalidUsage('bad-request') # block users with an older version from onboarding. and send them a push message if should_block_user_by_client_version(user_id): print( 'blocking + deactivating user %s on onboarding with older version' % user_id) # send_please_upgrade_push_2([user_id]) # and also, deactivate the user deactivate_user(user_id) abort(403) elif config.PHONE_VERIFICATION_REQUIRED and not is_user_phone_verified( user_id): raise InvalidUsage('user isnt phone verified') onboarded = is_onboarded(user_id) if onboarded is True: raise InvalidUsage('user already has an account and has been awarded') elif onboarded is None: raise InvalidUsage('no such user exists') else: # create an account, provided none is already being created lock = redis_lock.Lock(app.redis, 'address:%s' % public_address) if lock.acquire(blocking=False): try: if not active_account_exists(public_address): print('creating account with address %s and amount %s' % (public_address, config.STELLAR_INITIAL_ACCOUNT_BALANCE)) tx_id = create_account( public_address, config.STELLAR_INITIAL_ACCOUNT_BALANCE) if not tx_id: raise InternalError('failed to create account at %s' % public_address) elif not award_user(user_id, public_address): raise InternalError( 'unable to award user with %d Kin' % get_initial_reward()) elif not award_user(user_id, public_address): raise InternalError('unable to award user with %d Kin' % get_initial_reward()) except Exception as e: print('exception trying to create account:%s' % e) raise InternalError('unable to create account') finally: lock.release() else: raise InvalidUsage( 'already creating account for user_id: %s and address: %s' % (user_id, public_address)) increment_metric('user_onboarded') return jsonify(status='ok')
def register_api(): """ register a user to the system called once by every client until 200OK is received from the server. the payload may contain a optional push token. this function may be called by the client multiple times to update fields """ payload = request.get_json(silent=True) try: # add redis lock here? user_id = payload.get('user_id', None) os = payload.get('os', None) device_model = payload.get('device_model', None) time_zone = payload.get('time_zone', None) device_id = payload.get('device_id', None) app_ver = payload.get('app_ver', None) # optionals token = payload.get('token', None) package_id = payload.get('package_id', None) if None in ( user_id, os, device_model, time_zone, app_ver ): # token is optional, device-id is required but may be None raise InvalidUsage('bad-request') if os not in (utils.OS_ANDROID, utils.OS_IOS): raise InvalidUsage('bad-request') if 'Genymotion'.upper() in device_model.upper( ): # block android emulator print('refusing to register Genymotion devices. user_id %s' % user_id) raise InvalidUsage('bad-request') user_id = UUID(user_id) # throws exception on invalid uuid except Exception as e: raise InvalidUsage('bad-request') else: try: new_user_created = create_user(user_id, os, device_model, token, time_zone, device_id, app_ver, package_id) except InvalidUsage as e: raise InvalidUsage('duplicate-userid') else: if new_user_created: print('created user with user_id %s' % user_id) increment_metric('user_registered') else: print('updated userid %s data' % user_id) #TODO find a way to dry up this code which is redundant with get_user_config() # turn off phone verfication for older clients: disable_phone_verification = False disable_backup_nag = False if os == OS_ANDROID and LooseVersion(app_ver) <= LooseVersion( config.BLOCK_ONBOARDING_ANDROID_VERSION): disable_phone_verification = True disable_backup_nag = True elif os == OS_IOS and LooseVersion(app_ver) <= LooseVersion( config.BLOCK_ONBOARDING_IOS_VERSION): disable_phone_verification = True disable_backup_nag = True global_config = get_global_config() if disable_phone_verification: print( 'disabling phone verification for registering userid %s' % user_id) global_config['phone_verification_enabled'] = False if disable_backup_nag: print('disabling backup nag for registering userid %s' % user_id) global_config['backup_nag'] = False # if should_force_update(os, app_ver): # global_config['force_update'] = True # if is_update_available(os, app_ver): # global_config['is_update_available'] = True # return global config - the user doesn't have user-specific config (yet) return jsonify(status='ok', config=global_config)
def payment_service_callback_endpoint(): """an endpoint for the payment service.""" payload = request.get_json(silent=True) print(payload) #TODO remove eventually try: action = payload.get('action', None) obj = payload.get('object', None) state = payload.get('state', None) val = payload.get('value', None) if None in (action, obj, state, val): print( 'should never happen: cant process payment service callback: %s' % payload) increment_metric('payment-callback-error') return jsonify(status='error', reason='internal_error') # process payment: if action == 'send' and obj == 'payment': if state == 'success': memo = val.get('id', None) tx_hash = val.get('transaction_id', None) amount = val.get('amount', None) payment_ts = payload.get('timestamp', None) public_address = val.get('sender_address') if None in (memo, tx_hash, amount): print( 'should never happen: cant process successful payment callback: %s' % payload) increment_metric('payment-callback-error') return jsonify(status='error', reason='internal_error') # retrieve the user_id and task_id from the cache user_id, task_id, request_timestamp, send_push = read_payment_data_from_cache( memo) # compare the timestamp from the callback with the one from the original request, and # post as a gauge metric for tracking try: request_duration_sec = arrow.get(payment_ts) - arrow.get( request_timestamp) request_duration_sec = int( request_duration_sec.total_seconds()) print('payment request for tx_hash: %s took %s seconds' % (tx_hash, request_duration_sec)) gauge_metric('payment-req-dur', request_duration_sec) except Exception as e: log.error( 'failed to calculate payment request duration. e=%s' % e) create_tx(tx_hash, user_id, public_address, False, amount, { 'task_id': task_id, 'memo': memo }) increment_metric('payment-callback-success') # # if tx_hash and send_push: # send_push_tx_completed(user_id, tx_hash, amount, task_id, memo) try: redis_lock.Lock(app.redis, get_payment_lock_name(user_id, task_id)).release() except Exception as e: log.error( 'failed to release payment lock for user_id %s and task_id %s' % (user_id, task_id)) else: print('received failed tx from the payment service: %s' % payload) #TODO implement some retry mechanism here increment_metric('payment-callback-failed') else: print( 'should never happen: unhandled callback from the payment service: %s' % payload) except Exception as e: increment_metric('payment-callback-error') log.error('failed processing the payment service callback') print(e) return jsonify(status='error', reason='internal_error') return jsonify(status='ok')