server.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. r"""server is a module that enables using python through a web-server.
  2. This module can be used as the entry point to the application. In that case, it
  3. sets up a web-server.
  4. web-pages are determines by the command line arguments passed in.
  5. Use "--help" to list the supported arguments.
  6. """
  7. import argparse
  8. import asyncio
  9. import logging
  10. from wslink import websocket as wsl
  11. from wslink import backends
  12. ws_server = None
  13. # =============================================================================
  14. # Setup default arguments to be parsed
  15. # -s, --nosignalhandlers
  16. # -d, --debug
  17. # -i, --host localhost
  18. # -p, --port 8080
  19. # -t, --timeout 300 (seconds)
  20. # -c, --content '/www' (No content means WebSocket only)
  21. # -a, --authKey vtkweb-secret
  22. # =============================================================================
  23. def add_arguments(parser):
  24. """
  25. Add arguments known to this module. parser must be
  26. argparse.ArgumentParser instance.
  27. """
  28. parser.add_argument(
  29. "-d", "--debug", help="log debugging messages to stdout", action="store_true"
  30. )
  31. parser.add_argument(
  32. "-s",
  33. "--nosignalhandlers",
  34. help="Prevent installation of signal handlers so server can be started inside a thread.",
  35. action="store_true",
  36. )
  37. parser.add_argument(
  38. "-i",
  39. "--host",
  40. type=str,
  41. default="localhost",
  42. help="the interface for the web-server to listen on (default: 0.0.0.0)",
  43. )
  44. parser.add_argument(
  45. "-p",
  46. "--port",
  47. type=int,
  48. default=8080,
  49. help="port number for the web-server to listen on (default: 8080)",
  50. )
  51. parser.add_argument(
  52. "-t",
  53. "--timeout",
  54. type=int,
  55. default=300,
  56. help="timeout for reaping process on idle in seconds (default: 300s, 0 to disable)",
  57. )
  58. parser.add_argument(
  59. "-c",
  60. "--content",
  61. default="",
  62. help="root for web-pages to serve (default: none)",
  63. )
  64. parser.add_argument(
  65. "-a",
  66. "--authKey",
  67. default="wslink-secret",
  68. help="Authentication key for clients to connect to the WebSocket.",
  69. )
  70. parser.add_argument(
  71. "-ws",
  72. "--ws-endpoint",
  73. type=str,
  74. default="ws",
  75. dest="ws",
  76. help="Specify WebSocket endpoint. (e.g. foo/bar/ws, Default: ws)",
  77. )
  78. parser.add_argument(
  79. "--no-ws-endpoint",
  80. action="store_true",
  81. dest="nows",
  82. help="If provided, disables the websocket endpoint",
  83. )
  84. parser.add_argument(
  85. "--fs-endpoints",
  86. default="",
  87. dest="fsEndpoints",
  88. help="add another fs location to a specific endpoint (i.e: data=/Users/seb/Download|images=/Users/seb/Pictures)",
  89. )
  90. parser.add_argument(
  91. "--reverse-url",
  92. dest="reverse_url",
  93. help="Make the server act as a client to connect to a ws relay",
  94. )
  95. parser.add_argument(
  96. "--ssl",
  97. type=str,
  98. default="",
  99. dest="ssl",
  100. help="add a tuple file [certificate, key] (i.e: --ssl 'certificate,key') or adhoc string to generate temporary certificate (i.e: --ssl 'adhoc')",
  101. )
  102. return parser
  103. # =============================================================================
  104. # Parse arguments and start webserver
  105. # =============================================================================
  106. def start(argv=None, protocol=wsl.ServerProtocol, description="wslink web-server"):
  107. """
  108. Sets up the web-server using with __name__ == '__main__'. This can also be
  109. called directly. Pass the optional protocol to override the protocol used.
  110. Default is ServerProtocol.
  111. """
  112. parser = argparse.ArgumentParser(description=description)
  113. add_arguments(parser)
  114. args = parser.parse_args(argv)
  115. # configure protocol, if available
  116. try:
  117. protocol.configure(args)
  118. except AttributeError:
  119. pass
  120. start_webserver(options=args, protocol=protocol)
  121. # =============================================================================
  122. # Stop webserver
  123. # =============================================================================
  124. def stop_webserver():
  125. if ws_server:
  126. loop = asyncio.get_event_loop()
  127. return loop.create_task(ws_server.stop())
  128. # =============================================================================
  129. # Get webserver port (useful when 0 is provided and a dynamic one was picked)
  130. # =============================================================================
  131. def get_port():
  132. if ws_server:
  133. return ws_server.get_port()
  134. return -1
  135. # =============================================================================
  136. # Given a configuration file, create and return a webserver
  137. #
  138. # config = {
  139. # "host": "0.0.0.0",
  140. # "port": 8081
  141. # "ws": {
  142. # "/ws": serverProtocolInstance,
  143. # ...
  144. # },
  145. # static_routes: {
  146. # '/static': .../path/to/files,
  147. # ...
  148. # },
  149. # }
  150. # =============================================================================
  151. def create_webserver(server_config, backend="aiohttp"):
  152. return backends.create_webserver(server_config, backend=backend)
  153. # =============================================================================
  154. # Generate a webserver config from command line options, create a webserver,
  155. # and start it.
  156. # =============================================================================
  157. def start_webserver(
  158. options,
  159. protocol=wsl.ServerProtocol,
  160. disableLogging=False,
  161. backend="aiohttp",
  162. exec_mode="main",
  163. **kwargs,
  164. ):
  165. """
  166. Starts the web-server with the given protocol. Options must be an object
  167. with the following members:
  168. options.host : the interface for the web-server to listen on
  169. options.port : port number for the web-server to listen on
  170. options.timeout : timeout for reaping process on idle in seconds
  171. options.content : root for web-pages to serve.
  172. """
  173. global ws_server
  174. # Create default or custom ServerProtocol
  175. wslinkServer = protocol()
  176. if disableLogging:
  177. logging_level = None
  178. elif options.debug:
  179. logging_level = logging.DEBUG
  180. else:
  181. logging_level = logging.ERROR
  182. if options.reverse_url:
  183. server_config = {
  184. "reverse_url": options.reverse_url,
  185. "ws_protocol": wslinkServer,
  186. "logging_level": logging_level,
  187. }
  188. else:
  189. server_config = {
  190. "host": options.host,
  191. "port": options.port,
  192. "timeout": options.timeout,
  193. "logging_level": logging_level,
  194. }
  195. # Configure websocket endpoint
  196. if not options.nows:
  197. server_config["ws"] = {}
  198. server_config["ws"][options.ws] = wslinkServer
  199. # Configure default static route if --content requested
  200. if len(options.content) > 0:
  201. server_config["static"] = {}
  202. # Static HTTP + WebSocket
  203. server_config["static"]["/"] = options.content
  204. # Configure any other static routes
  205. if len(options.fsEndpoints) > 3:
  206. if "static" not in server_config:
  207. server_config["static"] = {}
  208. for fsResourceInfo in options.fsEndpoints.split("|"):
  209. infoSplit = fsResourceInfo.split("=")
  210. server_config["static"][infoSplit[0]] = infoSplit[1]
  211. # Confifugre SSL
  212. if len(options.ssl) > 0:
  213. from .ssl_context import generate_ssl_pair, ssl
  214. if options.ssl == "adhoc":
  215. options.ssl = generate_ssl_pair(server_config["host"])
  216. else:
  217. tokens = options.ssl.split(",")
  218. if len(tokens) != 2:
  219. raise Exception(
  220. f'ssl configure must be "adhoc" or a tuple of files "cert,key"'
  221. )
  222. options.ssl = tokens
  223. cert, key = options.ssl
  224. context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
  225. context.load_cert_chain(cert, key)
  226. server_config["ssl"] = context
  227. server_config["handle_signals"] = not options.nosignalhandlers
  228. # Create the webserver and start it
  229. ws_server = create_webserver(server_config, backend=backend)
  230. # Once we have python 3.7 minimum, we can start the server with asyncio.run()
  231. # asyncio.run(ws_server.start())
  232. # Until then, we can start the server this way
  233. loop = asyncio.get_event_loop()
  234. port_callback = None
  235. if hasattr(wslinkServer, "port_callback"):
  236. port_callback = wslinkServer.port_callback
  237. if hasattr(wslinkServer, "set_server"):
  238. wslinkServer.set_server(ws_server)
  239. def create_coroutine():
  240. return ws_server.start(port_callback)
  241. def main_exec():
  242. # Block until the loop finishes and then close the loop
  243. try:
  244. loop.run_until_complete(create_coroutine())
  245. finally:
  246. loop.close()
  247. def task_exec():
  248. return loop.create_task(create_coroutine())
  249. exec_modes = {
  250. "main": main_exec,
  251. "task": task_exec,
  252. "coroutine": create_coroutine,
  253. }
  254. if exec_mode not in exec_modes:
  255. raise Exception(f"Unknown exec_mode: {exec_mode}")
  256. return exec_modes[exec_mode]()
  257. if __name__ == "__main__":
  258. start()