fcgi_app.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. # Copyright (c) 2006 Allan Saddi <allan@saddi.com>
  2. # All rights reserved.
  3. #
  4. # Redistribution and use in source and binary forms, with or without
  5. # modification, are permitted provided that the following conditions
  6. # are met:
  7. # 1. Redistributions of source code must retain the above copyright
  8. # notice, this list of conditions and the following disclaimer.
  9. # 2. Redistributions in binary form must reproduce the above copyright
  10. # notice, this list of conditions and the following disclaimer in the
  11. # documentation and/or other materials provided with the distribution.
  12. #
  13. # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
  14. # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  15. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  16. # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
  17. # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  18. # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
  19. # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  20. # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  21. # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  22. # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  23. # SUCH DAMAGE.
  24. #
  25. # $Id$
  26. __author__ = 'Allan Saddi <allan@saddi.com>'
  27. __version__ = '$Revision$'
  28. import select
  29. import struct
  30. import socket
  31. import errno
  32. __all__ = ['FCGIApp']
  33. # Constants from the spec.
  34. FCGI_LISTENSOCK_FILENO = 0
  35. FCGI_HEADER_LEN = 8
  36. FCGI_VERSION_1 = 1
  37. FCGI_BEGIN_REQUEST = 1
  38. FCGI_ABORT_REQUEST = 2
  39. FCGI_END_REQUEST = 3
  40. FCGI_PARAMS = 4
  41. FCGI_STDIN = 5
  42. FCGI_STDOUT = 6
  43. FCGI_STDERR = 7
  44. FCGI_DATA = 8
  45. FCGI_GET_VALUES = 9
  46. FCGI_GET_VALUES_RESULT = 10
  47. FCGI_UNKNOWN_TYPE = 11
  48. FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE
  49. FCGI_NULL_REQUEST_ID = 0
  50. FCGI_KEEP_CONN = 1
  51. FCGI_RESPONDER = 1
  52. FCGI_AUTHORIZER = 2
  53. FCGI_FILTER = 3
  54. FCGI_REQUEST_COMPLETE = 0
  55. FCGI_CANT_MPX_CONN = 1
  56. FCGI_OVERLOADED = 2
  57. FCGI_UNKNOWN_ROLE = 3
  58. FCGI_MAX_CONNS = 'FCGI_MAX_CONNS'
  59. FCGI_MAX_REQS = 'FCGI_MAX_REQS'
  60. FCGI_MPXS_CONNS = 'FCGI_MPXS_CONNS'
  61. FCGI_Header = '!BBHHBx'
  62. FCGI_BeginRequestBody = '!HB5x'
  63. FCGI_EndRequestBody = '!LB3x'
  64. FCGI_UnknownTypeBody = '!B7x'
  65. FCGI_BeginRequestBody_LEN = struct.calcsize(FCGI_BeginRequestBody)
  66. FCGI_EndRequestBody_LEN = struct.calcsize(FCGI_EndRequestBody)
  67. FCGI_UnknownTypeBody_LEN = struct.calcsize(FCGI_UnknownTypeBody)
  68. if __debug__:
  69. import time
  70. # Set non-zero to write debug output to a file.
  71. DEBUG = 0
  72. DEBUGLOG = '/tmp/fcgi_app.log'
  73. def _debug(level, msg):
  74. if DEBUG < level:
  75. return
  76. try:
  77. f = open(DEBUGLOG, 'a')
  78. f.write('%sfcgi: %s\n' % (time.ctime()[4:-4], msg))
  79. f.close()
  80. except:
  81. pass
  82. def decode_pair(s, pos=0):
  83. """
  84. Decodes a name/value pair.
  85. The number of bytes decoded as well as the name/value pair
  86. are returned.
  87. """
  88. nameLength = ord(s[pos])
  89. if nameLength & 128:
  90. nameLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff
  91. pos += 4
  92. else:
  93. pos += 1
  94. valueLength = ord(s[pos])
  95. if valueLength & 128:
  96. valueLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff
  97. pos += 4
  98. else:
  99. pos += 1
  100. name = s[pos:pos+nameLength]
  101. pos += nameLength
  102. value = s[pos:pos+valueLength]
  103. pos += valueLength
  104. return (pos, (name, value))
  105. def encode_pair(name, value):
  106. """
  107. Encodes a name/value pair.
  108. The encoded string is returned.
  109. """
  110. nameLength = len(name)
  111. if nameLength < 128:
  112. s = chr(nameLength)
  113. else:
  114. s = struct.pack('!L', nameLength | 0x80000000L)
  115. valueLength = len(value)
  116. if valueLength < 128:
  117. s += chr(valueLength)
  118. else:
  119. s += struct.pack('!L', valueLength | 0x80000000L)
  120. return s + name + value
  121. class Record(object):
  122. """
  123. A FastCGI Record.
  124. Used for encoding/decoding records.
  125. """
  126. def __init__(self, type=FCGI_UNKNOWN_TYPE, requestId=FCGI_NULL_REQUEST_ID):
  127. self.version = FCGI_VERSION_1
  128. self.type = type
  129. self.requestId = requestId
  130. self.contentLength = 0
  131. self.paddingLength = 0
  132. self.contentData = ''
  133. def _recvall(sock, length):
  134. """
  135. Attempts to receive length bytes from a socket, blocking if necessary.
  136. (Socket may be blocking or non-blocking.)
  137. """
  138. dataList = []
  139. recvLen = 0
  140. while length:
  141. try:
  142. data = sock.recv(length)
  143. except socket.error, e:
  144. if e[0] == errno.EAGAIN:
  145. select.select([sock], [], [])
  146. continue
  147. else:
  148. raise
  149. if not data: # EOF
  150. break
  151. dataList.append(data)
  152. dataLen = len(data)
  153. recvLen += dataLen
  154. length -= dataLen
  155. return ''.join(dataList), recvLen
  156. _recvall = staticmethod(_recvall)
  157. def read(self, sock):
  158. """Read and decode a Record from a socket."""
  159. try:
  160. header, length = self._recvall(sock, FCGI_HEADER_LEN)
  161. except:
  162. raise EOFError
  163. if length < FCGI_HEADER_LEN:
  164. raise EOFError
  165. self.version, self.type, self.requestId, self.contentLength, \
  166. self.paddingLength = struct.unpack(FCGI_Header, header)
  167. if __debug__: _debug(9, 'read: fd = %d, type = %d, requestId = %d, '
  168. 'contentLength = %d' %
  169. (sock.fileno(), self.type, self.requestId,
  170. self.contentLength))
  171. if self.contentLength:
  172. try:
  173. self.contentData, length = self._recvall(sock,
  174. self.contentLength)
  175. except:
  176. raise EOFError
  177. if length < self.contentLength:
  178. raise EOFError
  179. if self.paddingLength:
  180. try:
  181. self._recvall(sock, self.paddingLength)
  182. except:
  183. raise EOFError
  184. def _sendall(sock, data):
  185. """
  186. Writes data to a socket and does not return until all the data is sent.
  187. """
  188. length = len(data)
  189. while length:
  190. try:
  191. sent = sock.send(data)
  192. except socket.error, e:
  193. if e[0] == errno.EAGAIN:
  194. select.select([], [sock], [])
  195. continue
  196. else:
  197. raise
  198. data = data[sent:]
  199. length -= sent
  200. _sendall = staticmethod(_sendall)
  201. def write(self, sock):
  202. """Encode and write a Record to a socket."""
  203. self.paddingLength = -self.contentLength & 7
  204. if __debug__: _debug(9, 'write: fd = %d, type = %d, requestId = %d, '
  205. 'contentLength = %d' %
  206. (sock.fileno(), self.type, self.requestId,
  207. self.contentLength))
  208. header = struct.pack(FCGI_Header, self.version, self.type,
  209. self.requestId, self.contentLength,
  210. self.paddingLength)
  211. self._sendall(sock, header)
  212. if self.contentLength:
  213. self._sendall(sock, self.contentData)
  214. if self.paddingLength:
  215. self._sendall(sock, '\x00'*self.paddingLength)
  216. class FCGIApp(object):
  217. def __init__(self, command=None, connect=None, host=None, port=None,
  218. filterEnviron=True):
  219. if host is not None:
  220. assert port is not None
  221. connect=(host, port)
  222. assert (command is not None and connect is None) or \
  223. (command is None and connect is not None)
  224. self._command = command
  225. self._connect = connect
  226. self._filterEnviron = filterEnviron
  227. #sock = self._getConnection()
  228. #print self._fcgiGetValues(sock, ['FCGI_MAX_CONNS', 'FCGI_MAX_REQS', 'FCGI_MPXS_CONNS'])
  229. #sock.close()
  230. def __call__(self, environ, start_response):
  231. # For sanity's sake, we don't care about FCGI_MPXS_CONN
  232. # (connection multiplexing). For every request, we obtain a new
  233. # transport socket, perform the request, then discard the socket.
  234. # This is, I believe, how mod_fastcgi does things...
  235. sock = self._getConnection()
  236. # Since this is going to be the only request on this connection,
  237. # set the request ID to 1.
  238. requestId = 1
  239. # Begin the request
  240. rec = Record(FCGI_BEGIN_REQUEST, requestId)
  241. rec.contentData = struct.pack(FCGI_BeginRequestBody, FCGI_RESPONDER, 0)
  242. rec.contentLength = FCGI_BeginRequestBody_LEN
  243. rec.write(sock)
  244. # Filter WSGI environ and send it as FCGI_PARAMS
  245. if self._filterEnviron:
  246. params = self._defaultFilterEnviron(environ)
  247. else:
  248. params = self._lightFilterEnviron(environ)
  249. # TODO: Anything not from environ that needs to be sent also?
  250. self._fcgiParams(sock, requestId, params)
  251. self._fcgiParams(sock, requestId, {})
  252. # Transfer wsgi.input to FCGI_STDIN
  253. content_length = int(environ.get('CONTENT_LENGTH') or 0)
  254. while True:
  255. chunk_size = min(content_length, 4096)
  256. s = environ['wsgi.input'].read(chunk_size)
  257. content_length -= len(s)
  258. rec = Record(FCGI_STDIN, requestId)
  259. rec.contentData = s
  260. rec.contentLength = len(s)
  261. rec.write(sock)
  262. if not s: break
  263. # Empty FCGI_DATA stream
  264. rec = Record(FCGI_DATA, requestId)
  265. rec.write(sock)
  266. # Main loop. Process FCGI_STDOUT, FCGI_STDERR, FCGI_END_REQUEST
  267. # records from the application.
  268. result = []
  269. while True:
  270. inrec = Record()
  271. inrec.read(sock)
  272. if inrec.type == FCGI_STDOUT:
  273. if inrec.contentData:
  274. result.append(inrec.contentData)
  275. else:
  276. # TODO: Should probably be pedantic and no longer
  277. # accept FCGI_STDOUT records?
  278. pass
  279. elif inrec.type == FCGI_STDERR:
  280. # Simply forward to wsgi.errors
  281. environ['wsgi.errors'].write(inrec.contentData)
  282. elif inrec.type == FCGI_END_REQUEST:
  283. # TODO: Process appStatus/protocolStatus fields?
  284. break
  285. # Done with this transport socket, close it. (FCGI_KEEP_CONN was not
  286. # set in the FCGI_BEGIN_REQUEST record we sent above. So the
  287. # application is expected to do the same.)
  288. sock.close()
  289. result = ''.join(result)
  290. # Parse response headers from FCGI_STDOUT
  291. status = '200 OK'
  292. headers = []
  293. pos = 0
  294. while True:
  295. eolpos = result.find('\n', pos)
  296. if eolpos < 0: break
  297. line = result[pos:eolpos-1]
  298. pos = eolpos + 1
  299. # strip in case of CR. NB: This will also strip other
  300. # whitespace...
  301. line = line.strip()
  302. # Empty line signifies end of headers
  303. if not line: break
  304. # TODO: Better error handling
  305. header, value = line.split(':', 1)
  306. header = header.strip().lower()
  307. value = value.strip()
  308. if header == 'status':
  309. # Special handling of Status header
  310. status = value
  311. if status.find(' ') < 0:
  312. # Append a dummy reason phrase if one was not provided
  313. status += ' FCGIApp'
  314. else:
  315. headers.append((header, value))
  316. result = result[pos:]
  317. # Set WSGI status, headers, and return result.
  318. start_response(status, headers)
  319. return [result]
  320. def _getConnection(self):
  321. if self._connect is not None:
  322. # The simple case. Create a socket and connect to the
  323. # application.
  324. if type(self._connect) is str:
  325. sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  326. else:
  327. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  328. sock.connect(self._connect)
  329. return sock
  330. # To be done when I have more time...
  331. raise NotImplementedError, 'Launching and managing FastCGI programs not yet implemented'
  332. def _fcgiGetValues(self, sock, vars):
  333. # Construct FCGI_GET_VALUES record
  334. outrec = Record(FCGI_GET_VALUES)
  335. data = []
  336. for name in vars:
  337. data.append(encode_pair(name, ''))
  338. data = ''.join(data)
  339. outrec.contentData = data
  340. outrec.contentLength = len(data)
  341. outrec.write(sock)
  342. # Await response
  343. inrec = Record()
  344. inrec.read(sock)
  345. result = {}
  346. if inrec.type == FCGI_GET_VALUES_RESULT:
  347. pos = 0
  348. while pos < inrec.contentLength:
  349. pos, (name, value) = decode_pair(inrec.contentData, pos)
  350. result[name] = value
  351. return result
  352. def _fcgiParams(self, sock, requestId, params):
  353. rec = Record(FCGI_PARAMS, requestId)
  354. data = []
  355. for name,value in params.items():
  356. data.append(encode_pair(name, value))
  357. data = ''.join(data)
  358. rec.contentData = data
  359. rec.contentLength = len(data)
  360. rec.write(sock)
  361. _environPrefixes = ['SERVER_', 'HTTP_', 'REQUEST_', 'REMOTE_', 'PATH_',
  362. 'CONTENT_']
  363. _environCopies = ['SCRIPT_NAME', 'QUERY_STRING', 'AUTH_TYPE']
  364. _environRenames = {}
  365. def _defaultFilterEnviron(self, environ):
  366. result = {}
  367. for n in environ.keys():
  368. for p in self._environPrefixes:
  369. if n.startswith(p):
  370. result[n] = environ[n]
  371. if n in self._environCopies:
  372. result[n] = environ[n]
  373. if n in self._environRenames:
  374. result[self._environRenames[n]] = environ[n]
  375. return result
  376. def _lightFilterEnviron(self, environ):
  377. result = {}
  378. for n in environ.keys():
  379. if n.upper() == n:
  380. result[n] = environ[n]
  381. return result
  382. if __name__ == '__main__':
  383. from flup.server.ajp import WSGIServer
  384. app = FCGIApp(connect=('localhost', 4242))
  385. #import paste.lint
  386. #app = paste.lint.middleware(app)
  387. WSGIServer(app).run()