1 # Eloipool - Python Bitcoin pool server
2 # Copyright (C) 2011-2012 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):
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 '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 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]
126 valid = self.checkAuthentication(un, pw)
128 return self.doError('Error checking authorization')
130 self.Username = un.decode('utf8')
132 def doHeader_connection(self, value):
133 if value == b'close':
134 self.quirks['close'] = None
136 def doHeader_content_length(self, value):
139 def doHeader_x_forwarded_for(self, value):
140 if self.addr[0] in self.server.TrustedForwarders:
141 self.remoteHost = value.decode('ascii')
143 self.logger.debug("Ignoring X-Forwarded-For header from %s" % (self.addr[0],))
145 def doAuthenticate(self):
146 self.sendReply(401, headers={'WWW-Authenticate': 'Basic realm="%s"' % (self.server.ServerName,)})
148 def parse_headers(self, hs):
153 hs = re.split(br'\r?\n', hs)
154 data = hs.pop(0).split(b' ')
156 self.method = data[0]
163 self.quirks = dict(self.default_quirks)
164 if data[2:] != [b'HTTP/1.1']:
165 self.quirks['close'] = None
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):
175 getattr(self, method)(data[1])
176 except RequestAlreadyHandled:
177 # Ignore multiple errors and such
180 def found_terminator(self):
181 if self.reading_headers:
182 inbuf = b"".join(self.incoming)
184 m = re.match(br'^[\r\n]+', inbuf)
186 inbuf = inbuf[len(m.group(0)):]
190 self.reading_headers = False
191 self.parse_headers(inbuf)
193 self.set_terminator(self.CL)
196 self.set_terminator(None)
198 self.handle_request()
199 raise RequestNotHandled
200 except RequestHandled:
205 self.logger.error(traceback.format_exc())
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
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'})
222 dn = b'eloipool-' + _GitDesc + b'/'
223 for f in _SourceFiles:
224 fs = f.decode('utf8')
226 islink = stat.S_ISLNK(fstat.st_mode)
229 link = os.readlink(f)
231 with open("%s/%s" % (_srcdir, fs), 'rb') as ff:
236 h += f + bytes(max(0, 100 - len(f)))
237 h += ('%07o' % (fstat.st_mode,)[-7:]).encode('utf8') + b'\0'
239 h += ('%012o%012o' % (fstat.st_size, fstat.st_mtime)).encode('utf8')
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)
249 if p[-3:] == b'.py': ct = 'application/x-python'
250 elif p[-11:] == b'.py.example': ct = 'application/x-python'
252 with open("%s/%s" % (_srcdir, p), 'rb') as f:
253 self.sendReply(body=f.read(), headers={'Content-Type':ct})
255 def reset_request(self):
256 self.replySent = False
257 self.set_terminator( (b"\n\n", b"\r\n\r\n") )
258 self.reading_headers = True
259 self.changeTask(self.handle_timeout, time() + 150)
260 if 'close' in self.quirks:
262 # proxies can do multiple requests in one connection for multiple clients, so reset address every time
263 self.remoteHost = self.addr[0]
265 def __init__(self, *a, **ka):
266 super().__init__(*a, **ka)
267 self.quirks = dict(self.default_quirks)
270 setattr(HTTPHandler, 'doHeader_accept-encoding', HTTPHandler.doHeader_accept_encoding);
271 setattr(HTTPHandler, 'doHeader_content-length', HTTPHandler.doHeader_content_length);
272 setattr(HTTPHandler, 'doHeader_x-forwarded-for', HTTPHandler.doHeader_x_forwarded_for);