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.

76 lines
2.3 KiB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. import asyncio
  4. import dataclasses
  5. import socket
  6. import traceback
  7. import typing
  8. @dataclasses.dataclass
  9. class ScgiRequest:
  10. headers: typing.Dict[bytes, bytes]
  11. body: bytes
  12. async def parse_scgi_request(reader: asyncio.StreamReader) -> ScgiRequest:
  13. hlen = int((await reader.readuntil(b':'))[:-1])
  14. header_raw = await reader.readexactly(hlen + 1)
  15. assert len(header_raw) >= 16, "invalid request: too short (< 16)"
  16. assert header_raw[-2:] == b'\0,', f"Invalid request: missing header/netstring terminator '\\x00,', got {header_raw[-2:]!r}"
  17. header_list = header_raw[:-2].split(b'\0')
  18. assert len(header_list) % 2 == 0, f"Invalid request: odd numbers of header entries (must be pairs), got {len(header_list)}"
  19. assert header_list[0] == b'CONTENT_LENGTH', f"Invalid request: first header entry must be 'CONTENT_LENGTH', got {header_list[0]!r}"
  20. clen = int(header_list[1])
  21. headers = {}
  22. i = 0
  23. while i < len(header_list):
  24. key = header_list[i]
  25. value = header_list[i+1]
  26. i += 2
  27. assert not key in headers, f"Invalid request: duplicate header key {key!r}"
  28. headers[key] = value
  29. assert headers.get(b'SCGI') == b'1', "Invalid request: missing SCGI=1 header"
  30. body = await reader.readexactly(clen)
  31. return ScgiRequest(headers=headers, body=body)
  32. async def handle_scgi(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
  33. print(f"scgi-envcheck: Incoming connection", flush=True)
  34. try:
  35. req = await parse_scgi_request(reader)
  36. envvar = req.headers[b'QUERY_STRING']
  37. result = req.headers[envvar]
  38. except KeyboardInterrupt:
  39. raise
  40. except Exception as e:
  41. print(traceback.format_exc())
  42. writer.write(b"Status: 500\r\nContent-Type: text/plain\r\n\r\n" + str(e).encode('utf-8'))
  43. else:
  44. writer.write(b"Status: 200\r\nContent-Type: text/plain\r\n\r\n" + result)
  45. await writer.drain()
  46. writer.close()
  47. await writer.wait_closed()
  48. async def main():
  49. sock = socket.socket(fileno=0)
  50. if sock.type == socket.AF_UNIX:
  51. start_server = asyncio.start_unix_server
  52. else:
  53. start_server = asyncio.start_server
  54. server = await start_server(handle_scgi, sock=sock, start_serving=False)
  55. addr = server.sockets[0].getsockname()
  56. print(f'Serving on {addr}', flush=True)
  57. async with server:
  58. await server.serve_forever()
  59. try:
  60. asyncio.run(main())
  61. except KeyboardInterrupt:
  62. pass