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

Revision 17615, 11.9 KB (checked in by stephen, 4 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"""
6Okay, FastCGI is a pretty stupid protocol.
7Let me count some reasons:
8
91) Specifies ability to multiplex streams of data over a single
10socket, but has no form of flow control. This is fine for multiplexing
11stderr, but serving more than one request over a channel with no flow
12control is just *asking* for trouble. I avoid this and enforce one
13outstanding request per connection. This basically means the whole
14"requestId" field is worthless.
15
162) Has variable length packet padding. If you want padding, just make
17it always pad to 8 bytes fercrissake!
18
193) Why does every packet need to specify the version. How about just
20sending it once.
21
224) Name/value pair format. Come *on*. Is it *possible* to come up with
23a more complex format to send them with?? Even if you think you've
24gotten it down, you probably forgot that it's a stream, and the
25name/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
29from twisted.internet import protocol
30from twisted.web2 import responsecode
31from twisted.web2.channel import cgi
32
33class FastCGIError(Exception):
34    pass
35
36# Values for type component of FCGI_Header
37
38FCGI_BEGIN_REQUEST       = 1
39FCGI_ABORT_REQUEST       = 2
40FCGI_END_REQUEST         = 3
41FCGI_PARAMS              = 4
42FCGI_STDIN               = 5
43FCGI_STDOUT              = 6
44FCGI_STDERR              = 7
45FCGI_DATA                = 8
46FCGI_GET_VALUES          = 9
47FCGI_GET_VALUES_RESULT   = 10
48FCGI_UNKNOWN_TYPE        = 11
49
50typeNames = {
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
64FCGI_KEEP_CONN = 1
65
66# Values for role component of FCGI_BeginRequestBody
67FCGI_RESPONDER  = 1
68FCGI_AUTHORIZER = 2
69FCGI_FILTER     = 3
70
71# Values for protocolStatus component of FCGI_EndRequestBody
72
73FCGI_REQUEST_COMPLETE = 0
74FCGI_CANT_MPX_CONN    = 1
75FCGI_OVERLOADED       = 2
76FCGI_UNKNOWN_ROLE     = 3
77
78FCGI_MAX_PACKET_LEN = 0xFFFF
79
80class 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   
120def 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
143def 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
152def writeNameValue(name, value):
153    return getLenBytes(len(name)) + getLenBytes(len(value)) + name + value
154
155class 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
312class 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")
Note: See TracBrowser for help on using the browser.