Development of an internal social media platform with personalised dashboards for students
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.

wsgi.py 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. # -*- coding: utf-8 -
  2. #
  3. # This file is part of gunicorn released under the MIT license.
  4. # See the NOTICE for more information.
  5. import io
  6. import logging
  7. import os
  8. import re
  9. import sys
  10. from gunicorn._compat import unquote_to_wsgi_str
  11. from gunicorn.http.message import HEADER_RE
  12. from gunicorn.http.errors import InvalidHeader, InvalidHeaderName
  13. from gunicorn.six import string_types, binary_type, reraise
  14. from gunicorn import SERVER_SOFTWARE
  15. import gunicorn.util as util
  16. try:
  17. # Python 3.3 has os.sendfile().
  18. from os import sendfile
  19. except ImportError:
  20. try:
  21. from ._sendfile import sendfile
  22. except ImportError:
  23. sendfile = None
  24. # Send files in at most 1GB blocks as some operating systems can have problems
  25. # with sending files in blocks over 2GB.
  26. BLKSIZE = 0x3FFFFFFF
  27. NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+')
  28. HEADER_VALUE_RE = re.compile(r'[\x00-\x1F\x7F]')
  29. log = logging.getLogger(__name__)
  30. class FileWrapper(object):
  31. def __init__(self, filelike, blksize=8192):
  32. self.filelike = filelike
  33. self.blksize = blksize
  34. if hasattr(filelike, 'close'):
  35. self.close = filelike.close
  36. def __getitem__(self, key):
  37. data = self.filelike.read(self.blksize)
  38. if data:
  39. return data
  40. raise IndexError
  41. class WSGIErrorsWrapper(io.RawIOBase):
  42. def __init__(self, cfg):
  43. errorlog = logging.getLogger("gunicorn.error")
  44. handlers = errorlog.handlers
  45. self.streams = []
  46. if cfg.errorlog == "-":
  47. self.streams.append(sys.stderr)
  48. handlers = handlers[1:]
  49. for h in handlers:
  50. if hasattr(h, "stream"):
  51. self.streams.append(h.stream)
  52. def write(self, data):
  53. for stream in self.streams:
  54. try:
  55. stream.write(data)
  56. except UnicodeError:
  57. stream.write(data.encode("UTF-8"))
  58. stream.flush()
  59. def base_environ(cfg):
  60. return {
  61. "wsgi.errors": WSGIErrorsWrapper(cfg),
  62. "wsgi.version": (1, 0),
  63. "wsgi.multithread": False,
  64. "wsgi.multiprocess": (cfg.workers > 1),
  65. "wsgi.run_once": False,
  66. "wsgi.file_wrapper": FileWrapper,
  67. "SERVER_SOFTWARE": SERVER_SOFTWARE,
  68. }
  69. def default_environ(req, sock, cfg):
  70. env = base_environ(cfg)
  71. env.update({
  72. "wsgi.input": req.body,
  73. "gunicorn.socket": sock,
  74. "REQUEST_METHOD": req.method,
  75. "QUERY_STRING": req.query,
  76. "RAW_URI": req.uri,
  77. "SERVER_PROTOCOL": "HTTP/%s" % ".".join([str(v) for v in req.version])
  78. })
  79. return env
  80. def proxy_environ(req):
  81. info = req.proxy_protocol_info
  82. if not info:
  83. return {}
  84. return {
  85. "PROXY_PROTOCOL": info["proxy_protocol"],
  86. "REMOTE_ADDR": info["client_addr"],
  87. "REMOTE_PORT": str(info["client_port"]),
  88. "PROXY_ADDR": info["proxy_addr"],
  89. "PROXY_PORT": str(info["proxy_port"]),
  90. }
  91. def create(req, sock, client, server, cfg):
  92. resp = Response(req, sock, cfg)
  93. # set initial environ
  94. environ = default_environ(req, sock, cfg)
  95. # default variables
  96. host = None
  97. url_scheme = "https" if cfg.is_ssl else "http"
  98. script_name = os.environ.get("SCRIPT_NAME", "")
  99. # set secure_headers
  100. secure_headers = cfg.secure_scheme_headers
  101. if client and not isinstance(client, string_types):
  102. if ('*' not in cfg.forwarded_allow_ips
  103. and client[0] not in cfg.forwarded_allow_ips):
  104. secure_headers = {}
  105. # add the headers to the environ
  106. for hdr_name, hdr_value in req.headers:
  107. if hdr_name == "EXPECT":
  108. # handle expect
  109. if hdr_value.lower() == "100-continue":
  110. sock.send(b"HTTP/1.1 100 Continue\r\n\r\n")
  111. elif secure_headers and (hdr_name in secure_headers and
  112. hdr_value == secure_headers[hdr_name]):
  113. url_scheme = "https"
  114. elif hdr_name == 'HOST':
  115. host = hdr_value
  116. elif hdr_name == "SCRIPT_NAME":
  117. script_name = hdr_value
  118. elif hdr_name == "CONTENT-TYPE":
  119. environ['CONTENT_TYPE'] = hdr_value
  120. continue
  121. elif hdr_name == "CONTENT-LENGTH":
  122. environ['CONTENT_LENGTH'] = hdr_value
  123. continue
  124. key = 'HTTP_' + hdr_name.replace('-', '_')
  125. if key in environ:
  126. hdr_value = "%s,%s" % (environ[key], hdr_value)
  127. environ[key] = hdr_value
  128. # set the url scheme
  129. environ['wsgi.url_scheme'] = url_scheme
  130. # set the REMOTE_* keys in environ
  131. # authors should be aware that REMOTE_HOST and REMOTE_ADDR
  132. # may not qualify the remote addr:
  133. # http://www.ietf.org/rfc/rfc3875
  134. if isinstance(client, string_types):
  135. environ['REMOTE_ADDR'] = client
  136. elif isinstance(client, binary_type):
  137. environ['REMOTE_ADDR'] = str(client)
  138. else:
  139. environ['REMOTE_ADDR'] = client[0]
  140. environ['REMOTE_PORT'] = str(client[1])
  141. # handle the SERVER_*
  142. # Normally only the application should use the Host header but since the
  143. # WSGI spec doesn't support unix sockets, we are using it to create
  144. # viable SERVER_* if possible.
  145. if isinstance(server, string_types):
  146. server = server.split(":")
  147. if len(server) == 1:
  148. # unix socket
  149. if host and host is not None:
  150. server = host.split(':')
  151. if len(server) == 1:
  152. if url_scheme == "http":
  153. server.append(80),
  154. elif url_scheme == "https":
  155. server.append(443)
  156. else:
  157. server.append('')
  158. else:
  159. # no host header given which means that we are not behind a
  160. # proxy, so append an empty port.
  161. server.append('')
  162. environ['SERVER_NAME'] = server[0]
  163. environ['SERVER_PORT'] = str(server[1])
  164. # set the path and script name
  165. path_info = req.path
  166. if script_name:
  167. path_info = path_info.split(script_name, 1)[1]
  168. environ['PATH_INFO'] = unquote_to_wsgi_str(path_info)
  169. environ['SCRIPT_NAME'] = script_name
  170. # override the environ with the correct remote and server address if
  171. # we are behind a proxy using the proxy protocol.
  172. environ.update(proxy_environ(req))
  173. return resp, environ
  174. class Response(object):
  175. def __init__(self, req, sock, cfg):
  176. self.req = req
  177. self.sock = sock
  178. self.version = SERVER_SOFTWARE
  179. self.status = None
  180. self.chunked = False
  181. self.must_close = False
  182. self.headers = []
  183. self.headers_sent = False
  184. self.response_length = None
  185. self.sent = 0
  186. self.upgrade = False
  187. self.cfg = cfg
  188. def force_close(self):
  189. self.must_close = True
  190. def should_close(self):
  191. if self.must_close or self.req.should_close():
  192. return True
  193. if self.response_length is not None or self.chunked:
  194. return False
  195. if self.req.method == 'HEAD':
  196. return False
  197. if self.status_code < 200 or self.status_code in (204, 304):
  198. return False
  199. return True
  200. def start_response(self, status, headers, exc_info=None):
  201. if exc_info:
  202. try:
  203. if self.status and self.headers_sent:
  204. reraise(exc_info[0], exc_info[1], exc_info[2])
  205. finally:
  206. exc_info = None
  207. elif self.status is not None:
  208. raise AssertionError("Response headers already set!")
  209. self.status = status
  210. # get the status code from the response here so we can use it to check
  211. # the need for the connection header later without parsing the string
  212. # each time.
  213. try:
  214. self.status_code = int(self.status.split()[0])
  215. except ValueError:
  216. self.status_code = None
  217. self.process_headers(headers)
  218. self.chunked = self.is_chunked()
  219. return self.write
  220. def process_headers(self, headers):
  221. for name, value in headers:
  222. if not isinstance(name, string_types):
  223. raise TypeError('%r is not a string' % name)
  224. if HEADER_RE.search(name):
  225. raise InvalidHeaderName('%r' % name)
  226. if HEADER_VALUE_RE.search(value):
  227. raise InvalidHeader('%r' % value)
  228. value = str(value).strip()
  229. lname = name.lower().strip()
  230. if lname == "content-length":
  231. self.response_length = int(value)
  232. elif util.is_hoppish(name):
  233. if lname == "connection":
  234. # handle websocket
  235. if value.lower().strip() == "upgrade":
  236. self.upgrade = True
  237. elif lname == "upgrade":
  238. if value.lower().strip() == "websocket":
  239. self.headers.append((name.strip(), value))
  240. # ignore hopbyhop headers
  241. continue
  242. self.headers.append((name.strip(), value))
  243. def is_chunked(self):
  244. # Only use chunked responses when the client is
  245. # speaking HTTP/1.1 or newer and there was
  246. # no Content-Length header set.
  247. if self.response_length is not None:
  248. return False
  249. elif self.req.version <= (1, 0):
  250. return False
  251. elif self.req.method == 'HEAD':
  252. # Responses to a HEAD request MUST NOT contain a response body.
  253. return False
  254. elif self.status_code in (204, 304):
  255. # Do not use chunked responses when the response is guaranteed to
  256. # not have a response body.
  257. return False
  258. return True
  259. def default_headers(self):
  260. # set the connection header
  261. if self.upgrade:
  262. connection = "upgrade"
  263. elif self.should_close():
  264. connection = "close"
  265. else:
  266. connection = "keep-alive"
  267. headers = [
  268. "HTTP/%s.%s %s\r\n" % (self.req.version[0],
  269. self.req.version[1], self.status),
  270. "Server: %s\r\n" % self.version,
  271. "Date: %s\r\n" % util.http_date(),
  272. "Connection: %s\r\n" % connection
  273. ]
  274. if self.chunked:
  275. headers.append("Transfer-Encoding: chunked\r\n")
  276. return headers
  277. def send_headers(self):
  278. if self.headers_sent:
  279. return
  280. tosend = self.default_headers()
  281. tosend.extend(["%s: %s\r\n" % (k, v) for k, v in self.headers])
  282. header_str = "%s\r\n" % "".join(tosend)
  283. util.write(self.sock, util.to_bytestring(header_str, "ascii"))
  284. self.headers_sent = True
  285. def write(self, arg):
  286. self.send_headers()
  287. if not isinstance(arg, binary_type):
  288. raise TypeError('%r is not a byte' % arg)
  289. arglen = len(arg)
  290. tosend = arglen
  291. if self.response_length is not None:
  292. if self.sent >= self.response_length:
  293. # Never write more than self.response_length bytes
  294. return
  295. tosend = min(self.response_length - self.sent, tosend)
  296. if tosend < arglen:
  297. arg = arg[:tosend]
  298. # Sending an empty chunk signals the end of the
  299. # response and prematurely closes the response
  300. if self.chunked and tosend == 0:
  301. return
  302. self.sent += tosend
  303. util.write(self.sock, arg, self.chunked)
  304. def can_sendfile(self):
  305. return self.cfg.sendfile is not False and sendfile is not None
  306. def sendfile(self, respiter):
  307. if self.cfg.is_ssl or not self.can_sendfile():
  308. return False
  309. if not util.has_fileno(respiter.filelike):
  310. return False
  311. fileno = respiter.filelike.fileno()
  312. try:
  313. offset = os.lseek(fileno, 0, os.SEEK_CUR)
  314. if self.response_length is None:
  315. filesize = os.fstat(fileno).st_size
  316. # The file may be special and sendfile will fail.
  317. # It may also be zero-length, but that is okay.
  318. if filesize == 0:
  319. return False
  320. nbytes = filesize - offset
  321. else:
  322. nbytes = self.response_length
  323. except (OSError, io.UnsupportedOperation):
  324. return False
  325. self.send_headers()
  326. if self.is_chunked():
  327. chunk_size = "%X\r\n" % nbytes
  328. self.sock.sendall(chunk_size.encode('utf-8'))
  329. sockno = self.sock.fileno()
  330. sent = 0
  331. while sent != nbytes:
  332. count = min(nbytes - sent, BLKSIZE)
  333. sent += sendfile(sockno, fileno, offset + sent, count)
  334. if self.is_chunked():
  335. self.sock.sendall(b"\r\n")
  336. os.lseek(fileno, offset, os.SEEK_SET)
  337. return True
  338. def write_file(self, respiter):
  339. if not self.sendfile(respiter):
  340. for item in respiter:
  341. self.write(item)
  342. def close(self):
  343. if not self.headers_sent:
  344. self.send_headers()
  345. if self.chunked:
  346. util.write_chunk(self.sock, b"")