Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

cli.py 9.8KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import argparse
  2. import logging
  3. import sys
  4. from argparse import ArgumentError, Namespace
  5. from asgiref.compatibility import guarantee_single_callable
  6. from .access import AccessLogGenerator
  7. from .endpoints import build_endpoint_description_strings
  8. from .server import Server
  9. from .utils import import_by_path
  10. logger = logging.getLogger(__name__)
  11. DEFAULT_HOST = "127.0.0.1"
  12. DEFAULT_PORT = 8000
  13. class CommandLineInterface:
  14. """
  15. Acts as the main CLI entry point for running the server.
  16. """
  17. description = "Django HTTP/WebSocket server"
  18. server_class = Server
  19. def __init__(self):
  20. self.parser = argparse.ArgumentParser(description=self.description)
  21. self.parser.add_argument(
  22. "-p", "--port", type=int, help="Port number to listen on", default=None
  23. )
  24. self.parser.add_argument(
  25. "-b",
  26. "--bind",
  27. dest="host",
  28. help="The host/address to bind to",
  29. default=None,
  30. )
  31. self.parser.add_argument(
  32. "--websocket_timeout",
  33. type=int,
  34. help="Maximum time to allow a websocket to be connected. -1 for infinite.",
  35. default=86400,
  36. )
  37. self.parser.add_argument(
  38. "--websocket_connect_timeout",
  39. type=int,
  40. help="Maximum time to allow a connection to handshake. -1 for infinite",
  41. default=5,
  42. )
  43. self.parser.add_argument(
  44. "-u",
  45. "--unix-socket",
  46. dest="unix_socket",
  47. help="Bind to a UNIX socket rather than a TCP host/port",
  48. default=None,
  49. )
  50. self.parser.add_argument(
  51. "--fd",
  52. type=int,
  53. dest="file_descriptor",
  54. help="Bind to a file descriptor rather than a TCP host/port or named unix socket",
  55. default=None,
  56. )
  57. self.parser.add_argument(
  58. "-e",
  59. "--endpoint",
  60. dest="socket_strings",
  61. action="append",
  62. help="Use raw server strings passed directly to twisted",
  63. default=[],
  64. )
  65. self.parser.add_argument(
  66. "-v",
  67. "--verbosity",
  68. type=int,
  69. help="How verbose to make the output",
  70. default=1,
  71. )
  72. self.parser.add_argument(
  73. "-t",
  74. "--http-timeout",
  75. type=int,
  76. help="How long to wait for worker before timing out HTTP connections",
  77. default=None,
  78. )
  79. self.parser.add_argument(
  80. "--access-log",
  81. help="Where to write the access log (- for stdout, the default for verbosity=1)",
  82. default=None,
  83. )
  84. self.parser.add_argument(
  85. "--log-fmt",
  86. help="Log format to use",
  87. default="%(asctime)-15s %(levelname)-8s %(message)s",
  88. )
  89. self.parser.add_argument(
  90. "--ping-interval",
  91. type=int,
  92. help="The number of seconds a WebSocket must be idle before a keepalive ping is sent",
  93. default=20,
  94. )
  95. self.parser.add_argument(
  96. "--ping-timeout",
  97. type=int,
  98. help="The number of seconds before a WebSocket is closed if no response to a keepalive ping",
  99. default=30,
  100. )
  101. self.parser.add_argument(
  102. "--application-close-timeout",
  103. type=int,
  104. help="The number of seconds an ASGI application has to exit after client disconnect before it is killed",
  105. default=10,
  106. )
  107. self.parser.add_argument(
  108. "--root-path",
  109. dest="root_path",
  110. help="The setting for the ASGI root_path variable",
  111. default="",
  112. )
  113. self.parser.add_argument(
  114. "--proxy-headers",
  115. dest="proxy_headers",
  116. help="Enable parsing and using of X-Forwarded-For and X-Forwarded-Port headers and using that as the "
  117. "client address",
  118. default=False,
  119. action="store_true",
  120. )
  121. self.arg_proxy_host = self.parser.add_argument(
  122. "--proxy-headers-host",
  123. dest="proxy_headers_host",
  124. help="Specify which header will be used for getting the host "
  125. "part. Can be omitted, requires --proxy-headers to be specified "
  126. 'when passed. "X-Real-IP" (when passed by your webserver) is a '
  127. "good candidate for this.",
  128. default=False,
  129. action="store",
  130. )
  131. self.arg_proxy_port = self.parser.add_argument(
  132. "--proxy-headers-port",
  133. dest="proxy_headers_port",
  134. help="Specify which header will be used for getting the port "
  135. "part. Can be omitted, requires --proxy-headers to be specified "
  136. "when passed.",
  137. default=False,
  138. action="store",
  139. )
  140. self.parser.add_argument(
  141. "application",
  142. help="The application to dispatch to as path.to.module:instance.path",
  143. )
  144. self.parser.add_argument(
  145. "-s",
  146. "--server-name",
  147. dest="server_name",
  148. help="specify which value should be passed to response header Server attribute",
  149. default="daphne",
  150. )
  151. self.parser.add_argument(
  152. "--no-server-name", dest="server_name", action="store_const", const=""
  153. )
  154. self.server = None
  155. @classmethod
  156. def entrypoint(cls):
  157. """
  158. Main entrypoint for external starts.
  159. """
  160. cls().run(sys.argv[1:])
  161. def _check_proxy_headers_passed(self, argument: str, args: Namespace):
  162. """Raise if the `--proxy-headers` weren't specified."""
  163. if args.proxy_headers:
  164. return
  165. raise ArgumentError(
  166. argument=argument,
  167. message="--proxy-headers has to be passed for this parameter.",
  168. )
  169. def _get_forwarded_host(self, args: Namespace):
  170. """
  171. Return the default host header from which the remote hostname/ip
  172. will be extracted.
  173. """
  174. if args.proxy_headers_host:
  175. self._check_proxy_headers_passed(argument=self.arg_proxy_host, args=args)
  176. return args.proxy_headers_host
  177. if args.proxy_headers:
  178. return "X-Forwarded-For"
  179. def _get_forwarded_port(self, args: Namespace):
  180. """
  181. Return the default host header from which the remote hostname/ip
  182. will be extracted.
  183. """
  184. if args.proxy_headers_port:
  185. self._check_proxy_headers_passed(argument=self.arg_proxy_port, args=args)
  186. return args.proxy_headers_port
  187. if args.proxy_headers:
  188. return "X-Forwarded-Port"
  189. def run(self, args):
  190. """
  191. Pass in raw argument list and it will decode them
  192. and run the server.
  193. """
  194. # Decode args
  195. args = self.parser.parse_args(args)
  196. # Set up logging
  197. logging.basicConfig(
  198. level={
  199. 0: logging.WARN,
  200. 1: logging.INFO,
  201. 2: logging.DEBUG,
  202. 3: logging.DEBUG, # Also turns on asyncio debug
  203. }[args.verbosity],
  204. format=args.log_fmt,
  205. )
  206. # If verbosity is 1 or greater, or they told us explicitly, set up access log
  207. access_log_stream = None
  208. if args.access_log:
  209. if args.access_log == "-":
  210. access_log_stream = sys.stdout
  211. else:
  212. access_log_stream = open(args.access_log, "a", 1)
  213. elif args.verbosity >= 1:
  214. access_log_stream = sys.stdout
  215. # Import application
  216. sys.path.insert(0, ".")
  217. application = import_by_path(args.application)
  218. application = guarantee_single_callable(application)
  219. # Set up port/host bindings
  220. if not any(
  221. [
  222. args.host,
  223. args.port is not None,
  224. args.unix_socket,
  225. args.file_descriptor is not None,
  226. args.socket_strings,
  227. ]
  228. ):
  229. # no advanced binding options passed, patch in defaults
  230. args.host = DEFAULT_HOST
  231. args.port = DEFAULT_PORT
  232. elif args.host and args.port is None:
  233. args.port = DEFAULT_PORT
  234. elif args.port is not None and not args.host:
  235. args.host = DEFAULT_HOST
  236. # Build endpoint description strings from (optional) cli arguments
  237. endpoints = build_endpoint_description_strings(
  238. host=args.host,
  239. port=args.port,
  240. unix_socket=args.unix_socket,
  241. file_descriptor=args.file_descriptor,
  242. )
  243. endpoints = sorted(args.socket_strings + endpoints)
  244. # Start the server
  245. logger.info("Starting server at {}".format(", ".join(endpoints)))
  246. self.server = self.server_class(
  247. application=application,
  248. endpoints=endpoints,
  249. http_timeout=args.http_timeout,
  250. ping_interval=args.ping_interval,
  251. ping_timeout=args.ping_timeout,
  252. websocket_timeout=args.websocket_timeout,
  253. websocket_connect_timeout=args.websocket_connect_timeout,
  254. websocket_handshake_timeout=args.websocket_connect_timeout,
  255. application_close_timeout=args.application_close_timeout,
  256. action_logger=AccessLogGenerator(access_log_stream)
  257. if access_log_stream
  258. else None,
  259. root_path=args.root_path,
  260. verbosity=args.verbosity,
  261. proxy_forwarded_address_header=self._get_forwarded_host(args=args),
  262. proxy_forwarded_port_header=self._get_forwarded_port(args=args),
  263. proxy_forwarded_proto_header="X-Forwarded-Proto"
  264. if args.proxy_headers
  265. else None,
  266. server_name=args.server_name,
  267. )
  268. self.server.run()