transcoding option for web-video added
[vpproxy.git] / vphttp.py
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3 '''
4 VPProxy: HTTP/HLS Stream to HTTP Multiplexing Proxy
5
6 Based on AceProxy (https://github.com/ValdikSS/AceProxy) design
7 '''
8 import traceback
9 import gevent
10 import gevent.monkey
11 # Monkeypatching and all the stuff
12
13 gevent.monkey.patch_all()
14 import glob
15 import os
16 import signal
17 import sys
18 import logging
19 import psutil
20 import BaseHTTPServer
21 import SocketServer
22 from socket import error as SocketException
23 from socket import SHUT_RDWR
24 import urllib2
25 import hashlib
26 import vpconfig
27 from vpconfig import VPConfig
28 import vlcclient
29 import plugins.modules.ipaddr as ipaddr
30 from clientcounter import ClientCounter
31 from plugins.modules.PluginInterface import VPProxyPlugin
32 try:
33     import pwd
34     import grp
35 except ImportError:
36     pass
37
38
39
40 class HTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
41
42     requestlist = []
43
44     def handle_one_request(self):
45         '''
46         Add request to requestlist, handle request and remove from the list
47         '''
48         HTTPHandler.requestlist.append(self)
49         BaseHTTPServer.BaseHTTPRequestHandler.handle_one_request(self)
50         HTTPHandler.requestlist.remove(self)
51
52     def closeConnection(self):
53         '''
54         Disconnecting client
55         '''
56         if self.clientconnected:
57             self.clientconnected = False
58             try:
59                 self.wfile.close()
60                 self.rfile.close()
61                 self.connection.shutdown(SHUT_RDWR)
62             except:
63                 pass
64
65     def dieWithError(self, errorcode=500):
66         '''
67         Close connection with error
68         '''
69         logging.warning("Dying with error")
70         if self.clientconnected:
71             self.send_error(errorcode)
72             self.end_headers()
73             self.closeConnection()
74
75     def proxyReadWrite(self):
76         '''
77         Read video stream and send it to client
78         '''
79         logger = logging.getLogger('http_proxyReadWrite')
80         logger.debug("Started")
81
82         self.vlcstate = True
83         self.streamstate = True
84
85         try:
86             while True:
87
88                 if not self.clientconnected:
89                     logger.debug("Client is not connected, terminating")
90                     break
91
92                 data = self.video.read(4096)
93                 if data and self.clientconnected:
94                     self.wfile.write(data)
95                 else:
96                     logger.warning("Video connection closed")
97                     break
98
99         except SocketException:
100             # Video connection dropped
101             logger.warning("Video connection dropped")
102         finally:
103             self.video.close()
104             self.closeConnection()
105
106     def hangDetector(self):
107         '''
108         Detect client disconnection while in the middle of something
109         or just normal connection close.
110         '''
111         logger = logging.getLogger('http_hangDetector')
112         try:
113             while True:
114                 if not self.rfile.read():
115                     break
116         except:
117             pass
118         finally:
119             self.clientconnected = False
120             logger.debug("Client disconnected")
121             try:
122                 self.requestgreenlet.kill()
123             except:
124                 pass
125             finally:
126                 gevent.sleep()
127             return
128
129     def do_HEAD(self):
130         return self.do_GET(headers_only=True)
131
132     def do_GET(self, headers_only=False):
133         '''
134         GET request handler
135         '''
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
142         # Current greenlet
143         self.requestgreenlet = gevent.getcurrent()
144         # Connected client IP address
145         self.clientip = self.request.getpeername()[0]
146
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))
151
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 ' + \
155                                 'firewall rules')
156                     self.dieWithError(403)  # 403 Forbidden
157                     return
158
159         logger.info("Accepted connection from " + self.clientip + " path " + self.path)
160
161         try:
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
165             # by plugin
166             if not (self.reqtype in ('get','mp4') or self.reqtype in VPStuff.pluginshandlers):
167                 self.dieWithError(400)  # 400 Bad Request
168                 return
169         except IndexError:
170             self.dieWithError(400)  # 400 Bad Request
171             return
172
173         # Handle request with plugin handler
174         if self.reqtype in VPStuff.pluginshandlers:
175             try:
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())
180                 self.dieWithError()
181             finally:
182                 self.closeConnection()
183                 return
184         self.handleRequest(headers_only)
185
186     def handleRequest(self, headers_only):
187
188         # Limit concurrent connections
189         print VPStuff.clientcounter.total
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
193             return
194
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:
200             if 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")
205             self.end_headers()
206             self.closeConnection()
207             return
208
209         self.path_unquoted = urllib2.unquote('/'.join(self.splittedpath[2:]))
210         # Make list with parameters
211         self.params = list()
212         for i in xrange(3, 8):
213             try:
214                 self.params.append(int(self.splittedpath[i]))
215             except (IndexError, ValueError):
216                 self.params.append('0')
217
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
222
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
227         else:
228             logger.debug("Can reuse existing session")
229             shouldcreatevp = False
230
231         self.vlcid = hashlib.md5(self.reqtype+'\\'+self.path_unquoted).hexdigest()
232
233         # Send fake headers if this User-Agent is in fakeheaderuas tuple
234         if fakeua:
235             logger.debug(
236                 "Sending fake headers for " + useragent)
237             self.send_response(200)
238             self.send_header("Content-Type", "video/mpeg")
239             self.end_headers()
240             # Do not send real headers at all
241             self.headerssent = True
242
243         try:
244             self.hanggreenlet = gevent.spawn(self.hangDetector)
245             logger.debug("hangDetector spawned")
246             gevent.sleep()
247
248             # Getting URL
249             self.errorhappened = False
250
251             if shouldcreatevp:
252                 logger.debug("Got url " + self.path_unquoted)
253                 # Force ffmpeg demuxing if set in config
254                 if VPConfig.vlcforceffmpeg:
255                     self.vlcprefix = 'http/ffmpeg://'
256                 else:
257                     self.vlcprefix = ''
258
259                 logger.debug("Ready to start broadcast....")                    
260                 VPStuff.vlcclient.startBroadcast(
261                     self.vlcid, self.vlcprefix + self.path_unquoted, VPConfig.vlcmux, VPConfig.vlcpreaccess, self.reqtype)
262                 # Sleep a bit, because sometimes VLC doesn't open port in
263                 # time
264                 gevent.sleep(0.5)
265
266             # Building new VLC url
267             self.url = 'http://' + VPConfig.vlchost + \
268                 ':' + str(VPConfig.vlcoutport) + '/' + self.vlcid
269             logger.debug("VLC url " + self.url)
270
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])
275
276             self.video = urllib2.urlopen(self.video)
277
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']
289
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
293                 self.end_headers()
294                 logger.debug("Headers sent")
295
296             # Run proxyReadWrite
297             self.proxyReadWrite()
298
299             # Waiting until hangDetector is joined
300             self.hanggreenlet.join()
301             logger.debug("Request handler finished")
302
303         except (vpclient.VPException, vlcclient.VlcException, urllib2.URLError) as e:
304             logger.error("Exception: " + repr(e))
305             self.errorhappened = True
306             self.dieWithError()
307         except gevent.GreenletExit:
308             # hangDetector told us about client disconnection
309             pass
310         except Exception:
311             # Unknown exception
312             logger.error(traceback.format_exc())
313             self.errorhappened = True
314             self.dieWithError()
315         finally:
316             logger.debug("END REQUEST")
317             VPStuff.clientcounter.delete(self.reqtype+'\\'+self.path_unquoted, self.clientip)
318             if not VPStuff.clientcounter.get(self.reqtype+'\\'+self.path_unquoted):
319                 try:
320                     logger.debug("That was the last client, destroying VPClient")
321                     VPStuff.vlcclient.stopBroadcast(self.vlcid)
322                 except:
323                     pass
324                 self.vp.destroy()
325
326
327 class HTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
328
329     def handle_error(self, request, client_address):
330         # Do not print HTTP tracebacks
331         pass
332
333
334 class VPStuff(object):
335     '''
336     Inter-class interaction class
337     '''
338     vlcclient=None
339
340 # taken from http://stackoverflow.com/questions/2699907/dropping-root-permissions-in-python
341 def drop_privileges(uid_name, gid_name='nogroup'):
342
343     # Get the uid/gid from the name
344     running_uid = pwd.getpwnam(uid_name).pw_uid
345     running_uid_home = pwd.getpwnam(uid_name).pw_dir
346     running_gid = grp.getgrnam(gid_name).gr_gid
347
348     # Remove group privileges
349     os.setgroups([])
350
351     # Try setting the new uid/gid
352     os.setgid(running_gid)
353     os.setuid(running_uid)
354
355     # Ensure a very conservative umask
356     old_umask = os.umask(077)
357
358     if os.getuid() == running_uid and os.getgid() == running_gid:
359         # could be useful
360         os.environ['HOME'] = running_uid_home
361         return True
362     return False
363
364 logging.basicConfig(
365     filename=VPConfig.logpath + 'vphttp.log' if VPConfig.loggingtoafile else None,
366     format='%(asctime)s %(levelname)s %(name)s: %(message)s', datefmt='%d.%m.%Y %H:%M:%S', level=VPConfig.debug)
367 logger = logging.getLogger('INIT')
368
369 # Loading plugins
370 # Trying to change dir (would fail in freezed state)
371 try:
372     os.chdir(os.path.dirname(os.path.realpath(__file__)))
373 except:
374     pass
375 # Creating dict of handlers
376 VPStuff.pluginshandlers = dict()
377 # And a list with plugin instances
378 VPStuff.pluginlist = list()
379 pluginsmatch = glob.glob('plugins/*_plugin.py')
380 sys.path.insert(0, 'plugins')
381 pluginslist = [os.path.splitext(os.path.basename(x))[0] for x in pluginsmatch]
382 for i in pluginslist:
383     plugin = __import__(i)
384     plugname = i.split('_')[0].capitalize()
385     try:
386         plugininstance = getattr(plugin, plugname)(VPConfig, VPStuff)
387     except Exception as e:
388         logger.error("Cannot load plugin " + plugname + ": " + repr(e))
389         continue
390     logger.debug('Plugin loaded: ' + plugname)
391     for j in plugininstance.handlers:
392         VPStuff.pluginshandlers[j] = plugininstance
393     VPStuff.pluginlist.append(plugininstance)
394
395 # Check whether we can bind to the defined port safely
396 if os.getuid() != 0 and VPConfig.httpport <= 1024:
397     logger.error("Cannot bind to port " + str(VPConfig.httpport) + " without root privileges")
398     sys.exit(1)
399
400 server = HTTPServer((VPConfig.httphost, VPConfig.httpport), HTTPHandler)
401 logger = logging.getLogger('HTTP')
402
403 # Dropping root privileges if needed
404 if VPConfig.vpproxyuser and os.getuid() == 0:
405     if drop_privileges(VPConfig.vpproxyuser):
406         logger.info("Dropped privileges to user " + VPConfig.vpproxyuser)
407     else:
408         logger.error("Cannot drop privileges to user " + VPConfig.vpproxyuser)
409         sys.exit(1)
410
411 # Creating ClientCounter
412 VPStuff.clientcounter = ClientCounter()
413
414 DEVNULL = open(os.devnull, 'wb')
415
416 # Spawning procedures
417 def spawnVLC(cmd, delay = 0):
418     try:
419         VPStuff.vlc = psutil.Popen(cmd) #, stdout=DEVNULL, stderr=DEVNULL)
420         gevent.sleep(delay)
421         return True
422     except:
423         return False
424
425 def connectVLC():
426     try:
427         VPStuff.vlcclient = vlcclient.VlcClient(
428             host=VPConfig.vlchost, port=VPConfig.vlcport, password=VPConfig.vlcpass,
429             out_port=VPConfig.vlcoutport)
430         return True
431     except vlcclient.VlcException as e:
432         return False
433
434 def isRunning(process):
435     if psutil.version_info[0] >= 2:
436         if process.is_running() and process.status() != psutil.STATUS_ZOMBIE:
437             return True
438     else:  # for older versions of psutil
439         if process.is_running() and process.status != psutil.STATUS_ZOMBIE:
440             return True
441     return False
442
443 def findProcess(name):
444     for proc in psutil.process_iter():
445         try:
446             pinfo = proc.as_dict(attrs=['pid', 'name'])
447             if pinfo['name'] == name:
448                 return pinfo['pid']
449         except psutil.AccessDenied:
450             # System process
451             pass
452         except psutil.NoSuchProcess:
453             # Process terminated
454             pass
455     return None
456
457 def clean_proc():
458     # Trying to close all spawned processes gracefully
459     if isRunning(VPStuff.vlc):
460         if VPStuff.vlcclient:
461             VPStuff.vlcclient.destroy()
462         gevent.sleep(1)
463     if isRunning(VPStuff.vlc):
464         # or not :)
465         VPStuff.vlc.kill()
466
467 # This is what we call to stop the server completely
468 def shutdown(signum = 0, frame = 0):
469     logger.info("Stopping server...")
470     # Closing all client connections
471     for connection in server.RequestHandlerClass.requestlist:
472         try:
473             # Set errorhappened to prevent waiting for videodestroydelay
474             connection.errorhappened = True
475             connection.closeConnection()
476         except:
477             logger.warning("Cannot kill a connection!")
478     clean_proc()
479     server.server_close()
480     sys.exit()
481
482 def _reloadconfig(signum=None, frame=None):
483     '''
484     Reload configuration file.
485     SIGHUP handler.
486     '''
487     global VPConfig
488
489     logger = logging.getLogger('reloadconfig')
490     reload(vpconfig)
491     from vpconfig import VPConfig
492     logger.info('Config reloaded')
493
494 # setting signal handlers
495 try:
496     gevent.signal(signal.SIGHUP, _reloadconfig)
497     gevent.signal(signal.SIGTERM, shutdown)
498 except AttributeError:
499     pass
500
501 name = 'vlc'
502 VPStuff.vlcProc = VPConfig.vlccmd.split()
503 if spawnVLC(VPStuff.vlcProc, VPConfig.vlcspawntimeout) and connectVLC():
504     logger.info("VLC spawned with pid " + str(VPStuff.vlc.pid))
505 else:
506     logger.error('Cannot spawn or connect to VLC!')
507     clean_proc()
508     sys.exit(1)
509
510 try:
511     logger.info("Using gevent %s" % gevent.__version__)
512     logger.info("Using psutil %s" % psutil.__version__)
513     logger.info("Using VLC %s" % VPStuff.vlcclient._vlcver)
514     logger.info("Server started.")
515     while True:
516         if not isRunning(VPStuff.vlc):
517             del VPStuff.vlc
518             if spawnVLC(VPStuff.vlcProc, VPConfig.vlcspawntimeout) and connectVLC():
519                 logger.info("VLC died, respawned it with pid " + str(VPStuff.vlc.pid))
520             else:
521                 logger.error("Cannot spawn VLC!")
522                 clean_proc()
523                 sys.exit(1)
524         # Return to our server tasks
525         server.handle_request()
526 except (KeyboardInterrupt, SystemExit):
527     shutdown()