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
30 import plugins.modules.ipaddr as ipaddr
31 from clientcounter import ClientCounter
32 from plugins.modules.PluginInterface import VPProxyPlugin
41 class HTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
45 def handle_one_request(self):
47 Add request to requestlist, handle request and remove from the list
49 HTTPHandler.requestlist.append(self)
50 BaseHTTPServer.BaseHTTPRequestHandler.handle_one_request(self)
51 HTTPHandler.requestlist.remove(self)
53 def closeConnection(self):
57 if self.clientconnected:
58 self.clientconnected = False
62 self.connection.shutdown(SHUT_RDWR)
66 def dieWithError(self, errorcode=500):
68 Close connection with error
70 logging.warning("Dying with error")
71 if self.clientconnected:
72 self.send_error(errorcode)
74 self.closeConnection()
76 def proxyReadWrite(self):
78 Read video stream and send it to client
80 logger = logging.getLogger('http_proxyReadWrite')
81 logger.debug("Started")
84 self.streamstate = True
89 if not self.clientconnected:
90 logger.debug("Client is not connected, terminating")
93 data = self.video.read(4096)
94 if data and self.clientconnected:
95 self.wfile.write(data)
97 logger.warning("Video connection closed")
100 except SocketException:
101 # Video connection dropped
102 logger.warning("Video connection dropped")
105 self.closeConnection()
107 def hangDetector(self):
109 Detect client disconnection while in the middle of something
110 or just normal connection close.
112 logger = logging.getLogger('http_hangDetector')
115 if not self.rfile.read():
120 self.clientconnected = False
121 logger.debug("Client disconnected")
123 self.requestgreenlet.kill()
131 return self.do_GET(headers_only=True)
133 def do_GET(self, headers_only=False):
137 logger = logging.getLogger('http_HTTPHandler')
138 self.clientconnected = True
139 # Don't wait videodestroydelay if error happened
140 self.errorhappened = True
141 # Headers sent flag for fake headers UAs
142 self.headerssent = False
144 self.requestgreenlet = gevent.getcurrent()
145 # Connected client IP address
146 self.clientip = self.request.getpeername()[0]
148 if VPConfig.firewall:
149 # If firewall enabled
150 self.clientinrange = any(map(lambda i: ipaddr.IPAddress(self.clientip) \
151 in ipaddr.IPNetwork(i), VPConfig.firewallnetranges))
153 if (VPConfig.firewallblacklistmode and self.clientinrange) or \
154 (not VPConfig.firewallblacklistmode and not self.clientinrange):
155 logger.info('Dropping connection from ' + self.clientip + ' due to ' + \
157 self.dieWithError(403) # 403 Forbidden
160 logger.info("Accepted connection from " + self.clientip + " path " + self.path)
163 self.splittedpath = self.path.split('/')
164 self.reqtype = self.splittedpath[1].lower()
165 # If first parameter is 'pid' or 'torrent' or it should be handled
167 if not (self.reqtype in ('get','mp4','ogg') or self.reqtype in VPStuff.pluginshandlers):
168 self.dieWithError(400) # 400 Bad Request
171 self.dieWithError(400) # 400 Bad Request
174 # Handle request with plugin handler
175 if self.reqtype in VPStuff.pluginshandlers:
177 VPStuff.pluginshandlers.get(self.reqtype).handle(self)
178 except Exception as e:
179 logger.error('Plugin exception: ' + repr(e))
180 logger.error(traceback.format_exc())
183 self.closeConnection()
185 self.handleRequest(headers_only)
187 def handleRequest(self, headers_only):
189 # Limit concurrent connections
190 if 0 < VPConfig.maxconns <= VPStuff.clientcounter.total:
191 logger.debug("Maximum connections reached, can't serve this")
192 self.dieWithError(503) # 503 Service Unavailable
195 # Pretend to work fine with Fake UAs or HEAD request.
196 useragent = self.headers.get('User-Agent')
197 logger.debug("HTTP User Agent:"+useragent)
198 fakeua = useragent and useragent in VPConfig.fakeuas
199 if headers_only or fakeua:
201 logger.debug("Got fake UA: " + self.headers.get('User-Agent'))
202 # Return 200 and exit
203 self.send_response(200)
204 self.send_header("Content-Type", "video/mpeg")
206 self.closeConnection()
209 self.path_unquoted = urllib2.unquote('/'.join(self.splittedpath[2:]))
210 # Make list with parameters
212 for i in xrange(3, 8):
214 self.params.append(int(self.splittedpath[i]))
215 except (IndexError, ValueError):
216 self.params.append('0')
218 # Adding client to clientcounter
219 clients = VPStuff.clientcounter.add(self.reqtype+'\\'+self.path_unquoted, self.clientip)
220 # If we are the one client, but sucessfully got vp instance from clientcounter,
221 # then somebody is waiting in the videodestroydelay state
223 # Check if we are first client
224 if VPStuff.clientcounter.get(self.reqtype+'\\'+self.path_unquoted)==1:
225 logger.debug("First client, should create VLC session")
226 shouldcreatevp = True
228 logger.debug("Can reuse existing session")
229 shouldcreatevp = False
231 self.vlcid = hashlib.md5(self.reqtype+'\\'+self.path_unquoted).hexdigest()
233 # Send fake headers if this User-Agent is in fakeheaderuas tuple
236 "Sending fake headers for " + useragent)
237 self.send_response(200)
238 if self.reqtype=="ogg":
239 self.send_header("Content-Type", "video/ogg")
241 self.send_header("Content-Type", "video/mpeg")
243 # Do not send real headers at all
244 self.headerssent = True
247 self.hanggreenlet = gevent.spawn(self.hangDetector)
248 logger.debug("hangDetector spawned")
252 self.errorhappened = False
255 logger.debug("Got url " + self.path_unquoted)
256 # Force ffmpeg demuxing if set in config
257 if VPConfig.vlcforceffmpeg:
258 self.vlcprefix = 'http/ffmpeg://'
262 logger.info("Starting broadcasting "+self.path)
263 VPStuff.vlcclient.startBroadcast(
264 self.vlcid, self.vlcprefix + self.path_unquoted, VPConfig.vlcmux, VPConfig.vlcpreaccess, self.reqtype)
265 # Sleep a bit, because sometimes VLC doesn't open port in
269 # Building new VLC url
270 self.url = 'http://' + VPConfig.vlchost + \
271 ':' + str(VPConfig.vlcoutport) + '/' + self.vlcid
272 logger.debug("VLC url " + self.url)
274 # Sending client headers to videostream
275 self.video = urllib2.Request(self.url)
276 for key in self.headers.dict:
277 self.video.add_header(key, self.headers.dict[key])
279 self.video = urllib2.urlopen(self.video)
281 # Sending videostream headers to client
282 if not self.headerssent:
283 self.send_response(self.video.getcode())
284 if self.video.info().dict.has_key('connection'):
285 del self.video.info().dict['connection']
286 if self.video.info().dict.has_key('server'):
287 del self.video.info().dict['server']
288 if self.video.info().dict.has_key('transfer-encoding'):
289 del self.video.info().dict['transfer-encoding']
290 if self.video.info().dict.has_key('content-type'):
291 del self.video.info().dict['content-type']
292 if self.video.info().dict.has_key('keep-alive'):
293 del self.video.info().dict['keep-alive']
295 for key in self.video.info().dict:
296 self.send_header(key, self.video.info().dict[key])
298 if self.reqtype=="ogg":
299 self.send_header("Content-Type", "video/ogg")
301 self.send_header("Content-Type", "video/mpeg")
303 # End headers. Next goes video data
305 logger.debug("Headers sent")
308 self.proxyReadWrite()
310 # Waiting until hangDetector is joined
311 self.hanggreenlet.join()
312 logger.debug("Request handler finished")
314 except (vpclient.VPException, vlcclient.VlcException, urllib2.URLError) as e:
315 logger.error("Exception: " + repr(e))
316 self.errorhappened = True
318 except gevent.GreenletExit:
319 # hangDetector told us about client disconnection
323 logger.error(traceback.format_exc())
324 self.errorhappened = True
327 logger.debug("END REQUEST")
328 logger.info("Closed connection from " + self.clientip + " path " + self.path)
329 VPStuff.clientcounter.delete(self.reqtype+'\\'+self.path_unquoted, self.clientip)
330 if not VPStuff.clientcounter.get(self.reqtype+'\\'+self.path_unquoted):
332 logger.debug("That was the last client, destroying VPClient")
333 logger.info("Stopping broadcasting " + self.path)
334 VPStuff.vlcclient.stopBroadcast(self.vlcid)
340 class HTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
342 def handle_error(self, request, client_address):
343 # Do not print HTTP tracebacks
347 class VPStuff(object):
349 Inter-class interaction class
353 # taken from http://stackoverflow.com/questions/2699907/dropping-root-permissions-in-python
354 def drop_privileges(uid_name, gid_name='nogroup'):
356 # Get the uid/gid from the name
357 running_uid = pwd.getpwnam(uid_name).pw_uid
358 running_uid_home = pwd.getpwnam(uid_name).pw_dir
359 running_gid = grp.getgrnam(gid_name).gr_gid
361 # Remove group privileges
364 # Try setting the new uid/gid
365 os.setgid(running_gid)
366 os.setuid(running_uid)
368 # Ensure a very conservative umask
369 old_umask = os.umask(077)
371 if os.getuid() == running_uid and os.getgid() == running_gid:
373 os.environ['HOME'] = running_uid_home
378 filename=VPConfig.logpath + 'vphttp.log' if VPConfig.loggingtoafile else None,
379 format='%(asctime)s %(levelname)s %(name)s: %(message)s', datefmt='%d.%m.%Y %H:%M:%S', level=VPConfig.debug)
380 logger = logging.getLogger('INIT')
383 # Trying to change dir (would fail in freezed state)
385 os.chdir(os.path.dirname(os.path.realpath(__file__)))
388 # Creating dict of handlers
389 VPStuff.pluginshandlers = dict()
390 # And a list with plugin instances
391 VPStuff.pluginlist = list()
392 pluginsmatch = glob.glob('plugins/*_plugin.py')
393 sys.path.insert(0, 'plugins')
394 pluginslist = [os.path.splitext(os.path.basename(x))[0] for x in pluginsmatch]
395 for i in pluginslist:
396 plugin = __import__(i)
397 plugname = i.split('_')[0].capitalize()
399 plugininstance = getattr(plugin, plugname)(VPConfig, VPStuff)
400 except Exception as e:
401 logger.error("Cannot load plugin " + plugname + ": " + repr(e))
403 logger.debug('Plugin loaded: ' + plugname)
404 for j in plugininstance.handlers:
405 VPStuff.pluginshandlers[j] = plugininstance
406 VPStuff.pluginlist.append(plugininstance)
408 # Check whether we can bind to the defined port safely
409 if os.getuid() != 0 and VPConfig.httpport <= 1024:
410 logger.error("Cannot bind to port " + str(VPConfig.httpport) + " without root privileges")
413 server = HTTPServer((VPConfig.httphost, VPConfig.httpport), HTTPHandler)
414 logger = logging.getLogger('HTTP')
416 # Dropping root privileges if needed
417 if VPConfig.vpproxyuser and os.getuid() == 0:
418 if drop_privileges(VPConfig.vpproxyuser):
419 logger.info("Dropped privileges to user " + VPConfig.vpproxyuser)
421 logger.error("Cannot drop privileges to user " + VPConfig.vpproxyuser)
424 # Creating ClientCounter
425 VPStuff.clientcounter = ClientCounter()
427 DEVNULL = open(os.devnull, 'wb')
429 # Spawning procedures
430 def spawnVLC(cmd, delay = 0):
432 VPStuff.vlc = psutil.Popen(cmd) #, stdout=DEVNULL, stderr=DEVNULL)
440 VPStuff.vlcclient = vlcclient.VlcClient(
441 host=VPConfig.vlchost, port=VPConfig.vlcport, password=VPConfig.vlcpass,
442 out_port=VPConfig.vlcoutport)
444 except vlcclient.VlcException as e:
447 def isRunning(process):
448 if psutil.version_info[0] >= 2:
449 if process.is_running() and process.status() != psutil.STATUS_ZOMBIE:
451 else: # for older versions of psutil
452 if process.is_running() and process.status != psutil.STATUS_ZOMBIE:
456 def findProcess(name):
457 for proc in psutil.process_iter():
459 pinfo = proc.as_dict(attrs=['pid', 'name'])
460 if pinfo['name'] == name:
462 except psutil.AccessDenied:
465 except psutil.NoSuchProcess:
471 # Trying to close all spawned processes gracefully
472 if isRunning(VPStuff.vlc):
473 if VPStuff.vlcclient:
474 VPStuff.vlcclient.destroy()
476 if isRunning(VPStuff.vlc):
480 # This is what we call to stop the server completely
481 def shutdown(signum = 0, frame = 0):
482 logger.info("Stopping server...")
483 # Closing all client connections
484 for connection in server.RequestHandlerClass.requestlist:
486 # Set errorhappened to prevent waiting for videodestroydelay
487 connection.errorhappened = True
488 connection.closeConnection()
490 logger.warning("Cannot kill a connection!")
492 server.server_close()
495 def _reloadconfig(signum=None, frame=None):
497 Reload configuration file.
502 logger = logging.getLogger('reloadconfig')
504 from vpconfig import VPConfig
505 logger.info('Config reloaded')
507 # setting signal handlers
509 gevent.signal(signal.SIGHUP, _reloadconfig)
510 gevent.signal(signal.SIGTERM, shutdown)
511 except AttributeError:
515 VPStuff.vlcProc = VPConfig.vlccmd.split()
516 if spawnVLC(VPStuff.vlcProc, VPConfig.vlcspawntimeout) and connectVLC():
517 logger.info("VLC spawned with pid " + str(VPStuff.vlc.pid))
519 logger.error('Cannot spawn or connect to VLC!')
524 logger.info("Using gevent %s" % gevent.__version__)
525 logger.info("Using psutil %s" % psutil.__version__)
526 logger.info("Using VLC %s" % VPStuff.vlcclient._vlcver)
527 logger.info("Server started.")
529 if not isRunning(VPStuff.vlc):
531 if spawnVLC(VPStuff.vlcProc, VPConfig.vlcspawntimeout) and connectVLC():
532 logger.info("VLC died, respawned it with pid " + str(VPStuff.vlc.pid))
534 logger.error("Cannot spawn VLC!")
537 # Return to our server tasks
538 server.handle_request()
539 except (KeyboardInterrupt, SystemExit):