UniqueSessionIdManager: Support for delaying releases of session ids, and picking...
[bitcoin:eloipool.git] / httpserver.py
1 # Eloipool - Python Bitcoin pool server
2 # Copyright (C) 2011-2012  Luke Dashjr <luke-jr+eloipool@utopios.org>
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as
6 # published by the Free Software Foundation, either version 3 of the
7 # License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 from base64 import b64decode
18 from datetime import datetime
19 from email.utils import formatdate
20 from gzip import GzipFile
21 import io
22 import logging
23 import networkserver
24 import os
25 import re
26 import stat
27 from struct import pack
28 from time import mktime, time
29 import traceback
30
31 # It is not legal to bypass or lie to this check. See LICENSE file for details.
32 try:
33         _srcdir = os.path.dirname(os.path.abspath(__file__))
34         if os.path.exists(_srcdir + '/.I_swear_that_I_am_Luke_Dashjr'):
35                 _SourceFiles = None
36         else:
37                 _SourceFiles = os.popen('cd \'%s\' && git ls-files' % (_srcdir,)).read().split('\n')
38                 try:
39                         _SourceFiles.remove('')
40                 except ValueError:
41                         pass
42                 if len(_SourceFiles) < 2:
43                         raise RuntimeError('Unknown error')
44                 _SourceFiles = tuple(x.encode('utf8') for x in _SourceFiles)
45                 _GitDesc = os.popen('cd \'%s\' && git describe --dirty --always' % (_srcdir,)).read().strip().encode('utf8')
46 except BaseException as e:
47         logging.getLogger('Licensing').critical('Error getting list of source files! AGPL requires this. To fix, be sure you are using git for Eloipool.\n' + traceback.format_exc())
48         import sys
49         sys.exit(1)
50
51 class AsyncRequest(BaseException):
52         pass
53
54 class RequestAlreadyHandled(BaseException):
55         pass
56
57 class RequestHandled(RequestAlreadyHandled):
58         pass
59
60 class RequestNotHandled(BaseException):
61         pass
62
63 class HTTPHandler(networkserver.SocketHandler):
64         HTTPStatus = {
65                 200: 'OK',
66                 401: 'Unauthorized',
67                 404: 'Not Found',
68                 405: 'Method Not Allowed',
69                 500: 'Internal Server Error',
70         }
71         
72         logger = logging.getLogger('HTTPHandler')
73         
74         default_quirks = {}
75         
76         def sendReply(self, status=200, body=b'', headers=None, tryCompression=True):
77                 if self.replySent:
78                         raise RequestAlreadyHandled
79                 buf = "HTTP/1.1 %d %s\r\n" % (status, self.HTTPStatus.get(status, 'Unknown'))
80                 headers = dict(headers) if headers else {}
81                 headers['Date'] = formatdate(timeval=mktime(datetime.now().timetuple()), localtime=False, usegmt=True)
82                 headers.setdefault('Server', 'Eloipool')
83                 if not _SourceFiles is None:
84                         headers.setdefault('X-Source-Code', '/src/')
85                 if body is None:
86                         headers.setdefault('Transfer-Encoding', 'chunked')
87                 else:
88                         if tryCompression and 'gzip' in self.quirks:
89                                 headers['Content-Encoding'] = 'gzip'
90                                 headers['Vary'] = 'Content-Encoding'
91                                 gz = io.BytesIO()
92                                 with GzipFile(fileobj=gz, mode='wb') as raw:
93                                         raw.write(body)
94                                 body = gz.getvalue()
95                         headers['Content-Length'] = len(body)
96                 for k, v in headers.items():
97                         if v is None: continue
98                         buf += "%s: %s\r\n" % (k, v)
99                 buf += "\r\n"
100                 buf = buf.encode('utf8')
101                 self.replySent = True
102                 if body is None:
103                         self.push(buf)
104                         return
105                 buf += body
106                 self.push(buf)
107                 raise RequestHandled
108         
109         def doError(self, reason = '', code = 100, headers = None):
110                 if headers is None: headers = {}
111                 headers.setdefault('Content-Type', 'text/plain')
112                 return self.sendReply(500, reason.encode('utf8'), headers)
113         
114         def doHeader_accept_encoding(self, value):
115                 if b'gzip' in value:
116                         self.quirks['gzip'] = True
117         
118         def doHeader_authorization(self, value):
119                 value = value.split(b' ')
120                 if len(value) != 2 or value[0] != b'Basic':
121                         return self.doError('Bad Authorization header')
122                 value = b64decode(value[1])
123                 (un, pw, *x) = value.split(b':', 1) + [None]
124                 valid = False
125                 try:
126                         valid = self.checkAuthentication(un, pw)
127                 except:
128                         return self.doError('Error checking authorization')
129                 if valid:
130                         self.Username = un.decode('utf8')
131         
132         def doHeader_connection(self, value):
133                 if value == b'close':
134                         self.quirks['close'] = None
135         
136         def doHeader_content_length(self, value):
137                 self.CL = int(value)
138         
139         def doHeader_x_forwarded_for(self, value):
140                 if self.addr[0] in self.server.TrustedForwarders:
141                         self.remoteHost = value.decode('ascii')
142                 else:
143                         self.logger.debug("Ignoring X-Forwarded-For header from %s" % (self.addr[0],))
144         
145         def doAuthenticate(self):
146                 self.sendReply(401, headers={'WWW-Authenticate': 'Basic realm="%s"' % (self.server.ServerName,)})
147         
148         def parse_headers(self, hs):
149                 self.CL = None
150                 self.Username = None
151                 self.method = None
152                 self.path = None
153                 hs = re.split(br'\r?\n', hs)
154                 data = hs.pop(0).split(b' ')
155                 try:
156                         self.method = data[0]
157                         self.path = data[1]
158                 except IndexError:
159                         self.close()
160                         return
161                 self.extensions = []
162                 self.reqinfo = {}
163                 self.quirks = dict(self.default_quirks)
164                 if data[2:] != [b'HTTP/1.1']:
165                         self.quirks['close'] = None
166                 while True:
167                         try:
168                                 data = hs.pop(0)
169                         except IndexError:
170                                 break
171                         data = tuple(map(lambda a: a.strip(), data.split(b':', 1)))
172                         method = 'doHeader_' + data[0].decode('ascii').lower()
173                         if hasattr(self, method):
174                                 try:
175                                         getattr(self, method)(data[1])
176                                 except RequestAlreadyHandled:
177                                         # Ignore multiple errors and such
178                                         pass
179         
180         def found_terminator(self):
181                 if self.reading_headers:
182                         inbuf = b"".join(self.incoming)
183                         self.incoming = []
184                         m = re.match(br'^[\r\n]+', inbuf)
185                         if m:
186                                 inbuf = inbuf[len(m.group(0)):]
187                         if not inbuf:
188                                 return
189                         
190                         self.reading_headers = False
191                         self.parse_headers(inbuf)
192                         if self.CL:
193                                 self.set_terminator(self.CL)
194                                 return
195                 
196                 self.set_terminator(None)
197                 try:
198                         self.handle_request()
199                         raise RequestNotHandled
200                 except RequestHandled:
201                         self.reset_request()
202                 except AsyncRequest:
203                         pass
204                 except:
205                         self.logger.error(traceback.format_exc())
206         
207         def handle_src_request(self):
208                 if _SourceFiles is None:
209                         return self.sendReply(404)
210                 # For AGPL compliance, allow direct downloads of source code
211                 p = self.path[5:]
212                 if p == b'':
213                         # List of files
214                         body = b'<html><head><title>Source Code</title></head><body>\t\n'
215                         body += b'\t<a href="tar">(tar archive of all files)</a><br><br>\n'
216                         for f in _SourceFiles:
217                                 body += b'\t<a href="' + f + b'">\n' + f + b'\n\t</a><br>\n'
218                         body += b'\t</body></html>\n'
219                         return self.sendReply(body=body, headers={'Content-Type':'text/html'})
220                 if p == b'tar':
221                         body = bytearray()
222                         dn = b'eloipool-' + _GitDesc + b'/'
223                         for f in _SourceFiles:
224                                 fs = f.decode('utf8')
225                                 fstat = os.lstat(fs)
226                                 islink = stat.S_ISLNK(fstat.st_mode)
227                                 if islink:
228                                         data = b''
229                                         link = os.readlink(f)
230                                 else:
231                                         with open("%s/%s" % (_srcdir, fs), 'rb') as ff:
232                                                 data = ff.read()
233                                         link = b''
234                                 h = bytearray()
235                                 f = dn + f
236                                 h += f + bytes(max(0, 100 - len(f)))
237                                 h += ('%07o' % (fstat.st_mode,)[-7:]).encode('utf8') + b'\0'
238                                 h += bytes(16)
239                                 h += ('%012o%012o' % (fstat.st_size, fstat.st_mtime)).encode('utf8')
240                                 h += b'        '  # chksum
241                                 h += b'2' if islink else b'0'
242                                 h += link + bytes(max(0, 355 - len(link)))
243                                 h[148:156] = ('%07o' % (sum(h),)).encode('utf8') + b'\0'
244                                 body += h + data + bytes(512 - ((fstat.st_size % 512) or 512))
245                         self.sendReply(body=body, headers={'Content-Type':'application/x-tar'})
246                 if p not in _SourceFiles:
247                         return self.sendReply(404)
248                 ct = 'text/plain'
249                 if p[-3:] == b'.py': ct = 'application/x-python'
250                 elif p[-11:] == b'.py.example': ct = 'application/x-python'
251                 p = p.decode('utf8')
252                 with open("%s/%s" % (_srcdir, p), 'rb') as f:
253                         self.sendReply(body=f.read(), headers={'Content-Type':ct})
254         
255         def reset_request(self):
256                 self.replySent = False
257                 self.incoming = []
258                 self.set_terminator( (b"\n\n", b"\r\n\r\n") )
259                 self.reading_headers = True
260                 self.changeTask(self.handle_timeout, time() + 150)
261                 if 'close' in self.quirks:
262                         self.close()
263                 # proxies can do multiple requests in one connection for multiple clients, so reset address every time
264                 self.remoteHost = self.addr[0]
265         
266         def __init__(self, *a, **ka):
267                 super().__init__(*a, **ka)
268                 self.quirks = dict(self.default_quirks)
269                 self.reset_request()
270         
271 setattr(HTTPHandler, 'doHeader_accept-encoding', HTTPHandler.doHeader_accept_encoding);
272 setattr(HTTPHandler, 'doHeader_content-length', HTTPHandler.doHeader_content_length);
273 setattr(HTTPHandler, 'doHeader_x-forwarded-for', HTTPHandler.doHeader_x_forwarded_for);