1 # Eloipool - Python Bitcoin pool server
2 # Copyright (C) 2011-2013 Luke Dashjr <luke-jr+eloipool@utopios.org>
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.
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.
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/>.
17 from base64 import b64decode
18 from datetime import datetime
19 from email.utils import formatdate
20 from gzip import GzipFile
27 from struct import pack
28 from time import mktime, time
31 # It is not legal to bypass or lie to this check. See LICENSE file for details.
33 _srcdir = os.path.dirname(os.path.abspath(__file__))
34 if os.path.exists(_srcdir + '/.I_swear_that_I_am_Luke_Dashjr'):
37 _SourceFiles = os.popen('cd \'%s\' && git ls-files' % (_srcdir,)).read().split('\n')
39 _SourceFiles.remove('')
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())
51 class AsyncRequest(BaseException):
54 class RequestAlreadyHandled(BaseException):
57 class RequestHandled(RequestAlreadyHandled):
60 class RequestNotHandled(BaseException):
63 class HTTPHandler(networkserver.SocketHandler):
68 405: 'Method Not Allowed',
69 500: 'Internal Server Error',
72 logger = logging.getLogger('HTTPHandler')
76 def sendReply(self, status=200, body=b'', headers=None, tryCompression=True):
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/')
86 headers.setdefault('Transfer-Encoding', 'chunked')
88 if tryCompression and 'gzip' in self.quirks:
89 headers['Content-Encoding'] = 'gzip'
90 headers['Vary'] = 'Content-Encoding'
92 with GzipFile(fileobj=gz, mode='wb') as raw:
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)
100 buf = buf.encode('utf8')
101 self.replySent = True
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)
114 def doHeader_accept_encoding(self, value):
116 self.quirks['gzip'] = True
118 def checkAuthentication(self, *a, **ka):
119 return self.server.checkAuthentication(*a, **ka)
121 def doHeader_authorization(self, value):
122 value = value.split(b' ')
123 if len(value) != 2 or value[0] != b'Basic':
124 return self.doError('Bad Authorization header')
125 value = b64decode(value[1])
126 (un, pw, *x) = value.split(b':', 1) + [None]
129 valid = self.checkAuthentication(un, pw)
131 return self.doError('Error checking authorization')
133 self.Username = un.decode('utf8')
135 def doHeader_connection(self, value):
136 if value == b'close':
137 self.quirks['close'] = None
139 def doHeader_content_length(self, value):
142 def doHeader_x_forwarded_for(self, value):
143 if self.addr[0] in self.server.TrustedForwarders:
144 self.remoteHost = value.decode('ascii')
146 self.logger.debug("Ignoring X-Forwarded-For header from %s" % (self.addr[0],))
148 def doAuthenticate(self):
149 self.sendReply(401, headers={'WWW-Authenticate': 'Basic realm="%s"' % (self.server.ServerName,)})
151 def parse_headers(self, hs):
156 hs = re.split(br'\r?\n', hs)
157 data = hs.pop(0).split(b' ')
159 self.method = data[0]
166 self.quirks = dict(self.default_quirks)
167 if data[2:] != [b'HTTP/1.1']:
168 self.quirks['close'] = None
174 data = tuple(map(lambda a: a.strip(), data.split(b':', 1)))
175 method = 'doHeader_' + data[0].decode('ascii').lower()
176 if hasattr(self, method):
178 getattr(self, method)(data[1])
179 except RequestAlreadyHandled:
180 # Ignore multiple errors and such
183 def found_terminator(self):
184 if self.reading_headers:
185 inbuf = b"".join(self.incoming)
187 m = re.match(br'^[\r\n]+', inbuf)
189 inbuf = inbuf[len(m.group(0)):]
193 self.reading_headers = False
194 self.parse_headers(inbuf)
196 self.set_terminator(self.CL)
199 self.set_terminator(None)
201 self.handle_request()
202 raise RequestNotHandled
203 except RequestHandled:
208 self.logger.error(traceback.format_exc())
210 def handle_src_request(self):
211 if _SourceFiles is None:
212 return self.sendReply(404)
213 # For AGPL compliance, allow direct downloads of source code
217 body = b'<html><head><title>Source Code</title></head><body>\t\n'
218 body += b'\t<a href="tar">(tar archive of all files)</a><br><br>\n'
219 for f in _SourceFiles:
220 body += b'\t<a href="' + f + b'">\n' + f + b'\n\t</a><br>\n'
221 body += b'\t</body></html>\n'
222 return self.sendReply(body=body, headers={'Content-Type':'text/html'})
225 dn = b'eloipool-' + _GitDesc + b'/'
226 for f in _SourceFiles:
227 fs = f.decode('utf8')
229 islink = stat.S_ISLNK(fstat.st_mode)
232 link = os.readlink(f)
234 with open("%s/%s" % (_srcdir, fs), 'rb') as ff:
239 h += f + bytes(max(0, 100 - len(f)))
240 h += ('%07o' % (fstat.st_mode,)[-7:]).encode('utf8') + b'\0'
242 h += ('%012o%012o' % (fstat.st_size, fstat.st_mtime)).encode('utf8')
244 h += b'2' if islink else b'0'
245 h += link + bytes(max(0, 355 - len(link)))
246 h[148:156] = ('%07o' % (sum(h),)).encode('utf8') + b'\0'
247 body += h + data + bytes(512 - ((fstat.st_size % 512) or 512))
248 self.sendReply(body=body, headers={'Content-Type':'application/x-tar'})
249 if p not in _SourceFiles:
250 return self.sendReply(404)
252 if p[-3:] == b'.py': ct = 'application/x-python'
253 elif p[-11:] == b'.py.example': ct = 'application/x-python'
255 with open("%s/%s" % (_srcdir, p), 'rb') as f:
256 self.sendReply(body=f.read(), headers={'Content-Type':ct})
258 def reset_request(self):
259 self.replySent = False
261 self.set_terminator( (b"\n\n", b"\r\n\r\n") )
262 self.reading_headers = True
263 self.changeTask(self.handle_timeout, time() + 150)
264 if 'close' in self.quirks:
266 # proxies can do multiple requests in one connection for multiple clients, so reset address every time
267 self.remoteHost = self.addr[0]
269 def __init__(self, *a, **ka):
270 super().__init__(*a, **ka)
271 self.quirks = dict(self.default_quirks)
274 setattr(HTTPHandler, 'doHeader_accept-encoding', HTTPHandler.doHeader_accept_encoding);
275 setattr(HTTPHandler, 'doHeader_content-length', HTTPHandler.doHeader_content_length);
276 setattr(HTTPHandler, 'doHeader_x-forwarded-for', HTTPHandler.doHeader_x_forwarded_for);