|
a/src/mediaserver/cdplugins/uprcl/uprclhttp.py |
|
b/src/mediaserver/cdplugins/uprcl/uprclhttp.py |
1 |
#!/usr/bin/env python
|
1 |
# Copyright (C) 2017 J.F.Dockes
|
|
|
2 |
# This program is free software; you can redistribute it and/or modify
|
|
|
3 |
# it under the terms of the GNU General Public License as published by
|
|
|
4 |
# the Free Software Foundation; either version 2 of the License, or
|
|
|
5 |
# (at your option) any later version.
|
2 |
#
|
6 |
#
|
3 |
# Copyright (C) 2017 J.F.Dockes
|
7 |
# This program is distributed in the hope that it will be useful,
|
|
|
8 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
9 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
10 |
# GNU General Public License for more details.
|
4 |
#
|
11 |
#
|
5 |
# HTTP Range code:
|
12 |
# You should have received a copy of the GNU General Public License
|
6 |
# Portions Copyright (C) 2009,2010 Xyne
|
13 |
# along with this program; if not, write to the
|
7 |
# Portions Copyright (C) 2011 Sean Goller
|
14 |
# Free Software Foundation, Inc.,
|
8 |
# https://github.com/smgoller/rangehttpserver/blob/master/RangeHTTPServer.py
|
15 |
# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
9 |
#
|
16 |
#
|
10 |
# This program is free software: you can redistribute it and/or modify
|
17 |
|
11 |
# it under the terms of the GNU General Public License as published by
|
|
|
12 |
# the Free Software Foundation, either version 3 of the License, or
|
|
|
13 |
# (at your option) any later version.
|
|
|
14 |
#
|
|
|
15 |
# This program is distributed in the hope that it will be useful,
|
|
|
16 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
17 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
18 |
# GNU General Public License for more details.
|
|
|
19 |
#
|
|
|
20 |
# You should have received a copy of the GNU General Public License
|
|
|
21 |
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
22 |
from __future__ import print_function
|
18 |
from __future__ import print_function
|
23 |
|
19 |
|
24 |
import SocketServer
|
|
|
25 |
import BaseHTTPServer
|
|
|
26 |
import os
|
20 |
import os
|
27 |
import posixpath
|
|
|
28 |
import urllib
|
|
|
29 |
import urlparse
|
|
|
30 |
import shutil
|
21 |
import time
|
31 |
import mimetypes
|
22 |
import bottle
|
32 |
import sys
|
|
|
33 |
|
23 |
|
34 |
import mutagen
|
24 |
from uprclutils import uplog, embedded_open
|
|
|
25 |
import uprclinit
|
35 |
|
26 |
|
36 |
try:
|
27 |
@bottle.route('/')
|
37 |
from cStringIO import StringIO
|
28 |
@bottle.post('/')
|
38 |
except ImportError:
|
29 |
@bottle.view('main')
|
39 |
from StringIO import StringIO
|
30 |
def main():
|
|
|
31 |
sub = bottle.request.forms.get('sub')
|
|
|
32 |
#uplog("Main: sub value is %s" % sub)
|
|
|
33 |
if uprclinit.updaterunning():
|
|
|
34 |
status = 'Updating'
|
|
|
35 |
else:
|
|
|
36 |
status = 'Ready'
|
40 |
|
37 |
|
41 |
from uprclutils import uplog,printable,embedded_open
|
38 |
if sub == 'Update Index':
|
|
|
39 |
uprclinit.start_update()
|
|
|
40 |
|
|
|
41 |
if sub:
|
|
|
42 |
headers = dict()
|
|
|
43 |
headers["Location"] = '/'
|
|
|
44 |
return bottle.HTTPResponse(status=302, **headers)
|
|
|
45 |
else:
|
|
|
46 |
return {'title':status, 'status':status,
|
|
|
47 |
'friendlyname':uprclinit.g_friendlyname}
|
|
|
48 |
|
|
|
49 |
@bottle.route('/static/<filepath:path>')
|
|
|
50 |
def static(filepath):
|
|
|
51 |
#uplog("control: static: filepath %s datadir %s" % (filepath, datadir))
|
|
|
52 |
return bottle.static_file(filepath, root=os.path.join(datadir, 'static'))
|
42 |
|
53 |
|
43 |
|
54 |
|
44 |
__version__ = "0.1"
|
55 |
# Object for streaming data from a given subtree (topdirs entry more
|
|
|
56 |
# or less). This is needed just because as far as I can see, a
|
|
|
57 |
# callback can't know the route it was called for, so we record it
|
|
|
58 |
# when creating the object.
|
|
|
59 |
class Streamer(object):
|
|
|
60 |
def __init__(self, root):
|
|
|
61 |
self.root = root
|
45 |
|
62 |
|
46 |
class RangeHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
63 |
def __call__(self, filepath):
|
|
|
64 |
embedded = True if 'embed' in bottle.request.query else False
|
|
|
65 |
if embedded:
|
|
|
66 |
# Embedded image urls have had a .jpg or .png
|
|
|
67 |
# appended. Remove it to restore the track path name.
|
|
|
68 |
i = filepath.rfind('.')
|
|
|
69 |
filepath = filepath[:i]
|
|
|
70 |
apath = os.path.join(self.root,filepath)
|
|
|
71 |
ctype, size, f = embedded_open(apath)
|
|
|
72 |
fs = os.stat(apath)
|
|
|
73 |
lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(fs.st_mtime))
|
|
|
74 |
bottle.response.set_header("Last-Modified", lm)
|
|
|
75 |
bottle.response.set_header("Content-type", ctype)
|
|
|
76 |
bottle.response.set_header("Content-Length", size)
|
|
|
77 |
return f
|
|
|
78 |
uplog("Uprcl: streaming: %s" % os.path.join(self.root,filepath))
|
|
|
79 |
return bottle.static_file(filepath, root=self.root)
|
47 |
|
80 |
|
48 |
"""Simple HTTP request handler with GET and HEAD commands.
|
81 |
|
49 |
|
82 |
|
50 |
The MIME type for files is determined by calling the .guess_type() method.
|
83 |
# Bottle handle both the streaming and control requests.
|
|
|
84 |
def runbottle(host='0.0.0.0', port=9278, pthstr='', pathprefix=''):
|
|
|
85 |
global datadir
|
|
|
86 |
uplog("runbottle: host %s port %d pthstr %s pathprefix %s" %
|
|
|
87 |
(host, port, pthstr, pathprefix))
|
|
|
88 |
datadir = os.path.dirname(__file__)
|
|
|
89 |
datadir = os.path.join(datadir, 'bottle')
|
|
|
90 |
bottle.TEMPLATE_PATH = (os.path.join(datadir, 'views'),)
|
51 |
|
91 |
|
52 |
The GET and HEAD requests are identical except that the HEAD
|
92 |
# All the file urls must be like /some/prefix/path where
|
53 |
request omits the actual contents of the file.
|
93 |
# /some/prefix must be in the path translation map (which I'm not
|
54 |
|
94 |
# sure what the use of is). By default the map is an identical
|
55 |
"""
|
95 |
# translation of all topdirs entries. We create one route for each
|
56 |
|
96 |
# prefix. As I don't know how a bottle method can retrieve the
|
57 |
server_version = "RangeHTTP/" + __version__
|
97 |
# route it was called from, we create a callable for each prefix.
|
58 |
|
98 |
# Each route is built on the translation input, and the processor
|
59 |
def do_GET(self):
|
99 |
# uses the translated path as root
|
60 |
"""Serve a GET request."""
|
|
|
61 |
f, start_range, end_range = self.send_head()
|
|
|
62 |
if f:
|
|
|
63 |
#uplog("do_GET: Got (%d,%d)" % (start_range,end_range))
|
|
|
64 |
f.seek(start_range, 0)
|
|
|
65 |
chunk = 0x1000
|
|
|
66 |
total = 0
|
|
|
67 |
while chunk > 0:
|
|
|
68 |
if start_range + chunk > end_range:
|
|
|
69 |
chunk = end_range - start_range
|
|
|
70 |
try:
|
|
|
71 |
self.wfile.write(f.read(chunk))
|
|
|
72 |
except:
|
|
|
73 |
break
|
|
|
74 |
total += chunk
|
|
|
75 |
start_range += chunk
|
|
|
76 |
f.close()
|
|
|
77 |
|
|
|
78 |
def do_HEAD(self):
|
|
|
79 |
"""Serve a HEAD request."""
|
|
|
80 |
f, start_range, end_range = self.send_head()
|
|
|
81 |
if f:
|
|
|
82 |
f.close()
|
|
|
83 |
|
|
|
84 |
def send_head(self):
|
|
|
85 |
"""Common code for GET and HEAD commands.
|
|
|
86 |
|
|
|
87 |
This sends the response code and MIME headers.
|
|
|
88 |
|
|
|
89 |
Return value is either a file object (which has to be copied
|
|
|
90 |
to the outputfile by the caller unless the command was HEAD,
|
|
|
91 |
and must be closed by the caller under all circumstances), or
|
|
|
92 |
None, in which case the caller has nothing further to do.
|
|
|
93 |
|
|
|
94 |
"""
|
|
|
95 |
|
|
|
96 |
path,embedded = self.translate_path(self.path)
|
|
|
97 |
#uplog("HTTP: translated: embedded %s path: %s" %
|
|
|
98 |
# (embedded, printable(path)))
|
|
|
99 |
|
|
|
100 |
if not path or not os.path.exists(path):
|
|
|
101 |
self.send_error(404)
|
|
|
102 |
return (None, 0, 0)
|
|
|
103 |
|
|
|
104 |
if not os.path.isfile(path):
|
|
|
105 |
self.send_error(405)
|
|
|
106 |
return (None, 0, 0)
|
|
|
107 |
|
|
|
108 |
f = None
|
|
|
109 |
try:
|
|
|
110 |
if embedded:
|
|
|
111 |
ctype, size, f = embedded_open(path)
|
|
|
112 |
fs = os.stat(path)
|
|
|
113 |
#uplog("embedded, got ctype %s size %s" %(ctype, size))
|
|
|
114 |
else:
|
|
|
115 |
ctype = self.guess_type(path)
|
|
|
116 |
f = open(path, 'rb')
|
|
|
117 |
fs = os.fstat(f.fileno())
|
|
|
118 |
size = int(fs[6])
|
|
|
119 |
except Exception as err:
|
|
|
120 |
self.send_error(404, "File not found")
|
|
|
121 |
return (None, 0, 0)
|
|
|
122 |
|
|
|
123 |
if "Range" in self.headers:
|
|
|
124 |
self.send_response(206)
|
|
|
125 |
else:
|
|
|
126 |
self.send_response(200)
|
|
|
127 |
|
|
|
128 |
self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
|
|
|
129 |
self.send_header("Content-type", ctype)
|
|
|
130 |
start_range = 0
|
|
|
131 |
end_range = size
|
|
|
132 |
self.send_header("Accept-Ranges", "bytes")
|
|
|
133 |
if "Range" in self.headers:
|
|
|
134 |
s, e = self.headers['range'][6:].split('-', 1)
|
|
|
135 |
sl = len(s)
|
|
|
136 |
el = len(e)
|
|
|
137 |
if sl > 0:
|
|
|
138 |
start_range = int(s)
|
|
|
139 |
if el > 0:
|
|
|
140 |
end_range = int(e) + 1
|
|
|
141 |
elif el > 0:
|
|
|
142 |
ei = int(e)
|
|
|
143 |
if ei < size:
|
|
|
144 |
start_range = size - ei
|
|
|
145 |
self.send_header("Content-Range",
|
|
|
146 |
'bytes ' + str(start_range) + '-' +
|
|
|
147 |
str(end_range - 1) + '/' + str(size))
|
|
|
148 |
self.send_header("Content-Length", end_range - start_range)
|
|
|
149 |
self.end_headers()
|
|
|
150 |
#uplog("Sending Bytes %d to %d" % (start_range, end_range))
|
|
|
151 |
return (f, start_range, end_range)
|
|
|
152 |
|
|
|
153 |
|
|
|
154 |
def translate_path(self, opath):
|
|
|
155 |
path = urllib.unquote(opath)
|
|
|
156 |
# pathprefix is something like /uprcl
|
|
|
157 |
path = path.replace(self.uprclpathprefix, '', 1)
|
|
|
158 |
|
|
|
159 |
q = urlparse.urlparse(path)
|
|
|
160 |
path = q.path
|
|
|
161 |
embedded = False
|
|
|
162 |
pq = urlparse.parse_qs(q.query)
|
|
|
163 |
if 'embed' in pq:
|
|
|
164 |
embedded = True
|
|
|
165 |
|
|
|
166 |
for fsp,htp in self.uprclpathmap.iteritems():
|
|
|
167 |
if path.startswith(fsp):
|
|
|
168 |
path = path.replace(fsp, htp, 1)
|
|
|
169 |
if embedded:
|
|
|
170 |
# Embedded image urls have had a .jpg or .png
|
|
|
171 |
# appended. Remove it to restore the track path
|
|
|
172 |
# name.
|
|
|
173 |
i = path.rfind('.')
|
|
|
174 |
path = path[:i]
|
|
|
175 |
return path, embedded
|
|
|
176 |
|
|
|
177 |
# Security feature here: never allow access to anything not in
|
|
|
178 |
# the path map
|
|
|
179 |
uplog("HTTP: translate_path: %s not found in path map" % opath)
|
|
|
180 |
return None, None
|
|
|
181 |
|
|
|
182 |
|
|
|
183 |
def guess_type(self, path):
|
|
|
184 |
"""Guess the type of a file.
|
|
|
185 |
|
|
|
186 |
Argument is a PATH (a filename).
|
|
|
187 |
|
|
|
188 |
Return value is a string of the form type/subtype,
|
|
|
189 |
usable for a MIME Content-type header.
|
|
|
190 |
|
|
|
191 |
The default implementation looks the file's extension
|
|
|
192 |
up in the table self.extensions_map, using application/octet-stream
|
|
|
193 |
as a default; however it would be permissible (if
|
|
|
194 |
slow) to look inside the data to make a better guess.
|
|
|
195 |
|
|
|
196 |
"""
|
|
|
197 |
|
|
|
198 |
base, ext = posixpath.splitext(path)
|
|
|
199 |
if ext in self.extensions_map:
|
|
|
200 |
return self.extensions_map[ext]
|
|
|
201 |
ext = ext.lower()
|
|
|
202 |
if ext in self.extensions_map:
|
|
|
203 |
return self.extensions_map[ext]
|
|
|
204 |
else:
|
|
|
205 |
return self.extensions_map['']
|
|
|
206 |
|
|
|
207 |
if not mimetypes.inited:
|
|
|
208 |
mimetypes.init() # try to read system mime.types
|
|
|
209 |
extensions_map = mimetypes.types_map.copy()
|
|
|
210 |
extensions_map.update({
|
|
|
211 |
'': 'application/octet-stream', # Default
|
|
|
212 |
'.mp4': 'video/mp4',
|
|
|
213 |
'.ogg': 'video/ogg',
|
|
|
214 |
})
|
|
|
215 |
|
|
|
216 |
|
|
|
217 |
class ThreadingSimpleServer(SocketServer.ThreadingMixIn,
|
|
|
218 |
BaseHTTPServer.HTTPServer):
|
|
|
219 |
# Override handle_error as the default version writes to stdout !
|
|
|
220 |
def handle_error(self, request, client_address):
|
|
|
221 |
# Actually, we generally don't care about errors...
|
|
|
222 |
return
|
|
|
223 |
|
|
|
224 |
uplog('-'*40)
|
|
|
225 |
uplog('Exception happened during processing of request from %s' %
|
|
|
226 |
str(client_address))
|
|
|
227 |
import traceback
|
|
|
228 |
traceback.print_exc() # XXX But this goes to stderr! (jf: yep :)
|
|
|
229 |
uplog('-'*40)
|
|
|
230 |
|
|
|
231 |
|
|
|
232 |
def runHttp(host='', port=8080, pthstr='', pathprefix=''):
|
|
|
233 |
|
|
|
234 |
lpth = pthstr.split(',')
|
100 |
lpth = pthstr.split(',')
|
235 |
pathmap = {}
|
|
|
236 |
for ptt in lpth:
|
101 |
for ptt in lpth:
|
237 |
l = ptt.split(':')
|
102 |
l = ptt.split(':')
|
238 |
pathmap[l[0]] = l[1]
|
103 |
rt = l[0]
|
239 |
# Set pathmap as request handler class variable
|
104 |
if rt[-1] != '/':
|
240 |
RangeHTTPRequestHandler.uprclpathmap = pathmap
|
105 |
rt += '/'
|
241 |
RangeHTTPRequestHandler.uprclpathprefix = pathprefix
|
106 |
rt += '<filepath:path>'
|
242 |
|
107 |
uplog("runbottle: adding route for: %s"%rt)
|
243 |
server = ThreadingSimpleServer((host, port), RangeHTTPRequestHandler)
|
108 |
# We build the streamer with the translated
|
244 |
while 1:
|
109 |
streamer = Streamer(l[1])
|
245 |
server.handle_request()
|
110 |
bottle.route(rt, 'GET', streamer)
|
246 |
|
111 |
|
247 |
if __name__ == '__main__':
|
112 |
bottle.run(server='paste', host=host, port=port)
|
248 |
if len(sys.argv) != 5:
|
|
|
249 |
print("Usage: uprclhttp.py <host> <port> <pthmap> <pthprefix>",
|
|
|
250 |
file=sys.stderr)
|
|
|
251 |
sys.exit(1)
|
|
|
252 |
runHttp(host=sys.argv[1], port = int(sys.argv[2]), pthstr=sys.argv[3],
|
|
|
253 |
pathprefix=sys.argv[4])
|
|
|