testserver.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. #!/usr/bin/env vpython3
  2. # Copyright 2013 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. """This is a simple HTTP/TCP/PROXY/BASIC_AUTH_PROXY/WEBSOCKET server used for
  6. testing Chrome.
  7. It supports several test URLs, as specified by the handlers in TestPageHandler.
  8. By default, it listens on an ephemeral port and sends the port number back to
  9. the originating process over a pipe. The originating process can specify an
  10. explicit port if necessary.
  11. """
  12. from __future__ import print_function
  13. import base64
  14. import logging
  15. import os
  16. import select
  17. from six.moves import BaseHTTPServer, socketserver
  18. import six.moves.urllib.parse as urlparse
  19. import socket
  20. import ssl
  21. import sys
  22. BASE_DIR = os.path.dirname(os.path.abspath(__file__))
  23. ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(BASE_DIR)))
  24. # Insert at the beginning of the path, we want to use our copies of the library
  25. # unconditionally (since they contain modifications from anything that might be
  26. # obtained from e.g. PyPi).
  27. sys.path.insert(0, os.path.join(ROOT_DIR, 'third_party', 'pywebsocket3', 'src'))
  28. import mod_pywebsocket.standalone
  29. from mod_pywebsocket.standalone import WebSocketServer
  30. # import manually
  31. mod_pywebsocket.standalone.ssl = ssl
  32. import testserver_base
  33. SERVER_UNSET = 0
  34. SERVER_BASIC_AUTH_PROXY = 1
  35. SERVER_WEBSOCKET = 2
  36. SERVER_PROXY = 3
  37. # Default request queue size for WebSocketServer.
  38. _DEFAULT_REQUEST_QUEUE_SIZE = 128
  39. class WebSocketOptions:
  40. """Holds options for WebSocketServer."""
  41. def __init__(self, host, port, data_dir):
  42. self.request_queue_size = _DEFAULT_REQUEST_QUEUE_SIZE
  43. self.server_host = host
  44. self.port = port
  45. self.websock_handlers = data_dir
  46. self.scan_dir = None
  47. self.allow_handlers_outside_root_dir = False
  48. self.websock_handlers_map_file = None
  49. self.cgi_directories = []
  50. self.is_executable_method = None
  51. self.use_tls = False
  52. self.private_key = None
  53. self.certificate = None
  54. self.tls_client_auth = False
  55. self.tls_client_ca = None
  56. self.use_basic_auth = False
  57. self.basic_auth_credential = 'Basic ' + base64.b64encode(
  58. b'test:test').decode()
  59. class ThreadingHTTPServer(socketserver.ThreadingMixIn,
  60. testserver_base.ClientRestrictingServerMixIn,
  61. testserver_base.BrokenPipeHandlerMixIn,
  62. testserver_base.StoppableHTTPServer):
  63. """This is a specialization of StoppableHTTPServer that adds client
  64. verification and creates a new thread for every connection. It
  65. should only be used with handlers that are known to be threadsafe."""
  66. pass
  67. class TestPageHandler(testserver_base.BasePageHandler):
  68. def __init__(self, request, client_address, socket_server):
  69. connect_handlers = [self.DefaultConnectResponseHandler]
  70. get_handlers = [self.DefaultResponseHandler]
  71. post_handlers = get_handlers
  72. put_handlers = get_handlers
  73. head_handlers = [self.DefaultResponseHandler]
  74. testserver_base.BasePageHandler.__init__(self, request, client_address,
  75. socket_server, connect_handlers,
  76. get_handlers, head_handlers,
  77. post_handlers, put_handlers)
  78. def DefaultResponseHandler(self):
  79. """This is the catch-all response handler for requests that aren't handled
  80. by one of the special handlers above.
  81. Note that we specify the content-length as without it the https connection
  82. is not closed properly (and the browser keeps expecting data)."""
  83. contents = "Default response given for path: " + self.path
  84. self.send_response(200)
  85. self.send_header('Content-Type', 'text/html')
  86. self.send_header('Content-Length', len(contents))
  87. self.end_headers()
  88. if (self.command != 'HEAD'):
  89. self.wfile.write(contents.encode('utf8'))
  90. return True
  91. def DefaultConnectResponseHandler(self):
  92. """This is the catch-all response handler for CONNECT requests that aren't
  93. handled by one of the special handlers above. Real Web servers respond
  94. with 400 to CONNECT requests."""
  95. contents = "Your client has issued a malformed or illegal request."
  96. self.send_response(400) # bad request
  97. self.send_header('Content-Type', 'text/html')
  98. self.send_header('Content-Length', len(contents))
  99. self.end_headers()
  100. self.wfile.write(contents.encode('utf8'))
  101. return True
  102. class ProxyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
  103. """A request handler that behaves as a proxy server. Only CONNECT, GET and
  104. HEAD methods are supported.
  105. """
  106. redirect_connect_to_localhost = False;
  107. def _start_read_write(self, sock):
  108. sock.setblocking(0)
  109. self.request.setblocking(0)
  110. rlist = [self.request, sock]
  111. while True:
  112. ready_sockets, _unused, errors = select.select(rlist, [], [])
  113. if errors:
  114. self.send_response(500)
  115. self.end_headers()
  116. return
  117. for s in ready_sockets:
  118. received = s.recv(1024)
  119. if len(received) == 0:
  120. return
  121. if s == self.request:
  122. other = sock
  123. else:
  124. other = self.request
  125. # This will lose data if the kernel write buffer fills up.
  126. # TODO(ricea): Correctly use the return value to track how much was
  127. # written and buffer the rest. Use select to determine when the socket
  128. # becomes writable again.
  129. other.send(received)
  130. def _do_common_method(self):
  131. url = urlparse.urlparse(self.path)
  132. port = url.port
  133. if not port:
  134. if url.scheme == 'http':
  135. port = 80
  136. elif url.scheme == 'https':
  137. port = 443
  138. if not url.hostname or not port:
  139. self.send_response(400)
  140. self.end_headers()
  141. return
  142. if len(url.path) == 0:
  143. path = '/'
  144. else:
  145. path = url.path
  146. if len(url.query) > 0:
  147. path = '%s?%s' % (url.path, url.query)
  148. sock = None
  149. try:
  150. sock = socket.create_connection((url.hostname, port))
  151. sock.send(('%s %s %s\r\n' %
  152. (self.command, path, self.protocol_version)).encode('utf-8'))
  153. for name, value in self.headers.items():
  154. if (name.lower().startswith('connection')
  155. or name.lower().startswith('proxy')):
  156. continue
  157. # HTTP headers are encoded in Latin-1.
  158. sock.send(b'%s: %s\r\n' %
  159. (name.encode('latin-1'), value.encode('latin-1')))
  160. sock.send(b'\r\n')
  161. # This is wrong: it will pass through connection-level headers and
  162. # misbehave on connection reuse. The only reason it works at all is that
  163. # our test servers have never supported connection reuse.
  164. # TODO(ricea): Use a proper HTTP client library instead.
  165. self._start_read_write(sock)
  166. except Exception:
  167. logging.exception('failure in common method: %s %s', self.command, path)
  168. self.send_response(500)
  169. self.end_headers()
  170. finally:
  171. if sock is not None:
  172. sock.close()
  173. def do_CONNECT(self):
  174. try:
  175. pos = self.path.rfind(':')
  176. host = self.path[:pos]
  177. port = int(self.path[pos+1:])
  178. except Exception:
  179. self.send_response(400)
  180. self.end_headers()
  181. if ProxyRequestHandler.redirect_connect_to_localhost:
  182. host = "127.0.0.1"
  183. sock = None
  184. try:
  185. sock = socket.create_connection((host, port))
  186. self.send_response(200, 'Connection established')
  187. self.end_headers()
  188. self._start_read_write(sock)
  189. except Exception:
  190. logging.exception('failure in CONNECT: %s', path)
  191. self.send_response(500)
  192. self.end_headers()
  193. finally:
  194. if sock is not None:
  195. sock.close()
  196. def do_GET(self):
  197. self._do_common_method()
  198. def do_HEAD(self):
  199. self._do_common_method()
  200. class BasicAuthProxyRequestHandler(ProxyRequestHandler):
  201. """A request handler that behaves as a proxy server which requires
  202. basic authentication.
  203. """
  204. _AUTH_CREDENTIAL = 'Basic Zm9vOmJhcg==' # foo:bar
  205. def parse_request(self):
  206. """Overrides parse_request to check credential."""
  207. if not ProxyRequestHandler.parse_request(self):
  208. return False
  209. auth = self.headers.get('Proxy-Authorization', None)
  210. if auth != self._AUTH_CREDENTIAL:
  211. self.send_response(407)
  212. self.send_header('Proxy-Authenticate', 'Basic realm="MyRealm1"')
  213. self.end_headers()
  214. return False
  215. return True
  216. class ServerRunner(testserver_base.TestServerRunner):
  217. """TestServerRunner for the net test servers."""
  218. def __init__(self):
  219. super(ServerRunner, self).__init__()
  220. def __make_data_dir(self):
  221. if self.options.data_dir:
  222. if not os.path.isdir(self.options.data_dir):
  223. raise testserver_base.OptionError('specified data dir not found: ' +
  224. self.options.data_dir + ' exiting...')
  225. my_data_dir = self.options.data_dir
  226. else:
  227. # Create the default path to our data dir, relative to the exe dir.
  228. my_data_dir = os.path.join(BASE_DIR, "..", "..", "data")
  229. return my_data_dir
  230. def create_server(self, server_data):
  231. port = self.options.port
  232. host = self.options.host
  233. logging.basicConfig()
  234. # Work around a bug in Mac OS 10.6. Spawning a WebSockets server
  235. # will result in a call to |getaddrinfo|, which fails with "nodename
  236. # nor servname provided" for localhost:0 on 10.6.
  237. # TODO(ricea): Remove this if no longer needed.
  238. if self.options.server_type == SERVER_WEBSOCKET and \
  239. host == "localhost" and \
  240. port == 0:
  241. host = "127.0.0.1"
  242. # Construct the subjectAltNames for any ad-hoc generated certificates.
  243. # As host can be either a DNS name or IP address, attempt to determine
  244. # which it is, so it can be placed in the appropriate SAN.
  245. dns_sans = None
  246. ip_sans = None
  247. ip = None
  248. try:
  249. ip = socket.inet_aton(host)
  250. ip_sans = [ip]
  251. except socket.error:
  252. pass
  253. if ip is None:
  254. dns_sans = [host]
  255. if self.options.server_type == SERVER_UNSET:
  256. raise testserver_base.OptionError('no server type specified')
  257. elif self.options.server_type == SERVER_WEBSOCKET:
  258. # TODO(toyoshim): Remove following os.chdir. Currently this operation
  259. # is required to work correctly. It should be fixed from pywebsocket side.
  260. os.chdir(self.__make_data_dir())
  261. websocket_options = WebSocketOptions(host, port, '.')
  262. scheme = "ws"
  263. if self.options.cert_and_key_file:
  264. scheme = "wss"
  265. websocket_options.use_tls = True
  266. key_path = os.path.join(ROOT_DIR, self.options.cert_and_key_file)
  267. if not os.path.isfile(key_path):
  268. raise testserver_base.OptionError(
  269. 'specified server cert file not found: ' +
  270. self.options.cert_and_key_file + ' exiting...')
  271. websocket_options.private_key = key_path
  272. websocket_options.certificate = key_path
  273. if self.options.ssl_client_auth:
  274. websocket_options.tls_client_cert_optional = False
  275. websocket_options.tls_client_auth = True
  276. if len(self.options.ssl_client_ca) != 1:
  277. raise testserver_base.OptionError(
  278. 'one trusted client CA file should be specified')
  279. if not os.path.isfile(self.options.ssl_client_ca[0]):
  280. raise testserver_base.OptionError(
  281. 'specified trusted client CA file not found: ' +
  282. self.options.ssl_client_ca[0] + ' exiting...')
  283. websocket_options.tls_client_ca = self.options.ssl_client_ca[0]
  284. print('Trying to start websocket server on %s://%s:%d...' %
  285. (scheme, websocket_options.server_host, websocket_options.port))
  286. server = WebSocketServer(websocket_options)
  287. print('WebSocket server started on %s://%s:%d...' %
  288. (scheme, host, server.server_port))
  289. server_data['port'] = server.server_port
  290. websocket_options.use_basic_auth = self.options.ws_basic_auth
  291. elif self.options.server_type == SERVER_PROXY:
  292. ProxyRequestHandler.redirect_connect_to_localhost = \
  293. self.options.redirect_connect_to_localhost
  294. server = ThreadingHTTPServer((host, port), ProxyRequestHandler)
  295. print('Proxy server started on port %d...' % server.server_port)
  296. server_data['port'] = server.server_port
  297. elif self.options.server_type == SERVER_BASIC_AUTH_PROXY:
  298. ProxyRequestHandler.redirect_connect_to_localhost = \
  299. self.options.redirect_connect_to_localhost
  300. server = ThreadingHTTPServer((host, port), BasicAuthProxyRequestHandler)
  301. print('BasicAuthProxy server started on port %d...' % server.server_port)
  302. server_data['port'] = server.server_port
  303. else:
  304. raise testserver_base.OptionError('unknown server type' +
  305. self.options.server_type)
  306. return server
  307. def add_options(self):
  308. testserver_base.TestServerRunner.add_options(self)
  309. self.option_parser.add_option('--proxy',
  310. action='store_const',
  311. const=SERVER_PROXY,
  312. default=SERVER_UNSET,
  313. dest='server_type',
  314. help='start up a proxy server.')
  315. self.option_parser.add_option('--basic-auth-proxy',
  316. action='store_const',
  317. const=SERVER_BASIC_AUTH_PROXY,
  318. default=SERVER_UNSET,
  319. dest='server_type',
  320. help='start up a proxy server which requires '
  321. 'basic authentication.')
  322. self.option_parser.add_option('--websocket',
  323. action='store_const',
  324. const=SERVER_WEBSOCKET,
  325. default=SERVER_UNSET,
  326. dest='server_type',
  327. help='start up a WebSocket server.')
  328. self.option_parser.add_option('--cert-and-key-file',
  329. dest='cert_and_key_file', help='specify the '
  330. 'path to the file containing the certificate '
  331. 'and private key for the server in PEM '
  332. 'format')
  333. self.option_parser.add_option('--ssl-client-auth', action='store_true',
  334. help='Require SSL client auth on every '
  335. 'connection.')
  336. self.option_parser.add_option('--ssl-client-ca', action='append',
  337. default=[], help='Specify that the client '
  338. 'certificate request should include the CA '
  339. 'named in the subject of the DER-encoded '
  340. 'certificate contained in the specified '
  341. 'file. This option may appear multiple '
  342. 'times, indicating multiple CA names should '
  343. 'be sent in the request.')
  344. self.option_parser.add_option('--file-root-url', default='/files/',
  345. help='Specify a root URL for files served.')
  346. # TODO(ricea): Generalize this to support basic auth for HTTP too.
  347. self.option_parser.add_option('--ws-basic-auth', action='store_true',
  348. dest='ws_basic_auth',
  349. help='Enable basic-auth for WebSocket')
  350. self.option_parser.add_option('--redirect-connect-to-localhost',
  351. dest='redirect_connect_to_localhost',
  352. default=False, action='store_true',
  353. help='If set, the Proxy server will connect '
  354. 'to localhost instead of the requested URL '
  355. 'on CONNECT requests')
  356. if __name__ == '__main__':
  357. sys.exit(ServerRunner().main())