Guest User

Untitled

a guest
Dec 8th, 2016
104
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 21.02 KB | None | 0 0
  1. # Embedded file name: modules\serverapi\main.py
  2. import urllib
  3. import base64
  4. import urllib2
  5. import json
  6. import time
  7. import logging
  8. import re
  9. import socket
  10. from mbroker import m_interface
  11. from functools import wraps
  12. log = logging.getLogger(__name__)
  13. API_CONFIG = {'base_url': 'https://api.nexon.net/'}
  14. NSS_CONFIG = {'base_url': 'https://nss.nexon.net/log/'}
  15. API_CACHE_DATA = {}
  16. SERVER_ENV = 'live'
  17. API_TIMEOUT = 30
  18. _running = False
  19. _listener = []
  20. nss_cookie = None
  21.  
  22. def m_run():
  23.     global _listener
  24.     global _running
  25.     _listener.append(m_interface.listen_async('serverapi', serverapi_message_handler))
  26.     _listener.append(m_interface.listen_async('profile', profile_message_handler))
  27.     _running = True
  28.  
  29.  
  30. def m_shutdown():
  31.     global _running
  32.     global API_CACHE_DATA
  33.     API_CACHE_DATA = {}
  34.     for t in _listener:
  35.         t.stop()
  36.         t.join()
  37.  
  38.     _running = False
  39.  
  40.  
  41. def serverapi_message_handler(message):
  42.     for key in message.keys():
  43.         try:
  44.             msg = message[key]
  45.             if 'error' in msg:
  46.                 return
  47.             if key.startswith('callApi'):
  48.                 g = key.split('_')
  49.                 l = len(g)
  50.                 if l < 3:
  51.                     resp = {'error': {'code': 20397,
  52.                                'message': 'Invalid API Endpoint'}}
  53.                 else:
  54.                     method = g[l - 1].upper()
  55.                     path = '/'.join(g[1:l - 1]).lower()
  56.                     req = {'method': method,
  57.                      'path': path}
  58.                     if msg is not None:
  59.                         if 'params' in msg:
  60.                             req['params'] = msg['params']
  61.                             if 'token' in msg['params']:
  62.                                 req['token'] = msg['params']['token']
  63.                         if 'use_cache' in msg and method == 'GET':
  64.                             req['use_cache'] = msg['use_cache']
  65.                         if 'token' in msg:
  66.                             req['token'] = msg['token']
  67.                         if 'retry_cnt' in msg:
  68.                             req['retry_cnt'] = msg['retry_cnt']
  69.                     resp = call_api(req)
  70.             elif key == 'getApiUrl':
  71.                 resp = get_api_url(msg)
  72.             elif key == 'getApiConfig':
  73.                 resp = get_api_config()
  74.             else:
  75.                 continue
  76.             if not isinstance(resp, dict) or 'error' not in resp and 'success' not in resp:
  77.                 resp = {'success': resp}
  78.         except Exception as e:
  79.             log.exception(str(e))
  80.             resp = {'error': {'code': 20399,
  81.                        'type': 'Internal Server Error'}}
  82.  
  83.         m_interface.publish('serverapi-events', {key: resp})
  84.  
  85.     return
  86.  
  87.  
  88. def profile_message_handler(message):
  89.     for key in message.keys():
  90.         try:
  91.             resp = None
  92.             msg = message[key]
  93.             if msg is None:
  94.                 msg = {}
  95.             log.debug('profile message handler - key: %s, message: %r ', key, msg)
  96.             if key == 'getProfile':
  97.                 req = {'method': 'GET',
  98.                  'resource': 'profile'}
  99.                 if 'uid' in msg:
  100.                     req['uid'] = msg['uid']
  101.                 if 'use_cache' in msg:
  102.                     req['use_cache'] = msg['use_cache']
  103.             elif key == 'addFriend':
  104.                 if 'uid' not in msg:
  105.                     resp = {'error': {'code': '5',
  106.                                'message': 'Parameter is invalid.'}}
  107.                 else:
  108.                     params = {'user_no': msg['uid']}
  109.                     req = {'method': 'POST',
  110.                      'resource': 'friends',
  111.                      'params': params}
  112.             elif key == 'removeFriend':
  113.                 if 'uid' not in msg:
  114.                     resp = {'error': {'code': '5',
  115.                                'message': 'Parameter is invalid.'}}
  116.                 else:
  117.                     params = {'user_no': msg['uid']}
  118.                     req = {'method': 'DELETE',
  119.                      'resource': 'friends',
  120.                      'params': params}
  121.             elif key == 'getProfilename':
  122.                 req = {'method': 'GET',
  123.                  'resource': 'profilename',
  124.                  'token': 'null'}
  125.                 if 'uid' in msg:
  126.                     req['uid'] = msg['uid']
  127.             elif key == 'getProfilenameHistory':
  128.                 params = {'type': 'history'}
  129.                 if 'limit' in msg:
  130.                     params['limit'] = msg['limit']
  131.                 req = {'method': 'GET',
  132.                  'resource': 'profilename',
  133.                  'params': params,
  134.                  'use_cache': False}
  135.                 if 'uid' in msg:
  136.                     req['uid'] = msg['uid']
  137.             elif key == 'getMyProfilename':
  138.                 req = {'method': 'GET',
  139.                  'resource': 'profilename'}
  140.             elif key == 'getAvatarList':
  141.                 req = {'method': 'GET',
  142.                  'resource': 'avatar',
  143.                  'params': {'type': 'list'}}
  144.             elif key == 'updateProfile':
  145.                 req = {'method': 'POST',
  146.                  'resource': 'profile',
  147.                  'params': msg}
  148.             elif key == 'searchUser':
  149.                 if 'query' not in msg:
  150.                     resp = {'error': {'code': '6',
  151.                                'message': 'Parameter is missing.'}}
  152.                 else:
  153.                     path = 'search/' + msg['query']
  154.                     params = {'type': 'user'}
  155.                     req = {'method': 'GET',
  156.                      'path': path,
  157.                      'params': params,
  158.                      'token': 'null',
  159.                      'use_cache': True}
  160.                     resp = call_api(req)
  161.             else:
  162.                 continue
  163.             if resp is None:
  164.                 resp = call_users_api(req)
  165.         except Exception as e:
  166.             err_msg = str(e)
  167.             log.exception(err_msg)
  168.             resp = {'error': {'code': 20399,
  169.                        'type': 'Internal Server Error',
  170.                        'message': err_msg}}
  171.  
  172.         if 'error' in resp:
  173.             m_interface.publish('profile-events', {key + 'Failed': resp})
  174.         else:
  175.             m_interface.publish('profile-events', {key + 'Success': resp})
  176.  
  177.     return
  178.  
  179.  
  180. def get_session_token():
  181.     from auth import main as auth
  182.     try:
  183.         token = auth.user.session.auth_token
  184.     except Exception as e:
  185.         log.debug(e)
  186.         token = None
  187.  
  188.     return token
  189.  
  190.  
  191. def get_api_config():
  192.     return API_CONFIG
  193.  
  194.  
  195. def get_api_url(path):
  196.     if path is None:
  197.         return
  198.     else:
  199.         m = re.search('[http://|https://]?api(\\-dev|\\-test)?.nexon.net/(.*)', path)
  200.         if m is not None:
  201.             path = m.group(2)
  202.         elif path.startswith('/'):
  203.             path = path[1:]
  204.         api_url = API_CONFIG['base_url'] + path
  205.         return api_url
  206.  
  207.  
  208. def get_nss_url(endpoint):
  209.     if endpoint is None:
  210.         return
  211.     else:
  212.         m = re.search('[http://|https://]?nss(\\-dev|\\-test)?.nexon.net/(.*)', endpoint)
  213.         if m is not None:
  214.             endpoint = m.group(2)
  215.         elif endpoint.startswith('/'):
  216.             endpoint = endpoint[1:]
  217.         nss_url = NSS_CONFIG['base_url'] + endpoint
  218.         return nss_url
  219.  
  220.  
  221. def call_users_api(input):
  222.     uid = None
  223.     if 'uid' in input:
  224.         uid = input['uid']
  225.     if uid is None or len(uid) == 0:
  226.         uid = 'me'
  227.     path = 'users/' + uid
  228.     if 'resource' in input:
  229.         path = path + '/' + input['resource']
  230.     input['path'] = path
  231.     return call_api(input)
  232.  
  233.  
  234. def retry(exception_to_check, tries = 5, delay = 1, backoff = 2):
  235.     """Retry calling the decorated function using an exponential backoff.
  236.    
  237.    http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/
  238.    original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry
  239.    
  240.    :param exception_to_check: the exception to check. may be a tuple of
  241.        exceptions to check
  242.    :type exception_to_check: Exception or tuple
  243.    :param tries: number of times to try (not retry) before giving up
  244.    :type tries: int
  245.    :param delay: initial delay between retries in seconds
  246.    :type delay: int
  247.    :param backoff: backoff multiplier e.g. value of 2 will double the delay
  248.        each retry
  249.    :type backoff: int
  250.    """
  251.  
  252.     def deco_retry(f):
  253.  
  254.         @wraps(f)
  255.         def f_retry(*args, **kwargs):
  256.             mtries, mdelay = tries, delay
  257.             err = None
  258.             start = time.time()
  259.             while mtries > 1:
  260.                 try:
  261.                     return f(*args, **kwargs)
  262.                 except urllib2.HTTPError as e:
  263.                     err = e
  264.                     if e.code < 500:
  265.                         raise
  266.                 except exception_to_check as e:
  267.                     err = e
  268.                     log.debug('%s, Retrying in %d seconds...' % (str(e), mdelay))
  269.  
  270.                 time.sleep(mdelay)
  271.                 mtries -= 1
  272.                 mdelay *= backoff
  273.                 if mtries <= 1 or time.time() - start > API_TIMEOUT:
  274.                     raise err
  275.  
  276.             return f(*args, **kwargs)
  277.  
  278.         return f_retry
  279.  
  280.     return deco_retry
  281.  
  282.  
  283. @retry((urllib2.URLError, socket.timeout), tries=5, delay=1)
  284. def urlopen_with_retry(req, timeout = 10):
  285.     return urllib2.urlopen(req, timeout=timeout)
  286.  
  287.  
  288. pending_requests = {}
  289.  
  290. class RequestTracker:
  291.  
  292.     def __init__(self, path):
  293.         self.path = path
  294.  
  295.     def __enter__(self):
  296.         global pending_requests
  297.         pending_requests[self.path] = True
  298.         return True
  299.  
  300.     def __exit__(self, exc_type, exc_value, traceback):
  301.         pending_requests[self.path] = False
  302.         if exc_type is not None:
  303.             log.debug('RequestTracker:exit - %s %s %s', exc_type, exc_value, traceback)
  304.             raise
  305.         return True
  306.  
  307.  
  308. def call_api(input, no_retry = False, timeout = 10):
  309.     if input is None or 'path' not in input:
  310.         return {'error': {'code': 2,
  311.                    'type': 'Bad Request',
  312.                    'message': 'This API End Point is invalid.'}}
  313.     else:
  314.         if 'method' in input:
  315.             method = input['method'].upper()
  316.             if method not in ('GET', 'POST', 'DELETE'):
  317.                 method = 'GET'
  318.         else:
  319.             method = 'GET'
  320.         params = {}
  321.         if 'params' in input and isinstance(input['params'], dict):
  322.             params = input['params']
  323.         if method == 'GET':
  324.             use_cache = True
  325.         if 'use_cache' in input:
  326.             use_cache = input['use_cache']
  327.         else:
  328.             use_cache = True
  329.         token = ''
  330.         if 'token' in input and input['token'] is not None:
  331.             token = input['token']
  332.         if token == 'null':
  333.             token = ''
  334.         elif len(token) == 0:
  335.             token = get_session_token()
  336.         if token is None:
  337.             return {'error': {'code': 20398,
  338.                        'type': 'Internal Server Error',
  339.                        'message': 'Invalid Token'}}
  340.         path = input['path']
  341.         api_endpoint = get_api_url(path)
  342.         url_params = {}
  343.         old_api = is_old_api(path)
  344.         if old_api:
  345.             header_data = {'Content-Type': 'application/x-www-form-urlencoded'}
  346.             if len(token) > 0:
  347.                 url_params['access_token'] = token
  348.         else:
  349.             header_data = {'Content-Type': 'application/json'}
  350.             if len(token) > 0:
  351.                 header_data['Cookie'] = 'nxtk=' + token + ';domain=.nexon.net;path=/;'
  352.                 header_data['Authorization'] = 'bearer ' + base64.b64encode(token)
  353.         from apps import launcher
  354.         header_data['User-Agent'] = 'NexonLauncher.' + launcher.get_version()
  355.         request_data = {'method': method,
  356.          'url': api_endpoint}
  357.         try:
  358.             log.debug('API Request init ( %s ): %r', api_endpoint, input)
  359.             if method == 'GET' and len(params) > 0:
  360.                 url_params.update(params)
  361.             api_url = api_endpoint
  362.             if len(url_params) > 0:
  363.                 api_url = api_url + '?' + urllib.urlencode(url_params)
  364.             result = None
  365.             if method == 'GET':
  366.                 cache_key = get_cache_key(url_params)
  367.                 if use_cache:
  368.                     result = get_cache('session', api_endpoint, cache_key)
  369.                 if result is None:
  370.                     req = urllib2.Request(api_url, headers=header_data)
  371.                     start_time = time.time()
  372.                     log.debug('API Request: %r', req.__dict__)
  373.                     if no_retry:
  374.                         conn = urllib2.urlopen(req, timeout=timeout)
  375.                     else:
  376.                         conn = urlopen_with_retry(req, timeout=timeout)
  377.                     resp = conn.read()
  378.                     response_time = time.time() - start_time
  379.                     log.debug('API Response: GET %s ==> %r', api_url, resp)
  380.                     if response_time > 3.0:
  381.                         log.error('Slow API response-time: %s - %d ms', api_url, response_time * 1000)
  382.                     else:
  383.                         log.debug('API response-time: %s - %d ms', api_url, response_time * 1000)
  384.                     conn.close()
  385.                     result = json.loads(resp)
  386.                     put_cache('session', api_endpoint, cache_key, result)
  387.             elif method == 'POST' or method == 'DELETE':
  388.                 data = ''
  389.                 if len(params) > 0:
  390.                     if old_api:
  391.                         data = urllib.urlencode(params)
  392.                     else:
  393.                         data = json.dumps(params)
  394.                     header_data['Content-Length'] = len(data)
  395.                 req = urllib2.Request(api_url, data, header_data)
  396.                 if method == 'DELETE':
  397.                     req.get_method = lambda : 'DELETE'
  398.                 log.debug('API Request: %r', req.__dict__)
  399.                 start_time = time.time()
  400.                 if no_retry:
  401.                     conn = urllib2.urlopen(req, timeout=timeout)
  402.                 else:
  403.                     conn = urlopen_with_retry(req, timeout=timeout)
  404.                 resp = conn.read()
  405.                 response_time = time.time() - start_time
  406.                 log.debug('API Response: (%d ms) %s %s => %r', response_time * 1000, method, api_url, resp)
  407.                 conn.close()
  408.                 result = json.loads(resp)
  409.                 if 'error' not in result:
  410.                     del_cache('session', api_endpoint)
  411.                     if 'productcode' in api_endpoint:
  412.                         clear_cache()
  413.         except urllib2.HTTPError as e:
  414.             resp = e.read()
  415.             result = json.loads(resp)
  416.             result['statuscode'] = e.code
  417.             if e.code == 401 and not no_retry:
  418.                 from auth import main as auth
  419.                 resp = auth.user.session.update_token()
  420.                 if 'error' not in resp:
  421.                     return call_api(input)
  422.                 result = resp
  423.             else:
  424.                 api_exception_log(e, api_endpoint, 20397)
  425.         except socket.timeout as e:
  426.             result = {'error': {'code': 20010,
  427.                        'type': 'Internal Server Error'}}
  428.             api_exception_log(e, request_data, 20010)
  429.         except Exception as e:
  430.             if hasattr(e, 'reason') and isinstance(e.reason, socket.timeout):
  431.                 result = {'error': {'code': 20010,
  432.                            'type': 'Internal Server Error'}}
  433.                 api_exception_log(e, api_endpoint, 20010)
  434.             else:
  435.                 result = {'error': {'code': 20399,
  436.                            'type': 'Internal Server Error'}}
  437.                 api_exception_log(e, api_endpoint, 20399)
  438.  
  439.         return result
  440.  
  441.  
  442. def api_exception_log(e, request_data, code):
  443.     try:
  444.         log.error(str(e), exc_info=True, extra={'data': {'type': type(e).__name__,
  445.                   'code': code,
  446.                   'extra': request_data}})
  447.     except Exception as e:
  448.         log.exception(str(e))
  449.  
  450.  
  451. def update_api_cache(input, data):
  452.     token = ''
  453.     if 'token' in input and input['token'] is not None:
  454.         token = input['token']
  455.     if token == 'null':
  456.         token = ''
  457.     elif len(token) == 0:
  458.         token = get_session_token()
  459.     if token is None:
  460.         return {'error': {'code': 20398,
  461.                    'type': 'Internal Server Error',
  462.                    'message': 'Invalid Token'}}
  463.     else:
  464.         path = input['path']
  465.         api_endpoint = get_api_url(path)
  466.         params = {}
  467.         if 'params' in input and isinstance(input['params'], dict):
  468.             params = input['params']
  469.         cache_key = get_cache_key(params)
  470.         put_cache('session', api_endpoint, cache_key, data)
  471.         return
  472.  
  473.  
  474. def call_nss(input):
  475.     global nss_cookie
  476.     if input is None or 'endpoint' not in input:
  477.         return {'error': {'code': 2,
  478.                    'type': 'Bad Request',
  479.                    'message': 'This NSS End Point is invalid.'}}
  480.     else:
  481.         params = {}
  482.         if 'params' in input and isinstance(input['params'], dict):
  483.             params = input['params']
  484.         endpoint = get_nss_url(input['endpoint'])
  485.         header_data = {'Content-Type': 'application/json'}
  486.         if nss_cookie is not None:
  487.             header_data['Cookie'] = nss_cookie
  488.         try:
  489.             log.debug('NSS Request init ( %s ): %r', endpoint, input)
  490.             api_url = endpoint
  491.             data = ''
  492.             if len(params) > 0:
  493.                 data = json.dumps(params)
  494.                 header_data['Content-Length'] = len(data)
  495.             from apps import launcher
  496.             header_data['User-Agent'] = 'NexonLauncher.' + launcher.get_version()
  497.             req = urllib2.Request(api_url, data, header_data)
  498.             log.debug('NSS Request: ' + str(req.__dict__))
  499.             start_time = time.time()
  500.             conn = urlopen_with_retry(req)
  501.             resp = conn.read()
  502.             new_cookie = conn.info().getheader('Set-Cookie')
  503.             if new_cookie is not None:
  504.                 nss_cookie = new_cookie
  505.             response_time = time.time() - start_time
  506.             log.debug('NSS Response: (%d ms) %s %s => %r', response_time * 1000, input['endpoint'], api_url, resp)
  507.             conn.close()
  508.         except Exception as e:
  509.             log.exception(str(e))
  510.  
  511.         return
  512.  
  513.  
  514. def post_event(product_id, event_type, comments = None):
  515.     try:
  516.         from auth import main as auth
  517.         path = 'log/' + event_type
  518.         params = {'access_token': get_session_token(),
  519.          'product_id': product_id,
  520.          'device_id': auth.device_id,
  521.          'channel_id': 'nxa'}
  522.         if comments:
  523.             params['comments'] = json.dumps(comments)
  524.         call_nss({'endpoint': event_type,
  525.          'params': params})
  526.     except Exception as e:
  527.         log.exception('failed to post_event: ' + str(e))
  528.  
  529.  
  530. def is_old_api(path):
  531.     ex = '^/?(login$|logout$|me$|session/renew$|library$|library/access$|store/games$|activate$|profile$|log/)'
  532.     m = re.search(ex, path)
  533.     return m is not None
  534.  
  535.  
  536. def get_cache_key(url_params):
  537.     if url_params is None or len(url_params) == 0:
  538.         return '-'
  539.     else:
  540.         arr = sorted(url_params.items())
  541.         return urllib.urlencode(arr)
  542.  
  543.  
  544. def get_cache(bucket_name, cache_root, cache_key):
  545.     if bucket_name is None or len(bucket_name) == 0:
  546.         bucket_name = '-'
  547.     if bucket_name not in API_CACHE_DATA or cache_root not in API_CACHE_DATA[bucket_name] or cache_key not in API_CACHE_DATA[bucket_name][cache_root]:
  548.         return
  549.     else:
  550.         cache_data = API_CACHE_DATA[bucket_name][cache_root][cache_key]
  551.         exp_ts = cache_data['exp_ts']
  552.         if exp_ts != 0 and exp_ts < time.time():
  553.             API_CACHE_DATA[bucket_name][cache_root].pop(cache_key, None)
  554.             return
  555.         data = cache_data['data']
  556.         return data
  557.  
  558.  
  559. def put_cache(bucket_name, cache_root, cache_key, data, duration = 3600):
  560.     if data is None:
  561.         return
  562.     else:
  563.         if bucket_name is None or len(bucket_name) == 0:
  564.             bucket_name = '-'
  565.         if bucket_name not in API_CACHE_DATA:
  566.             API_CACHE_DATA[bucket_name] = {}
  567.         if cache_root not in API_CACHE_DATA[bucket_name]:
  568.             API_CACHE_DATA[bucket_name][cache_root] = {}
  569.         if duration == 0:
  570.             exp_ts = 0
  571.         else:
  572.             exp_ts = time.time() + duration
  573.         API_CACHE_DATA[bucket_name][cache_root][cache_key] = {'exp_ts': exp_ts,
  574.          'data': data}
  575.         return
  576.  
  577.  
  578. def del_cache(bucket_name, cache_root = None):
  579.     if bucket_name is None or len(bucket_name) == 0:
  580.         bucket_name = '-'
  581.     if bucket_name not in API_CACHE_DATA:
  582.         return
  583.     elif cache_root is None:
  584.         API_CACHE_DATA.pop(bucket_name, None)
  585.         return
  586.     else:
  587.         API_CACHE_DATA[bucket_name].pop(cache_root, None)
  588.         return
  589.  
  590.  
  591. def clear_cache():
  592.     global API_CACHE_DATA
  593.     API_CACHE_DATA = {}
Add Comment
Please, Sign In to add comment