~twisted-dev/twisted-trac-integration/trunk

« back to all changes in this revision

Viewing changes to commit-bot/_http.py

  • Committer: tom.prince at ualberta
  • Date: 2013-05-21 17:27:11 UTC
  • Revision ID: tom.prince@ualberta.net-20130521172711-o9vifzpokcyf2i55
Remove commit-bot and diffresource.

The code and history for them now reside in seperate repositories at
https://github.com/twisted-infra.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
 
2
 
import urlparse
3
 
import md5, sha
4
 
 
5
 
from twisted.web import client, http
6
 
from twisted.internet import reactor
7
 
 
8
 
 
9
 
class Token(str):
10
 
    __slots__=[]
11
 
    tokens = {}
12
 
    def __new__(self, char):
13
 
        token = Token.tokens.get(char)
14
 
        if token is None:
15
 
            Token.tokens[char] = token = str.__new__(self, char)
16
 
        return token
17
 
 
18
 
    def __repr__(self):
19
 
        return "Token(%s)" % str.__repr__(self)
20
 
 
21
 
 
22
 
http_tokens = " \t\"()<>@,;:\\/[]?={}"
23
 
http_ctls = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f"
24
 
 
25
 
 
26
 
def tokenize(header, foldCase=True):
27
 
    """Tokenize a string according to normal HTTP header parsing rules.
28
 
 
29
 
    In particular:
30
 
     - Whitespace is irrelevant and eaten next to special separator tokens.
31
 
       Its existance (but not amount) is important between character strings.
32
 
     - Quoted string support including embedded backslashes.
33
 
     - Case is insignificant (and thus lowercased), except in quoted strings.
34
 
        (unless foldCase=False)
35
 
     - Multiple headers are concatenated with ','
36
 
 
37
 
    NOTE: not all headers can be parsed with this function.
38
 
 
39
 
    Takes a raw header value (list of strings), and
40
 
    Returns a generator of strings and Token class instances.
41
 
    """
42
 
    tokens=http_tokens
43
 
    ctls=http_ctls
44
 
 
45
 
    string = ",".join(header)
46
 
    list = []
47
 
    start = 0
48
 
    cur = 0
49
 
    quoted = False
50
 
    qpair = False
51
 
    inSpaces = -1
52
 
    qstring = None
53
 
 
54
 
    for x in string:
55
 
        if quoted:
56
 
            if qpair:
57
 
                qpair = False
58
 
                qstring = qstring+string[start:cur-1]+x
59
 
                start = cur+1
60
 
            elif x == '\\':
61
 
                qpair = True
62
 
            elif x == '"':
63
 
                quoted = False
64
 
                yield qstring+string[start:cur]
65
 
                qstring=None
66
 
                start = cur+1
67
 
        elif x in tokens:
68
 
            if start != cur:
69
 
                if foldCase:
70
 
                    yield string[start:cur].lower()
71
 
                else:
72
 
                    yield string[start:cur]
73
 
 
74
 
            start = cur+1
75
 
            if x == '"':
76
 
                quoted = True
77
 
                qstring = ""
78
 
                inSpaces = False
79
 
            elif x in " \t":
80
 
                if inSpaces is False:
81
 
                    inSpaces = True
82
 
            else:
83
 
                inSpaces = -1
84
 
                yield Token(x)
85
 
        elif x in ctls:
86
 
            raise ValueError("Invalid control character: %d in header" % ord(x))
87
 
        else:
88
 
            if inSpaces is True:
89
 
                yield Token(' ')
90
 
                inSpaces = False
91
 
 
92
 
            inSpaces = False
93
 
        cur = cur+1
94
 
 
95
 
    if qpair:
96
 
        raise ValueError, "Missing character after '\\'"
97
 
    if quoted:
98
 
        raise ValueError, "Missing end quote"
99
 
 
100
 
    if start != cur:
101
 
        if foldCase:
102
 
            yield string[start:cur].lower()
103
 
        else:
104
 
            yield string[start:cur]
105
 
 
106
 
 
107
 
def parseWWWAuthenticate(tokenized):
108
 
    headers = []
109
 
 
110
 
    tokenList = list(tokenized)
111
 
 
112
 
    while tokenList:
113
 
        scheme = tokenList.pop(0)
114
 
        challenge = {}
115
 
        last = None
116
 
        kvChallenge = False
117
 
 
118
 
        while tokenList:
119
 
            token = tokenList.pop(0)
120
 
            if token == Token('='):
121
 
                kvChallenge = True
122
 
                challenge[last] = tokenList.pop(0)
123
 
                last = None
124
 
 
125
 
            elif token == Token(','):
126
 
                if kvChallenge:
127
 
                    if len(tokenList) > 1 and tokenList[1] != Token('='):
128
 
                        break
129
 
 
130
 
                else:
131
 
                    break
132
 
 
133
 
            else:
134
 
                last = token
135
 
 
136
 
        if last and scheme and not challenge and not kvChallenge:
137
 
            challenge = last
138
 
            last = None
139
 
 
140
 
        headers.append((scheme, challenge))
141
 
 
142
 
    if last and last not in (Token('='), Token(',')):
143
 
        if headers[-1] == (scheme, challenge):
144
 
            scheme = last
145
 
            challenge = {}
146
 
            headers.append((scheme, challenge))
147
 
 
148
 
    return headers
149
 
 
150
 
 
151
 
def parse(url, defaultPort=None):
152
 
    """
153
 
    Split the given URL into the scheme, host, port, and path.
154
 
 
155
 
    @type url: C{str}
156
 
    @param url: An URL to parse.
157
 
 
158
 
    @type defaultPort: C{int} or C{None}
159
 
    @param defaultPort: An alternate value to use as the port if the URL does
160
 
    not include one.
161
 
 
162
 
    @return: A four-tuple of the scheme, host, port, and path of the URL.  All
163
 
    of these are C{str} instances except for port, which is an C{int}.
164
 
    """
165
 
    url = url.strip()
166
 
    parsed = http.urlparse(url)
167
 
    scheme = parsed[0]
168
 
    path = urlparse.urlunparse(('','')+parsed[2:])
169
 
    if defaultPort is None:
170
 
        if scheme == 'https':
171
 
            defaultPort = 443
172
 
        else:
173
 
            defaultPort = 80
174
 
    host, port = parsed[1], defaultPort
175
 
    if ':' in host:
176
 
        host, port = host.split(':')
177
 
        port = int(port)
178
 
    if path == "":
179
 
        path = "/"
180
 
    return scheme, host, port, path
181
 
 
182
 
 
183
 
def makeGetterFactory(url, factoryFactory, contextFactory=None,
184
 
                      *args, **kwargs):
185
 
    """
186
 
    Create and connect an HTTP page getting factory.
187
 
 
188
 
    Any additional positional or keyword arguments are used when calling
189
 
    C{factoryFactory}.
190
 
 
191
 
    @param factoryFactory: Factory factory that is called with C{url}, C{args}
192
 
        and C{kwargs} to produce the getter
193
 
 
194
 
    @param contextFactory: Context factory to use when creating a secure
195
 
        connection, defaulting to C{None}
196
 
 
197
 
    @return: The factory created by C{factoryFactory}
198
 
    """
199
 
    scheme, host, port, path = parse(url)
200
 
    factory = factoryFactory(url, *args, **kwargs)
201
 
    if scheme == 'https':
202
 
        from twisted.internet import ssl
203
 
        if contextFactory is None:
204
 
            contextFactory = ssl.ClientContextFactory()
205
 
        reactor.connectSSL(host, port, factory, contextFactory)
206
 
    else:
207
 
        reactor.connectTCP(host, port, factory)
208
 
    return factory
209
 
 
210
 
 
211
 
def getPage(url, contextFactory=None, *args, **kwargs):
212
 
    """
213
 
    Download a web page as a string.
214
 
 
215
 
    Download a page. Return a deferred, which will callback with a
216
 
    page (as a string) or errback with a description of the error.
217
 
 
218
 
    See HTTPClientFactory to see what extra args can be passed.
219
 
    """
220
 
    return makeGetterFactory(
221
 
        url,
222
 
        client.HTTPClientFactory,
223
 
        contextFactory=contextFactory,
224
 
        *args, **kwargs)
225
 
 
226
 
algorithms = {
227
 
    'md5': md5.new,
228
 
 
229
 
    # md5-sess is more complicated than just another algorithm.  It requires
230
 
    # H(A1) state to be remembered from the first WWW-Authenticate challenge
231
 
    # issued and re-used to process any Authorization header in response to
232
 
    # that WWW-Authenticate challenge.  It is *not* correct to simply
233
 
    # recalculate H(A1) each time an Authorization header is received.  Read
234
 
    # RFC 2617, section 3.2.2.2 and do not try to make DigestCredentialFactory
235
 
    # support this unless you completely understand it. -exarkun
236
 
    'md5-sess': md5.new,
237
 
 
238
 
    'sha': sha.new,
239
 
}
240
 
 
241
 
# DigestCalcHA1
242
 
def calcHA1(pszAlg, pszUserName, pszRealm, pszPassword, pszNonce, pszCNonce,
243
 
            preHA1=None):
244
 
    """
245
 
    Compute H(A1) from RFC 2617.
246
 
 
247
 
    @param pszAlg: The name of the algorithm to use to calculate the digest.
248
 
        Currently supported are md5, md5-sess, and sha.
249
 
    @param pszUserName: The username
250
 
    @param pszRealm: The realm
251
 
    @param pszPassword: The password
252
 
    @param pszNonce: The nonce
253
 
    @param pszCNonce: The cnonce
254
 
 
255
 
    @param preHA1: If available this is a str containing a previously
256
 
       calculated H(A1) as a hex string.  If this is given then the values for
257
 
       pszUserName, pszRealm, and pszPassword must be C{None} and are ignored.
258
 
    """
259
 
 
260
 
    if (preHA1 and (pszUserName or pszRealm or pszPassword)):
261
 
        raise TypeError(("preHA1 is incompatible with the pszUserName, "
262
 
                         "pszRealm, and pszPassword arguments"))
263
 
 
264
 
    if preHA1 is None:
265
 
        # We need to calculate the HA1 from the username:realm:password
266
 
        m = algorithms[pszAlg]()
267
 
        m.update(pszUserName)
268
 
        m.update(":")
269
 
        m.update(pszRealm)
270
 
        m.update(":")
271
 
        m.update(pszPassword)
272
 
        HA1 = m.digest()
273
 
    else:
274
 
        # We were given a username:realm:password
275
 
        HA1 = preHA1.decode('hex')
276
 
 
277
 
    if pszAlg == "md5-sess":
278
 
        m = algorithms[pszAlg]()
279
 
        m.update(HA1)
280
 
        m.update(":")
281
 
        m.update(pszNonce)
282
 
        m.update(":")
283
 
        m.update(pszCNonce)
284
 
        HA1 = m.digest()
285
 
 
286
 
    return HA1.encode('hex')
287
 
 
288
 
 
289
 
def calcHA2(algo, pszMethod, pszDigestUri, pszQop, pszHEntity):
290
 
    """
291
 
    Compute H(A2) from RFC 2617.
292
 
 
293
 
    @param pszAlg: The name of the algorithm to use to calculate the digest.
294
 
        Currently supported are md5, md5-sess, and sha.
295
 
    @param pszMethod: The request method.
296
 
    @param pszDigestUri: The request URI.
297
 
    @param pszQop: The Quality-of-Protection value.
298
 
    @param pszHEntity: The hash of the entity body or C{None} if C{pszQop} is
299
 
        not C{'auth-int'}.
300
 
    @return: The hash of the A2 value for the calculation of the response
301
 
        digest.
302
 
    """
303
 
    m = algorithms[algo]()
304
 
    m.update(pszMethod)
305
 
    m.update(":")
306
 
    m.update(pszDigestUri)
307
 
    if pszQop == "auth-int":
308
 
        m.update(":")
309
 
        m.update(pszHEntity)
310
 
    return m.digest().encode('hex')
311
 
 
312
 
 
313
 
def calcResponse(HA1, HA2, algo, pszNonce, pszNonceCount, pszCNonce, pszQop):
314
 
    """
315
 
    Compute the digest for the given parameters.
316
 
 
317
 
    @param HA1: The H(A1) value, as computed by L{calcHA1}.
318
 
    @param HA2: The H(A2) value, as computed by L{calcHA2}.
319
 
    @param pszNonce: The challenge nonce.
320
 
    @param pszNonceCount: The (client) nonce count value for this response.
321
 
    @param pszCNonce: The client nonce.
322
 
    @param pszQop: The Quality-of-Protection value.
323
 
    """
324
 
    m = algorithms[algo]()
325
 
    m.update(HA1)
326
 
    m.update(":")
327
 
    m.update(pszNonce)
328
 
    m.update(":")
329
 
    if pszNonceCount and pszCNonce:
330
 
        m.update(pszNonceCount)
331
 
        m.update(":")
332
 
        m.update(pszCNonce)
333
 
        m.update(":")
334
 
        m.update(pszQop)
335
 
        m.update(":")
336
 
    m.update(HA2)
337
 
    respHash = m.digest().encode('hex')
338
 
    return respHash