Initial release.
[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=='get' 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         if 0 < VPConfig.maxconns <= VPStuff.clientcounter.total:
190             logger.debug("Maximum connections reached, can't serve this")
191             self.dieWithError(503)  # 503 Service Unavailable
192             return
193
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:
198             if 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")
203             self.end_headers()
204             self.closeConnection()
205             return
206
207         self.path_unquoted = urllib2.unquote('/'.join(self.splittedpath[2:]))
208         # Make list with parameters
209         self.params = list()
210         for i in xrange(3, 8):
211             try:
212                 self.params.append(int(self.splittedpath[i]))
213             except (IndexError, ValueError):
214                 self.params.append('0')
215
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
220
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
225         else:
226             logger.debug("Can reuse existing session")
227             shouldcreatevp = False
228
229         self.vlcid = hashlib.md5(self.path_unquoted).hexdigest()
230
231         # Send fake headers if this User-Agent is in fakeheaderuas tuple
232         if fakeua:
233             logger.debug(
234                 "Sending fake headers for " + useragent)
235             self.send_response(200)
236             self.send_header("Content-Type", "video/mpeg")
237             self.end_headers()
238             # Do not send real headers at all
239             self.headerssent = True
240
241         try:
242             self.hanggreenlet = gevent.spawn(self.hangDetector)
243             logger.debug("hangDetector spawned")
244             gevent.sleep()
245
246             # Initializing VPClient
247
248             # Getting URL
249             self.errorhappened = False
250
251             print shouldcreatevp
252             if shouldcreatevp:
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://'
257                 else:
258                     self.vlcprefix = ''
259
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
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             raise
315             self.dieWithError()
316         finally:
317             logger.debug("END REQUEST")
318             VPStuff.clientcounter.delete(self.path_unquoted, self.clientip)
319             if not VPStuff.clientcounter.get(self.path_unquoted):
320                 try:
321                     logger.debug("That was the last client, destroying VPClient")
322                     VPStuff.vlcclient.stopBroadcast(self.vlcid)
323                 except:
324                     pass
325                 self.vp.destroy()
326
327
328 class HTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
329
330     def handle_error(self, request, client_address):
331         # Do not print HTTP tracebacks
332         pass
333
334
335 class VPStuff(object):
336     '''
337     Inter-class interaction class
338     '''
339     vlcclient=None
340
341 # taken from http://stackoverflow.com/questions/2699907/dropping-root-permissions-in-python
342 def drop_privileges(uid_name, gid_name='nogroup'):
343
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
348
349     # Remove group privileges
350     os.setgroups([])
351
352     # Try setting the new uid/gid
353     os.setgid(running_gid)
354     os.setuid(running_uid)
355
356     # Ensure a very conservative umask
357     old_umask = os.umask(077)
358
359     if os.getuid() == running_uid and os.getgid() == running_gid:
360         # could be useful
361         os.environ['HOME'] = running_uid_home
362         return True
363     return False
364
365 logging.basicConfig(
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')
369
370 # Loading plugins
371 # Trying to change dir (would fail in freezed state)
372 try:
373     os.chdir(os.path.dirname(os.path.realpath(__file__)))
374 except:
375     pass
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()
386     try:
387         plugininstance = getattr(plugin, plugname)(VPConfig, VPStuff)
388     except Exception as e:
389         logger.error("Cannot load plugin " + plugname + ": " + repr(e))
390         continue
391     logger.debug('Plugin loaded: ' + plugname)
392     for j in plugininstance.handlers:
393         VPStuff.pluginshandlers[j] = plugininstance
394     VPStuff.pluginlist.append(plugininstance)
395
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")
399     sys.exit(1)
400
401 server = HTTPServer((VPConfig.httphost, VPConfig.httpport), HTTPHandler)
402 logger = logging.getLogger('HTTP')
403
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)
408     else:
409         logger.error("Cannot drop privileges to user " + VPConfig.vpproxyuser)
410         sys.exit(1)
411
412 # Creating ClientCounter
413 VPStuff.clientcounter = ClientCounter()
414
415 DEVNULL = open(os.devnull, 'wb')
416
417 # Spawning procedures
418 def spawnVLC(cmd, delay = 0):
419     try:
420         VPStuff.vlc = psutil.Popen(cmd, stdout=DEVNULL, stderr=DEVNULL)
421         gevent.sleep(delay)
422         return True
423     except:
424         return False
425
426 def connectVLC():
427     try:
428         VPStuff.vlcclient = vlcclient.VlcClient(
429             host=VPConfig.vlchost, port=VPConfig.vlcport, password=VPConfig.vlcpass,
430             out_port=VPConfig.vlcoutport)
431         return True
432     except vlcclient.VlcException as e:
433         print repr(e)
434         return False
435
436 def isRunning(process):
437     if psutil.version_info[0] >= 2:
438         if process.is_running() and process.status() != psutil.STATUS_ZOMBIE:
439             return True
440     else:  # for older versions of psutil
441         if process.is_running() and process.status != psutil.STATUS_ZOMBIE:
442             return True
443     return False
444
445 def findProcess(name):
446     for proc in psutil.process_iter():
447         try:
448             pinfo = proc.as_dict(attrs=['pid', 'name'])
449             if pinfo['name'] == name:
450                 return pinfo['pid']
451         except psutil.AccessDenied:
452             # System process
453             pass
454         except psutil.NoSuchProcess:
455             # Process terminated
456             pass
457     return None
458
459 def clean_proc():
460     # Trying to close all spawned processes gracefully
461     if isRunning(VPStuff.vlc):
462         if VPStuff.vlcclient:
463             VPStuff.vlcclient.destroy()
464         gevent.sleep(1)
465     if isRunning(VPStuff.vlc):
466         # or not :)
467         VPStuff.vlc.kill()
468
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:
474         try:
475             # Set errorhappened to prevent waiting for videodestroydelay
476             connection.errorhappened = True
477             connection.closeConnection()
478         except:
479             logger.warning("Cannot kill a connection!")
480     clean_proc()
481     server.server_close()
482     sys.exit()
483
484 def _reloadconfig(signum=None, frame=None):
485     '''
486     Reload configuration file.
487     SIGHUP handler.
488     '''
489     global VPConfig
490
491     logger = logging.getLogger('reloadconfig')
492     reload(vpconfig)
493     from vpconfig import VPConfig
494     logger.info('Config reloaded')
495
496 # setting signal handlers
497 try:
498     gevent.signal(signal.SIGHUP, _reloadconfig)
499     gevent.signal(signal.SIGTERM, shutdown)
500 except AttributeError:
501     pass
502
503 name = 'vlc'
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))
507 else:
508     logger.error('Cannot spawn or connect to VLC!')
509     clean_proc()
510     sys.exit(1)
511
512 try:
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.")
517     while True:
518         if not isRunning(VPStuff.vlc):
519             del VPStuff.vlc
520             if spawnVLC(VPStuff.vlcProc, VPConfig.vlcspawntimeout) and connectVLC():
521                 logger.info("VLC died, respawned it with pid " + str(VPStuff.vlc.pid))
522             else:
523                 logger.error("Cannot spawn VLC!")
524                 clean_proc()
525                 sys.exit(1)
526         # Return to our server tasks
527         server.handle_request()
528 except (KeyboardInterrupt, SystemExit):
529     shutdown()