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
12 gevent.monkey.patch_all()
13 # Startup delay for daemon restart
23 from socket import error as SocketException
24 from socket import SHUT_RDWR
28 from vpconfig import VPConfig
31 import plugins.modules.ipaddr as ipaddr
32 from clientcounter import ClientCounter
33 from plugins.modules.PluginInterface import VPProxyPlugin
42 from apscheduler.schedulers.background import BackgroundScheduler
44 class HTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
48 def handle_one_request(self):
50 Add request to requestlist, handle request and remove from the list
52 HTTPHandler.requestlist.append(self)
53 BaseHTTPServer.BaseHTTPRequestHandler.handle_one_request(self)
54 HTTPHandler.requestlist.remove(self)
56 def closeConnection(self):
60 if self.clientconnected:
61 self.clientconnected = False
65 self.connection.shutdown(SHUT_RDWR)
69 def dieWithError(self, errorcode=500):
71 Close connection with error
73 logging.warning("Dying with error")
74 if self.clientconnected:
75 self.send_error(errorcode)
77 self.closeConnection()
79 def proxyReadWrite(self):
81 Read video stream and send it to client
83 logger = logging.getLogger('http_proxyReadWrite')
84 logger.debug("Started")
87 self.streamstate = True
92 if not self.clientconnected:
93 logger.debug("Client is not connected, terminating")
96 VPStuff.vlcclient.mark(self.vlcid)
97 data = self.video.read(4096)
98 if data and self.clientconnected:
99 self.wfile.write(data)
101 logger.warning("Video connection closed")
104 except SocketException:
105 # Video connection dropped
106 logger.warning("Video connection dropped")
109 self.closeConnection()
111 def hangDetector(self):
113 Detect client disconnection while in the middle of something
114 or just normal connection close.
116 logger = logging.getLogger('http_hangDetector')
119 if not self.rfile.read():
124 self.clientconnected = False
125 logger.debug("Client disconnected")
127 self.requestgreenlet.kill()
135 return self.do_GET(headers_only=True)
137 def do_GET(self, headers_only=False):
142 logger = logging.getLogger('http_HTTPHandler')
143 self.clientconnected = True
144 # Don't wait videodestroydelay if error happened
145 self.errorhappened = True
146 # Headers sent flag for fake headers UAs
147 self.headerssent = False
149 self.requestgreenlet = gevent.getcurrent()
150 # Connected client IP address
151 self.clientip = self.request.getpeername()[0]
153 req_headers = self.headers
156 'forwarded-for': req_headers.get('X-Forwarded-For'),
157 'client-agent': req_headers.get('User-Agent'),
161 if VPConfig.firewall:
162 # If firewall enabled
163 self.clientinrange = any(map(lambda i: ipaddr.IPAddress(self.clientip) \
164 in ipaddr.IPNetwork(i), VPConfig.firewallnetranges))
166 if (VPConfig.firewallblacklistmode and self.clientinrange) or \
167 (not VPConfig.firewallblacklistmode and not self.clientinrange):
168 logger.info('Dropping connection from ' + self.clientip + ' due to ' + \
170 self.dieWithError(403) # 403 Forbidden
173 logger.info("Accepted connection from " + self.clientip + " path " + self.path)
176 self.splittedpath = self.path.split('/')
177 self.reqtype = self.splittedpath[1].lower()
178 # If first parameter is 'pid' or 'torrent' or it should be handled
180 if not (self.reqtype in ('get','mp4','ogg','ogv') or self.reqtype in VPStuff.pluginshandlers):
181 self.dieWithError(400) # 400 Bad Request
184 self.dieWithError(400) # 400 Bad Request
187 # Handle request with plugin handler
188 if self.reqtype in VPStuff.pluginshandlers:
190 VPStuff.pluginshandlers.get(self.reqtype).handle(self)
191 except Exception as e:
192 logger.error('Plugin exception: ' + repr(e))
193 logger.error(traceback.format_exc())
196 self.closeConnection()
198 self.handleRequest(headers_only)
200 def handleRequest(self, headers_only):
202 # Limit concurrent connections
203 if 0 < VPConfig.maxconns <= VPStuff.clientcounter.total:
204 logger.debug("Maximum connections reached, can't serve this")
205 self.dieWithError(503) # 503 Service Unavailable
208 # Pretend to work fine with Fake UAs or HEAD request.
209 useragent = self.headers.get('User-Agent')
210 logger.debug("HTTP User Agent:"+useragent)
211 fakeua = useragent and useragent in VPConfig.fakeuas
212 if headers_only or fakeua:
214 logger.debug("Got fake UA: " + self.headers.get('User-Agent'))
215 # Return 200 and exit
216 self.send_response(200)
217 self.send_header("Content-Type", "video/mpeg")
219 self.closeConnection()
222 self.path_unquoted = urllib2.unquote('/'.join(self.splittedpath[2:]))
223 # Make list with parameters
225 for i in xrange(3, 8):
227 self.params.append(int(self.splittedpath[i]))
228 except (IndexError, ValueError):
229 self.params.append('0')
231 # Adding client to clientcounter
232 clients = VPStuff.clientcounter.add(self.reqtype+'/'+self.path_unquoted, self.client_data)
233 # If we are the one client, but sucessfully got vp instance from clientcounter,
234 # then somebody is waiting in the videodestroydelay state
236 # Check if we are first client
238 self.vlcid = hashlib.md5(self.reqtype+'/'+self.path_unquoted).hexdigest()
241 if not VPStuff.vlcclient.check_stream(self.vlcid):
242 logger.debug("First client, should create VLC session")
243 shouldcreatevp = True
245 logger.debug("Can reuse existing session")
246 shouldcreatevp = False
247 except Exception as e:
248 logger.error('Plugin exception: ' + repr(e))
249 logger.error(traceback.format_exc())
252 # Send fake headers if this User-Agent is in fakeheaderuas tuple
255 "Sending fake headers for " + useragent)
256 self.send_response(200)
257 self.send_header('Cache-Control','no-cache, no-store, must-revalidate');
258 self.send_header('Pragma','no-cache');
259 if self.reqtype in ("ogg","ogv"):
260 self.send_header("Content-Type", "video/ogg")
262 self.send_header("Content-Type", "video/mpeg")
264 # Do not send real headers at all
265 self.headerssent = True
268 self.hanggreenlet = gevent.spawn(self.hangDetector)
269 logger.debug("hangDetector spawned")
273 self.errorhappened = False
276 logger.debug("Got url " + self.path_unquoted)
277 # Force ffmpeg demuxing if set in config
278 if VPConfig.vlcforceffmpeg:
279 self.vlcprefix = 'http/ffmpeg://'
283 logger.info("Starting broadcasting "+self.path)
284 VPStuff.vlcclient.startBroadcast(
285 self.vlcid, self.vlcprefix + self.path_unquoted, VPConfig.vlcmux, VPConfig.vlcpreaccess, self.reqtype)
286 # Sleep a bit, because sometimes VLC doesn't open port in
290 # Building new VLC url
291 self.url = 'http://' + VPConfig.vlchost + \
292 ':' + str(VPConfig.vlcoutport) + '/' + self.vlcid
293 logger.debug("VLC url " + self.url)
295 # Sending client headers to videostream
296 self.video = urllib2.Request(self.url)
297 for key in self.headers.dict:
298 self.video.add_header(key, self.headers.dict[key])
300 self.video = urllib2.urlopen(self.video)
302 # Sending videostream headers to client
303 if not self.headerssent:
304 self.send_response(self.video.getcode())
305 if self.video.info().dict.has_key('connection'):
306 del self.video.info().dict['connection']
307 if self.video.info().dict.has_key('server'):
308 del self.video.info().dict['server']
309 if self.video.info().dict.has_key('transfer-encoding'):
310 del self.video.info().dict['transfer-encoding']
311 if self.video.info().dict.has_key('content-type'):
312 del self.video.info().dict['content-type']
313 if self.video.info().dict.has_key('keep-alive'):
314 del self.video.info().dict['keep-alive']
316 for key in self.video.info().dict:
317 self.send_header(key, self.video.info().dict[key])
319 self.send_header('Cache-Control','no-cache, no-store, must-revalidate');
320 self.send_header('Pragma','no-cache');
322 if self.reqtype=="ogg":
323 self.send_header("Content-Type", "video/ogg")
325 self.send_header("Content-Type", "video/mpeg")
327 # End headers. Next goes video data
329 logger.debug("Headers sent")
330 self.headerssent = True
333 self.proxyReadWrite()
335 # Waiting until hangDetector is joined
336 self.hanggreenlet.join()
337 logger.debug("Request handler finished")
338 except (vlcclient.VlcException) as e:
339 logger.error("Exception: " + repr(e))
340 VPStuff.vlcerrors = VPStuff.vlcerrors + 1
341 logger.error("%s error(s) communicating VLC")
342 self.errorhappened = True
344 except (vpclient.VPException, vlcclient.VlcException, urllib2.URLError) as e:
345 logger.error("Exception: " + repr(e))
346 self.errorhappened = True
348 except gevent.GreenletExit:
349 # hangDetector told us about client disconnection
353 logger.error(traceback.format_exc())
354 self.errorhappened = True
357 logger.debug("END REQUEST")
358 logger.info("Closed connection from " + self.clientip + " path " + self.path)
359 VPStuff.clientcounter.delete(self.reqtype+'/'+self.path_unquoted, self.client_data)
362 class HTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
364 def handle_error(self, request, client_address):
365 # Do not print HTTP tracebacks
369 class VPStuff(object):
371 Inter-class interaction class
376 # taken from http://stackoverflow.com/questions/2699907/dropping-root-permissions-in-python
377 def drop_privileges(uid_name, gid_name='nogroup'):
379 # Get the uid/gid from the name
380 running_uid = pwd.getpwnam(uid_name).pw_uid
381 running_uid_home = pwd.getpwnam(uid_name).pw_dir
382 running_gid = grp.getgrnam(gid_name).gr_gid
384 # Remove group privileges
387 # Try setting the new uid/gid
388 os.setgid(running_gid)
389 os.setuid(running_uid)
391 # Ensure a very conservative umask
392 old_umask = os.umask(077)
394 if os.getuid() == running_uid and os.getgid() == running_gid:
396 os.environ['HOME'] = running_uid_home
401 filename=VPConfig.logpath + 'vphttp.log' if VPConfig.loggingtoafile else None,
402 format='%(asctime)s %(levelname)s %(name)s: %(message)s', datefmt='%d.%m.%Y %H:%M:%S', level=VPConfig.debug)
403 logger = logging.getLogger('INIT')
406 # Trying to change dir (would fail in freezed state)
408 os.chdir(os.path.dirname(os.path.realpath(__file__)))
411 # Creating dict of handlers
412 VPStuff.pluginshandlers = dict()
413 # And a list with plugin instances
414 VPStuff.pluginlist = list()
415 pluginsmatch = glob.glob('plugins/*_plugin.py')
416 sys.path.insert(0, 'plugins')
417 pluginslist = [os.path.splitext(os.path.basename(x))[0] for x in pluginsmatch]
418 for i in pluginslist:
419 plugin = __import__(i)
420 plugname = i.split('_')[0].capitalize()
422 plugininstance = getattr(plugin, plugname)(VPConfig, VPStuff)
423 except Exception as e:
424 logger.error("Cannot load plugin " + plugname + ": " + repr(e))
426 logger.debug('Plugin loaded: ' + plugname)
427 for j in plugininstance.handlers:
428 logger.info("Registering handler '" + j +"'")
429 VPStuff.pluginshandlers[j] = plugininstance
430 VPStuff.pluginlist.append(plugininstance)
432 # Check whether we can bind to the defined port safely
433 if os.getuid() != 0 and VPConfig.httpport <= 1024:
434 logger.error("Cannot bind to port " + str(VPConfig.httpport) + " without root privileges")
437 server = HTTPServer((VPConfig.httphost, VPConfig.httpport), HTTPHandler)
438 logger = logging.getLogger('HTTP')
440 # Dropping root privileges if needed
441 if VPConfig.vpproxyuser and os.getuid() == 0:
442 if drop_privileges(VPConfig.vpproxyuser):
443 logger.info("Dropped privileges to user " + VPConfig.vpproxyuser)
445 logger.error("Cannot drop privileges to user " + VPConfig.vpproxyuser)
448 # Creating ClientCounter
449 VPStuff.clientcounter = ClientCounter()
451 DEVNULL = open(os.devnull, 'wb')
453 # Spawning procedures
454 def spawnVLC(cmd, delay = 0):
456 VPStuff.vlc = psutil.Popen(cmd) #, stdout=DEVNULL, stderr=DEVNULL)
457 VPStuff.vlcerrors = 0
465 VPStuff.vlcclient = vlcclient.VlcClient(
466 host=VPConfig.vlchost, port=VPConfig.vlcport, password=VPConfig.vlcpass,
467 out_port=VPConfig.vlcoutport)
469 except vlcclient.VlcException as e:
472 def isRunning(process):
473 if psutil.version_info[0] >= 2:
474 if process.is_running() and process.status() != psutil.STATUS_ZOMBIE:
476 else: # for older versions of psutil
477 if process.is_running() and process.status != psutil.STATUS_ZOMBIE:
481 def findProcess(name):
482 for proc in psutil.process_iter():
484 pinfo = proc.as_dict(attrs=['pid', 'name'])
485 if pinfo['name'] == name:
487 except psutil.AccessDenied:
490 except psutil.NoSuchProcess:
496 # Trying to close all spawned processes gracefully
497 if isRunning(VPStuff.vlc):
498 if VPStuff.vlcclient:
499 VPStuff.vlcclient.destroy()
501 if isRunning(VPStuff.vlc):
503 VPStuff.vlc.terminate()
505 if isRunning(VPStuff.vlc):
509 def restartVLC(cmd, delay = 0):
511 if spawnVLC(cmd, delay):
516 # This is what we call to stop the server completely
517 def shutdown(signum = 0, frame = 0):
518 logger.info("Stopping server...")
519 # Closing all client connections
520 for connection in server.RequestHandlerClass.requestlist:
522 # Set errorhappened to prevent waiting for videodestroydelay
523 connection.errorhappened = True
524 connection.closeConnection()
526 logger.warning("Cannot kill a connection!")
528 server.server_close()
531 def _reloadconfig(signum=None, frame=None):
533 Reload configuration file.
538 logger = logging.getLogger('reloadconfig')
540 from vpconfig import VPConfig
541 logger.info('Config reloaded')
543 sched = BackgroundScheduler()
547 if VPStuff.vlcclient:
548 VPStuff.vlcclient.clean_streams(VPConfig.videodestroydelay)
550 job = sched.add_job(clean_streams, 'interval', seconds=15)
552 # setting signal handlers
554 gevent.signal(signal.SIGHUP, _reloadconfig)
555 gevent.signal(signal.SIGTERM, shutdown)
556 except AttributeError:
560 VPStuff.vlcProc = VPConfig.vlccmd.split()
561 if spawnVLC(VPStuff.vlcProc, VPConfig.vlcspawntimeout) and connectVLC():
562 logger.info("VLC spawned with pid " + str(VPStuff.vlc.pid))
564 logger.error('Cannot spawn or connect to VLC!')
569 logger.info("Using gevent %s" % gevent.__version__)
570 logger.info("Usig psutil %s" % psutil.__version__)
571 logger.info("Using VLC %s" % VPStuff.vlcclient._vlcver)
572 logger.info("Server started.")
575 if not isRunning(VPStuff.vlc):
578 if spawnVLC(VPStuff.vlcProc, VPConfig.vlcspawntimeout) and connectVLC():
579 logger.info("VLC died, respawned it with pid " + str(VPStuff.vlc.pid))
581 logger.error("Cannot spawn VLC!")
585 # Return to our server tasks
586 server.handle_request()
588 if VPStuff.vlcerrors>5:
589 if restartVLC(VPStuff.vlcProc, VPConfig.vlcspawntimeout):
590 logger.info("VLC hung, respawned it with pid " + str(VPStuff.vlc.pid))
592 logger.error("Cannot spawn VLC!")
596 except (KeyboardInterrupt, SystemExit):