Rename project to apt-p2p.
[quix0rs-apt-p2p.git] / apt_dht / 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 server HTTP requests
141     
142     """
143     
144     addSlash = True
145     
146     def __init__(self, directory, db, manager):
147         """Initialize the instance.
148         
149         @type directory: L{twisted.python.filepath.FilePath}
150         @param directory: the directory to check for cached files
151         @type db: L{db.DB}
152         @param db: the database to use for looking up files and hashes
153         @type manager: L{apt_p2p.AptP2P}
154         @param manager: the main program object to send requests to
155         """
156         self.directory = directory
157         self.db = db
158         self.manager = manager
159         self.factory = None
160
161     def getHTTPFactory(self):
162         """Initialize and get the factory for this HTTP server."""
163         if self.factory is None:
164             self.factory = channel.HTTPFactory(server.Site(self),
165                                                **{'maxPipeline': 10, 
166                                                   'betweenRequestsTimeOut': 60})
167             self.factory = ThrottlingFactory(self.factory, writeLimit = 30*1024)
168         return self.factory
169
170     def render(self, ctx):
171         """Render a web page with descriptive statistics."""
172         return http.Response(
173             200,
174             {'content-type': http_headers.MimeType('text', 'html')},
175             """<html><body>
176             <h2>Statistics</h2>
177             <p>TODO: eventually some stats will be shown here.</body></html>""")
178
179     def locateChild(self, request, segments):
180         """Process the incoming request."""
181         log.msg('Got HTTP request for %s from %s' % (request.uri, request.remoteAddr))
182         name = segments[0]
183         
184         # If the request is for a shared file (from a peer)
185         if name == '~':
186             if len(segments) != 2:
187                 log.msg('Got a malformed request from %s' % request.remoteAddr)
188                 return None, ()
189             
190             # Find the file in the database
191             hash = unquote_plus(segments[1])
192             files = self.db.lookupHash(hash)
193             if files:
194                 # If it is a file, return it
195                 if 'path' in files[0]:
196                     log.msg('Sharing %s with %s' % (files[0]['path'].path, request.remoteAddr))
197                     return FileUploader(files[0]['path'].path), ()
198                 else:
199                     # It's not for a file, but for a piece string, so return that
200                     log.msg('Sending torrent string %s to %s' % (b2a_hex(hash), request.remoteAddr))
201                     return static.Data(bencode({'t': files[0]['pieces']}), 'application/x-bencoded'), ()
202             else:
203                 log.msg('Hash could not be found in database: %s' % hash)
204
205         # Only local requests (apt) get past this point
206         if request.remoteAddr.host != "127.0.0.1":
207             log.msg('Blocked illegal access to %s from %s' % (request.uri, request.remoteAddr))
208             return None, ()
209             
210         if len(name) > 1:
211             # It's a request from apt
212             return FileDownloader(self.directory.path, self.manager), segments[0:]
213         else:
214             # Will render the statistics page
215             return self, ()
216         
217         log.msg('Got a malformed request for "%s" from %s' % (request.uri, request.remoteAddr))
218         return None, ()
219
220 if __name__ == '__builtin__':
221     # Running from twistd -ny HTTPServer.py
222     # Then test with:
223     #   wget -S 'http://localhost:18080/~/whatever'
224     #   wget -S 'http://localhost:18080/~/pieces'
225
226     import os.path
227     from twisted.python.filepath import FilePath
228     
229     class DB:
230         def lookupHash(self, hash):
231             if hash == 'pieces':
232                 return [{'pieces': 'abcdefghij0123456789\xca\xec\xb8\x0c\x00\xe7\x07\xf8~])\x8f\x9d\xe5_B\xff\x1a\xc4!'}]
233             return [{'path': FilePath(os.path.expanduser('~/school/optout'))}]
234     
235     t = TopLevel(FilePath(os.path.expanduser('~')), DB(), None)
236     factory = t.getHTTPFactory()
237     
238     # Standard twisted application Boilerplate
239     from twisted.application import service, strports
240     application = service.Application("demoserver")
241     s = strports.service('tcp:18080', factory)
242     s.setServiceParent(application)