Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python
- ################################################################################
- ##
- ## DecafMUD - Green Lantern
- ## Copyright (c) 2010
- ##
- ## A Python server using Twisted for enabling WebSocket connectivity for the
- ## DecafMUD client. In addition, it can act as a Flash policy server and a proxy
- ## for connecting to MUDs on other servers.
- ##
- ## Green Lantern has an ACL, allowing you to block certain IP addresses from
- ## connecting or requiring the client to login with a username and password
- ## before allowing a connection to form.
- ##
- ## Author: Stendec <stendec365@gmail.com>
- ##
- ################################################################################
- __version__ = '0.1.1'
- ## Imports ##
- #############
- from optparse import OptionGroup, OptionParser
- from types import ClassType
- import asyncore, hashlib, logging, os, re, socket, sys, zlib
- import codec_wsraw
- ## Variables ##
- ###############
- # Telnet characters
- IAC = chr(255)
- DO = chr(253)
- DONT = chr(254)
- WILL = chr(251)
- WONT = chr(252)
- SB = chr(250)
- COMPRESS = chr(85)
- COMPRESS2 = chr(86)
- ECHO = chr(1)
- # MCCP negotiation matcher
- _COMP_REG = re.compile('\\xFF(?:[\\xFB-\\xFE][\\x55\\x56]|\\xFA(?:\\x55\\xFB\\xF0|\\x56\\xFF\\xF0))')
- # Mandatory headers for WebSocket connections
- _MANDATORY_HEADERS = {
- 'Upgrade' : 'WebSocket',
- 'Connection' : 'Upgrade',
- 'Host' : None,
- 'Origin' : None,
- }
- # Status line matcher for WebSocket connections
- _STATUS_REG = re.compile('^GET /([\S]*) HTTP/1.1$')
- ## Helper Functions ##
- ######################
- def _readFrameLength(data):
- '''Read a WebSocket-encoded frame length from data, and return both the
- length and the bytes consumed determining that length.'''
- length = 0
- consumed = 0
- for char in data:
- consumed += 1
- b = ord(char)
- length = length * 128 + (b & 0x7F)
- if (b & 0x80) == 0:
- break
- return length, consumed
- ## Class: LanternProxy ##
- #########################
- class LanternProxy(asyncore.dispatcher):
- '''This class is responsible for the connections to the remote server, and
- handles MCCP for easing bandwidth requirements when connecting to remote
- hosts.'''
- def __init__(self, id = '', allow_compress = True):
- self.socket = None
- self.in_buf = ''
- self.out_buf = ''
- self.mccp_level = 0
- self.mccp_remote = False
- self.allow_compress = allow_compress
- if allow_compress:
- self.decompressor = zlib.decompressobj()
- self.id = id
- self.log = logging.getLogger('GL.Proxy')
- self.log.setLevel(logging.DEBUG)
- asyncore.dispatcher.__init__(self)
- def logInfo(self, text):
- self.log.info('[%s] %s' % (self.id, text))
- def logDebug(self, text):
- self.log.debug('[%s] %s' % (self.id, text))
- def setSocket(self, sock, remote_ip, remote_port):
- '''Attach to a GreenLantern, then connect to the given IP and port.'''
- self.proxied = sock
- self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
- self.connect((remote_ip, remote_port))
- def handle_connect(self):
- '''Notify the GreenLantern that we're connected.'''
- if self.proxied:
- self.proxied.state = 'connected'
- def handle_close(self):
- self.close()
- if self.proxied:
- # Make sure nothing's trapped in our buffers.
- if self.mccp_remote:
- data = self.decompressor.flush()
- if data: self.proxied.message(data)
- self.proxied.proxy = None
- self.proxied.handle_close()
- def handle_read(self):
- read = self.recv(4096)
- self.in_buf += read
- if self.proxied:
- self._process()
- def handle_write(self):
- sent = self.send(self.out_buf)
- self.out_buf = self.out_buf[sent:]
- def message(self, text):
- self.out_buf += text
- def _process(self):
- '''First, intercept sequences of IAC WILL/WONT/DO/DONT COMPRESS/COMPRESSv2
- and IAC SB COMPRESS/COMPRESSv2 (IAC) SE.
- Second, if MCCP is active, decompress the input. Otherwise just send the
- buffer along to the client.'''
- if not self.allow_compress:
- self.proxied.message(self.in_buf)
- self.in_buf = ''
- return
- if self.mccp_remote:
- # MCCP is enabled. Decompress the text.
- self.in_buf = self.decompressor.decompress(self.in_buf)
- data = self.decompressor.unused_data
- if data:
- # The remote server has ended the compressed stream. Shut off the MCCP
- # flag and create a fresh decompressor in case they start again.
- self.logInfo('MCCP Compression ended.')
- self.decompressor = zlib.decompressobj()
- self.mccp_remote = False
- self.mccp_level = 0
- else:
- # MCCP isn't enabled. Pat yourself on the back or something.
- data = ''
- # Check the data for MCCP negotiation.
- m = _COMP_REG.search(self.in_buf)
- if m:
- # We have negotiation. First, send the text up to the negotiation to the
- # client.
- self.proxied.message(self.in_buf[:m.start()])
- # Determine what to do.
- seq = m.group()
- op = seq[1]
- mccp_level = 2 if seq[2] == COMPRESS2 else 1
- if op == WILL:
- # We support both, but respond to the first received
- self.logDebug('RCVD IAC WILL ' + ('COMPRESSv2' if seq[2] == COMPRESS2 else 'COMPRESS'))
- if not self.mccp_level:
- self.out_buf += IAC + DO + seq[2]
- self.mccp_level = mccp_level
- self.logDebug('SENT IAC DO ' + ('COMPRESSv2' if seq[2] == COMPRESS2 else 'COMPRESS'))
- else:
- self.out_buf += IAC + DONT + seq[2]
- self.logDebug('SENT IAC DONT ' + ('COMPRESSv2' if seq[2] == COMPRESS2 else 'COMPRESS'))
- elif op == WONT:
- self.logDebug('RCVD IAC WONT ' + ('COMPRESSv2' if seq[2] == COMPRESS2 else 'COMPRESS'))
- if mccp_level == self.mccp_level:
- self.mccp_level = 0
- elif op == SB:
- self.logDebug('RCVD IAC SB ' + ('COMPRESSv2' if seq[2] == COMPRESS2 else 'COMPRESS'))
- # Start a compressed stream.
- self.mccp_remote = True
- self.logInfo('MCCP Compression started.')
- # Update the in_buf
- self.in_buf = self.in_buf[m.end():] + data
- else:
- # No negotiation. Send the in_buf in full.
- self.proxied.message(self.in_buf)
- self.in_buf = data
- # If we still have in_buf, reprocess
- if self.in_buf:
- self._process()
- def writable(self):
- return len(self.out_buf) > 0
- ## Class: LoginHandler ##
- #########################
- class MenuLogin:
- def __init__(self, menu):
- self.menu = menu
- self.state = 0
- self.username = ''
- self.banner = False
- def display(self):
- if not self.banner:
- self.menu.sock.sendBanner()
- self.banner = True
- if self.state == 0:
- self.menu.send_raw('\x1b[38;5;2mUsername\x1b[38;5;8m:\x1b[0m ')
- else:
- self.menu.send_raw('\x1b[38;5;2mPassword\x1b[38;5;8m:\x1b[0m ' + IAC + WILL + ECHO)
- def handle(self, data):
- if self.state == 0:
- if re.sub('[^a-zA-Z0-9\-_!@#$%^&*(),.<>]', '', data) == data and data:
- self.username = data
- self.state = 1
- elif not data and self.menu.can_without:
- self.menu.pop_ih()
- self.menu.push_ih(MenuMenu)
- else:
- self.menu.send('\x1b[38;5;1mInvalid username.')
- elif self.state == 1:
- self.menu.send(IAC + WONT + ECHO)
- if data:
- # Try to authenticate.
- user = self.menu.oa.checkUser(self.username, data)
- if user:
- self.menu.send_raw('\n')
- self.menu.user = user
- self.menu.pop_ih()
- self.menu.push_ih(MenuMenu)
- else:
- self.menu.send('\x1b[38;5;1mInvalid username or password.')
- else:
- self.state = 0
- class MenuMenu:
- '''The actual menu to select a MUD.'''
- def __init__(self, menu):
- self.menu = menu
- self.state = 0
- # Get the available MUDs.
- self.muds = []
- self.longest = 0
- for k,host in self.menu.oa.config['hosts'].iteritems():
- if self.menu.oa.isAuthServer(host, self.menu.user):
- name = k
- if host.has_key('name'): name = host['name']
- if len(name) > self.longest: self.longest = len(name)
- host['__name'] = name
- self.muds.append(host)
- self.fmt = '\x1b[38;5;120m%2d\x1b[38;5;8m)\x1b[0m %-'+str(self.longest)+'s '
- self.cols = 80 / (self.longest + 6)
- self.banner = False
- def display(self):
- if not self.banner:
- self.banner = True
- self.menu.sock.sendBanner()
- col = 0
- ind = 0
- for i in range(len(self.muds)):
- ind += 1
- col += 1
- self.menu.send_raw(self.fmt % (ind, self.muds[i]['__name']))
- if col > self.cols:
- col = 0
- self.menu.send_raw('\n')
- self.menu.send_raw('\n\n')
- if self.menu.user:
- self.menu.send_raw('\x1b[38;5;2mSelect a mud, or Q to quit\x1b[38;5;8m:\x1b[0m ')
- else:
- self.menu.send_raw('\x1b[38;5;2mSelect a mud, L to log in, or Q to quit\x1b[38;5;8m:\x1b[0m ')
- def handle(self, data):
- if not data:
- return
- if 'login'.startswith(data.lower()):
- self.menu.pop_ih()
- self.menu.push_ih(MenuLogin)
- return
- if 'quit'.startswith(data.lower()):
- self.menu.pop_ih()
- return
- try: data = int(data)
- except: data = 0
- if data < 1 or data > len(self.muds):
- self.menu.send('\x1b[38;5;1mInvalid choice.')
- return
- # Connect it.
- self.menu.expected_close = True
- self.menu.pop_ih()
- self.menu.sock.connectHost(self.muds[data-1], self.menu.user)
- ## Class: LanternMenu ##
- ########################
- class LanternMenu:
- '''This class provides a simple in-telnet menu for Green Lantern, allowing a
- user to login if it wasn't done automatically (if it's necessary at all)
- and providing a list of MUDs to choose from.'''
- def __init__(self, sock, oa, user = None):
- self.expected_close = False
- self.oa = oa
- self.sock = sock
- self.id = sock.id
- self.user = user
- self.in_buf = ''
- self.buffer = ''
- self.log = logging.getLogger('GL.Menu')
- self.log.setLevel(logging.DEBUG)
- self.logInfo('New menu.')
- # Create a stack of input handlers, and push the default on. Then force it
- # to draw the current handler.
- self.ih = []
- self.can_without = False
- for host in self.oa.config['hosts'].values():
- if self.oa.isAuthServer(host, self.user):
- self.can_without = True
- break
- if self.can_without:
- self.push_ih(MenuMenu)
- else:
- self.push_ih(MenuLogin)
- self.ih[0].display()
- def logInfo(self, text):
- self.log.info('[%s] %s' % (self.id,text))
- def logDebug(self, text):
- self.log.debug('[%s] %s' % (self.id, text))
- def handleBuffer(self):
- '''Handle text sent to the buffer. Generally, this waits until we have a
- full line of text and process it all at once.'''
- ind = self.buffer.find('\n')
- if ind != -1:
- data = self.buffer[:ind]
- self.buffer = self.buffer[ind+1:]
- if data.endswith('\r'): data = data[:-1]
- # Call the current input handler's handler function.
- self.ih[0].handle(data)
- # If we have an input handler now, call its display function. Else, if
- # this isn't expected, close the socket.
- if len(self.ih) > 0:
- self.ih[0].display()
- elif not self.expected_close:
- self.sock.handle_close()
- def message(self, text):
- '''Append the text to the in_buf, and process it.'''
- self.in_buf += text
- self.process()
- def process(self):
- '''This function should be called by GreenLantern whenever additional data
- is sent to the menu, since this isn't updated by asyncore's loop.
- This is in charge of stripping out telnet sequences before handing data
- to the line handler.'''
- ind = self.in_buf.find('\xFF')
- if ind != -1:
- # We have TELNET. Buffer everything before it, and handle it.
- self.buffer += self.in_buf[:ind]
- self.in_buf = self.in_buf[ind:]
- # Process the buffered text, then read the IAC sequence.
- self.handleBuffer()
- self.readIACSequence()
- else:
- self.buffer += self.in_buf
- self.in_buf = ''
- self.handleBuffer()
- # If we still have in_buf, reprocess.
- #if len(self.in_buf) > 0:
- # self.process()
- def push_ih(self, ih):
- '''Push an input handler onto the stack.'''
- if type(ih) is ClassType:
- h = ih(self)
- else:
- h = ih
- self.ih.insert(0, h)
- def pop_ih(self):
- '''Remove the first input handler.'''
- self.ih.pop(0)
- def readIACSequence(self):
- '''Read a telnet sequence and wipe it off the input buffer.'''
- self.in_buf = self.in_buf.replace('\xFF\xFF','\xFF')
- if self.in_buf[0] == IAC:
- if self.in_buf[1] in (DO,DONT,WILL,WONT):
- self.in_buf = self.in_buf[3:]
- elif self.in_buf[1] == SB:
- ind = self.in_buf.find(IAC + SE)
- if ind != -1:
- self.in_buf = self.in_buf[ind:]
- def send(self, text):
- '''Send the text to the client, with a line return.'''
- self.send_raw(text + '\n')
- def send_raw(self, text):
- '''Send the text to the client.'''
- self.sock.message(text)
- ## Class: GreenLantern ##
- ########################
- class GreenLantern(asyncore.dispatcher):
- '''This class handles the connections to the clients themselves, generally
- DecafMUD. It hands out Flash policy files, proxies normal Flash connections,
- proxies WebSocket connections with translation, and provides a menu for
- doing ACL stuff.'''
- def __init__(self, sock, oa):
- self.oa = oa
- self.close_empty = False
- self.proxy = None
- self.state = 'initial'
- self.state_ws = False
- self.buffer = ''
- self.ws_buf = ''
- self.in_buf = ''
- self.out_buf = ''
- self.expect_frame = True
- self.frame = 0
- self.frame_len = 0
- sn = sock.getpeername()
- self.id = '%s:%s' % (sn[0], sn[1])
- self.log = logging.getLogger('GL.Core')
- self.log.setLevel(logging.DEBUG)
- self.logInfo('New connection.')
- asyncore.dispatcher.__init__(self, sock=sock)
- def logInfo(self, text):
- self.log.info('[%s] %s' % (self.id, text))
- def logDebug(self, text):
- self.log.debug('[%s] %s' % (self.id, text))
- def connectHost(self, host, user = None):
- '''Attempt to connect to the given host.'''
- if type(host) is not tuple:
- if not self.oa.isAuthServer(host, user):
- self.handle_close()
- return
- host = (host['host'], host['port'], False if host['host'] == '127.0.0.1' or host['host'].lower() == 'localhost' else True)
- # If we have a proxy, destroy it.
- if self.proxy:
- if isinstance(self.proxy, LanternProxy):
- self.proxy.proxied = None
- self.proxy.close()
- self.proxy = None
- # Send a message, then create a proxy.
- host, port, compress = host
- if compress:
- 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))
- else:
- self.message('\x1b[38;5;10mGreen Lantern \x1b[0m is forwarding you to port %d...\n\n' % port)
- self.logInfo('Connecting to: %s:%d' % (host,port))
- self.state = 'waiting'
- self.proxy = LanternProxy(self.id, compress)
- self.proxy.setSocket(self, host, port)
- def handle_close(self):
- self.logInfo('Connection closed.')
- self.close()
- if isinstance(self.proxy, LanternProxy):
- self.proxy.proxied = None
- self.proxy.close()
- # Remove the lantern from Oa.
- self.oa.clients.remove(self)
- def handle_read(self):
- read = self.recv(4096)
- self.in_buf += read
- if len(self.in_buf):
- if self.state_ws:
- self.readWebSocket()
- else:
- getattr(self, '_read_'+self.state)(self.in_buf)
- self.in_buf = ''
- def handle_write(self):
- sent = self.send(self.out_buf)
- self.out_buf = self.out_buf[sent:]
- if self.close_empty and not self.out_buf:
- self.handle_close()
- def readWebSocket(self):
- '''Decode incoming websocket data.'''
- if self.expect_frame:
- self.expect_frame = False
- self.frame = ord(self.in_buf[0])
- if self.frame & 0x80 == 0x80:
- self.frame_len, consumed = _readFrameLength(self.in_buf)
- self.in_buf = self.in_buf[consumed:]
- else:
- self.in_buf = self.in_buf[1:]
- if self.frame & 0x80 == 0:
- # Read until 0xFF
- ind = self.in_buf.find('\xFF')
- if ind != -1:
- # We have a 0xFF byte. Read everything up to it, decode it with wsraw,
- # and hand that to whatever logic we should be using.
- data = self.in_buf[:ind].decode('wsraw')
- self.in_buf = self.in_buf[ind+1:]
- getattr(self, '_read_'+self.state)(data)
- self.expect_frame = True
- # If we still have data, run through it again.
- if self.in_buf:
- self.readWebSocket()
- else:
- # Length based frames. Read until we have enough bytes.
- expected = self.frame_len - len(self.ws_buf)
- self.ws_buf += self.in_buf[:expected]
- self.in_buf = self.in_buf[expected:]
- if len(self.ws_buf) == self.frame_len:
- # We have enough. Hand the buffer off to the appropriate logic.
- getattr(self, '_read_'+self.state)(self.ws_buf)
- self.ws_buf = ''
- self.expect_frame = True
- # If we still have data, run through it again.
- if self.in_buf:
- self.readWebSocket()
- def handleHTTP(self, headers):
- '''Handle a HTTP request and serve basic files.'''
- self.logInfo('HTTP request for: /' + self.path)
- # If path is empty, default to 'index.htm'
- if self.path == '': path = 'index.htm'
- else: path = self.path
- # Chop anything extra off the end.
- if '?' in path: path, extra = path.split('?',1)
- # Be sure to close after this message.
- self.close_empty = True
- self.state = 'waiting'
- # Try getting the file, make sure it's within path.
- path = os.path.realpath(os.path.join(self.oa.config['path'], path))
- if not os.path.exists(path) and self.path == '':
- path += 'l'
- if not os.path.exists(path) and path.endswith('.html'):
- path = path[:-4] + 'tpl'
- elif not os.path.exists(path) and path.endswith('.htm'):
- path = path[:-3] + 'tpl'
- if not path.startswith(self.oa.config['path']):
- 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>'
- 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]
- self.message('\r\n'.join(out))
- return
- # If authentication is required, go for it.
- if not self.oa.config.has_key('freeweb') and self.oa.config.has_key('users'):
- # Check for a HTTP Authorization header.
- authorized = False
- if headers.has_key('Authorization'):
- username = headers['Authorization']
- if username.startswith('Basic '):
- import base64
- username, password = base64.decodestring(username[6:]).split(':',1)
- user = self.oa.checkUser(username, password)
- if user:
- authorized = True
- if not authorized:
- 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>'
- out = ['HTTP/1.1 401 Unauthorized','Server: GreenLantern/%s' % (__version__,), 'Connection: close','Content-Length: %d' % (len(content),),'Content-Type: text/html; encoding=UTF-8']
- out.append('WWW-Authenticate: Basic realm="DecafMUD (with Green Lantern)"')
- self.message('\r\n'.join(out) + '\r\n\r\n' + content)
- return
- else:
- user = None
- username = password = ''
- if os.path.exists(path) and os.path.isfile(path):
- # It's a valid file. Go for it.
- ftype = 'text/plain'
- ext = path.lower()[-4:]
- if ext.endswith('.js'): ftype = 'application/x-javascript; charset=UTF-8'
- elif ext == '.css': ftype = 'text/css'
- elif ext == '.swf': ftype = 'application/x-shockwave-flash'
- elif ext == '.tpl': ftype = 'text/html'
- elif ext == '.htm': ftype = 'text/html'
- elif path.lower()[-5:] == '.html': ftype = 'text/html'
- elif ext == '.jpg': ftype = 'image/jpeg'
- elif ext == '.png': ftype = 'image/png'
- elif ext == '.gif': ftype = 'image/gif'
- elif ext == '.ico': ftype = 'image/vnd.microsoft.icon'
- if ftype.startswith('text/'): ftype += '; charset=UTF-8'
- with open(path,'rb') as f:
- content = f.read()
- # If we're working with a TPL, replace variables.
- if ext == '.tpl':
- content = content % {'username':username,'password':password}
- # If we can, use zlib to compress.
- if headers.has_key('Accept-Encoding') and 'gzip' in headers['Accept-Encoding']:
- import StringIO, gzip
- stream = StringIO.StringIO()
- zipper = gzip.GzipFile(mode='wb',fileobj=stream)
- zipper.write(content)
- zipper.close()
- content = stream.getvalue()
- stream.close()
- compressed = True
- else:
- compressed = False
- out = [
- 'HTTP/1.1 200 OK',
- 'Server: GreenLantern/%s' % __version__,
- 'Content-Length: %d' % len(content),
- 'Connection: close',
- 'Content-Type: %s' % ftype
- ]
- if compressed: out.append('Content-Encoding: gzip')
- self.message('\r\n'.join(out) + '\r\n\r\n' + content)
- else:
- # File not found.
- content = '<html><head><title>404 Not Found</title></head><body><h1>404 Not Found</h1>The requested file does not exist.</body></html>'
- 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]
- self.message('\r\n'.join(out))
- def _parse_header(self):
- '''Attempt to parse a WebSocket header.'''
- response, extra = self.buffer.split('\r\n\r\n', 1)
- try: status, headers = response.split('\r\n', 1)
- except:
- state = response
- headers = {}
- if type(headers) is str:
- headers = dict([ d.split(': ') for d in headers.split('\r\n')])
- # Check the validity of the status line and load the path.
- m = _STATUS_REG.match(status)
- if m: self.path = m.group(1)
- else:
- self.handle_close()
- return
- # Check for the Upgrade header. If it doesn't exist, or is not WebSocket,
- # and Oa has a path, try serving a file, HTTP style.
- if (not headers.has_key('Upgrade') or headers['Upgrade'] != 'WebSocket') and self.oa.config.has_key('path'):
- self.handleHTTP(headers)
- return
- # Check the validity of the provided headers
- is_valid = False
- for k,v in _MANDATORY_HEADERS.iteritems():
- if not headers.has_key(k): break
- if v and headers[k] != v: break
- else:
- is_valid = True
- if headers.has_key('Host'):
- self.location = 'ws://%s/%s' % (headers['Host'], self.path)
- if headers.has_key('Origin'):
- self.origin = headers['Origin']
- is_valid &= self.oa.checkOrigin(self.origin)
- # If the connection is valid, send a handshake. Otherwise, close.
- if not is_valid:
- self.handle_close()
- return
- # Send our reply handshake and turn on WebSocket mode.
- self.sendHandshake()
- self.state_ws = True
- # Send our banner.
- self.sendBanner()
- # Try getting a username and password from path.
- user = None
- if '@' in self.path:
- username, self.path = self.path.split('@', 1)
- if ':' in username: username, password = username.split(':', 1)
- else: password = ''
- if username:
- user = self.oa.checkUser(username, password)
- if user: self.logInfo('Authenticated as: %s' % username)
- elif password and not user: self.logInfo('Invalid login: %s, %s' % (username, password))
- # If path isn't 'menu', try creating a LanternProxy. Look up the path with
- # Oa to see if there's a proper server.
- self.logDebug('GET /' + self.path)
- if self.path != 'menu':
- host, port, compress = self.oa.findServer(self.path, user)
- if host:
- self.connectHost((host, port, compress), user)
- return
- else:
- self.logInfo('Not authorized for: %s' % self.path)
- # Still here? In that case, show the menu.
- self.state = 'connected'
- self.proxy = LanternMenu(self, self.oa, user)
- def sendBanner(self):
- '''Send a Green Lantern banner.'''
- 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__)
- clients = len(self.oa.clients)
- if clients == 1:
- self.message(' \x1b[0mThere is currently \x1b[38;5;10m%d\x1b[0m user connected with Green Lantern.\n\n' % clients)
- else:
- self.message(' \x1b[0mThere are currently \x1b[38;5;10m%d\x1b[0m users connected with Green Lantern.\n\n' % clients)
- def _read_connected(self, data):
- '''Pass incoming data through to the proxy connection.'''
- if self.proxy:
- self.proxy.message(data)
- def _read_initial(self, data):
- '''Initially, read data until we receive either a valid Flash policy file
- request, or a WebSocket header.'''
- self.buffer += data
- # Is it a Flash policy request?
- if self.buffer == '<policy-file-request/>\x00':
- self.sendFlashPolicy()
- # Is it a WebSocket header?
- elif self.buffer.endswith('\r\n\r\n'):
- self._parse_header()
- self.buffer = ''
- def _read_waiting(self, data):
- '''Don't do anything. We're waiting on the proxy.'''
- pass
- def message(self, text):
- '''Send text to the client. If we're in WebSocket mode, encode the outgoing
- data stream with the wsraw codec to allow proper binary transfer through
- UTF-8. Also, wrap the message with 0x00 and 0xFF if this is a websocket.'''
- if self.state_ws:
- self.out_buf += '\x00%s\xFF' % text.encode('wsraw')
- else:
- self.out_buf += text
- def sendFlashPolicy(self):
- '''Send a flash policy file to the client and close. If not specified as a
- starting parameter, Green Lantern will automatically generate a policy.'''
- self.logInfo('Sent Flash policy.')
- self.out_buf += self.oa.getFlashPolicy()
- self.close_empty = True
- def sendHandshake(self):
- '''Send a WebSocket handshake in preparation for engaging websocket mode.'''
- out = [
- 'HTTP/1.1 101 Web Socket Protocol Handshake',
- 'Upgrade: WebSocket',
- 'Connection: Upgrade',
- 'WebSocket-Origin: %s' % self.origin,
- 'WebSocket-Location: %s' % self.location,
- '\r\n']
- self.out_buf += '\r\n'.join(out)
- def writable(self):
- return len(self.out_buf) > 0
- ## Class: Oa ##
- ###############
- class Oa(asyncore.dispatcher):
- '''The Oa dispatcher keeps track of settings, and listens for new incoming
- connections.'''
- def __init__(self, configuration):
- self.config = configuration
- self.clients = []
- self.buildLog()
- self.log.info('Created Oa')
- asyncore.dispatcher.__init__(self)
- self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
- self.set_reuse_addr()
- self.bind((self.config['ip'], self.config['port']))
- self.listen(self.config['backlog'])
- self.log.info('Listening on %s:%d' % (self.config['ip'], self.config['port']))
- def handle_accept(self):
- conn, addr = self.accept()
- # Ensure that the connection isn't blacklisted.
- allow, rule = self.checkBlacklist(addr)
- if not allow:
- self.log.info('Blocked connection from %s:%d by: %s' % (addr[0], addr[1], str(rule)))
- conn.close()
- return
- # Start a client.
- self.clients.append(GreenLantern(conn, self))
- def buildLog(self):
- self.log = logging.getLogger('GL')
- self.log.setLevel(logging.DEBUG)
- sh = logging.StreamHandler()
- sh.setLevel(logging.DEBUG)
- self.log.addHandler(sh)
- fmt = logging.Formatter("%(asctime)s %(name)-10s %(levelname)-8s %(message)s", "%Y-%m-%d %H:%M:%S")
- sh.setFormatter(fmt)
- def checkBlacklist(self, address):
- '''Check to ensure an IP isn't blacklisted.'''
- if not self.config.has_key('blacklist'): return True, None
- if self.config.has_key('whitelist') and address[0] in self.config['whitelist']: return True, None
- ip = address[0]
- for bl in self.config['blacklist']:
- if re.match(bl, ip):
- return False, bl
- return True, None
- def checkOrigin(self, origin):
- '''Check an Origin string to ensure it's valid.'''
- if not self.config.has_key('origin'): return True
- if type(self.config['origin']) is list:
- # Hallowed are the Ori.
- for ori in self.config['origin']:
- if re.match(ori, origin):
- return True
- else:
- if re.match(self.config['origin'], origin):
- return True
- return False
- def checkUser(self, username, password):
- '''Check a username and password to see if they're valid. If so, return the
- user's name and the user's group.'''
- if self.config.has_key('users') and self.config['users'].has_key(username):
- user = self.config['users'][username]
- if user['password'] == hashlib.sha1(password).hexdigest():
- user['username'] = username
- return user
- return None
- def isAuthServer(self, server, user):
- '''Check to see if the user can connect to the server.'''
- return (not server.has_key('users') and not server.has_key('groups')) or \
- (user and server.has_key('users') and user['username'] in server['users']) or \
- (user and server.has_key('groups') and user['group'] in server['groups'])
- def findServer(self, path, user = None):
- '''Attempt to find a server with the given path, and return it. If we can't,
- try returning the default server. If we can't, what the hell? That
- should never happen, but oh well.
- Return: host, port, use_compression'''
- host = None
- if self.config['hosts'].has_key(self.config['default']):
- host = self.config['hosts'][self.config['default']]
- if self.config['hosts'].has_key(path):
- host = self.config['hosts'][path]
- elif path.startswith('port_'):
- try: path = int(path[5:])
- except: path = path[5:]
- # We have a port, so return the first host that matches it.
- for h in self.config['hosts'].values():
- if h['port'] == path and h['host'] == '127.0.0.1' or h['host'].lower() == 'localhost':
- host = h
- break
- if not host:
- return None, None, None
- # Return True as the second variable to inform the Lantern that it's an
- # authentication issue.
- if not self.isAuthServer(host, user):
- return None, True, None
- # We have a host. Is it local?
- if not host.has_key('compress'):
- if host['host'] == '127.0.0.1' or host['host'].lower() == 'localhost':
- host['compress'] = False
- else:
- host['compress'] = True
- # Return it
- return host['host'], host['port'], host['compress']
- def getFlashPolicy(self):
- '''Return the Flash policy.'''
- return self.config['fpd']
- ## Application Startup Logic ##
- ###############################
- def main(options):
- host = options.host if options.host else '127.0.0.1'
- config = {
- 'port' : options.port,
- 'ip' : options.ip,
- 'backlog' : options.backlog,
- 'default' : 'simple',
- 'hosts' : {
- 'simple' : {
- 'host' : host,
- 'port' : options.remote_port,
- },
- },
- }
- # If file is set, try loading the file with json
- if options.file:
- if not os.path.exists(options.file):
- print >> sys.stderr, "The configuration file does not exist."
- sys.exit(1)
- config = readFile(options.file, config)
- else:
- config['hosts']['simple'] = {
- 'host' : host,
- 'port' : options.remote_port
- }
- if options.username:
- if not options.password:
- print >> sys.stderr, "You must set a password too."
- sys.exit(1)
- config['hosts']['simple']['users'] = [options.username]
- config['users'] = { options.username : {
- 'password' : hashlib.sha1(options.password).hexdigest(),
- 'group' : 'users'
- } }
- # If path is set, make sure it's a valid path before setting it.
- if options.path:
- path = os.path.realpath(options.path)
- if os.path.exists(path):
- config['path'] = path
- if options.freeweb:
- config['freeweb'] = True
- # If we have a Flash policy file, load it. Otherwise, use a VERY open one.
- if not config.has_key('fpd') and options.fpd:
- config['fpd'] = options.fpd
- if config.has_key('fpd'):
- if os.path.exists(config['fpd']):
- with open(config['fpd'],fb) as f:
- config['fpd'] = f.read()
- else:
- del config['fpd']
- if not config.has_key('fpd'):
- ports = []
- for host in config['hosts'].values():
- if host['host'] == '127.0.0.1' or host['host'].lower() == 'localhost':
- ports.append(str(host['port']))
- config['fpd'] = """<?xml version="1.0"?>
- <!DOCTYPE cross-domain-policy SYSTEM "/xml/dtds/cross-domain-policy.dtd">
- <cross-domain-policy>
- <allow-access-from domain="*" to-ports="%s" />
- </cross-domain-policy>""" % ','.join(ports)
- # Create the Oa instance, and start listening.
- oa = Oa(config)
- try:
- asyncore.loop()
- except KeyboardInterrupt:
- print ''
- # Return a success code.
- sys.exit(0)
- def readFile(path, config):
- '''Use ConfigParser to read configuration data.'''
- import ConfigParser
- fconf = ConfigParser.ConfigParser()
- fconf.read(path)
- # If there's a general section, load variables from it.
- if fconf.has_section('general'):
- if fconf.has_option('general','policy'):
- config['fpd'] = os.path.realpath(fconf.get('general','policy'))
- if fconf.has_option('general','path'):
- pth = os.path.realpath(fconf.get('general','path'))
- if os.path.exists(pth):
- config['path'] = pth
- if fconf.has_option('general','port'):
- config['port'] = fconf.getint('general','port')
- if fconf.has_option('general','bind-to'):
- config['ip'] = fconf.get('general','bind-to')
- if fconf.has_option('general','backlog'):
- config['backlog'] = fconf.getint('general','backlog')
- if fconf.has_option('general','default'):
- config['default'] = fconf.get('general','default')
- if fconf.has_option('general','free-web'):
- config['freeweb'] = fconf.getboolean('general','free-web')
- if not config.has_key('users'): config['users'] = {}
- if not config.has_key('hosts'): config['hosts'] = {}
- if config['hosts'].has_key('simple'):
- del config['hosts']['simple']
- # Loop through all the sections, looking for users and hosts.
- for sect in fconf.sections():
- # Is it a host?
- if sect.startswith('host:'):
- h = config['hosts'][sect[5:]] = {}
- if fconf.has_option(sect, 'name'):
- h['name'] = fconf.get(sect, 'name')
- if fconf.has_option(sect, 'host'):
- h['host'] = fconf.get(sect, 'host')
- if fconf.has_option(sect, 'port'):
- h['port'] = fconf.getint(sect, 'port')
- if fconf.has_option(sect, 'users'):
- h['users'] = [u.strip() for u in fconf.get(sect,'users').split(',')]
- if fconf.has_option(sect, 'groups'):
- h['groups'] = [g.strip() for g in fconf.get(sect,'groups').split(',')]
- # Is it a user?
- if sect.startswith('user:'):
- u = config['users'][sect[5:]] = {}
- if fconf.has_option(sect, 'password'):
- u['password'] = fconf.get(sect, 'password')
- if fconf.has_option(sect, 'group'):
- u['group'] = fconf.get(sect, 'group')
- # If we don't have any actual users, kill the array.
- if len(config['users'].keys()) == 0:
- del config['users']
- return config
- def parseOpts(args = None):
- '''Parse command line arguments into usable data.'''
- if not args: args = sys.argv[1:]
- usage = "usage: %prog [options]"
- version = "%prog " + __version__
- parser = OptionParser(usage, version=version) #, prog="greenlantern")
- parser.add_option('-p','--port', dest="port", type="int", default=843,
- help="Listen on port PORT")
- parser.add_option('-i','--ip', dest="ip", default="0.0.0.0",
- help="Bind the server to IP")
- parser.add_option('-b','--backlog', dest="backlog", type="int", default=5,
- help="The number of connecting clients to queue.")
- group = OptionGroup(parser, "Simple Mode",
- "Use these options to start Green Lantern with very "
- "basic configuration, no authorization, and forwarding "
- "to just one host.")
- group.add_option('-s','--server', dest="host",
- help="Run in basic mode, forwarding to HOST")
- group.add_option('-r', dest="remote_port", type="int", default=4000,
- help="Connect to this port on the remote server. Defaults to port 4000")
- group.add_option('--fpd', dest="fpd",
- help="Use the policy file FPD")
- parser.add_option_group(group)
- g2 = OptionGroup(parser, "Authentication",
- "You can set basic authentication to secure Green Lantern "
- "if using it for private use.")
- g2.add_option('-u','--user', dest="username",
- help="Create user with USERNAME")
- g2.add_option('-w','--pass', dest="password",
- help="Set user's password to PASSWORD.")
- g2.add_option('--free-web', dest="freeweb", action="store_true",
- help="Don't require a password (if a user exists) for accessing the "
- "internal HTTP server.")
- parser.add_option_group(g2)
- g3 = OptionGroup(parser, "Advanced",
- "In advanced mode, Green Lantern supports authentication, "
- "blacklisting client IPs, and connecting to more than one "
- "MUD server with a menu for selection.")
- g3.add_option('-c','--config', dest="file",
- help="Load configuration from FILE")
- g3.add_option('--path', dest="path",
- help="Serve the contents of PATH with basic webserver functionality.")
- parser.add_option_group(g3)
- return parser.parse_args()
- if __name__ == '__main__':
- (options, args) = parseOpts()
- main(options)
Add Comment
Please, Sign In to add comment