root / trunk / twisted / web2 / channel / fastcgi.py

Revision 17615, 11.9 kB (checked in by stephen, 3 years ago)

Fix typos in web2.channel.fastcgi that cause tracebacks in some circumstances.

Author: jerub, jeremy
Reviewer: jknight, hagbard
Closes: #1915

Line 
1 """
2    Twisted.web2 FastCGI backend support.
3 """
4
5 """
6 Okay, FastCGI is a pretty stupid protocol.
7 Let me count some reasons:
8
9 1) Specifies ability to multiplex streams of data over a single
10 socket, but has no form of flow control. This is fine for multiplexing
11 stderr, but serving more than one request over a channel with no flow
12 control is just *asking* for trouble. I avoid this and enforce one
13 outstanding request per connection. This basically means the whole
14 "requestId" field is worthless.
15
16 2) Has variable length packet padding. If you want padding, just make
17 it always pad to 8 bytes fercrissake!
18
19 3) Why does every packet need to specify the version. How about just
20 sending it once.
21
22 4) Name/value pair format. Come *on*. Is it *possible* to come up with
23 a more complex format to send them with?? Even if you think you've
24 gotten it down, you probably forgot that it's a stream, and the
25 name/values can be split between two packets. (Yes, this means
26 *you*. Don't even try to pretend you didn't miss this detail.)
27 """
28
29 from twisted.internet import protocol
30 from twisted.web2 import responsecode
31 from twisted.web2.channel import cgi
32
33 class FastCGIError(Exception):
34     pass
35
36 # Values for type component of FCGI_Header
37
38 FCGI_BEGIN_REQUEST       = 1
39 FCGI_ABORT_REQUEST       = 2
40 FCGI_END_REQUEST         = 3
41 FCGI_PARAMS              = 4
42 FCGI_STDIN               = 5
43 FCGI_STDOUT              = 6
44 FCGI_STDERR              = 7
45 FCGI_DATA                = 8
46 FCGI_GET_VALUES          = 9
47 FCGI_GET_VALUES_RESULT   = 10
48 FCGI_UNKNOWN_TYPE        = 11
49
50 typeNames = {
51     FCGI_BEGIN_REQUEST    : 'fcgi_begin_request',
52     FCGI_ABORT_REQUEST    : 'fcgi_abort_request',
53     FCGI_END_REQUEST      : 'fcgi_end_request',
54     FCGI_PARAMS           : 'fcgi_params',
55     FCGI_STDIN            : 'fcgi_stdin',
56     FCGI_STDOUT           : 'fcgi_stdout',
57     FCGI_STDERR           : 'fcgi_stderr',
58     FCGI_DATA             : 'fcgi_data',
59     FCGI_GET_VALUES       : 'fcgi_get_values',
60     FCGI_GET_VALUES_RESULT: 'fcgi_get_values_result',
61     FCGI_UNKNOWN_TYPE     : 'fcgi_unknown_type'}
62
63 # Mask for flags component of FCGI_BeginRequestBody
64 FCGI_KEEP_CONN = 1
65
66 # Values for role component of FCGI_BeginRequestBody
67 FCGI_RESPONDER  = 1
68 FCGI_AUTHORIZER = 2
69 FCGI_FILTER     = 3
70
71 # Values for protocolStatus component of FCGI_EndRequestBody
72
73 FCGI_REQUEST_COMPLETE = 0
74 FCGI_CANT_MPX_CONN    = 1
75 FCGI_OVERLOADED       = 2
76 FCGI_UNKNOWN_ROLE     = 3
77
78 FCGI_MAX_PACKET_LEN = 0xFFFF
79
80 class Record(object):
81     def __init__(self, type, reqId, content, version=1):
82         self.version = version
83         self.type = type
84         self.reqId = reqId
85         self.content = content
86         self.length = len(content)
87         if self.length > FCGI_MAX_PACKET_LEN:
88             raise ValueError("Record length too long: %d > %d" %
89                              (self.length, FCGI_MAX_PACKET_LEN))
90         self.padding = 8 - (self.length & 7)
91         self.reserved = 0
92        
93     def fromHeaderString(clz, rec):
94         self = object.__new__(clz)
95         self.version = ord(rec[0])
96         self.type = ord(rec[1])
97         self.reqId = (ord(rec[2])<<8)|ord(rec[3])
98         self.length = (ord(rec[4])<<8)|ord(rec[5])
99         self.padding = ord(rec[6])
100         self.reserved = ord(rec[7])
101         self.content = None
102         return self
103    
104     fromHeaderString = classmethod(fromHeaderString)
105
106     def toOutputString(self):
107         return "%c%c%c%c%c%c%c%c" % (
108             self.version, self.type,
109             (self.reqId&0xFF00)>>8, self.reqId&0xFF,
110             (self.length&0xFF00)>>8, self.length & 0xFF,
111             self.padding, self.reserved) + self.content + '\0'*self.padding
112        
113     def totalLength(self):
114         return 8 + self.length + self.padding
115
116     def __repr__(self):
117         return "<FastCGIRecord version=%d type=%d(%s) reqId=%d content=%r>" % (
118             self.version, self.type, typeNames.get(self.type), self.reqId, self.content)
119    
120 def parseNameValues(s):
121     '''
122     @param s: String containing valid name/value data, of the form:
123               'namelength + valuelength + name + value' repeated 0 or more
124               times. See C{fastcgi.writeNameValue} for how to create this
125               string.
126     @return: Generator of tuples of the form (name, value)
127     '''
128     off = 0
129     while off < len(s):
130         nameLen = ord(s[off])
131         off += 1
132         if nameLen&0x80:
133             nameLen=(nameLen&0x7F)<<24 | ord(s[off])<<16 | ord(s[off+1])<<8 | ord(s[off+2])
134             off += 3
135         valueLen=ord(s[off])
136         off += 1
137         if valueLen&0x80:
138             valueLen=(valueLen&0x7F)<<24 | ord(s[off])<<16 | ord(s[off+1])<<8 | ord(s[off+2])
139             off += 3
140         yield (s[off:off+nameLen], s[off+nameLen:off+nameLen+valueLen])
141         off += nameLen + valueLen
142
143 def getLenBytes(length):
144     if length<0x80:
145         return chr(length)
146     elif 0 < length <= 0x7FFFFFFF:
147         return (chr(0x80|(length>>24)&0x7F) + chr((length>>16)&0xFF) +
148                 chr((length>>8)&0xFF) + chr(length&0xFF))
149     else:
150         raise ValueError("Name length too long.")
151
152 def writeNameValue(name, value):
153     return getLenBytes(len(name)) + getLenBytes(len(value)) + name + value
154
155 class FastCGIChannelRequest(cgi.BaseCGIChannelRequest):
156     maxConnections = 100
157     reqId = 0
158     request = None
159    
160     ## High level protocol
161     def packetReceived(self, packet):
162         '''
163         @param packet: instance of C{fastcgi.Record}.
164         @raise: FastCGIError on invalid version or where the type does not exist
165                 in funName
166         '''
167         if packet.version != 1:
168             raise FastCGIError("FastCGI packet received with version != 1")
169        
170         funName = typeNames.get(packet.type)
171         if funName is None:
172             raise FastCGIError("Unknown FastCGI packet type: %d" % packet.type)
173         getattr(self, funName)(packet)
174
175     def fcgi_get_values(self, packet):
176         if packet.reqId != 0:
177             raise ValueError("Should be 0!")
178        
179         content = ""
180         for name,value in parseNameValues(packet.content):
181             outval = None
182             if name == "FCGI_MAX_CONNS":
183                 outval = str(self.maxConnections)
184             elif name == "FCGI_MAX_REQS":
185                 outval = str(self.maxConnections)
186             elif name == "FCGI_MPXS_CONNS":
187                 outval = "0"
188             if outval:
189                 content += writeNameValue(name, outval)
190         self.writePacket(Record(FCGI_GET_VALUES_RESULT, 0, content))
191    
192     def fcgi_unknown_type(self, packet):
193         # Unused, reserved for future expansion
194         pass
195
196     def fcgi_begin_request(self, packet):
197         role = ord(packet.content[0])<<8 | ord(packet.content[1])
198         flags = ord(packet.content[2])
199         if packet.reqId == 0:
200             raise ValueError("ReqId shouldn't be 0!")
201         if self.reqId != 0:
202             self.writePacket(Record(FCGI_END_REQUEST, self.reqId,
203                                     "\0\0\0\0"+chr(FCGI_CANT_MPX_CONN)+"\0\0\0"))
204         if role != FCGI_RESPONDER:
205             self.writePacket(Record(FCGI_END_REQUEST, self.reqId,
206                                     "\0\0\0\0"+chr(FCGI_UNKNOWN_ROLE)+"\0\0\0"))
207        
208         self.reqId = packet.reqId
209         self.keepalive = flags & FCGI_KEEP_CONN
210         self.params = ""
211        
212     def fcgi_abort_request(self, packet):
213         if packet.reqId != self.reqId:
214             return
215
216         self.request.connectionLost()
217        
218     def fcgi_params(self, packet):
219         if packet.reqId != self.reqId:
220             return
221        
222         # I don't feel like doing the work to incrementally parse this stupid
223         # protocol, so we'll just buffer all the params data before parsing.
224         if not packet.content:
225             self.makeRequest(dict(parseNameValues(self.params)))
226             self.request.process()
227         self.params += packet.content
228        
229     def fcgi_stdin(self, packet):
230         if packet.reqId != self.reqId:
231             return
232        
233         if not packet.content:
234             self.request.handleContentComplete()
235         else:
236             self.request.handleContentChunk(packet.content)
237        
238     def fcgi_data(self, packet):
239         # For filter roles only, which is currently unsupported.
240         pass
241
242     def write(self, data):
243         if len(data) > FCGI_MAX_PACKET_LEN:
244             n = 0
245             while 1:
246                 d = data[n*FCGI_MAX_PACKET_LEN:(n+1)*FCGI_MAX_PACKET_LEN]
247                 if not d:
248                     break
249                 self.write(d)
250             return
251        
252         self.writePacket(Record(FCGI_STDOUT, self.reqId, data))
253        
254     def writeHeaders(self, code, headers):
255         l = []
256         code_message = responsecode.RESPONSES.get(code, "Unknown Status")
257        
258         l.append("Status: %s %s\n" % (code, code_message))
259         if headers is not None:
260             for name, valuelist in headers.getAllRawHeaders():
261                 for value in valuelist:
262                     l.append("%s: %s\n" % (name, value))
263         l.append('\n')
264         self.write(''.join(l))
265
266     def finish(self):
267         if self.request is None:
268             raise RuntimeError("Request.finish called when no request was outstanding.")
269         self.writePacket(Record(FCGI_END_REQUEST, self.reqId,
270                                 "\0\0\0\0"+chr(FCGI_REQUEST_COMPLETE)+"\0\0\0"))
271         del self.reqId, self.request
272         if not self.keepalive:
273             self.transport.loseConnection()
274        
275 ## Low level protocol
276     paused = False
277     _lastRecord = None
278     recvd = ""
279    
280     def writePacket(self, packet):
281         #print "Writing record", packet
282         self.transport.write(packet.toOutputString())
283        
284     def dataReceived(self, recd):
285         self.recvd = self.recvd + recd
286         record = self._lastRecord
287         self._lastRecord = None
288         while len(self.recvd) >= 8 and not self.paused:
289             if record is None:
290                 record = Record.fromHeaderString(self.recvd[:8])
291             if len(self.recvd) < record.totalLength():
292                 self._lastRecord = record
293                 break
294             record.content = self.recvd[8:record.length+8]
295             self.recvd = self.recvd[record.totalLength():]
296             self.packetReceived(record)
297             record = None
298
299     def pauseProducing(self):
300         self.paused = True
301         self.transport.pauseProducing()
302
303     def resumeProducing(self):
304         self.paused = False
305         self.transport.resumeProducing()
306         self.dataReceived('')
307
308     def stopProducing(self):
309         self.paused = True
310         self.transport.stopProducing()
311
312 class FastCGIFactory(protocol.ServerFactory):
313     protocol = FastCGIChannelRequest
314     def __init__(self, requestFactory):
315         self.requestFactory=requestFactory
316
317     def buildProtocol(self, addr):
318         p = protocol.ServerFactory.buildProtocol(self, addr)
319         p.requestFactory=self.requestFactory
320         return p
321
322
323 # import socket
324 # import fcntl
325 # from twisted.web2 import tcp
326
327 # class ExistingFDTCPPort(tcp.Port):
328 #     def __init__(self, socknum, factory):
329 #         tcp.Port.__init__(self, 0, factory)
330
331 #         # Part of base.createInternetSocket
332 #         skt = socket.fromfd(socknum, self.addressFamily, self.socketType)
333 #         skt.setblocking(0)
334 #         if fcntl and hasattr(fcntl, 'FD_CLOEXEC'):
335 #             old = fcntl.fcntl(skt.fileno(), fcntl.F_GETFD)
336 #             fcntl.fcntl(skt.fileno(), fcntl.F_SETFD, old | fcntl.FD_CLOEXEC)
337
338 #         # Part of tcp.startListening
339 #         self._realPortNumber = skt.getsockname()[1]
340 #         log.msg("%s starting on %s" % (self.factory.__class__, self._realPortNumber))
341         
342 #         # The order of the next 6 lines is kind of bizarre.  If no one
343 #         # can explain it, perhaps we should re-arrange them.
344 #         self.factory.doStart()
345 #         skt.listen(self.backlog)
346 #         self.connected = 1
347 #         self.socket = skt
348 #         self.fileno = self.socket.fileno
349 #         self.numberAccepts = 100
350
351 #         self.startReading()
352         
353
354 #     def startListening(self):
355 #         raise NotImplementedError("Cannot startListening on an ExistingFDTCPPort")
356
Note: See TracBrowser for help on using the browser.