~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

Viewing changes to twisted/web2/client/http.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2007-01-17 14:52:35 UTC
  • mfrom: (1.1.5 upstream) (2.1.2 etch)
  • Revision ID: james.westby@ubuntu.com-20070117145235-btmig6qfmqfen0om
Tags: 2.5.0-0ubuntu1
New upstream version, compatible with python2.5.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
from zope.interface import implements
 
2
 
 
3
from twisted.internet import defer, protocol
 
4
from twisted.protocols import basic, policies
 
5
from twisted.web2 import stream as stream_mod, http, http_headers, responsecode
 
6
from twisted.web2.channel import http as httpchan
 
7
from twisted.web2.channel.http import PERSIST_NO_PIPELINE, PERSIST_PIPELINE
 
8
from twisted.web2.client import interfaces
 
9
 
 
10
#from twisted.python.util import tracer
 
11
 
 
12
class ProtocolError(Exception):
 
13
    pass
 
14
 
 
15
class ClientRequest(object):
 
16
    """A class for describing an HTTP request to be sent to the server.
 
17
    """
 
18
 
 
19
    def __init__(self, method, uri, headers, stream):
 
20
        """
 
21
        @param method: The HTTP method to for this request, ex: 'GET', 'HEAD',
 
22
            'POST', etc.
 
23
        @type method: C{str}
 
24
        
 
25
        @param uri: The URI of the resource to request, this may be absolute or
 
26
            relative, however the interpretation of this URI is left up to the 
 
27
            remote server.
 
28
        @type uri: C{str}
 
29
 
 
30
        @param headers: Headers to be sent to the server.  It is important to 
 
31
            note that this object does not create any implicit headers.  So it 
 
32
            is up to the HTTP Client to add required headers such as 'Host'.
 
33
        @type headers: C{dict}, L{twisted.web2.http_headers.Headers}, or 
 
34
            C{None}
 
35
    
 
36
        @param stream: Content body to send to the remote HTTP server.
 
37
        @type stream: L{twisted.web2.stream.IByteStream}
 
38
        """
 
39
 
 
40
        self.method = method
 
41
        self.uri = uri
 
42
        if isinstance(headers, http_headers.Headers):
 
43
            self.headers = headers
 
44
        else:
 
45
            self.headers = http_headers.Headers(headers or {})
 
46
            
 
47
        if stream is not None:
 
48
            self.stream = stream_mod.IByteStream(stream)
 
49
        else:
 
50
            self.stream = None
 
51
 
 
52
 
 
53
class HTTPClientChannelRequest(httpchan.HTTPParser):
 
54
    parseCloseAsEnd = True
 
55
    outgoing_version = "HTTP/1.1"
 
56
    chunkedOut = False
 
57
    finished = False
 
58
    
 
59
    closeAfter = False
 
60
    
 
61
    def __init__(self, channel, request, closeAfter):
 
62
        httpchan.HTTPParser.__init__(self, channel)
 
63
        self.request = request
 
64
        self.closeAfter = closeAfter
 
65
        self.transport = self.channel.transport
 
66
        self.responseDefer = defer.Deferred()
 
67
        
 
68
    def submit(self):
 
69
        l = []
 
70
        request = self.request
 
71
        if request.method == "HEAD":
 
72
            # No incoming data will arrive.
 
73
            self.length = 0
 
74
        
 
75
        l.append('%s %s %s\r\n' % (request.method, request.uri,
 
76
                                   self.outgoing_version))
 
77
        if request.headers is not None:
 
78
            for name, valuelist in request.headers.getAllRawHeaders():
 
79
                for value in valuelist:
 
80
                    l.append("%s: %s\r\n" % (name, value))
 
81
        
 
82
        if request.stream is not None:
 
83
            if request.stream.length is not None:
 
84
                l.append("%s: %s\r\n" % ('Content-Length', request.stream.length))
 
85
            else:
 
86
                # Got a stream with no length. Send as chunked and hope, against
 
87
                # the odds, that the server actually supports chunked uploads.
 
88
                l.append("%s: %s\r\n" % ('Transfer-Encoding', 'chunked'))
 
89
                self.chunkedOut = True
 
90
 
 
91
        if self.closeAfter:
 
92
            l.append("%s: %s\r\n" % ('Connection', 'close'))
 
93
        else:
 
94
            l.append("%s: %s\r\n" % ('Connection', 'Keep-Alive'))
 
95
        
 
96
        l.append("\r\n")
 
97
        self.transport.writeSequence(l)
 
98
        
 
99
        d = stream_mod.StreamProducer(request.stream).beginProducing(self)
 
100
        d.addCallback(self._finish).addErrback(self._error)
 
101
 
 
102
    def registerProducer(self, producer, streaming):
 
103
        """Register a producer.
 
104
        """
 
105
        self.transport.registerProducer(producer, streaming)
 
106
 
 
107
    def unregisterProducer(self):
 
108
        self.transport.unregisterProducer()
 
109
        
 
110
    def write(self, data):
 
111
        if not data:
 
112
            return
 
113
        elif self.chunkedOut:
 
114
            self.transport.writeSequence(("%X\r\n" % len(data), data, "\r\n"))
 
115
        else:
 
116
            self.transport.write(data)
 
117
 
 
118
    def _finish(self, x):
 
119
        """We are finished writing data."""
 
120
        if self.chunkedOut:
 
121
            # write last chunk and closing CRLF
 
122
            self.transport.write("0\r\n\r\n")
 
123
        
 
124
        self.finished = True
 
125
        self.channel.requestWriteFinished(self)
 
126
        del self.transport
 
127
 
 
128
    def _error(self, err):
 
129
        self.abortParse()
 
130
        self.responseDefer.errback(err)
 
131
 
 
132
    def _abortWithError(self, errcode, text):
 
133
        self.abortParse()
 
134
        self.responseDefer.errback(ProtocolError(text))
 
135
 
 
136
    def connectionLost(self, reason):
 
137
        ### FIXME!
 
138
        pass
 
139
    
 
140
    def gotInitialLine(self, initialLine):
 
141
        parts = initialLine.split(' ', 2)
 
142
        
 
143
        # Parse the initial request line
 
144
        if len(parts) != 3:
 
145
            self._abortWithError(responsecode.BAD_REQUEST, 'Bad response line: %s' % initialLine)
 
146
            return
 
147
 
 
148
        strversion, self.code, message = parts
 
149
        
 
150
        try:
 
151
            protovers = http.parseVersion(strversion)
 
152
            if protovers[0] != 'http':
 
153
                raise ValueError()
 
154
        except ValueError:
 
155
            self._abortWithError(responsecode.BAD_REQUEST, "Unknown protocol: %s" % strversion)
 
156
            return
 
157
        
 
158
        self.version = protovers[1:3]
 
159
 
 
160
        # Ensure HTTP 0 or HTTP 1.
 
161
        if self.version[0] != 1:
 
162
            self._abortWithError(responsecode.HTTP_VERSION_NOT_SUPPORTED, 'Only HTTP 1.x is supported.')
 
163
            return
 
164
 
 
165
    ## FIXME: Actually creates Response, function is badly named!
 
166
    def createRequest(self):
 
167
        self.stream = stream_mod.ProducerStream()
 
168
        self.response = http.Response(self.code, self.inHeaders, self.stream)
 
169
        self.stream.registerProducer(self, True)
 
170
        
 
171
        del self.inHeaders
 
172
 
 
173
    ## FIXME: Actually processes Response, function is badly named!
 
174
    def processRequest(self):
 
175
        self.responseDefer.callback(self.response)
 
176
        
 
177
    def handleContentChunk(self, data):
 
178
        self.stream.write(data)
 
179
 
 
180
    def handleContentComplete(self):
 
181
        self.stream.finish()
 
182
 
 
183
 
 
184
class EmptyHTTPClientManager(object):
 
185
    """A dummy HTTPClientManager.  It doesn't do any client management, and is 
 
186
    meant to be used only when creating an HTTPClientProtocol directly.
 
187
    """
 
188
 
 
189
    implements(interfaces.IHTTPClientManager)
 
190
 
 
191
    def clientBusy(self, proto):
 
192
        pass
 
193
    
 
194
    def clientIdle(self, proto):
 
195
        pass
 
196
 
 
197
    def clientPipelining(self, proto):
 
198
        pass
 
199
    
 
200
    def clientGone(self, proto):
 
201
        pass
 
202
    
 
203
 
 
204
class HTTPClientProtocol(basic.LineReceiver, policies.TimeoutMixin, object):
 
205
    """A HTTP 1.1 Client with request pipelining support."""
 
206
    
 
207
    chanRequest = None
 
208
    maxHeaderLength = 10240
 
209
    firstLine = 1
 
210
    readPersistent = PERSIST_NO_PIPELINE
 
211
    
 
212
    # inputTimeOut should be pending whenever a complete request has
 
213
    # been written but the complete response has not yet been
 
214
    # received, and be reset every time data is received.
 
215
    inputTimeOut = 60 * 4
 
216
 
 
217
    def __init__(self, manager=None):
 
218
        """
 
219
        @param manager: The object this client reports it state to.
 
220
        @type manager: L{interfaces.IHTTPClientManager}
 
221
        """
 
222
 
 
223
        self.outRequest = None
 
224
        self.inRequests = []
 
225
        if manager is None:
 
226
            manager = EmptyHTTPClientManager()
 
227
        self.manager = manager
 
228
 
 
229
    def lineReceived(self, line):
 
230
        if not self.inRequests:
 
231
            # server sending random unrequested data.
 
232
            self.transport.loseConnection()
 
233
            return
 
234
 
 
235
        # If not currently writing this request, set timeout
 
236
        if self.inRequests[0] is not self.outRequest:
 
237
            self.setTimeout(self.inputTimeOut)
 
238
            
 
239
        if self.firstLine:
 
240
            self.firstLine = 0
 
241
            self.inRequests[0].gotInitialLine(line)
 
242
        else:
 
243
            self.inRequests[0].lineReceived(line)
 
244
 
 
245
    def rawDataReceived(self, data):
 
246
        if not self.inRequests:
 
247
            print "Extra raw data!"
 
248
            # server sending random unrequested data.
 
249
            self.transport.loseConnection()
 
250
            return
 
251
        
 
252
        # If not currently writing this request, set timeout
 
253
        if self.inRequests[0] is not self.outRequest:
 
254
            self.setTimeout(self.inputTimeOut)
 
255
            
 
256
        self.inRequests[0].rawDataReceived(data)
 
257
        
 
258
    def submitRequest(self, request, closeAfter=True):
 
259
        """
 
260
        @param request: The request to send to a remote server.
 
261
        @type request: L{ClientRequest}
 
262
 
 
263
        @param closeAfter: If True the 'Connection: close' header will be sent,
 
264
            otherwise 'Connection: keep-alive'
 
265
        @type closeAfter: C{bool}
 
266
 
 
267
        @return: L{twisted.internet.defer.Deferred} 
 
268
        @callback: L{twisted.web2.http.Response} from the server.
 
269
        """
 
270
 
 
271
        # Assert we're in a valid state to submit more
 
272
        assert self.outRequest is None
 
273
        assert ((self.readPersistent is PERSIST_NO_PIPELINE and not self.inRequests)
 
274
                or self.readPersistent is PERSIST_PIPELINE)
 
275
        
 
276
        self.manager.clientBusy(self)
 
277
        if closeAfter:
 
278
            self.readPersistent = False
 
279
        
 
280
        self.outRequest = chanRequest = HTTPClientChannelRequest(self, request, closeAfter)
 
281
        self.inRequests.append(chanRequest)
 
282
        
 
283
        chanRequest.submit()
 
284
        return chanRequest.responseDefer
 
285
 
 
286
    def requestWriteFinished(self, request):
 
287
        assert request is self.outRequest
 
288
        
 
289
        self.outRequest = None
 
290
        # Tell the manager if more requests can be submitted.
 
291
        self.setTimeout(self.inputTimeOut)
 
292
        if self.readPersistent is PERSIST_PIPELINE:
 
293
            self.manager.clientPipelining(self)
 
294
 
 
295
    def requestReadFinished(self, request):
 
296
        assert self.inRequests[0] is request
 
297
        
 
298
        del self.inRequests[0]
 
299
        self.firstLine = True
 
300
        
 
301
        if not self.inRequests:
 
302
            if self.readPersistent:
 
303
                self.setTimeout(None)
 
304
                self.manager.clientIdle(self)
 
305
            else:
 
306
#                 print "No more requests, closing"
 
307
                self.transport.loseConnection()
 
308
 
 
309
    def setReadPersistent(self, persist):
 
310
        self.readPersistent = persist
 
311
        if not persist:
 
312
            # Tell all requests but first to abort.
 
313
            for request in self.inRequests[1:]:
 
314
                request.connectionLost(None)
 
315
            del self.inRequests[1:]
 
316
    
 
317
    def connectionLost(self, reason):
 
318
        self.readPersistent = False
 
319
        self.setTimeout(None)
 
320
        self.manager.clientGone(self)
 
321
        # Tell all requests to abort.
 
322
        for request in self.inRequests:
 
323
            if request is not None:
 
324
                request.connectionLost(reason)
 
325
    
 
326
    #isLastRequest = tracer(isLastRequest)
 
327
    #lineReceived = tracer(lineReceived)
 
328
    #rawDataReceived = tracer(rawDataReceived)
 
329
    #connectionLost = tracer(connectionLost)
 
330
    #requestReadFinished = tracer(requestReadFinished)
 
331
    #requestWriteFinished = tracer(requestWriteFinished)
 
332
    #submitRequest = tracer(submitRequest)
 
333
    
 
334
 
 
335
def testConn(host):
 
336
    from twisted.internet import reactor
 
337
    d = protocol.ClientCreator(reactor, HTTPClientProtocol).connectTCP(host, 80)
 
338
    def gotResp(resp, num):
 
339
        def print_(n):
 
340
            print "DATA %s: %r" % (num, n)
 
341
        def printdone(n):
 
342
            print "DONE %s" % num
 
343
        print "GOT RESPONSE %s: %s" % (num, resp)
 
344
        stream_mod.readStream(resp.stream, print_).addCallback(printdone)
 
345
    def sendReqs(proto):
 
346
        proto.submitRequest(ClientRequest("GET", "/", {'Host':host}, None)).addCallback(gotResp, 1)
 
347
        proto.submitRequest(ClientRequest("GET", "/foo", {'Host':host}, None)).addCallback(gotResp, 2)
 
348
    d.addCallback(sendReqs)
 
349
    del d
 
350
    reactor.run()
 
351