Content-Type fixed
[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('content-type'):
288                     del self.video.info().dict['content-type']
289                 if self.video.info().dict.has_key('keep-alive'):
290                     del self.video.info().dict['keep-alive']
291
292                 for key in self.video.info().dict:
293                     self.send_header(key, self.video.info().dict[key])
294                 self.send_header("Content-Type", "video/mpeg")
295                 # End headers. Next goes video data
296                 self.end_headers()
297                 logger.debug("Headers sent")
298
299             # Run proxyReadWrite
300             self.proxyReadWrite()
301
302             # Waiting until hangDetector is joined
303             self.hanggreenlet.join()
304             logger.debug("Request handler finished")
305
306         except (vpclient.VPException, vlcclient.VlcException, urllib2.URLError) as e:
307             logger.error("Exception: " + repr(e))
308             self.errorhappened = True
309             self.dieWithError()
310         except gevent.GreenletExit:
311             # hangDetector told us about client disconnection
312             pass
313         except Exception:
314             # Unknown exception
315             logger.error(traceback.format_exc())
316             self.errorhappened = True
317             self.dieWithError()
318         finally:
319             logger.debug("END REQUEST")
320             VPStuff.clientcounter.delete(self.reqtype+'\\'+self.path_unquoted, self.clientip)
321             if not VPStuff.clientcounter.get(self.reqtype+'\\'+self.path_unquoted):
322                 try:
323                     logger.debug("That was the last client, destroying VPClient")
324                     VPStuff.vlcclient.stopBroadcast(self.vlcid)
325                 except:
326                     pass
327                 self.vp.destroy()
328
329
330 class HTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
331
332     def handle_error(self, request, client_address):
333         # Do not print HTTP tracebacks
334         pass
335
336
337 class VPStuff(object):
338     '''
339     Inter-class interaction class
340     '''
341     vlcclient=None
342
343 # taken from http://stackoverflow.com/questions/2699907/dropping-root-permissions-in-python
344 def drop_privileges(uid_name, gid_name='nogroup'):
345
346     # Get the uid/gid from the name
347     running_uid = pwd.getpwnam(uid_name).pw_uid
348     running_uid_home = pwd.getpwnam(uid_name).pw_dir
349     running_gid = grp.getgrnam(gid_name).gr_gid
350
351     # Remove group privileges
352     os.setgroups([])
353
354     # Try setting the new uid/gid
355     os.setgid(running_gid)
356     os.setuid(running_uid)
357
358     # Ensure a very conservative umask
359     old_umask = os.umask(077)
360
361     if os.getuid() == running_uid and os.getgid() == running_gid:
362         # could be useful
363         os.environ['HOME'] = running_uid_home
364         return True
365     return False
366
367 logging.basicConfig(
368     filename=VPConfig.logpath + 'vphttp.log' if VPConfig.loggingtoafile else None,
369     format='%(asctime)s %(levelname)s %(name)s: %(message)s', datefmt='%d.%m.%Y %H:%M:%S', level=VPConfig.debug)
370 logger = logging.getLogger('INIT')
371
372 # Loading plugins
373 # Trying to change dir (would fail in freezed state)
374 try:
375     os.chdir(os.path.dirname(os.path.realpath(__file__)))
376 except:
377     pass
378 # Creating dict of handlers
379 VPStuff.pluginshandlers = dict()
380 # And a list with plugin instances
381 VPStuff.pluginlist = list()
382 pluginsmatch = glob.glob('plugins/*_plugin.py')
383 sys.path.insert(0, 'plugins')
384 pluginslist = [os.path.splitext(os.path.basename(x))[0] for x in pluginsmatch]
385 for i in pluginslist:
386     plugin = __import__(i)
387     plugname = i.split('_')[0].capitalize()
388     try:
389         plugininstance = getattr(plugin, plugname)(VPConfig, VPStuff)
390     except Exception as e:
391         logger.error("Cannot load plugin " + plugname + ": " + repr(e))
392         continue
393     logger.debug('Plugin loaded: ' + plugname)
394     for j in plugininstance.handlers:
395         VPStuff.pluginshandlers[j] = plugininstance
396     VPStuff.pluginlist.append(plugininstance)
397
398 # Check whether we can bind to the defined port safely
399 if os.getuid() != 0 and VPConfig.httpport <= 1024:
400     logger.error("Cannot bind to port " + str(VPConfig.httpport) + " without root privileges")
401     sys.exit(1)
402
403 server = HTTPServer((VPConfig.httphost, VPConfig.httpport), HTTPHandler)
404 logger = logging.getLogger('HTTP')
405
406 # Dropping root privileges if needed
407 if VPConfig.vpproxyuser and os.getuid() == 0:
408     if drop_privileges(VPConfig.vpproxyuser):
409         logger.info("Dropped privileges to user " + VPConfig.vpproxyuser)
410     else:
411         logger.error("Cannot drop privileges to user " + VPConfig.vpproxyuser)
412         sys.exit(1)
413
414 # Creating ClientCounter
415 VPStuff.clientcounter = ClientCounter()
416
417 DEVNULL = open(os.devnull, 'wb')
418
419 # Spawning procedures
420 def spawnVLC(cmd, delay = 0):
421     try:
422         VPStuff.vlc = psutil.Popen(cmd) #, stdout=DEVNULL, stderr=DEVNULL)
423         gevent.sleep(delay)
424         return True
425     except:
426         return False
427
428 def connectVLC():
429     try:
430         VPStuff.vlcclient = vlcclient.VlcClient(
431             host=VPConfig.vlchost, port=VPConfig.vlcport, password=VPConfig.vlcpass,
432             out_port=VPConfig.vlcoutport)
433         return True
434     except vlcclient.VlcException as e:
435         return False
436
437 def isRunning(process):
438     if psutil.version_info[0] >= 2:
439         if process.is_running() and process.status() != psutil.STATUS_ZOMBIE:
440             return True
441     else:  # for older versions of psutil
442         if process.is_running() and process.status != psutil.STATUS_ZOMBIE:
443             return True
444     return False
445
446 def findProcess(name):
447     for proc in psutil.process_iter():
448         try:
449             pinfo = proc.as_dict(attrs=['pid', 'name'])
450             if pinfo['name'] == name:
451                 return pinfo['pid']
452         except psutil.AccessDenied:
453             # System process
454             pass
455         except psutil.NoSuchProcess:
456             # Process terminated
457             pass
458     return None
459
460 def clean_proc():
461     # Trying to close all spawned processes gracefully
462     if isRunning(VPStuff.vlc):
463         if VPStuff.vlcclient:
464             VPStuff.vlcclient.destroy()
465         gevent.sleep(1)
466     if isRunning(VPStuff.vlc):
467         # or not :)
468         VPStuff.vlc.kill()
469
470 # This is what we call to stop the server completely
471 def shutdown(signum = 0, frame = 0):
472     logger.info("Stopping server...")
473     # Closing all client connections
474     for connection in server.RequestHandlerClass.requestlist:
475         try:
476             # Set errorhappened to prevent waiting for videodestroydelay
477             connection.errorhappened = True
478             connection.closeConnection()
479         except:
480             logger.warning("Cannot kill a connection!")
481     clean_proc()
482     server.server_close()
483     sys.exit()
484
485 def _reloadconfig(signum=None, frame=None):
486     '''
487     Reload configuration file.
488     SIGHUP handler.
489     '''
490     global VPConfig
491
492     logger = logging.getLogger('reloadconfig')
493     reload(vpconfig)
494     from vpconfig import VPConfig
495     logger.info('Config reloaded')
496
497 # setting signal handlers
498 try:
499     gevent.signal(signal.SIGHUP, _reloadconfig)
500     gevent.signal(signal.SIGTERM, shutdown)
501 except AttributeError:
502     pass
503
504 name = 'vlc'
505 VPStuff.vlcProc = VPConfig.vlccmd.split()
506 if spawnVLC(VPStuff.vlcProc, VPConfig.vlcspawntimeout) and connectVLC():
507     logger.info("VLC spawned with pid " + str(VPStuff.vlc.pid))
508 else:
509     logger.error('Cannot spawn or connect to VLC!')
510     clean_proc()
511     sys.exit(1)
512
513 try:
514     logger.info("Using gevent %s" % gevent.__version__)
515     logger.info("Using psutil %s" % psutil.__version__)
516     logger.info("Using VLC %s" % VPStuff.vlcclient._vlcver)
517     logger.info("Server started.")
518     while True:
519         if not isRunning(VPStuff.vlc):
520             del VPStuff.vlc
521             if spawnVLC(VPStuff.vlcProc, VPConfig.vlcspawntimeout) and connectVLC():
522                 logger.info("VLC died, respawned it with pid " + str(VPStuff.vlc.pid))
523             else:
524                 logger.error("Cannot spawn VLC!")
525                 clean_proc()
526                 sys.exit(1)
527         # Return to our server tasks
528         server.handle_request()
529 except (KeyboardInterrupt, SystemExit):
530     shutdown()