Workaround for bug in Python's math.log function
[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 import asynchat
18 from base64 import b64decode
19 from datetime import datetime
20 from email.utils import formatdate
21 import logging
22 import networkserver
23 import os
24 import re
25 import stat
26 from struct import pack
27 from time import mktime, time
28 import traceback
29
30 # It is not legal to bypass or lie to this check. See LICENSE file for details.
31 try:
32         _srcdir = os.path.dirname(os.path.abspath(__file__))
33         if os.path.exists(_srcdir + '/.I_swear_that_I_am_Luke_Dashjr'):
34                 _SourceFiles = None
35         else:
36                 _SourceFiles = os.popen('cd \'%s\' && git ls-files' % (_srcdir,)).read().split('\n')
37                 try:
38                         _SourceFiles.remove('')
39                 except ValueError:
40                         pass
41                 if len(_SourceFiles) < 2:
42                         raise RuntimeError('Unknown error')
43                 _SourceFiles = tuple(x.encode('utf8') for x in _SourceFiles)
44                 _GitDesc = os.popen('cd \'%s\' && git describe --dirty --always' % (_srcdir,)).read().strip().encode('utf8')
45 except BaseException as e:
46         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())
47         import sys
48         sys.exit(1)
49
50 class AsyncRequest(BaseException):
51         pass
52
53 class RequestAlreadyHandled(BaseException):
54         pass
55
56 class RequestHandled(RequestAlreadyHandled):
57         pass
58
59 class RequestNotHandled(BaseException):
60         pass
61
62 class HTTPHandler(networkserver.SocketHandler):
63         HTTPStatus = {
64                 200: 'OK',
65                 401: 'Unauthorized',
66                 404: 'Not Found',
67                 405: 'Method Not Allowed',
68                 500: 'Internal Server Error',
69         }
70         
71         logger = logging.getLogger('HTTPHandler')
72         
73         default_quirks = {}
74         
75         def sendReply(self, status=200, body=b'', headers=None):
76                 if self.replySent:
77                         raise RequestAlreadyHandled
78                 buf = "HTTP/1.1 %d %s\r\n" % (status, self.HTTPStatus.get(status, 'Unknown'))
79                 headers = dict(headers) if headers else {}
80                 headers['Date'] = formatdate(timeval=mktime(datetime.now().timetuple()), localtime=False, usegmt=True)
81                 headers.setdefault('Server', 'Eloipool')
82                 if not _SourceFiles is None:
83                         headers.setdefault('X-Source-Code', '/src/')
84                 if body is None:
85                         headers.setdefault('Transfer-Encoding', 'chunked')
86                 else:
87                         headers['Content-Length'] = len(body)
88                 for k, v in headers.items():
89                         if v is None: continue
90                         buf += "%s: %s\r\n" % (k, v)
91                 buf += "\r\n"
92                 buf = buf.encode('utf8')
93                 self.replySent = True
94                 if body is None:
95                         self.push(buf)
96                         return
97                 buf += body
98                 self.push(buf)
99                 raise RequestHandled
100         
101         def doError(self, reason = '', code = 100, headers = None):
102                 if headers is None: headers = {}
103                 headers.setdefault('Content-Type', 'text/plain')
104                 return self.sendReply(500, reason.encode('utf8'), headers)
105         
106         def doHeader_authorization(self, value):
107                 value = value.split(b' ')
108                 if len(value) != 2 or value[0] != b'Basic':
109                         return self.doError('Bad Authorization header')
110                 value = b64decode(value[1])
111                 (un, pw, *x) = value.split(b':', 1) + [None]
112                 valid = False
113                 try:
114                         valid = self.checkAuthentication(un, pw)
115                 except:
116                         return self.doError('Error checking authorization')
117                 if valid:
118                         self.Username = un.decode('utf8')
119         
120         def doHeader_connection(self, value):
121                 if value == b'close':
122                         self.quirks['close'] = None
123         
124         def doHeader_content_length(self, value):
125                 self.CL = int(value)
126         
127         def doHeader_x_forwarded_for(self, value):
128                 if self.addr[0] in self.server.TrustedForwarders:
129                         self.remoteHost = value.decode('ascii')
130                 else:
131                         self.logger.debug("Ignoring X-Forwarded-For header from %s" % (self.addr[0],))
132         
133         def doAuthenticate(self):
134                 self.sendReply(401, headers={'WWW-Authenticate': 'Basic realm="%s"' % (self.server.ServerName,)})
135         
136         def parse_headers(self, hs):
137                 self.CL = None
138                 self.Username = None
139                 self.method = None
140                 self.path = None
141                 hs = re.split(br'\r?\n', hs)
142                 data = hs.pop(0).split(b' ')
143                 try:
144                         self.method = data[0]
145                         self.path = data[1]
146                 except IndexError:
147                         self.close()
148                         return
149                 self.extensions = []
150                 self.reqinfo = {}
151                 self.quirks = dict(self.default_quirks)
152                 if data[2:] != [b'HTTP/1.1']:
153                         self.quirks['close'] = None
154                 while True:
155                         try:
156                                 data = hs.pop(0)
157                         except IndexError:
158                                 break
159                         data = tuple(map(lambda a: a.strip(), data.split(b':', 1)))
160                         method = 'doHeader_' + data[0].decode('ascii').lower()
161                         if hasattr(self, method):
162                                 try:
163                                         getattr(self, method)(data[1])
164                                 except RequestAlreadyHandled:
165                                         # Ignore multiple errors and such
166                                         pass
167         
168         def found_terminator(self):
169                 if self.reading_headers:
170                         inbuf = b"".join(self.incoming)
171                         self.incoming = []
172                         m = re.match(br'^[\r\n]+', inbuf)
173                         if m:
174                                 inbuf = inbuf[len(m.group(0)):]
175                         if not inbuf:
176                                 return
177                         
178                         self.reading_headers = False
179                         self.parse_headers(inbuf)
180                         if self.CL:
181                                 self.set_terminator(self.CL)
182                                 return
183                 
184                 self.set_terminator(None)
185                 try:
186                         self.handle_request()
187                         raise RequestNotHandled
188                 except RequestHandled:
189                         self.reset_request()
190                 except AsyncRequest:
191                         pass
192                 except:
193                         self.logger.error(traceback.format_exc())
194         
195         def handle_error(self):
196                 self.logger.debug(traceback.format_exc())
197                 self.handle_close()
198         
199         get_terminator = asynchat.async_chat.get_terminator
200         set_terminator = asynchat.async_chat.set_terminator
201         
202         def handle_readbuf(self):
203                 while self.ac_in_buffer:
204                         lb = len(self.ac_in_buffer)
205                         terminator = self.get_terminator()
206                         if not terminator:
207                                 # no terminator, collect it all
208                                 self.collect_incoming_data (self.ac_in_buffer)
209                                 self.ac_in_buffer = b''
210                         elif isinstance(terminator, int):
211                                 # numeric terminator
212                                 n = terminator
213                                 if lb < n:
214                                         self.collect_incoming_data (self.ac_in_buffer)
215                                         self.ac_in_buffer = b''
216                                         self.terminator = self.terminator - lb
217                                 else:
218                                         self.collect_incoming_data (self.ac_in_buffer[:n])
219                                         self.ac_in_buffer = self.ac_in_buffer[n:]
220                                         self.terminator = 0
221                                         self.found_terminator()
222                         else:
223                                 # 3 cases:
224                                 # 1) end of buffer matches terminator exactly:
225                                 #    collect data, transition
226                                 # 2) end of buffer matches some prefix:
227                                 #    collect data to the prefix
228                                 # 3) end of buffer does not match any prefix:
229                                 #    collect data
230                                 # NOTE: this supports multiple different terminators, but
231                                 #       NOT ones that are prefixes of others...
232                                 if isinstance(self.ac_in_buffer, type(terminator)):
233                                         terminator = (terminator,)
234                                 termidx = tuple(map(self.ac_in_buffer.find, terminator))
235                                 try:
236                                         index = min(x for x in termidx if x >= 0)
237                                 except ValueError:
238                                         index = -1
239                                 if index != -1:
240                                         # we found the terminator
241                                         if index > 0:
242                                                 # don't bother reporting the empty string (source of subtle bugs)
243                                                 self.collect_incoming_data (self.ac_in_buffer[:index])
244                                         specific_terminator = terminator[termidx.index(index)]
245                                         terminator_len = len(specific_terminator)
246                                         self.ac_in_buffer = self.ac_in_buffer[index+terminator_len:]
247                                         # This does the Right Thing if the terminator is changed here.
248                                         self.found_terminator()
249                                 else:
250                                         # check for a prefix of the terminator
251                                         termidx = tuple(map(lambda a: asynchat.find_prefix_at_end (self.ac_in_buffer, a), terminator))
252                                         index = max(termidx)
253                                         if index:
254                                                 if index != lb:
255                                                         # we found a prefix, collect up to the prefix
256                                                         self.collect_incoming_data (self.ac_in_buffer[:-index])
257                                                         self.ac_in_buffer = self.ac_in_buffer[-index:]
258                                                 break
259                                         else:
260                                                 # no prefix, collect it all
261                                                 self.collect_incoming_data (self.ac_in_buffer)
262                                                 self.ac_in_buffer = b''
263         
264         def handle_src_request(self):
265                 if _SourceFiles is None:
266                         return self.sendReply(404)
267                 # For AGPL compliance, allow direct downloads of source code
268                 p = self.path[5:]
269                 if p == b'':
270                         # List of files
271                         body = b'<html><head><title>Source Code</title></head><body>\t\n'
272                         body += b'\t<a href="tar">(tar archive of all files)</a><br><br>\n'
273                         for f in _SourceFiles:
274                                 body += b'\t<a href="' + f + b'">\n' + f + b'\n\t</a><br>\n'
275                         body += b'\t</body></html>\n'
276                         return self.sendReply(body=body, headers={'Content-Type':'text/html'})
277                 if p == b'tar':
278                         body = bytearray()
279                         dn = b'eloipool-' + _GitDesc + b'/'
280                         for f in _SourceFiles:
281                                 fs = f.decode('utf8')
282                                 fstat = os.lstat(fs)
283                                 islink = stat.S_ISLNK(fstat.st_mode)
284                                 if islink:
285                                         data = b''
286                                         link = os.readlink(f)
287                                 else:
288                                         with open("%s/%s" % (_srcdir, fs), 'rb') as ff:
289                                                 data = ff.read()
290                                         link = b''
291                                 h = bytearray()
292                                 f = dn + f
293                                 h += f + bytes(max(0, 100 - len(f)))
294                                 h += ('%07o' % (fstat.st_mode,)[-7:]).encode('utf8') + b'\0'
295                                 h += bytes(16)
296                                 h += ('%012o%012o' % (fstat.st_size, fstat.st_mtime)).encode('utf8')
297                                 h += b'        '  # chksum
298                                 h += b'2' if islink else b'0'
299                                 h += link + bytes(max(0, 355 - len(link)))
300                                 h[148:156] = ('%07o' % (sum(h),)).encode('utf8') + b'\0'
301                                 body += h + data + bytes(512 - ((fstat.st_size % 512) or 512))
302                         self.sendReply(body=body, headers={'Content-Type':'application/x-tar'})
303                 if p not in _SourceFiles:
304                         return self.sendReply(404)
305                 ct = 'text/plain'
306                 if p[-3:] == b'.py': ct = 'application/x-python'
307                 elif p[-11:] == b'.py.example': ct = 'application/x-python'
308                 p = p.decode('utf8')
309                 with open("%s/%s" % (_srcdir, p), 'rb') as f:
310                         self.sendReply(body=f.read(), headers={'Content-Type':ct})
311         
312         def reset_request(self):
313                 self.replySent = False
314                 self.incoming = []
315                 self.set_terminator( (b"\n\n", b"\r\n\r\n") )
316                 self.reading_headers = True
317                 self.changeTask(self.handle_timeout, time() + 150)
318                 if 'close' in self.quirks:
319                         self.close()
320                 # proxies can do multiple requests in one connection for multiple clients, so reset address every time
321                 self.remoteHost = self.addr[0]
322         
323         collect_incoming_data = asynchat.async_chat._collect_incoming_data
324         
325         def __init__(self, *a, **ka):
326                 super().__init__(*a, **ka)
327                 self.quirks = dict(self.default_quirks)
328                 self.reset_request()
329         
330 setattr(HTTPHandler, 'doHeader_content-length', HTTPHandler.doHeader_content_length);
331 setattr(HTTPHandler, 'doHeader_x-forwarded-for', HTTPHandler.doHeader_x_forwarded_for);