HTTPServer responds correctly to requests for torrent strings.
[quix0rs-apt-p2p.git] / apt_dht / HTTPServer.py
1
2 from urllib import unquote_plus
3 from binascii import b2a_hex
4
5 from twisted.python import log
6 from twisted.internet import defer
7 from twisted.web2 import server, http, resource, channel, stream
8 from twisted.web2 import static, http_headers, responsecode
9
10 from policies import ThrottlingFactory
11 from apt_dht_Khashmir.bencode import bencode
12
13 class FileDownloader(static.File):
14     
15     def __init__(self, path, manager, defaultType="text/plain", ignoredExts=(), processors=None, indexNames=None):
16         self.manager = manager
17         super(FileDownloader, self).__init__(path, defaultType, ignoredExts, processors, indexNames)
18         
19     def renderHTTP(self, req):
20         log.msg('Got request for %s from %s' % (req.uri, req.remoteAddr))
21         resp = super(FileDownloader, self).renderHTTP(req)
22         if isinstance(resp, defer.Deferred):
23             resp.addCallback(self._renderHTTP_done, req)
24         else:
25             resp = self._renderHTTP_done(resp, req)
26         return resp
27         
28     def _renderHTTP_done(self, resp, req):
29         log.msg('Initial response to %s: %r' % (req.uri, resp))
30         
31         if self.manager:
32             path = 'http:/' + req.uri
33             if resp.code >= 200 and resp.code < 400:
34                 return self.manager.check_freshness(req, path, resp.headers.getHeader('Last-Modified'), resp)
35             
36             log.msg('Not found, trying other methods for %s' % req.uri)
37             return self.manager.get_resp(req, path)
38         
39         return resp
40
41     def createSimilarFile(self, path):
42         return self.__class__(path, self.manager, self.defaultType, self.ignoredExts,
43                               self.processors, self.indexNames[:])
44         
45 class FileUploaderStream(stream.FileStream):
46
47     CHUNK_SIZE = 4*1024
48     
49     def read(self, sendfile=False):
50         if self.f is None:
51             return None
52
53         length = self.length
54         if length == 0:
55             self.f = None
56             return None
57
58         readSize = min(length, self.CHUNK_SIZE)
59
60         self.f.seek(self.start)
61         b = self.f.read(readSize)
62         bytesRead = len(b)
63         if not bytesRead:
64             raise RuntimeError("Ran out of data reading file %r, expected %d more bytes" % (self.f, length))
65         else:
66             self.length -= bytesRead
67             self.start += bytesRead
68             return b
69
70
71 class FileUploader(static.File):
72
73     def render(self, req):
74         if not self.fp.exists():
75             return responsecode.NOT_FOUND
76
77         if self.fp.isdir():
78             return responsecode.NOT_FOUND
79
80         try:
81             f = self.fp.open()
82         except IOError, e:
83             import errno
84             if e[0] == errno.EACCES:
85                 return responsecode.FORBIDDEN
86             elif e[0] == errno.ENOENT:
87                 return responsecode.NOT_FOUND
88             else:
89                 raise
90
91         response = http.Response()
92         response.stream = FileUploaderStream(f, 0, self.fp.getsize())
93
94         for (header, value) in (
95             ("content-type", self.contentType()),
96             ("content-encoding", self.contentEncoding()),
97         ):
98             if value is not None:
99                 response.headers.setHeader(header, value)
100
101         return response
102
103 class TopLevel(resource.Resource):
104     addSlash = True
105     
106     def __init__(self, directory, db, manager):
107         self.directory = directory
108         self.db = db
109         self.manager = manager
110         self.factory = None
111
112     def getHTTPFactory(self):
113         if self.factory is None:
114             self.factory = channel.HTTPFactory(server.Site(self),
115                                                **{'maxPipeline': 10, 
116                                                   'betweenRequestsTimeOut': 60})
117             self.factory = ThrottlingFactory(self.factory, writeLimit = 30*1024)
118         return self.factory
119
120     def render(self, ctx):
121         return http.Response(
122             200,
123             {'content-type': http_headers.MimeType('text', 'html')},
124             """<html><body>
125             <h2>Statistics</h2>
126             <p>TODO: eventually some stats will be shown here.</body></html>""")
127
128     def locateChild(self, request, segments):
129         log.msg('Got HTTP request for %s from %s' % (request.uri, request.remoteAddr))
130         name = segments[0]
131         if name == '~':
132             if len(segments) != 2:
133                 log.msg('Got a malformed request from %s' % request.remoteAddr)
134                 return None, ()
135             hash = unquote_plus(segments[1])
136             files = self.db.lookupHash(hash)
137             if files:
138                 if 'path' in files[0]:
139                     log.msg('Sharing %s with %s' % (files[0]['path'].path, request.remoteAddr))
140                     return FileUploader(files[0]['path'].path), ()
141                 else:
142                     log.msg('Sending torrent string %s to %s' % (b2a_hex(hash), request.remoteAddr))
143                     return static.Data(bencode({'t': files[0]['pieces']}), 'application/bencoded'), ()
144             else:
145                 log.msg('Hash could not be found in database: %s' % hash)
146         
147         if request.remoteAddr.host != "127.0.0.1":
148             log.msg('Blocked illegal access to %s from %s' % (request.uri, request.remoteAddr))
149             return None, ()
150             
151         if len(name) > 1:
152             return FileDownloader(self.directory.path, self.manager), segments[0:]
153         else:
154             return self, ()
155         
156         log.msg('Got a malformed request for "%s" from %s' % (request.uri, request.remoteAddr))
157         return None, ()
158
159 if __name__ == '__builtin__':
160     # Running from twistd -ny HTTPServer.py
161     # Then test with:
162     #   wget -S 'http://localhost:18080/~/whatever'
163     #   wget -S 'http://localhost:18080/~/pieces'
164
165     import os.path
166     from twisted.python.filepath import FilePath
167     
168     class DB:
169         def lookupHash(self, hash):
170             if hash == 'pieces':
171                 return [{'pieces': 'abcdefghij0123456789\xca\xec\xb8\x0c\x00\xe7\x07\xf8~])\x8f\x9d\xe5_B\xff\x1a\xc4!'}]
172             return [{'path': FilePath(os.path.expanduser('~/school/optout'))}]
173     
174     t = TopLevel(FilePath(os.path.expanduser('~')), DB(), None)
175     factory = t.getHTTPFactory()
176     
177     # Standard twisted application Boilerplate
178     from twisted.application import service, strports
179     application = service.Application("demoserver")
180     s = strports.service('tcp:18080', factory)
181     s.setServiceParent(application)