2 # -*- coding: utf-8 -*-
4 VPProxy: HTTP/HLS Stream to HTTP Multiplexing Proxy
6 Based on AceProxy (https://github.com/ValdikSS/AceProxy) design
11 # Monkeypatching and all the stuff
13 gevent.monkey.patch_all()
22 from socket import error as SocketException
23 from socket import SHUT_RDWR
27 from vpconfig import VPConfig
29 import plugins.modules.ipaddr as ipaddr
30 from clientcounter import ClientCounter
31 from plugins.modules.PluginInterface import VPProxyPlugin
40 class HTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
44 def handle_one_request(self):
46 Add request to requestlist, handle request and remove from the list
48 HTTPHandler.requestlist.append(self)
49 BaseHTTPServer.BaseHTTPRequestHandler.handle_one_request(self)
50 HTTPHandler.requestlist.remove(self)
52 def closeConnection(self):
56 if self.clientconnected:
57 self.clientconnected = False
61 self.connection.shutdown(SHUT_RDWR)
65 def dieWithError(self, errorcode=500):
67 Close connection with error
69 logging.warning("Dying with error")
70 if self.clientconnected:
71 self.send_error(errorcode)
73 self.closeConnection()
75 def proxyReadWrite(self):
77 Read video stream and send it to client
79 logger = logging.getLogger('http_proxyReadWrite')
80 logger.debug("Started")
83 self.streamstate = True
88 if not self.clientconnected:
89 logger.debug("Client is not connected, terminating")
92 data = self.video.read(4096)
93 if data and self.clientconnected:
94 self.wfile.write(data)
96 logger.warning("Video connection closed")
99 except SocketException:
100 # Video connection dropped
101 logger.warning("Video connection dropped")
104 self.closeConnection()
106 def hangDetector(self):
108 Detect client disconnection while in the middle of something
109 or just normal connection close.
111 logger = logging.getLogger('http_hangDetector')
114 if not self.rfile.read():
119 self.clientconnected = False
120 logger.debug("Client disconnected")
122 self.requestgreenlet.kill()
130 return self.do_GET(headers_only=True)
132 def do_GET(self, headers_only=False):
136 logger = logging.getLogger('http_HTTPHandler')
137 self.clientconnected = True
138 # Don't wait videodestroydelay if error happened
139 self.errorhappened = True
140 # Headers sent flag for fake headers UAs
141 self.headerssent = False
143 self.requestgreenlet = gevent.getcurrent()
144 # Connected client IP address
145 self.clientip = self.request.getpeername()[0]
147 if VPConfig.firewall:
148 # If firewall enabled
149 self.clientinrange = any(map(lambda i: ipaddr.IPAddress(self.clientip) \
150 in ipaddr.IPNetwork(i), VPConfig.firewallnetranges))
152 if (VPConfig.firewallblacklistmode and self.clientinrange) or \
153 (not VPConfig.firewallblacklistmode and not self.clientinrange):
154 logger.info('Dropping connection from ' + self.clientip + ' due to ' + \
156 self.dieWithError(403) # 403 Forbidden
159 logger.info("Accepted connection from " + self.clientip + " path " + self.path)
162 self.splittedpath = self.path.split('/')
163 self.reqtype = self.splittedpath[1].lower()
164 # If first parameter is 'pid' or 'torrent' or it should be handled
166 if not (self.reqtype=='get' or self.reqtype in VPStuff.pluginshandlers):
167 self.dieWithError(400) # 400 Bad Request
170 self.dieWithError(400) # 400 Bad Request
173 # Handle request with plugin handler
174 if self.reqtype in VPStuff.pluginshandlers:
176 VPStuff.pluginshandlers.get(self.reqtype).handle(self)
177 except Exception as e:
178 logger.error('Plugin exception: ' + repr(e))
179 logger.error(traceback.format_exc())
182 self.closeConnection()
184 self.handleRequest(headers_only)
186 def handleRequest(self, headers_only):
188 # Limit concurrent connections
189 if 0 < VPConfig.maxconns <= VPStuff.clientcounter.total:
190 logger.debug("Maximum connections reached, can't serve this")
191 self.dieWithError(503) # 503 Service Unavailable
194 # Pretend to work fine with Fake UAs or HEAD request.
195 useragent = self.headers.get('User-Agent')
196 fakeua = useragent and useragent in VPConfig.fakeuas
197 if headers_only or fakeua:
199 logger.debug("Got fake UA: " + self.headers.get('User-Agent'))
200 # Return 200 and exit
201 self.send_response(200)
202 self.send_header("Content-Type", "video/mpeg")
204 self.closeConnection()
207 self.path_unquoted = urllib2.unquote('/'.join(self.splittedpath[2:]))
208 # Make list with parameters
210 for i in xrange(3, 8):
212 self.params.append(int(self.splittedpath[i]))
213 except (IndexError, ValueError):
214 self.params.append('0')
216 # Adding client to clientcounter
217 clients = VPStuff.clientcounter.add(self.path_unquoted, self.clientip)
218 # If we are the one client, but sucessfully got vp instance from clientcounter,
219 # then somebody is waiting in the videodestroydelay state
221 # Check if we are first client
222 if VPStuff.clientcounter.get(self.path_unquoted)==1:
223 logger.debug("First client, should create VLC session")
224 shouldcreatevp = True
226 logger.debug("Can reuse existing session")
227 shouldcreatevp = False
229 self.vlcid = hashlib.md5(self.path_unquoted).hexdigest()
231 # Send fake headers if this User-Agent is in fakeheaderuas tuple
234 "Sending fake headers for " + useragent)
235 self.send_response(200)
236 self.send_header("Content-Type", "video/mpeg")
238 # Do not send real headers at all
239 self.headerssent = True
242 self.hanggreenlet = gevent.spawn(self.hangDetector)
243 logger.debug("hangDetector spawned")
246 # Initializing VPClient
249 self.errorhappened = False
253 logger.debug("Got url " + self.path_unquoted)
254 # Force ffmpeg demuxing if set in config
255 if VPConfig.vlcforceffmpeg:
256 self.vlcprefix = 'http/ffmpeg://'
260 VPStuff.vlcclient.startBroadcast(
261 self.vlcid, self.vlcprefix + self.path_unquoted, VPConfig.vlcmux, VPConfig.vlcpreaccess)
262 # Sleep a bit, because sometimes VLC doesn't open port in
266 # Building new VLC url
267 self.url = 'http://' + VPConfig.vlchost + \
268 ':' + str(VPConfig.vlcoutport) + '/' + self.vlcid
269 logger.debug("VLC url " + self.url)
271 # Sending client headers to videostream
272 self.video = urllib2.Request(self.url)
273 for key in self.headers.dict:
274 self.video.add_header(key, self.headers.dict[key])
276 self.video = urllib2.urlopen(self.video)
278 # Sending videostream headers to client
279 if not self.headerssent:
280 self.send_response(self.video.getcode())
281 if self.video.info().dict.has_key('connection'):
282 del self.video.info().dict['connection']
283 if self.video.info().dict.has_key('server'):
284 del self.video.info().dict['server']
285 if self.video.info().dict.has_key('transfer-encoding'):
286 del self.video.info().dict['transfer-encoding']
287 if self.video.info().dict.has_key('keep-alive'):
288 del self.video.info().dict['keep-alive']
290 for key in self.video.info().dict:
291 self.send_header(key, self.video.info().dict[key])
292 # End headers. Next goes video data
294 logger.debug("Headers sent")
297 self.proxyReadWrite()
299 # Waiting until hangDetector is joined
300 self.hanggreenlet.join()
301 logger.debug("Request handler finished")
303 except (vpclient.VPException, vlcclient.VlcException, urllib2.URLError) as e:
304 logger.error("Exception: " + repr(e))
305 self.errorhappened = True
307 except gevent.GreenletExit:
308 # hangDetector told us about client disconnection
312 logger.error(traceback.format_exc())
313 self.errorhappened = True
317 logger.debug("END REQUEST")
318 VPStuff.clientcounter.delete(self.path_unquoted, self.clientip)
319 if not VPStuff.clientcounter.get(self.path_unquoted):
321 logger.debug("That was the last client, destroying VPClient")
322 VPStuff.vlcclient.stopBroadcast(self.vlcid)
328 class HTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
330 def handle_error(self, request, client_address):
331 # Do not print HTTP tracebacks
335 class VPStuff(object):
337 Inter-class interaction class
341 # taken from http://stackoverflow.com/questions/2699907/dropping-root-permissions-in-python
342 def drop_privileges(uid_name, gid_name='nogroup'):
344 # Get the uid/gid from the name
345 running_uid = pwd.getpwnam(uid_name).pw_uid
346 running_uid_home = pwd.getpwnam(uid_name).pw_dir
347 running_gid = grp.getgrnam(gid_name).gr_gid
349 # Remove group privileges
352 # Try setting the new uid/gid
353 os.setgid(running_gid)
354 os.setuid(running_uid)
356 # Ensure a very conservative umask
357 old_umask = os.umask(077)
359 if os.getuid() == running_uid and os.getgid() == running_gid:
361 os.environ['HOME'] = running_uid_home
366 filename=VPConfig.logpath + 'vphttp.log' if VPConfig.loggingtoafile else None,
367 format='%(asctime)s %(levelname)s %(name)s: %(message)s', datefmt='%d.%m.%Y %H:%M:%S', level=VPConfig.debug)
368 logger = logging.getLogger('INIT')
371 # Trying to change dir (would fail in freezed state)
373 os.chdir(os.path.dirname(os.path.realpath(__file__)))
376 # Creating dict of handlers
377 VPStuff.pluginshandlers = dict()
378 # And a list with plugin instances
379 VPStuff.pluginlist = list()
380 pluginsmatch = glob.glob('plugins/*_plugin.py')
381 sys.path.insert(0, 'plugins')
382 pluginslist = [os.path.splitext(os.path.basename(x))[0] for x in pluginsmatch]
383 for i in pluginslist:
384 plugin = __import__(i)
385 plugname = i.split('_')[0].capitalize()
387 plugininstance = getattr(plugin, plugname)(VPConfig, VPStuff)
388 except Exception as e:
389 logger.error("Cannot load plugin " + plugname + ": " + repr(e))
391 logger.debug('Plugin loaded: ' + plugname)
392 for j in plugininstance.handlers:
393 VPStuff.pluginshandlers[j] = plugininstance
394 VPStuff.pluginlist.append(plugininstance)
396 # Check whether we can bind to the defined port safely
397 if os.getuid() != 0 and VPConfig.httpport <= 1024:
398 logger.error("Cannot bind to port " + str(VPConfig.httpport) + " without root privileges")
401 server = HTTPServer((VPConfig.httphost, VPConfig.httpport), HTTPHandler)
402 logger = logging.getLogger('HTTP')
404 # Dropping root privileges if needed
405 if VPConfig.vpproxyuser and os.getuid() == 0:
406 if drop_privileges(VPConfig.vpproxyuser):
407 logger.info("Dropped privileges to user " + VPConfig.vpproxyuser)
409 logger.error("Cannot drop privileges to user " + VPConfig.vpproxyuser)
412 # Creating ClientCounter
413 VPStuff.clientcounter = ClientCounter()
415 DEVNULL = open(os.devnull, 'wb')
417 # Spawning procedures
418 def spawnVLC(cmd, delay = 0):
420 VPStuff.vlc = psutil.Popen(cmd, stdout=DEVNULL, stderr=DEVNULL)
428 VPStuff.vlcclient = vlcclient.VlcClient(
429 host=VPConfig.vlchost, port=VPConfig.vlcport, password=VPConfig.vlcpass,
430 out_port=VPConfig.vlcoutport)
432 except vlcclient.VlcException as e:
436 def isRunning(process):
437 if psutil.version_info[0] >= 2:
438 if process.is_running() and process.status() != psutil.STATUS_ZOMBIE:
440 else: # for older versions of psutil
441 if process.is_running() and process.status != psutil.STATUS_ZOMBIE:
445 def findProcess(name):
446 for proc in psutil.process_iter():
448 pinfo = proc.as_dict(attrs=['pid', 'name'])
449 if pinfo['name'] == name:
451 except psutil.AccessDenied:
454 except psutil.NoSuchProcess:
460 # Trying to close all spawned processes gracefully
461 if isRunning(VPStuff.vlc):
462 if VPStuff.vlcclient:
463 VPStuff.vlcclient.destroy()
465 if isRunning(VPStuff.vlc):
469 # This is what we call to stop the server completely
470 def shutdown(signum = 0, frame = 0):
471 logger.info("Stopping server...")
472 # Closing all client connections
473 for connection in server.RequestHandlerClass.requestlist:
475 # Set errorhappened to prevent waiting for videodestroydelay
476 connection.errorhappened = True
477 connection.closeConnection()
479 logger.warning("Cannot kill a connection!")
481 server.server_close()
484 def _reloadconfig(signum=None, frame=None):
486 Reload configuration file.
491 logger = logging.getLogger('reloadconfig')
493 from vpconfig import VPConfig
494 logger.info('Config reloaded')
496 # setting signal handlers
498 gevent.signal(signal.SIGHUP, _reloadconfig)
499 gevent.signal(signal.SIGTERM, shutdown)
500 except AttributeError:
504 VPStuff.vlcProc = VPConfig.vlccmd.split()
505 if spawnVLC(VPStuff.vlcProc, VPConfig.vlcspawntimeout) and connectVLC():
506 logger.info("VLC spawned with pid " + str(VPStuff.vlc.pid))
508 logger.error('Cannot spawn or connect to VLC!')
513 logger.info("Using gevent %s" % gevent.__version__)
514 logger.info("Using psutil %s" % psutil.__version__)
515 logger.info("Using VLC %s" % VPStuff.vlcclient._vlcver)
516 logger.info("Server started.")
518 if not isRunning(VPStuff.vlc):
520 if spawnVLC(VPStuff.vlcProc, VPConfig.vlcspawntimeout) and connectVLC():
521 logger.info("VLC died, respawned it with pid " + str(VPStuff.vlc.pid))
523 logger.error("Cannot spawn VLC!")
526 # Return to our server tasks
527 server.handle_request()
528 except (KeyboardInterrupt, SystemExit):