utilities.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. ##############################################################################
  2. #
  3. # Copyright (c) 2004 Zope Foundation and Contributors.
  4. # All Rights Reserved.
  5. #
  6. # This software is subject to the provisions of the Zope Public License,
  7. # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
  8. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
  9. # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  10. # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
  11. # FOR A PARTICULAR PURPOSE.
  12. #
  13. ##############################################################################
  14. """Utility functions
  15. """
  16. import calendar
  17. import errno
  18. import logging
  19. import os
  20. import re
  21. import stat
  22. import time
  23. from .rfc7230 import QUOTED_PAIR_RE, QUOTED_STRING_RE
  24. logger = logging.getLogger("waitress")
  25. queue_logger = logging.getLogger("waitress.queue")
  26. def find_double_newline(s):
  27. """Returns the position just after a double newline in the given string."""
  28. pos = s.find(b"\r\n\r\n")
  29. if pos >= 0:
  30. pos += 4
  31. return pos
  32. def concat(*args):
  33. return "".join(args)
  34. def join(seq, field=" "):
  35. return field.join(seq)
  36. def group(s):
  37. return "(" + s + ")"
  38. short_days = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"]
  39. long_days = [
  40. "sunday",
  41. "monday",
  42. "tuesday",
  43. "wednesday",
  44. "thursday",
  45. "friday",
  46. "saturday",
  47. ]
  48. short_day_reg = group(join(short_days, "|"))
  49. long_day_reg = group(join(long_days, "|"))
  50. daymap = {}
  51. for i in range(7):
  52. daymap[short_days[i]] = i
  53. daymap[long_days[i]] = i
  54. hms_reg = join(3 * [group("[0-9][0-9]")], ":")
  55. months = [
  56. "jan",
  57. "feb",
  58. "mar",
  59. "apr",
  60. "may",
  61. "jun",
  62. "jul",
  63. "aug",
  64. "sep",
  65. "oct",
  66. "nov",
  67. "dec",
  68. ]
  69. monmap = {}
  70. for i in range(12):
  71. monmap[months[i]] = i + 1
  72. months_reg = group(join(months, "|"))
  73. # From draft-ietf-http-v11-spec-07.txt/3.3.1
  74. # Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123
  75. # Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
  76. # Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
  77. # rfc822 format
  78. rfc822_date = join(
  79. [
  80. concat(short_day_reg, ","), # day
  81. group("[0-9][0-9]?"), # date
  82. months_reg, # month
  83. group("[0-9]+"), # year
  84. hms_reg, # hour minute second
  85. "gmt",
  86. ],
  87. " ",
  88. )
  89. rfc822_reg = re.compile(rfc822_date)
  90. def unpack_rfc822(m):
  91. g = m.group
  92. return (
  93. int(g(4)), # year
  94. monmap[g(3)], # month
  95. int(g(2)), # day
  96. int(g(5)), # hour
  97. int(g(6)), # minute
  98. int(g(7)), # second
  99. 0,
  100. 0,
  101. 0,
  102. )
  103. # rfc850 format
  104. rfc850_date = join(
  105. [
  106. concat(long_day_reg, ","),
  107. join([group("[0-9][0-9]?"), months_reg, group("[0-9]+")], "-"),
  108. hms_reg,
  109. "gmt",
  110. ],
  111. " ",
  112. )
  113. rfc850_reg = re.compile(rfc850_date)
  114. # they actually unpack the same way
  115. def unpack_rfc850(m):
  116. g = m.group
  117. yr = g(4)
  118. if len(yr) == 2:
  119. yr = "19" + yr
  120. return (
  121. int(yr), # year
  122. monmap[g(3)], # month
  123. int(g(2)), # day
  124. int(g(5)), # hour
  125. int(g(6)), # minute
  126. int(g(7)), # second
  127. 0,
  128. 0,
  129. 0,
  130. )
  131. # parsdate.parsedate - ~700/sec.
  132. # parse_http_date - ~1333/sec.
  133. weekdayname = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
  134. monthname = [
  135. None,
  136. "Jan",
  137. "Feb",
  138. "Mar",
  139. "Apr",
  140. "May",
  141. "Jun",
  142. "Jul",
  143. "Aug",
  144. "Sep",
  145. "Oct",
  146. "Nov",
  147. "Dec",
  148. ]
  149. def build_http_date(when):
  150. year, month, day, hh, mm, ss, wd, y, z = time.gmtime(when)
  151. return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
  152. weekdayname[wd],
  153. day,
  154. monthname[month],
  155. year,
  156. hh,
  157. mm,
  158. ss,
  159. )
  160. def parse_http_date(d):
  161. d = d.lower()
  162. m = rfc850_reg.match(d)
  163. if m and m.end() == len(d):
  164. retval = int(calendar.timegm(unpack_rfc850(m)))
  165. else:
  166. m = rfc822_reg.match(d)
  167. if m and m.end() == len(d):
  168. retval = int(calendar.timegm(unpack_rfc822(m)))
  169. else:
  170. return 0
  171. return retval
  172. def undquote(value):
  173. if value.startswith('"') and value.endswith('"'):
  174. # So it claims to be DQUOTE'ed, let's validate that
  175. matches = QUOTED_STRING_RE.match(value)
  176. if matches and matches.end() == len(value):
  177. # Remove the DQUOTE's from the value
  178. value = value[1:-1]
  179. # Remove all backslashes that are followed by a valid vchar or
  180. # obs-text
  181. value = QUOTED_PAIR_RE.sub(r"\1", value)
  182. return value
  183. elif not value.startswith('"') and not value.endswith('"'):
  184. return value
  185. raise ValueError("Invalid quoting in value")
  186. def cleanup_unix_socket(path):
  187. try:
  188. st = os.stat(path)
  189. except OSError as exc:
  190. if exc.errno != errno.ENOENT:
  191. raise # pragma: no cover
  192. else:
  193. if stat.S_ISSOCK(st.st_mode):
  194. try:
  195. os.remove(path)
  196. except OSError: # pragma: no cover
  197. # avoid race condition error during tests
  198. pass
  199. class Error:
  200. code = 500
  201. reason = "Internal Server Error"
  202. def __init__(self, body):
  203. self.body = body
  204. def to_response(self):
  205. status = f"{self.code} {self.reason}"
  206. body = f"{self.reason}\r\n\r\n{self.body}"
  207. tag = "\r\n\r\n(generated by waitress)"
  208. body = (body + tag).encode("utf-8")
  209. headers = [("Content-Type", "text/plain; charset=utf-8")]
  210. return status, headers, body
  211. def wsgi_response(self, environ, start_response):
  212. status, headers, body = self.to_response()
  213. start_response(status, headers)
  214. yield body
  215. class BadRequest(Error):
  216. code = 400
  217. reason = "Bad Request"
  218. class RequestHeaderFieldsTooLarge(BadRequest):
  219. code = 431
  220. reason = "Request Header Fields Too Large"
  221. class RequestEntityTooLarge(BadRequest):
  222. code = 413
  223. reason = "Request Entity Too Large"
  224. class InternalServerError(Error):
  225. code = 500
  226. reason = "Internal Server Error"
  227. class ServerNotImplemented(Error):
  228. code = 501
  229. reason = "Not Implemented"