base_request.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. # Copyright (C) 2013 Google Inc.
  2. #
  3. # This file is part of YouCompleteMe.
  4. #
  5. # YouCompleteMe is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # YouCompleteMe is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
  17. from __future__ import unicode_literals
  18. from __future__ import print_function
  19. from __future__ import division
  20. from __future__ import absolute_import
  21. from future import standard_library
  22. standard_library.install_aliases()
  23. from builtins import * # noqa
  24. import requests
  25. import urllib.parse
  26. import json
  27. from base64 import b64decode, b64encode
  28. from retries import retries
  29. from requests_futures.sessions import FuturesSession
  30. from ycm.unsafe_thread_pool_executor import UnsafeThreadPoolExecutor
  31. from ycm import vimsupport
  32. from ycmd.utils import ToBytes
  33. from ycmd.hmac_utils import CreateRequestHmac, CreateHmac, SecureBytesEqual
  34. from ycmd.responses import ServerError, UnknownExtraConf
  35. _HEADERS = {'content-type': 'application/json'}
  36. _EXECUTOR = UnsafeThreadPoolExecutor( max_workers = 30 )
  37. # Setting this to None seems to screw up the Requests/urllib3 libs.
  38. _DEFAULT_TIMEOUT_SEC = 30
  39. _HMAC_HEADER = 'x-ycm-hmac'
  40. class BaseRequest( object ):
  41. def __init__( self ):
  42. pass
  43. def Start( self ):
  44. pass
  45. def Done( self ):
  46. return True
  47. def Response( self ):
  48. return {}
  49. # This method blocks
  50. # |timeout| is num seconds to tolerate no response from server before giving
  51. # up; see Requests docs for details (we just pass the param along).
  52. @staticmethod
  53. def GetDataFromHandler( handler, timeout = _DEFAULT_TIMEOUT_SEC ):
  54. return JsonFromFuture( BaseRequest._TalkToHandlerAsync( '',
  55. handler,
  56. 'GET',
  57. timeout ) )
  58. # This is the blocking version of the method. See below for async.
  59. # |timeout| is num seconds to tolerate no response from server before giving
  60. # up; see Requests docs for details (we just pass the param along).
  61. @staticmethod
  62. def PostDataToHandler( data, handler, timeout = _DEFAULT_TIMEOUT_SEC ):
  63. return JsonFromFuture( BaseRequest.PostDataToHandlerAsync( data,
  64. handler,
  65. timeout ) )
  66. # This returns a future! Use JsonFromFuture to get the value.
  67. # |timeout| is num seconds to tolerate no response from server before giving
  68. # up; see Requests docs for details (we just pass the param along).
  69. @staticmethod
  70. def PostDataToHandlerAsync( data, handler, timeout = _DEFAULT_TIMEOUT_SEC ):
  71. return BaseRequest._TalkToHandlerAsync( data, handler, 'POST', timeout )
  72. # This returns a future! Use JsonFromFuture to get the value.
  73. # |method| is either 'POST' or 'GET'.
  74. # |timeout| is num seconds to tolerate no response from server before giving
  75. # up; see Requests docs for details (we just pass the param along).
  76. @staticmethod
  77. def _TalkToHandlerAsync( data,
  78. handler,
  79. method,
  80. timeout = _DEFAULT_TIMEOUT_SEC ):
  81. def SendRequest( data, handler, method, timeout ):
  82. request_uri = _BuildUri( handler )
  83. if method == 'POST':
  84. sent_data = _ToUtf8Json( data )
  85. return BaseRequest.session.post(
  86. request_uri,
  87. data = sent_data,
  88. headers = BaseRequest._ExtraHeaders( method,
  89. request_uri,
  90. sent_data ),
  91. timeout = timeout )
  92. if method == 'GET':
  93. return BaseRequest.session.get(
  94. request_uri,
  95. headers = BaseRequest._ExtraHeaders( method, request_uri ),
  96. timeout = timeout )
  97. @retries( 5, delay = 0.5, backoff = 1.5 )
  98. def DelayedSendRequest( data, handler, method ):
  99. request_uri = _BuildUri( handler )
  100. if method == 'POST':
  101. sent_data = _ToUtf8Json( data )
  102. return requests.post(
  103. request_uri,
  104. data = sent_data,
  105. headers = BaseRequest._ExtraHeaders( method,
  106. request_uri,
  107. sent_data ) )
  108. if method == 'GET':
  109. return requests.get(
  110. request_uri,
  111. headers = BaseRequest._ExtraHeaders( method, request_uri ) )
  112. if not _CheckServerIsHealthyWithCache():
  113. return _EXECUTOR.submit( DelayedSendRequest, data, handler, method )
  114. return SendRequest( data, handler, method, timeout )
  115. @staticmethod
  116. def _ExtraHeaders( method, request_uri, request_body = None ):
  117. if not request_body:
  118. request_body = ''
  119. headers = dict( _HEADERS )
  120. headers[ _HMAC_HEADER ] = b64encode(
  121. CreateRequestHmac( method,
  122. urllib.parse.urlparse( request_uri ).path,
  123. request_body,
  124. BaseRequest.hmac_secret ) )
  125. return headers
  126. session = FuturesSession( executor = _EXECUTOR )
  127. server_location = ''
  128. hmac_secret = ''
  129. def BuildRequestData( include_buffer_data = True ):
  130. line, column = vimsupport.CurrentLineAndColumn()
  131. filepath = vimsupport.GetCurrentBufferFilepath()
  132. request_data = {
  133. 'line_num': line + 1,
  134. 'column_num': column + 1,
  135. 'filepath': filepath
  136. }
  137. if include_buffer_data:
  138. request_data[ 'file_data' ] = vimsupport.GetUnsavedAndCurrentBufferData()
  139. return request_data
  140. def JsonFromFuture( future ):
  141. response = future.result()
  142. _ValidateResponseObject( response )
  143. if response.status_code == requests.codes.server_error:
  144. raise MakeServerException( response.json() )
  145. # We let Requests handle the other status types, we only handle the 500
  146. # error code.
  147. response.raise_for_status()
  148. if response.text:
  149. return response.json()
  150. return None
  151. def HandleServerException( exception ):
  152. serialized_exception = str( exception )
  153. # We ignore the exception about the file already being parsed since it comes
  154. # up often and isn't something that's actionable by the user.
  155. if 'already being parsed' in serialized_exception:
  156. return
  157. vimsupport.PostVimMessage( serialized_exception )
  158. def _ToUtf8Json( data ):
  159. return ToBytes( json.dumps( data ) if data else None )
  160. def _ValidateResponseObject( response ):
  161. our_hmac = CreateHmac( response.content, BaseRequest.hmac_secret )
  162. their_hmac = ToBytes( b64decode( response.headers[ _HMAC_HEADER ] ) )
  163. if not SecureBytesEqual( our_hmac, their_hmac ):
  164. raise RuntimeError( 'Received invalid HMAC for response!' )
  165. return True
  166. def _BuildUri( handler ):
  167. return urllib.parse.urljoin( BaseRequest.server_location, handler )
  168. SERVER_HEALTHY = False
  169. def _CheckServerIsHealthyWithCache():
  170. global SERVER_HEALTHY
  171. def _ServerIsHealthy():
  172. request_uri = _BuildUri( 'healthy' )
  173. response = requests.get( request_uri,
  174. headers = BaseRequest._ExtraHeaders(
  175. 'GET', request_uri, '' ) )
  176. _ValidateResponseObject( response )
  177. response.raise_for_status()
  178. return response.json()
  179. if SERVER_HEALTHY:
  180. return True
  181. try:
  182. SERVER_HEALTHY = _ServerIsHealthy()
  183. return SERVER_HEALTHY
  184. except:
  185. return False
  186. def MakeServerException( data ):
  187. if data[ 'exception' ][ 'TYPE' ] == UnknownExtraConf.__name__:
  188. return UnknownExtraConf( data[ 'exception' ][ 'extra_conf_file' ] )
  189. return ServerError( '{0}: {1}'.format( data[ 'exception' ][ 'TYPE' ],
  190. data[ 'message' ] ) )