|
a |
|
b/src/mediaserver/cdplugins/uprcl/uprclhttp.py |
|
|
1 |
#!/usr/bin/env python
|
|
|
2 |
#
|
|
|
3 |
# Copyright (C) 2017 J.F.Dockes
|
|
|
4 |
#
|
|
|
5 |
# HTTP Range code:
|
|
|
6 |
# Portions Copyright (C) 2009,2010 Xyne
|
|
|
7 |
# Portions Copyright (C) 2011 Sean Goller
|
|
|
8 |
# https://github.com/smgoller/rangehttpserver/blob/master/RangeHTTPServer.py
|
|
|
9 |
#
|
|
|
10 |
# This program is free software: you can redistribute it and/or modify
|
|
|
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 |
|
|
|
23 |
import SocketServer
|
|
|
24 |
import BaseHTTPServer
|
|
|
25 |
import SimpleHTTPServer
|
|
|
26 |
import os
|
|
|
27 |
import posixpath
|
|
|
28 |
import BaseHTTPServer
|
|
|
29 |
import urllib
|
|
|
30 |
import cgi
|
|
|
31 |
import shutil
|
|
|
32 |
import mimetypes
|
|
|
33 |
try:
|
|
|
34 |
from cStringIO import StringIO
|
|
|
35 |
except ImportError:
|
|
|
36 |
from StringIO import StringIO
|
|
|
37 |
|
|
|
38 |
from uprclutils import uplog
|
|
|
39 |
|
|
|
40 |
|
|
|
41 |
|
|
|
42 |
__version__ = "0.1"
|
|
|
43 |
|
|
|
44 |
class RangeHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
|
|
45 |
|
|
|
46 |
"""Simple HTTP request handler with GET and HEAD commands.
|
|
|
47 |
|
|
|
48 |
This serves files from the current directory and any of its
|
|
|
49 |
subdirectories. The MIME type for files is determined by
|
|
|
50 |
calling the .guess_type() method.
|
|
|
51 |
|
|
|
52 |
The GET and HEAD requests are identical except that the HEAD
|
|
|
53 |
request omits the actual contents of the file.
|
|
|
54 |
|
|
|
55 |
"""
|
|
|
56 |
|
|
|
57 |
server_version = "RangeHTTP/" + __version__
|
|
|
58 |
|
|
|
59 |
def do_GET(self):
|
|
|
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 |
uplog("HTTP: path: %s" % self.path)
|
|
|
96 |
path = self.translate_path(self.path)
|
|
|
97 |
if not path or not os.path.exists(path):
|
|
|
98 |
self.send_error(404)
|
|
|
99 |
return (None, 0, 0)
|
|
|
100 |
|
|
|
101 |
if not os.path.isfile(path):
|
|
|
102 |
self.send_error(405)
|
|
|
103 |
return (None, 0, 0)
|
|
|
104 |
|
|
|
105 |
f = None
|
|
|
106 |
ctype = self.guess_type(path)
|
|
|
107 |
try:
|
|
|
108 |
f = open(path, 'rb')
|
|
|
109 |
except:
|
|
|
110 |
self.send_error(404, "File not found")
|
|
|
111 |
return (None, 0, 0)
|
|
|
112 |
|
|
|
113 |
if "Range" in self.headers:
|
|
|
114 |
self.send_response(206)
|
|
|
115 |
else:
|
|
|
116 |
self.send_response(200)
|
|
|
117 |
|
|
|
118 |
self.send_header("Content-type", ctype)
|
|
|
119 |
fs = os.fstat(f.fileno())
|
|
|
120 |
size = int(fs[6])
|
|
|
121 |
start_range = 0
|
|
|
122 |
end_range = size
|
|
|
123 |
self.send_header("Accept-Ranges", "bytes")
|
|
|
124 |
if "Range" in self.headers:
|
|
|
125 |
s, e = self.headers['range'][6:].split('-', 1)
|
|
|
126 |
sl = len(s)
|
|
|
127 |
el = len(e)
|
|
|
128 |
if sl > 0:
|
|
|
129 |
start_range = int(s)
|
|
|
130 |
if el > 0:
|
|
|
131 |
end_range = int(e) + 1
|
|
|
132 |
elif el > 0:
|
|
|
133 |
ei = int(e)
|
|
|
134 |
if ei < size:
|
|
|
135 |
start_range = size - ei
|
|
|
136 |
self.send_header("Content-Range",
|
|
|
137 |
'bytes ' + str(start_range) + '-' +
|
|
|
138 |
str(end_range - 1) + '/' + str(size))
|
|
|
139 |
self.send_header("Content-Length", end_range - start_range)
|
|
|
140 |
self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
|
|
|
141 |
self.end_headers()
|
|
|
142 |
#uplog("Sending Bytes %d to %d" % (start_range, end_range))
|
|
|
143 |
return (f, start_range, end_range)
|
|
|
144 |
|
|
|
145 |
|
|
|
146 |
def translate_path(self, opath):
|
|
|
147 |
path = urllib.unquote(opath)
|
|
|
148 |
for p in self.uprclpathmap.itervalues():
|
|
|
149 |
if path.startswith(p):
|
|
|
150 |
return path
|
|
|
151 |
uplog("HTTP: translate_path: %s not found in path map" % opath)
|
|
|
152 |
return None
|
|
|
153 |
|
|
|
154 |
def guess_type(self, path):
|
|
|
155 |
"""Guess the type of a file.
|
|
|
156 |
|
|
|
157 |
Argument is a PATH (a filename).
|
|
|
158 |
|
|
|
159 |
Return value is a string of the form type/subtype,
|
|
|
160 |
usable for a MIME Content-type header.
|
|
|
161 |
|
|
|
162 |
The default implementation looks the file's extension
|
|
|
163 |
up in the table self.extensions_map, using application/octet-stream
|
|
|
164 |
as a default; however it would be permissible (if
|
|
|
165 |
slow) to look inside the data to make a better guess.
|
|
|
166 |
|
|
|
167 |
"""
|
|
|
168 |
|
|
|
169 |
base, ext = posixpath.splitext(path)
|
|
|
170 |
if ext in self.extensions_map:
|
|
|
171 |
return self.extensions_map[ext]
|
|
|
172 |
ext = ext.lower()
|
|
|
173 |
if ext in self.extensions_map:
|
|
|
174 |
return self.extensions_map[ext]
|
|
|
175 |
else:
|
|
|
176 |
return self.extensions_map['']
|
|
|
177 |
|
|
|
178 |
if not mimetypes.inited:
|
|
|
179 |
mimetypes.init() # try to read system mime.types
|
|
|
180 |
extensions_map = mimetypes.types_map.copy()
|
|
|
181 |
extensions_map.update({
|
|
|
182 |
'': 'application/octet-stream', # Default
|
|
|
183 |
'.mp4': 'video/mp4',
|
|
|
184 |
'.ogg': 'video/ogg',
|
|
|
185 |
})
|
|
|
186 |
|
|
|
187 |
|
|
|
188 |
class ThreadingSimpleServer(SocketServer.ThreadingMixIn,
|
|
|
189 |
BaseHTTPServer.HTTPServer):
|
|
|
190 |
pass
|
|
|
191 |
|
|
|
192 |
|
|
|
193 |
def runHttp(host='', port=8080, pathmap={}):
|
|
|
194 |
|
|
|
195 |
# Set pathmap as request handler class variable
|
|
|
196 |
RangeHTTPRequestHandler.uprclpathmap = pathmap
|
|
|
197 |
|
|
|
198 |
server = ThreadingSimpleServer((host, port), RangeHTTPRequestHandler)
|
|
|
199 |
while 1:
|
|
|
200 |
server.handle_request()
|