Guest User

Untitled

a guest
May 13th, 2017
570
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 39.15 KB | None | 0 0
  1. #!/usr/bin/env python
  2. ################################################################################
  3. ##
  4. ## DecafMUD - Green Lantern
  5. ## Copyright (c) 2010
  6. ##
  7. ## A Python server using Twisted for enabling WebSocket connectivity for the
  8. ## DecafMUD client. In addition, it can act as a Flash policy server and a proxy
  9. ## for connecting to MUDs on other servers.
  10. ##
  11. ## Green Lantern has an ACL, allowing you to block certain IP addresses from
  12. ## connecting or requiring the client to login with a username and password
  13. ## before allowing a connection to form.
  14. ##
  15. ## Author: Stendec <stendec365@gmail.com>
  16. ##
  17. ################################################################################
  18.  
  19. __version__ = '0.1.1'
  20.  
  21. ## Imports ##
  22. #############
  23. from optparse import OptionGroup, OptionParser
  24. from types import ClassType
  25. import asyncore, hashlib, logging, os, re, socket, sys, zlib
  26. import codec_wsraw
  27.  
  28. ## Variables ##
  29. ###############
  30. # Telnet characters
  31. IAC       = chr(255)
  32. DO        = chr(253)
  33. DONT      = chr(254)
  34. WILL      = chr(251)
  35. WONT      = chr(252)
  36. SB        = chr(250)
  37. COMPRESS  = chr(85)
  38. COMPRESS2 = chr(86)
  39. ECHO      = chr(1)
  40.  
  41. # MCCP negotiation matcher
  42. _COMP_REG = re.compile('\\xFF(?:[\\xFB-\\xFE][\\x55\\x56]|\\xFA(?:\\x55\\xFB\\xF0|\\x56\\xFF\\xF0))')
  43.  
  44. # Mandatory headers for WebSocket connections
  45. _MANDATORY_HEADERS = {
  46.   'Upgrade' : 'WebSocket',
  47.   'Connection' : 'Upgrade',
  48.   'Host' : None,
  49.   'Origin' : None,
  50. }
  51.  
  52. # Status line matcher for WebSocket connections
  53. _STATUS_REG = re.compile('^GET /([\S]*) HTTP/1.1$')
  54.  
  55. ## Helper Functions ##
  56. ######################
  57. def _readFrameLength(data):
  58.   '''Read a WebSocket-encoded frame length from data, and return both the
  59.     length and the bytes consumed determining that length.'''
  60.   length = 0
  61.   consumed = 0
  62.   for char in data:
  63.     consumed += 1
  64.     b = ord(char)
  65.     length = length * 128 + (b & 0x7F)
  66.     if (b & 0x80) == 0:
  67.       break
  68.   return length, consumed
  69.  
  70. ## Class: LanternProxy ##
  71. #########################
  72. class LanternProxy(asyncore.dispatcher):
  73.   '''This class is responsible for the connections to the remote server, and
  74.     handles MCCP for easing bandwidth requirements when connecting to remote
  75.     hosts.'''
  76.  
  77.   def __init__(self, id = '', allow_compress = True):
  78.     self.socket         = None
  79.     self.in_buf         = ''
  80.     self.out_buf        = ''
  81.    
  82.     self.mccp_level     = 0
  83.     self.mccp_remote    = False
  84.     self.allow_compress = allow_compress
  85.     if allow_compress:
  86.       self.decompressor = zlib.decompressobj()
  87.    
  88.     self.id = id
  89.     self.log = logging.getLogger('GL.Proxy')
  90.     self.log.setLevel(logging.DEBUG)
  91.     asyncore.dispatcher.__init__(self)
  92.  
  93.   def logInfo(self, text):
  94.     self.log.info('[%s] %s' % (self.id, text))
  95.  
  96.   def logDebug(self, text):
  97.     self.log.debug('[%s] %s' % (self.id, text))
  98.  
  99.   def setSocket(self, sock, remote_ip, remote_port):
  100.     '''Attach to a GreenLantern, then connect to the given IP and port.'''
  101.     self.proxied = sock
  102.     self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
  103.     self.connect((remote_ip, remote_port))
  104.  
  105.   def handle_connect(self):
  106.     '''Notify the GreenLantern that we're connected.'''
  107.     if self.proxied:
  108.       self.proxied.state = 'connected'
  109.  
  110.   def handle_close(self):
  111.     self.close()
  112.     if self.proxied:
  113.       # Make sure nothing's trapped in our buffers.
  114.       if self.mccp_remote:
  115.         data = self.decompressor.flush()
  116.         if data: self.proxied.message(data)
  117.       self.proxied.proxy = None
  118.       self.proxied.handle_close()
  119.  
  120.   def handle_read(self):
  121.     read = self.recv(4096)
  122.     self.in_buf += read
  123.     if self.proxied:
  124.       self._process()
  125.  
  126.   def handle_write(self):
  127.     sent = self.send(self.out_buf)
  128.     self.out_buf = self.out_buf[sent:]
  129.  
  130.   def message(self, text):
  131.     self.out_buf += text
  132.  
  133.   def _process(self):
  134.     '''First, intercept sequences of IAC WILL/WONT/DO/DONT COMPRESS/COMPRESSv2
  135.       and IAC SB COMPRESS/COMPRESSv2 (IAC) SE.
  136.      
  137.       Second, if MCCP is active, decompress the input. Otherwise just send the
  138.       buffer along to the client.'''
  139.     if not self.allow_compress:
  140.       self.proxied.message(self.in_buf)
  141.       self.in_buf = ''
  142.       return
  143.    
  144.     if self.mccp_remote:
  145.       # MCCP is enabled. Decompress the text.
  146.       self.in_buf = self.decompressor.decompress(self.in_buf)
  147.       data = self.decompressor.unused_data
  148.       if data:
  149.         # The remote server has ended the compressed stream. Shut off the MCCP
  150.         # flag and create a fresh decompressor in case they start again.
  151.         self.logInfo('MCCP Compression ended.')
  152.         self.decompressor = zlib.decompressobj()
  153.         self.mccp_remote = False
  154.         self.mccp_level = 0
  155.     else:
  156.       # MCCP isn't enabled. Pat yourself on the back or something.
  157.       data = ''
  158.    
  159.     # Check the data for MCCP negotiation.
  160.     m = _COMP_REG.search(self.in_buf)
  161.     if m:
  162.       # We have negotiation. First, send the text up to the negotiation to the
  163.       # client.
  164.       self.proxied.message(self.in_buf[:m.start()])
  165.      
  166.       # Determine what to do.
  167.       seq = m.group()
  168.       op = seq[1]
  169.       mccp_level = 2 if seq[2] == COMPRESS2 else 1
  170.      
  171.       if op == WILL:
  172.         # We support both, but respond to the first received
  173.         self.logDebug('RCVD IAC WILL ' + ('COMPRESSv2' if seq[2] == COMPRESS2 else 'COMPRESS'))
  174.         if not self.mccp_level:
  175.           self.out_buf += IAC + DO + seq[2]
  176.           self.mccp_level = mccp_level
  177.           self.logDebug('SENT IAC DO   ' + ('COMPRESSv2' if seq[2] == COMPRESS2 else 'COMPRESS'))
  178.         else:
  179.           self.out_buf += IAC + DONT + seq[2]
  180.           self.logDebug('SENT IAC DONT ' + ('COMPRESSv2' if seq[2] == COMPRESS2 else 'COMPRESS'))
  181.      
  182.       elif op == WONT:
  183.         self.logDebug('RCVD IAC WONT ' + ('COMPRESSv2' if seq[2] == COMPRESS2 else 'COMPRESS'))
  184.         if mccp_level == self.mccp_level:
  185.           self.mccp_level = 0
  186.      
  187.       elif op == SB:
  188.         self.logDebug('RCVD IAC SB   ' + ('COMPRESSv2' if seq[2] == COMPRESS2 else 'COMPRESS'))
  189.         # Start a compressed stream.
  190.         self.mccp_remote = True
  191.         self.logInfo('MCCP Compression started.')
  192.      
  193.       # Update the in_buf
  194.       self.in_buf = self.in_buf[m.end():] + data
  195.    
  196.     else:
  197.       # No negotiation. Send the in_buf in full.
  198.       self.proxied.message(self.in_buf)
  199.       self.in_buf = data
  200.    
  201.     # If we still have in_buf, reprocess
  202.     if self.in_buf:
  203.       self._process()
  204.  
  205.   def writable(self):
  206.     return len(self.out_buf) > 0
  207.  
  208. ## Class: LoginHandler ##
  209. #########################
  210. class MenuLogin:
  211.   def __init__(self, menu):
  212.     self.menu = menu
  213.     self.state = 0
  214.     self.username = ''
  215.    
  216.     self.banner = False
  217.  
  218.   def display(self):
  219.     if not self.banner:
  220.       self.menu.sock.sendBanner()
  221.       self.banner = True
  222.  
  223.     if self.state == 0:
  224.       self.menu.send_raw('\x1b[38;5;2mUsername\x1b[38;5;8m:\x1b[0m ')
  225.     else:
  226.       self.menu.send_raw('\x1b[38;5;2mPassword\x1b[38;5;8m:\x1b[0m ' + IAC + WILL + ECHO)
  227.  
  228.   def handle(self, data):
  229.     if self.state == 0:
  230.       if re.sub('[^a-zA-Z0-9\-_!@#$%^&*(),.<>]', '', data) == data and data:
  231.         self.username = data
  232.         self.state = 1
  233.       elif not data and self.menu.can_without:
  234.         self.menu.pop_ih()
  235.         self.menu.push_ih(MenuMenu)
  236.       else:
  237.         self.menu.send('\x1b[38;5;1mInvalid username.')
  238.     elif self.state == 1:
  239.       self.menu.send(IAC + WONT + ECHO)
  240.       if data:
  241.         # Try to authenticate.
  242.         user = self.menu.oa.checkUser(self.username, data)
  243.         if user:
  244.           self.menu.send_raw('\n')
  245.           self.menu.user = user
  246.           self.menu.pop_ih()
  247.           self.menu.push_ih(MenuMenu)
  248.         else:
  249.           self.menu.send('\x1b[38;5;1mInvalid username or password.')
  250.       else:
  251.         self.state = 0
  252.  
  253. class MenuMenu:
  254.   '''The actual menu to select a MUD.'''
  255.  
  256.   def __init__(self, menu):
  257.     self.menu = menu
  258.     self.state = 0
  259.    
  260.     # Get the available MUDs.
  261.     self.muds = []
  262.     self.longest = 0
  263.     for k,host in self.menu.oa.config['hosts'].iteritems():
  264.       if self.menu.oa.isAuthServer(host, self.menu.user):
  265.         name = k
  266.         if host.has_key('name'): name = host['name']
  267.         if len(name) > self.longest: self.longest = len(name)
  268.         host['__name'] = name
  269.         self.muds.append(host)
  270.    
  271.     self.fmt = '\x1b[38;5;120m%2d\x1b[38;5;8m)\x1b[0m %-'+str(self.longest)+'s  '
  272.     self.cols = 80 / (self.longest + 6)
  273.     self.banner = False
  274.  
  275.   def display(self):
  276.     if not self.banner:
  277.       self.banner = True
  278.       self.menu.sock.sendBanner()
  279.      
  280.       col = 0
  281.       ind = 0
  282.       for i in range(len(self.muds)):
  283.         ind += 1
  284.         col += 1
  285.         self.menu.send_raw(self.fmt % (ind, self.muds[i]['__name']))
  286.         if col > self.cols:
  287.           col = 0
  288.           self.menu.send_raw('\n')
  289.       self.menu.send_raw('\n\n')
  290.     if self.menu.user:
  291.       self.menu.send_raw('\x1b[38;5;2mSelect a mud, or Q to quit\x1b[38;5;8m:\x1b[0m ')
  292.     else:
  293.       self.menu.send_raw('\x1b[38;5;2mSelect a mud, L to log in, or Q to quit\x1b[38;5;8m:\x1b[0m ')
  294.  
  295.   def handle(self, data):
  296.     if not data:
  297.       return
  298.    
  299.     if 'login'.startswith(data.lower()):
  300.       self.menu.pop_ih()
  301.       self.menu.push_ih(MenuLogin)
  302.       return
  303.    
  304.     if 'quit'.startswith(data.lower()):
  305.       self.menu.pop_ih()
  306.       return
  307.    
  308.     try: data = int(data)
  309.     except: data = 0
  310.    
  311.     if data < 1 or data > len(self.muds):
  312.       self.menu.send('\x1b[38;5;1mInvalid choice.')
  313.       return
  314.    
  315.     # Connect it.
  316.     self.menu.expected_close = True
  317.     self.menu.pop_ih()
  318.     self.menu.sock.connectHost(self.muds[data-1], self.menu.user)
  319.  
  320. ## Class: LanternMenu ##
  321. ########################
  322. class LanternMenu:
  323.   '''This class provides a simple in-telnet menu for Green Lantern, allowing a
  324.     user to login if it wasn't done automatically (if it's necessary at all)
  325.     and providing a list of MUDs to choose from.'''
  326.  
  327.   def __init__(self, sock, oa, user = None):
  328.     self.expected_close = False
  329.    
  330.     self.oa       = oa
  331.     self.sock     = sock
  332.     self.id       = sock.id
  333.     self.user     = user
  334.    
  335.     self.in_buf   = ''
  336.     self.buffer   = ''
  337.  
  338.     self.log = logging.getLogger('GL.Menu')
  339.     self.log.setLevel(logging.DEBUG)
  340.     self.logInfo('New menu.')
  341.    
  342.     # Create a stack of input handlers, and push the default on. Then force it
  343.     # to draw the current handler.
  344.     self.ih = []
  345.    
  346.     self.can_without = False
  347.     for host in self.oa.config['hosts'].values():
  348.       if self.oa.isAuthServer(host, self.user):
  349.         self.can_without = True
  350.         break
  351.    
  352.     if self.can_without:
  353.       self.push_ih(MenuMenu)
  354.     else:
  355.       self.push_ih(MenuLogin)
  356.    
  357.     self.ih[0].display()
  358.  
  359.   def logInfo(self, text):
  360.     self.log.info('[%s] %s' % (self.id,text))
  361.  
  362.   def logDebug(self, text):
  363.     self.log.debug('[%s] %s' % (self.id, text))
  364.  
  365.   def handleBuffer(self):
  366.     '''Handle text sent to the buffer. Generally, this waits until we have a
  367.       full line of text and process it all at once.'''
  368.     ind = self.buffer.find('\n')
  369.     if ind != -1:
  370.       data = self.buffer[:ind]
  371.       self.buffer = self.buffer[ind+1:]
  372.       if data.endswith('\r'): data = data[:-1]
  373.      
  374.       # Call the current input handler's handler function.
  375.       self.ih[0].handle(data)
  376.      
  377.       # If we have an input handler now, call its display function. Else, if
  378.       # this isn't expected, close the socket.
  379.       if len(self.ih) > 0:
  380.         self.ih[0].display()
  381.       elif not self.expected_close:
  382.         self.sock.handle_close()
  383.  
  384.   def message(self, text):
  385.     '''Append the text to the in_buf, and process it.'''
  386.     self.in_buf += text
  387.     self.process()
  388.  
  389.   def process(self):
  390.     '''This function should be called by GreenLantern whenever additional data
  391.       is sent to the menu, since this isn't updated by asyncore's loop.
  392.      
  393.       This is in charge of stripping out telnet sequences before handing data
  394.       to the line handler.'''
  395.     ind = self.in_buf.find('\xFF')
  396.     if ind != -1:
  397.       # We have TELNET. Buffer everything before it, and handle it.
  398.       self.buffer += self.in_buf[:ind]
  399.       self.in_buf = self.in_buf[ind:]
  400.      
  401.       # Process the buffered text, then read the IAC sequence.
  402.       self.handleBuffer()
  403.       self.readIACSequence()
  404.     else:
  405.       self.buffer += self.in_buf
  406.       self.in_buf = ''
  407.       self.handleBuffer()
  408.    
  409.     # If we still have in_buf, reprocess.
  410.     #if len(self.in_buf) > 0:
  411.     #  self.process()
  412.  
  413.   def push_ih(self, ih):
  414.     '''Push an input handler onto the stack.'''
  415.     if type(ih) is ClassType:
  416.       h = ih(self)
  417.     else:
  418.       h = ih
  419.     self.ih.insert(0, h)
  420.  
  421.   def pop_ih(self):
  422.     '''Remove the first input handler.'''
  423.     self.ih.pop(0)
  424.  
  425.   def readIACSequence(self):
  426.     '''Read a telnet sequence and wipe it off the input buffer.'''
  427.     self.in_buf = self.in_buf.replace('\xFF\xFF','\xFF')
  428.    
  429.     if self.in_buf[0] == IAC:
  430.       if self.in_buf[1] in (DO,DONT,WILL,WONT):
  431.         self.in_buf = self.in_buf[3:]
  432.       elif self.in_buf[1] == SB:
  433.         ind = self.in_buf.find(IAC + SE)
  434.         if ind != -1:
  435.           self.in_buf = self.in_buf[ind:]
  436.  
  437.   def send(self, text):
  438.     '''Send the text to the client, with a line return.'''
  439.     self.send_raw(text + '\n')
  440.  
  441.   def send_raw(self, text):
  442.     '''Send the text to the client.'''
  443.     self.sock.message(text)
  444.  
  445. ## Class: GreenLantern ##
  446. ########################
  447. class GreenLantern(asyncore.dispatcher):
  448.   '''This class handles the connections to the clients themselves, generally
  449.     DecafMUD. It hands out Flash policy files, proxies normal Flash connections,
  450.     proxies WebSocket connections with translation, and provides a menu for
  451.     doing ACL stuff.'''
  452.  
  453.   def __init__(self, sock, oa):
  454.     self.oa           = oa
  455.     self.close_empty  = False
  456.    
  457.     self.proxy        = None
  458.     self.state        = 'initial'
  459.     self.state_ws     = False
  460.     self.buffer       = ''
  461.     self.ws_buf       = ''
  462.     self.in_buf       = ''
  463.     self.out_buf      = ''
  464.     self.expect_frame = True
  465.     self.frame        = 0
  466.     self.frame_len    = 0
  467.    
  468.     sn = sock.getpeername()
  469.     self.id = '%s:%s' % (sn[0], sn[1])
  470.    
  471.     self.log          = logging.getLogger('GL.Core')
  472.     self.log.setLevel(logging.DEBUG)
  473.     self.logInfo('New connection.')
  474.     asyncore.dispatcher.__init__(self, sock=sock)
  475.  
  476.   def logInfo(self, text):
  477.     self.log.info('[%s] %s' % (self.id, text))
  478.  
  479.   def logDebug(self, text):
  480.     self.log.debug('[%s] %s' % (self.id, text))
  481.  
  482.   def connectHost(self, host, user = None):
  483.     '''Attempt to connect to the given host.'''
  484.     if type(host) is not tuple:
  485.       if not self.oa.isAuthServer(host, user):
  486.         self.handle_close()
  487.         return
  488.      
  489.       host = (host['host'], host['port'], False if host['host'] == '127.0.0.1' or host['host'].lower() == 'localhost' else True)
  490.    
  491.     # If we have a proxy, destroy it.
  492.     if self.proxy:
  493.       if isinstance(self.proxy, LanternProxy):
  494.         self.proxy.proxied = None
  495.         self.proxy.close()
  496.       self.proxy = None
  497.    
  498.     # Send a message, then create a proxy.
  499.     host, port, compress = host
  500.     if compress:
  501.       self.message('\x1b[38;5;10mGreen Lantern \x1b[0m is forwarding you to: %s\x1b[38;5;8m:\x1b[0m%d...\n\n' % (host,port))
  502.     else:
  503.       self.message('\x1b[38;5;10mGreen Lantern \x1b[0m is forwarding you to port %d...\n\n' % port)
  504.     self.logInfo('Connecting to: %s:%d' % (host,port))
  505.     self.state = 'waiting'
  506.     self.proxy = LanternProxy(self.id, compress)
  507.     self.proxy.setSocket(self, host, port)
  508.  
  509.   def handle_close(self):
  510.     self.logInfo('Connection closed.')
  511.     self.close()
  512.     if isinstance(self.proxy, LanternProxy):
  513.       self.proxy.proxied = None
  514.       self.proxy.close()
  515.    
  516.     # Remove the lantern from Oa.
  517.     self.oa.clients.remove(self)
  518.  
  519.   def handle_read(self):
  520.     read = self.recv(4096)
  521.     self.in_buf += read
  522.    
  523.     if len(self.in_buf):
  524.       if self.state_ws:
  525.         self.readWebSocket()
  526.       else:
  527.         getattr(self, '_read_'+self.state)(self.in_buf)
  528.         self.in_buf = ''
  529.  
  530.   def handle_write(self):
  531.     sent = self.send(self.out_buf)
  532.     self.out_buf = self.out_buf[sent:]
  533.    
  534.     if self.close_empty and not self.out_buf:
  535.       self.handle_close()
  536.  
  537.   def readWebSocket(self):
  538.     '''Decode incoming websocket data.'''
  539.     if self.expect_frame:
  540.       self.expect_frame = False
  541.       self.frame = ord(self.in_buf[0])
  542.       if self.frame & 0x80 == 0x80:
  543.         self.frame_len, consumed = _readFrameLength(self.in_buf)
  544.         self.in_buf = self.in_buf[consumed:]
  545.       else:
  546.         self.in_buf = self.in_buf[1:]
  547.    
  548.     if self.frame & 0x80 == 0:
  549.       # Read until 0xFF
  550.       ind = self.in_buf.find('\xFF')
  551.       if ind != -1:
  552.         # We have a 0xFF byte. Read everything up to it, decode it with wsraw,
  553.         # and hand that to whatever logic we should be using.
  554.         data = self.in_buf[:ind].decode('wsraw')
  555.         self.in_buf = self.in_buf[ind+1:]
  556.         getattr(self, '_read_'+self.state)(data)
  557.         self.expect_frame = True
  558.        
  559.         # If we still have data, run through it again.
  560.         if self.in_buf:
  561.           self.readWebSocket()
  562.    
  563.     else:
  564.       # Length based frames. Read until we have enough bytes.
  565.       expected = self.frame_len - len(self.ws_buf)
  566.       self.ws_buf += self.in_buf[:expected]
  567.       self.in_buf = self.in_buf[expected:]
  568.       if len(self.ws_buf) == self.frame_len:
  569.         # We have enough. Hand the buffer off to the appropriate logic.
  570.         getattr(self, '_read_'+self.state)(self.ws_buf)
  571.         self.ws_buf = ''
  572.         self.expect_frame = True
  573.        
  574.         # If we still have data, run through it again.
  575.         if self.in_buf:
  576.           self.readWebSocket()
  577.  
  578.   def handleHTTP(self, headers):
  579.     '''Handle a HTTP request and serve basic files.'''
  580.     self.logInfo('HTTP request for: /' + self.path)
  581.    
  582.     # If path is empty, default to 'index.htm'
  583.     if self.path == '': path = 'index.htm'
  584.     else: path = self.path
  585.    
  586.     # Chop anything extra off the end.
  587.     if '?' in path: path, extra = path.split('?',1)
  588.    
  589.     # Be sure to close after this message.
  590.     self.close_empty = True
  591.     self.state = 'waiting'
  592.    
  593.     # Try getting the file, make sure it's within path.
  594.     path = os.path.realpath(os.path.join(self.oa.config['path'], path))
  595.     if not os.path.exists(path) and self.path == '':
  596.       path += 'l'
  597.     if not os.path.exists(path) and path.endswith('.html'):
  598.       path = path[:-4] + 'tpl'
  599.     elif not os.path.exists(path) and path.endswith('.htm'):
  600.       path = path[:-3] + 'tpl'
  601.    
  602.     if not path.startswith(self.oa.config['path']):
  603.       content = '<html><head><title>403 Forbidden</title></head><body><h1>403 Forbidden</h1><p>You do not have permission to view this file.</p></body></html>'
  604.       out = ['HTTP/1.1 403 Forbidden','Server: GreenLantern/%s' % (__version__,), 'Connection: close','Content-Length: %d' % (len(content),),'Content-Type: text/html; encoding=UTF-8','',content]
  605.       self.message('\r\n'.join(out))
  606.       return
  607.    
  608.     # If authentication is required, go for it.
  609.     if not self.oa.config.has_key('freeweb') and self.oa.config.has_key('users'):
  610.       # Check for a HTTP Authorization header.
  611.       authorized = False
  612.       if headers.has_key('Authorization'):
  613.         username = headers['Authorization']
  614.         if username.startswith('Basic '):
  615.           import base64
  616.           username, password = base64.decodestring(username[6:]).split(':',1)
  617.           user = self.oa.checkUser(username, password)
  618.           if user:
  619.             authorized = True
  620.      
  621.       if not authorized:
  622.         content = '<html><head><title>401 Unauthorized</title></head><body><h1>401 Unauthorized</h1><p>You are not authorized to view this file.</p></body></html>'
  623.         out = ['HTTP/1.1 401 Unauthorized','Server: GreenLantern/%s' % (__version__,), 'Connection: close','Content-Length: %d' % (len(content),),'Content-Type: text/html; encoding=UTF-8']
  624.         out.append('WWW-Authenticate: Basic realm="DecafMUD (with Green Lantern)"')
  625.         self.message('\r\n'.join(out) + '\r\n\r\n' + content)
  626.         return
  627.     else:
  628.       user = None
  629.       username = password = ''
  630.    
  631.     if os.path.exists(path) and os.path.isfile(path):
  632.       # It's a valid file. Go for it.
  633.       ftype = 'text/plain'
  634.       ext = path.lower()[-4:]
  635.       if ext.endswith('.js'): ftype = 'application/x-javascript; charset=UTF-8'
  636.       elif ext == '.css':     ftype = 'text/css'
  637.       elif ext == '.swf':     ftype = 'application/x-shockwave-flash'
  638.       elif ext == '.tpl':     ftype = 'text/html'
  639.       elif ext == '.htm':     ftype = 'text/html'
  640.       elif path.lower()[-5:] == '.html': ftype = 'text/html'
  641.       elif ext == '.jpg':     ftype = 'image/jpeg'
  642.       elif ext == '.png':     ftype = 'image/png'
  643.       elif ext == '.gif':     ftype = 'image/gif'
  644.       elif ext == '.ico':     ftype = 'image/vnd.microsoft.icon'
  645.       if ftype.startswith('text/'): ftype += '; charset=UTF-8'
  646.      
  647.       with open(path,'rb') as f:
  648.         content = f.read()
  649.      
  650.       # If we're working with a TPL, replace variables.
  651.       if ext == '.tpl':
  652.         content = content % {'username':username,'password':password}
  653.      
  654.       # If we can, use zlib to compress.
  655.       if headers.has_key('Accept-Encoding') and 'gzip' in headers['Accept-Encoding']:
  656.         import StringIO, gzip
  657.         stream = StringIO.StringIO()
  658.         zipper = gzip.GzipFile(mode='wb',fileobj=stream)
  659.         zipper.write(content)
  660.         zipper.close()
  661.         content = stream.getvalue()
  662.         stream.close()
  663.         compressed = True
  664.       else:
  665.         compressed = False
  666.      
  667.       out = [
  668.         'HTTP/1.1 200 OK',
  669.         'Server: GreenLantern/%s' % __version__,
  670.         'Content-Length: %d' % len(content),
  671.         'Connection: close',
  672.         'Content-Type: %s' % ftype
  673.       ]
  674.      
  675.       if compressed: out.append('Content-Encoding: gzip')
  676.       self.message('\r\n'.join(out) + '\r\n\r\n' + content)
  677.      
  678.     else:
  679.       # File not found.
  680.       content = '<html><head><title>404 Not Found</title></head><body><h1>404 Not Found</h1>The requested file does not exist.</body></html>'
  681.       out = ['HTTP/1.1 404 Not Found','Server: GreenLantern/%s' % (__version__,), 'Connection: close','Content-Length: %d' % (len(content),),'Content-Type: text/html; encoding=UTF-8','',content]
  682.       self.message('\r\n'.join(out))
  683.  
  684.   def _parse_header(self):
  685.     '''Attempt to parse a WebSocket header.'''
  686.     response, extra = self.buffer.split('\r\n\r\n', 1)
  687.     try: status, headers = response.split('\r\n', 1)
  688.     except:
  689.       state = response
  690.       headers = {}
  691.     if type(headers) is str:
  692.       headers = dict([ d.split(': ') for d in headers.split('\r\n')])
  693.    
  694.     # Check the validity of the status line and load the path.
  695.     m = _STATUS_REG.match(status)
  696.     if m: self.path = m.group(1)
  697.     else:
  698.       self.handle_close()
  699.       return
  700.    
  701.     # Check for the Upgrade header. If it doesn't exist, or is not WebSocket,
  702.     # and Oa has a path, try serving a file, HTTP style.
  703.     if (not headers.has_key('Upgrade') or headers['Upgrade'] != 'WebSocket') and self.oa.config.has_key('path'):
  704.       self.handleHTTP(headers)
  705.       return
  706.    
  707.     # Check the validity of the provided headers
  708.     is_valid = False
  709.     for k,v in _MANDATORY_HEADERS.iteritems():
  710.       if not headers.has_key(k): break
  711.       if v and headers[k] != v: break
  712.     else:
  713.       is_valid = True
  714.    
  715.     if headers.has_key('Host'):
  716.       self.location = 'ws://%s/%s' % (headers['Host'], self.path)
  717.    
  718.     if headers.has_key('Origin'):
  719.       self.origin = headers['Origin']
  720.       is_valid &= self.oa.checkOrigin(self.origin)
  721.    
  722.     # If the connection is valid, send a handshake. Otherwise, close.
  723.     if not is_valid:
  724.       self.handle_close()
  725.       return
  726.    
  727.     # Send our reply handshake and turn on WebSocket mode.
  728.     self.sendHandshake()
  729.     self.state_ws = True
  730.    
  731.     # Send our banner.
  732.     self.sendBanner()
  733.    
  734.     # Try getting a username and password from path.
  735.     user = None
  736.     if '@' in self.path:
  737.       username, self.path = self.path.split('@', 1)
  738.       if ':' in username: username, password = username.split(':', 1)
  739.       else: password = ''
  740.       if username:
  741.         user = self.oa.checkUser(username, password)
  742.         if user: self.logInfo('Authenticated as: %s' % username)
  743.         elif password and not user: self.logInfo('Invalid login: %s, %s' % (username, password))
  744.    
  745.     # If path isn't 'menu', try creating a LanternProxy. Look up the path with
  746.     # Oa to see if there's a proper server.
  747.     self.logDebug('GET /' + self.path)
  748.     if self.path != 'menu':
  749.       host, port, compress = self.oa.findServer(self.path, user)
  750.       if host:
  751.         self.connectHost((host, port, compress), user)
  752.         return
  753.       else:
  754.         self.logInfo('Not authorized for: %s' % self.path)
  755.    
  756.     # Still here? In that case, show the menu.
  757.     self.state = 'connected'
  758.     self.proxy = LanternMenu(self, self.oa, user)
  759.  
  760.   def sendBanner(self):
  761.     '''Send a Green Lantern banner.'''
  762.     self.message('\x1b[H\x1b[J\x1b[38;5;10mGreen Lantern MUD Proxy \x1b[38;5;8m(\x1b[38;5;10mv%s\x1b[38;5;8m) for\x1b[0m \x1b[38;5;10mDecafMUD \x1b[38;5;8mby \x1b[38;5;10mStendec\x1b[38;5;120m\n  In brightest day, in darkest night... no firewall obscure my site.\n\n' % __version__)
  763.     clients = len(self.oa.clients)
  764.     if clients == 1:
  765.       self.message('  \x1b[0mThere is currently \x1b[38;5;10m%d\x1b[0m user connected with Green Lantern.\n\n' % clients)
  766.     else:
  767.       self.message('  \x1b[0mThere are currently \x1b[38;5;10m%d\x1b[0m users connected with Green Lantern.\n\n' % clients)
  768.  
  769.   def _read_connected(self, data):
  770.     '''Pass incoming data through to the proxy connection.'''
  771.     if self.proxy:
  772.       self.proxy.message(data)
  773.  
  774.   def _read_initial(self, data):
  775.     '''Initially, read data until we receive either a valid Flash policy file
  776.       request, or a WebSocket header.'''
  777.     self.buffer += data
  778.    
  779.     # Is it a Flash policy request?
  780.     if self.buffer == '<policy-file-request/>\x00':
  781.       self.sendFlashPolicy()
  782.    
  783.     # Is it a WebSocket header?
  784.     elif self.buffer.endswith('\r\n\r\n'):
  785.       self._parse_header()
  786.       self.buffer = ''
  787.  
  788.   def _read_waiting(self, data):
  789.     '''Don't do anything. We're waiting on the proxy.'''
  790.     pass
  791.  
  792.   def message(self, text):
  793.     '''Send text to the client. If we're in WebSocket mode, encode the outgoing
  794.       data stream with the wsraw codec to allow proper binary transfer through
  795.       UTF-8. Also, wrap the message with 0x00 and 0xFF if this is a websocket.'''
  796.     if self.state_ws:
  797.       self.out_buf += '\x00%s\xFF' % text.encode('wsraw')
  798.     else:
  799.       self.out_buf += text
  800.  
  801.   def sendFlashPolicy(self):
  802.     '''Send a flash policy file to the client and close. If not specified as a
  803.       starting parameter, Green Lantern will automatically generate a policy.'''
  804.     self.logInfo('Sent Flash policy.')
  805.     self.out_buf += self.oa.getFlashPolicy()
  806.     self.close_empty = True
  807.  
  808.   def sendHandshake(self):
  809.     '''Send a WebSocket handshake in preparation for engaging websocket mode.'''
  810.     out = [
  811.       'HTTP/1.1 101 Web Socket Protocol Handshake',
  812.       'Upgrade: WebSocket',
  813.       'Connection: Upgrade',
  814.       'WebSocket-Origin: %s' % self.origin,
  815.       'WebSocket-Location: %s' % self.location,
  816.       '\r\n']
  817.     self.out_buf += '\r\n'.join(out)
  818.  
  819.   def writable(self):
  820.     return len(self.out_buf) > 0
  821.  
  822. ## Class: Oa ##
  823. ###############
  824. class Oa(asyncore.dispatcher):
  825.   '''The Oa dispatcher keeps track of settings, and listens for new incoming
  826.     connections.'''
  827.  
  828.   def __init__(self, configuration):
  829.     self.config = configuration
  830.     self.clients = []
  831.    
  832.     self.buildLog()
  833.     self.log.info('Created Oa')
  834.    
  835.     asyncore.dispatcher.__init__(self)
  836.     self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
  837.     self.set_reuse_addr()
  838.     self.bind((self.config['ip'], self.config['port']))
  839.     self.listen(self.config['backlog'])
  840.    
  841.     self.log.info('Listening on %s:%d' % (self.config['ip'], self.config['port']))
  842.  
  843.   def handle_accept(self):
  844.     conn, addr = self.accept()
  845.    
  846.     # Ensure that the connection isn't blacklisted.
  847.     allow, rule = self.checkBlacklist(addr)
  848.     if not allow:
  849.       self.log.info('Blocked connection from %s:%d by: %s' % (addr[0], addr[1], str(rule)))
  850.       conn.close()
  851.       return
  852.    
  853.     # Start a client.
  854.     self.clients.append(GreenLantern(conn, self))
  855.  
  856.   def buildLog(self):
  857.     self.log = logging.getLogger('GL')
  858.     self.log.setLevel(logging.DEBUG)
  859.    
  860.     sh = logging.StreamHandler()
  861.     sh.setLevel(logging.DEBUG)
  862.     self.log.addHandler(sh)
  863.    
  864.     fmt = logging.Formatter("%(asctime)s %(name)-10s %(levelname)-8s %(message)s", "%Y-%m-%d %H:%M:%S")
  865.     sh.setFormatter(fmt)
  866.  
  867.   def checkBlacklist(self, address):
  868.     '''Check to ensure an IP isn't blacklisted.'''
  869.     if not self.config.has_key('blacklist'): return True, None
  870.     if self.config.has_key('whitelist') and address[0] in self.config['whitelist']: return True, None
  871.    
  872.     ip = address[0]
  873.     for bl in self.config['blacklist']:
  874.       if re.match(bl, ip):
  875.         return False, bl
  876.    
  877.     return True, None
  878.  
  879.   def checkOrigin(self, origin):
  880.     '''Check an Origin string to ensure it's valid.'''
  881.     if not self.config.has_key('origin'): return True
  882.     if type(self.config['origin']) is list:
  883.       # Hallowed are the Ori.
  884.       for ori in self.config['origin']:
  885.         if re.match(ori, origin):
  886.           return True
  887.     else:
  888.       if re.match(self.config['origin'], origin):
  889.         return True
  890.    
  891.     return False
  892.  
  893.   def checkUser(self, username, password):
  894.     '''Check a username and password to see if they're valid. If so, return the
  895.       user's name and the user's group.'''
  896.     if self.config.has_key('users') and self.config['users'].has_key(username):
  897.       user = self.config['users'][username]
  898.       if user['password'] == hashlib.sha1(password).hexdigest():
  899.         user['username'] = username
  900.         return user
  901.     return None
  902.  
  903.   def isAuthServer(self, server, user):
  904.     '''Check to see if the user can connect to the server.'''
  905.     return (not server.has_key('users') and not server.has_key('groups')) or \
  906.       (user and server.has_key('users') and user['username'] in server['users']) or \
  907.       (user and server.has_key('groups') and user['group'] in server['groups'])
  908.  
  909.   def findServer(self, path, user = None):
  910.     '''Attempt to find a server with the given path, and return it. If we can't,
  911.       try returning the default server. If we can't, what the hell? That
  912.       should never happen, but oh well.
  913.      
  914.       Return: host, port, use_compression'''
  915.     host = None
  916.     if self.config['hosts'].has_key(self.config['default']):
  917.       host = self.config['hosts'][self.config['default']]
  918.    
  919.     if self.config['hosts'].has_key(path):
  920.       host = self.config['hosts'][path]
  921.     elif path.startswith('port_'):
  922.       try: path = int(path[5:])
  923.       except: path = path[5:]
  924.       # We have a port, so return the first host that matches it.
  925.       for h in self.config['hosts'].values():
  926.         if h['port'] == path and h['host'] == '127.0.0.1' or h['host'].lower() == 'localhost':
  927.           host = h
  928.           break
  929.    
  930.     if not host:
  931.       return None, None, None
  932.    
  933.     # Return True as the second variable to inform the Lantern that it's an
  934.     # authentication issue.
  935.     if not self.isAuthServer(host, user):
  936.       return None, True, None
  937.    
  938.     # We have a host. Is it local?
  939.     if not host.has_key('compress'):
  940.       if host['host'] == '127.0.0.1' or host['host'].lower() == 'localhost':
  941.         host['compress'] = False
  942.       else:
  943.         host['compress'] = True
  944.    
  945.     # Return it
  946.     return host['host'], host['port'], host['compress']
  947.  
  948.   def getFlashPolicy(self):
  949.     '''Return the Flash policy.'''
  950.     return self.config['fpd']
  951.  
  952. ## Application Startup Logic ##
  953. ###############################
  954. def main(options):
  955.   host = options.host if options.host else '127.0.0.1'
  956.   config = {
  957.     'port'    : options.port,
  958.     'ip'      : options.ip,
  959.     'backlog' : options.backlog,
  960.    
  961.     'default' : 'simple',
  962.    
  963.     'hosts'   : {
  964.       'simple'  : {
  965.         'host'      : host,
  966.         'port'      : options.remote_port,
  967.       },
  968.     },
  969.   }
  970.  
  971.   # If file is set, try loading the file with json
  972.   if options.file:
  973.     if not os.path.exists(options.file):
  974.       print >> sys.stderr, "The configuration file does not exist."
  975.       sys.exit(1)
  976.     config = readFile(options.file, config)
  977.   else:
  978.     config['hosts']['simple'] = {
  979.       'host' : host,
  980.       'port' : options.remote_port
  981.     }
  982.    
  983.     if options.username:
  984.       if not options.password:
  985.         print >> sys.stderr, "You must set a password too."
  986.         sys.exit(1)
  987.       config['hosts']['simple']['users'] = [options.username]
  988.       config['users'] = { options.username : {
  989.         'password' : hashlib.sha1(options.password).hexdigest(),
  990.         'group' : 'users'
  991.       } }
  992.  
  993.   # If path is set, make sure it's a valid path before setting it.
  994.   if options.path:
  995.     path = os.path.realpath(options.path)
  996.     if os.path.exists(path):
  997.       config['path'] = path
  998.   if options.freeweb:
  999.     config['freeweb'] = True
  1000.  
  1001.   # If we have a Flash policy file, load it. Otherwise, use a VERY open one.
  1002.   if not config.has_key('fpd') and options.fpd:
  1003.     config['fpd'] = options.fpd
  1004.  
  1005.   if config.has_key('fpd'):
  1006.     if os.path.exists(config['fpd']):
  1007.       with open(config['fpd'],fb) as f:
  1008.         config['fpd'] = f.read()
  1009.     else:
  1010.       del config['fpd']
  1011.  
  1012.   if not config.has_key('fpd'):
  1013.     ports = []
  1014.     for host in config['hosts'].values():
  1015.       if host['host'] == '127.0.0.1' or host['host'].lower() == 'localhost':
  1016.         ports.append(str(host['port']))
  1017.     config['fpd'] = """<?xml version="1.0"?>
  1018. <!DOCTYPE cross-domain-policy SYSTEM "/xml/dtds/cross-domain-policy.dtd">
  1019. <cross-domain-policy>
  1020.  <allow-access-from domain="*" to-ports="%s" />
  1021. </cross-domain-policy>""" % ','.join(ports)
  1022.  
  1023.   # Create the Oa instance, and start listening.
  1024.   oa = Oa(config)
  1025.   try:
  1026.     asyncore.loop()
  1027.   except KeyboardInterrupt:
  1028.     print ''
  1029.  
  1030.   # Return a success code.
  1031.   sys.exit(0)
  1032.  
  1033. def readFile(path, config):
  1034.   '''Use ConfigParser to read configuration data.'''
  1035.   import ConfigParser
  1036.   fconf = ConfigParser.ConfigParser()
  1037.   fconf.read(path)
  1038.  
  1039.   # If there's a general section, load variables from it.
  1040.   if fconf.has_section('general'):
  1041.     if fconf.has_option('general','policy'):
  1042.       config['fpd'] = os.path.realpath(fconf.get('general','policy'))
  1043.     if fconf.has_option('general','path'):
  1044.       pth = os.path.realpath(fconf.get('general','path'))
  1045.       if os.path.exists(pth):
  1046.         config['path'] = pth
  1047.     if fconf.has_option('general','port'):
  1048.       config['port'] = fconf.getint('general','port')
  1049.     if fconf.has_option('general','bind-to'):
  1050.       config['ip'] = fconf.get('general','bind-to')
  1051.     if fconf.has_option('general','backlog'):
  1052.       config['backlog'] = fconf.getint('general','backlog')
  1053.     if fconf.has_option('general','default'):
  1054.       config['default'] = fconf.get('general','default')
  1055.     if fconf.has_option('general','free-web'):
  1056.       config['freeweb'] = fconf.getboolean('general','free-web')
  1057.  
  1058.   if not config.has_key('users'): config['users'] = {}
  1059.   if not config.has_key('hosts'): config['hosts'] = {}
  1060.  
  1061.   if config['hosts'].has_key('simple'):
  1062.     del config['hosts']['simple']
  1063.  
  1064.   # Loop through all the sections, looking for users and hosts.
  1065.   for sect in fconf.sections():
  1066.     # Is it a host?
  1067.     if sect.startswith('host:'):
  1068.       h = config['hosts'][sect[5:]] = {}
  1069.       if fconf.has_option(sect, 'name'):
  1070.         h['name'] = fconf.get(sect, 'name')
  1071.       if fconf.has_option(sect, 'host'):
  1072.         h['host'] = fconf.get(sect, 'host')
  1073.       if fconf.has_option(sect, 'port'):
  1074.         h['port'] = fconf.getint(sect, 'port')
  1075.       if fconf.has_option(sect, 'users'):
  1076.         h['users'] = [u.strip() for u in fconf.get(sect,'users').split(',')]
  1077.       if fconf.has_option(sect, 'groups'):
  1078.         h['groups'] = [g.strip() for g in fconf.get(sect,'groups').split(',')]
  1079.    
  1080.     # Is it a user?
  1081.     if sect.startswith('user:'):
  1082.       u = config['users'][sect[5:]] = {}
  1083.       if fconf.has_option(sect, 'password'):
  1084.         u['password'] = fconf.get(sect, 'password')
  1085.       if fconf.has_option(sect, 'group'):
  1086.         u['group'] = fconf.get(sect, 'group')
  1087.  
  1088.   # If we don't have any actual users, kill the array.
  1089.   if len(config['users'].keys()) == 0:
  1090.     del config['users']
  1091.   return config
  1092.  
  1093. def parseOpts(args = None):
  1094.   '''Parse command line arguments into usable data.'''
  1095.   if not args: args = sys.argv[1:]
  1096.  
  1097.   usage = "usage: %prog [options]"
  1098.   version = "%prog " + __version__
  1099.   parser = OptionParser(usage, version=version) #, prog="greenlantern")
  1100.   parser.add_option('-p','--port', dest="port", type="int", default=843,
  1101.     help="Listen on port PORT")
  1102.   parser.add_option('-i','--ip', dest="ip", default="0.0.0.0",
  1103.     help="Bind the server to IP")
  1104.   parser.add_option('-b','--backlog', dest="backlog", type="int", default=5,
  1105.     help="The number of connecting clients to queue.")
  1106.  
  1107.   group = OptionGroup(parser, "Simple Mode",
  1108.                       "Use these options to start Green Lantern with very "
  1109.                       "basic configuration, no authorization, and forwarding "
  1110.                       "to just one host.")
  1111.   group.add_option('-s','--server', dest="host",
  1112.     help="Run in basic mode, forwarding to HOST")
  1113.   group.add_option('-r', dest="remote_port", type="int", default=4000,
  1114.     help="Connect to this port on the remote server. Defaults to port 4000")
  1115.   group.add_option('--fpd', dest="fpd",
  1116.     help="Use the policy file FPD")
  1117.   parser.add_option_group(group)
  1118.  
  1119.   g2 = OptionGroup(parser, "Authentication",
  1120.                    "You can set basic authentication to secure Green Lantern "
  1121.                    "if using it for private use.")
  1122.   g2.add_option('-u','--user', dest="username",
  1123.     help="Create user with USERNAME")
  1124.   g2.add_option('-w','--pass', dest="password",
  1125.     help="Set user's password to PASSWORD.")
  1126.   g2.add_option('--free-web', dest="freeweb", action="store_true",
  1127.     help="Don't require a password (if a user exists) for accessing the "
  1128.          "internal HTTP server.")
  1129.   parser.add_option_group(g2)
  1130.  
  1131.   g3 = OptionGroup(parser, "Advanced",
  1132.                    "In advanced mode, Green Lantern supports authentication, "
  1133.                    "blacklisting client IPs, and connecting to more than one "
  1134.                    "MUD server with a menu for selection.")
  1135.   g3.add_option('-c','--config', dest="file",
  1136.     help="Load configuration from FILE")
  1137.   g3.add_option('--path', dest="path",
  1138.     help="Serve the contents of PATH with basic webserver functionality.")
  1139.   parser.add_option_group(g3)
  1140.  
  1141.   return parser.parse_args()
  1142.  
  1143. if __name__ == '__main__':
  1144.   (options, args) = parseOpts()
  1145.   main(options)
Add Comment
Please, Sign In to add comment