Display DHT statistics to the HTTP user.
[quix0rs-apt-p2p.git] / apt_p2p / HTTPServer.py
1
2 """Serve local requests from apt and remote requests from peers."""
3
4 from urllib import unquote_plus
5 from binascii import b2a_hex
6
7 from twisted.python import log
8 from twisted.internet import defer
9 from twisted.web2 import server, http, resource, channel, stream
10 from twisted.web2 import static, http_headers, responsecode
11
12 from policies import ThrottlingFactory
13 from apt_p2p_Khashmir.bencode import bencode
14
15 class FileDownloader(static.File):
16     """Modified to make it suitable for apt requests.
17     
18     Tries to find requests in the cache. Found files are first checked for
19     freshness before being sent. Requests for unfound and stale files are
20     forwarded to the main program for downloading.
21     
22     @type manager: L{apt_p2p.AptP2P}
23     @ivar manager: the main program to query 
24     """
25     
26     def __init__(self, path, manager, defaultType="text/plain", ignoredExts=(), processors=None, indexNames=None):
27         self.manager = manager
28         super(FileDownloader, self).__init__(path, defaultType, ignoredExts, processors, indexNames)
29         
30     def renderHTTP(self, req):
31         log.msg('Got request for %s from %s' % (req.uri, req.remoteAddr))
32         resp = super(FileDownloader, self).renderHTTP(req)
33         if isinstance(resp, defer.Deferred):
34             resp.addCallback(self._renderHTTP_done, req)
35         else:
36             resp = self._renderHTTP_done(resp, req)
37         return resp
38         
39     def _renderHTTP_done(self, resp, req):
40         log.msg('Initial response to %s: %r' % (req.uri, resp))
41         
42         if self.manager:
43             path = 'http:/' + req.uri
44             if resp.code >= 200 and resp.code < 400:
45                 return self.manager.check_freshness(req, path, resp.headers.getHeader('Last-Modified'), resp)
46             
47             log.msg('Not found, trying other methods for %s' % req.uri)
48             return self.manager.get_resp(req, path)
49         
50         return resp
51
52     def createSimilarFile(self, path):
53         return self.__class__(path, self.manager, self.defaultType, self.ignoredExts,
54                               self.processors, self.indexNames[:])
55         
56 class FileUploaderStream(stream.FileStream):
57     """Modified to make it suitable for streaming to peers.
58     
59     Streams the file is small chunks to make it easier to throttle the
60     streaming to peers.
61     
62     @ivar CHUNK_SIZE: the size of chunks of data to send at a time
63     """
64
65     CHUNK_SIZE = 4*1024
66     
67     def read(self, sendfile=False):
68         if self.f is None:
69             return None
70
71         length = self.length
72         if length == 0:
73             self.f = None
74             return None
75         
76         # Remove the SendFileBuffer and mmap use, just use string reads and writes
77
78         readSize = min(length, self.CHUNK_SIZE)
79
80         self.f.seek(self.start)
81         b = self.f.read(readSize)
82         bytesRead = len(b)
83         if not bytesRead:
84             raise RuntimeError("Ran out of data reading file %r, expected %d more bytes" % (self.f, length))
85         else:
86             self.length -= bytesRead
87             self.start += bytesRead
88             return b
89
90
91 class FileUploader(static.File):
92     """Modified to make it suitable for peer requests.
93     
94     Uses the modified L{FileUploaderStream} to stream the file for throttling,
95     and doesn't do any listing of directory contents.
96     """
97
98     def render(self, req):
99         if not self.fp.exists():
100             return responsecode.NOT_FOUND
101
102         if self.fp.isdir():
103             # Don't try to render a directory listing
104             return responsecode.NOT_FOUND
105
106         try:
107             f = self.fp.open()
108         except IOError, e:
109             import errno
110             if e[0] == errno.EACCES:
111                 return responsecode.FORBIDDEN
112             elif e[0] == errno.ENOENT:
113                 return responsecode.NOT_FOUND
114             else:
115                 raise
116
117         response = http.Response()
118         # Use the modified FileStream
119         response.stream = FileUploaderStream(f, 0, self.fp.getsize())
120
121         for (header, value) in (
122             ("content-type", self.contentType()),
123             ("content-encoding", self.contentEncoding()),
124         ):
125             if value is not None:
126                 response.headers.setHeader(header, value)
127
128         return response
129
130 class TopLevel(resource.Resource):
131     """The HTTP server for all requests, both from peers and apt.
132     
133     @type directory: L{twisted.python.filepath.FilePath}
134     @ivar directory: the directory to check for cached files
135     @type db: L{db.DB}
136     @ivar db: the database to use for looking up files and hashes
137     @type manager: L{apt_p2p.AptP2P}
138     @ivar manager: the main program object to send requests to
139     @type factory: L{twisted.web2.channel.HTTPFactory} or L{policies.ThrottlingFactory}
140     @ivar factory: the factory to use to serve HTTP requests
141     """
142     
143     addSlash = True
144     
145     def __init__(self, directory, db, manager):
146         """Initialize the instance.
147         
148         @type directory: L{twisted.python.filepath.FilePath}
149         @param directory: the directory to check for cached files
150         @type db: L{db.DB}
151         @param db: the database to use for looking up files and hashes
152         @type manager: L{apt_p2p.AptP2P}
153         @param manager: the main program object to send requests to
154         """
155         self.directory = directory
156         self.db = db
157         self.manager = manager
158         self.factory = None
159
160     def getHTTPFactory(self):
161         """Initialize and get the factory for this HTTP server."""
162         if self.factory is None:
163             self.factory = channel.HTTPFactory(server.Site(self),
164                                                **{'maxPipeline': 10, 
165                                                   'betweenRequestsTimeOut': 60})
166             self.factory = ThrottlingFactory(self.factory, writeLimit = 30*1024)
167         return self.factory
168
169     def render(self, ctx):
170         """Render a web page with descriptive statistics."""
171         return http.Response(
172             200,
173             {'content-type': http_headers.MimeType('text', 'html')},
174             self.manager.getStats())
175
176     def locateChild(self, request, segments):
177         """Process the incoming request."""
178         log.msg('Got HTTP request for %s from %s' % (request.uri, request.remoteAddr))
179         name = segments[0]
180         
181         # If the request is for a shared file (from a peer)
182         if name == '~':
183             if len(segments) != 2:
184                 log.msg('Got a malformed request from %s' % request.remoteAddr)
185                 return None, ()
186             
187             # Find the file in the database
188             hash = unquote_plus(segments[1])
189             files = self.db.lookupHash(hash)
190             if files:
191                 # If it is a file, return it
192                 if 'path' in files[0]:
193                     log.msg('Sharing %s with %s' % (files[0]['path'].path, request.remoteAddr))
194                     return FileUploader(files[0]['path'].path), ()
195                 else:
196                     # It's not for a file, but for a piece string, so return that
197                     log.msg('Sending torrent string %s to %s' % (b2a_hex(hash), request.remoteAddr))
198                     return static.Data(bencode({'t': files[0]['pieces']}), 'application/x-bencoded'), ()
199             else:
200                 log.msg('Hash could not be found in database: %s' % hash)
201
202         # Only local requests (apt) get past this point
203         if request.remoteAddr.host != "127.0.0.1":
204             log.msg('Blocked illegal access to %s from %s' % (request.uri, request.remoteAddr))
205             return None, ()
206             
207         if len(name) > 1:
208             # It's a request from apt
209             return FileDownloader(self.directory.path, self.manager), segments[0:]
210         else:
211             # Will render the statistics page
212             return self, ()
213         
214         log.msg('Got a malformed request for "%s" from %s' % (request.uri, request.remoteAddr))
215         return None, ()
216
217 if __name__ == '__builtin__':
218     # Running from twistd -ny HTTPServer.py
219     # Then test with:
220     #   wget -S 'http://localhost:18080/~/whatever'
221     #   wget -S 'http://localhost:18080/~/pieces'
222
223     import os.path
224     from twisted.python.filepath import FilePath
225     
226     class DB:
227         def lookupHash(self, hash):
228             if hash == 'pieces':
229                 return [{'pieces': 'abcdefghij0123456789\xca\xec\xb8\x0c\x00\xe7\x07\xf8~])\x8f\x9d\xe5_B\xff\x1a\xc4!'}]
230             return [{'path': FilePath(os.path.expanduser('~/school/optout'))}]
231     
232     t = TopLevel(FilePath(os.path.expanduser('~')), DB(), None)
233     factory = t.getHTTPFactory()
234     
235     # Standard twisted application Boilerplate
236     from twisted.application import service, strports
237     application = service.Application("demoserver")
238     s = strports.service('tcp:18080', factory)
239     s.setServiceParent(application)