Added SSH support, based on patch by hagna (issue 166).

This commit is contained in:
Griatch 2011-05-27 17:47:35 +00:00
parent d2400a8a6b
commit 7c56c69cea
7 changed files with 445 additions and 102 deletions

View file

@ -1,7 +1,7 @@
"""
This defines the cmdset for the red_button. Here we have defined
the commands and the cmdset in the same module, but if you
have many different commands to merge it if often better
have many different commands to merge it is often better
to define the cmdset separately, picking and choosing from
among the available commands as to what should be included in the
cmdset - this way you can often re-use the commands too.
@ -10,11 +10,13 @@ cmdset - this way you can often re-use the commands too.
import random
from src.commands.cmdset import CmdSet
from game.gamesrc.commands.basecommand import Command
from game.gamesrc.scripts.examples import red_button_scripts as scriptexamples
# Some simple commands for the red button
#------------------------------------------------------------
# Commands defined for the red button
# Commands defined on the red button
#------------------------------------------------------------
class CmdNudge(Command):
@ -33,24 +35,17 @@ class CmdNudge(Command):
def func(self):
"""
nudge the lid.
nudge the lid. Random chance of success to open it.
"""
rand = random.random()
open_ok = False
if rand < 0.5:
string = "You nudge at the lid. It seems stuck."
self.caller.msg("You nudge at the lid. It seems stuck.")
elif 0.5 <= rand < 0.7:
string = "You move the lid back and forth. It won't budge."
self.caller.msg("You move the lid back and forth. It won't budge.")
else:
string = "You manage to get a nail under the lid. It pops open."
open_ok = True
self.caller.msg(string)
if open_ok:
"""open_lid() does its own emits, so defer it until we speak"""
self.obj.open_lid()
self.caller.msg("You manage to get a nail under the lid.")
self.caller.execute_cmd("open lid")
class CmdPush(Command):
"""
Push the red button
@ -66,7 +61,7 @@ class CmdPush(Command):
"""
Note that we choose to implement this with checking for
if the lid is open/closed. This is because this command
is likely to be tries regardless of the state of the lid.
is likely to be tried regardless of the state of the lid.
An alternative would be to make two versions of this command
and tuck them into the cmdset linked to the Open and Closed
@ -75,11 +70,17 @@ class CmdPush(Command):
"""
if self.obj.db.lid_open:
# assign the blind state script to the caller.
# this will assign the restricted BlindCmdset to
# the caller at startup, and remove it again
# once the time has run out
self.caller.scripts.add(scriptexamples.BlindedState)
string = "You reach out to press the big red button ..."
string += "\n\nA BOOM! A bright light blinds you!"
string += "\nThe world goes dark ..."
self.caller.msg(string)
self.obj.press_button(self.caller)
self.caller.msg(string)
self.caller.location.msg_contents("%s presses the button. BOOM! %s is blinded by a flash!" %
(self.caller.name, self.caller.name), exclude=self.caller)
else:
@ -111,10 +112,8 @@ class CmdSmashGlass(Command):
string = "You smash your hand against the glass"
string += " with all your might. The lid won't budge"
string += " but you cause quite the tremor through the button's mount."
self.caller.msg(string) # have to be called before breakage since that
# also gives a return feedback to the room.
self.obj.break_lamp()
return
string += "\nIt looks like the button's lamp stopped working for the time being."
self.obj.lamp_works = False
elif rand < 0.6:
string = "You hit the lid hard. It doesn't move an inch."
else:
@ -143,7 +142,20 @@ class CmdOpenLid(Command):
if self.obj.db.lid_locked:
self.caller.msg("This lid seems locked in place for the moment.")
return
string += "\nA ticking sound is heard, like a winding mechanism. Seems "
string += "the lid will soon close again."
self.db.lid_open = False
# add the relevant cmdsets to button
self.obj.cmdset.add(LidClosedCmdsSet)
# add the lid-close ticker script
self.obj.scripts.add(scriptexamples.LidCloseTimer)
# add more info to the button description
desc = self.obj.db.closed_desc
self.obj.db.temp_desc = desc
self.obj.db.desc = "%s\n%s" % (desc, "Its glass cover is open and the button exposed.")
self.caller.msg(string)
self.caller.location.msg_contents("%s opens the lid of the button." %
(self.caller.name), exclude=self.caller)
self.obj.open_lid()
@ -163,10 +175,17 @@ class CmdCloseLid(Command):
def func(self):
"Close the lid"
self.obj.close_lid()
if self.db.closed_desc:
self.obj.desc = self.db.closed_desc
self.obj.db.lid_open = False
# this will clean out scripts dependent on lid being open.
self.obj.scripts.validate()
self.caller.msg("You close the button's lid. It clicks back into place.")
self.caller.location.msg_contents("%s closes the button's lid." %
(self.caller.name), exclude=self.caller)
class CmdBlindLook(Command):
"""
Looking around in darkness
@ -253,7 +272,7 @@ class LidClosedCmdSet(CmdSet):
"""
key = "LidClosedCmdSet"
# default Union is used *except* if we are adding to a
# cmdset named RedButtonOpen - this one we replace
# cmdset named LidOpenCmdSet - this one we replace
# completely.
key_mergetype = {"LidOpenCmdSet": "Replace"}
@ -269,7 +288,7 @@ class LidOpenCmdSet(CmdSet):
"""
key = "LidOpenCmdSet"
# default Union is used *except* if we are adding to a
# cmdset named RedButtonClose - this one we replace
# cmdset named LidClosedCmdSet - this one we replace
# completely.
key_mergetype = {"LidClosedCmdSet": "Replace"}

View file

@ -1,21 +1,14 @@
"""
An example script parent for a nice red button object. It has
custom commands defined on itself that are only useful in relation to this
particular object. See example.py in gamesrc/commands for more info
on the pluggable command system.
Assuming this script remains in gamesrc/parents/examples, create an object
of this type using @create button:examples.red_button
This is a more advanced example object. It combines functions from
script.examples as well as commands.examples to make an interactive
button typeclass.
This file also shows the use of the Event system to make the button
send a message to the players at regular intervals. To show the use of
Events, we are tying two types of events to the red button, one which cause ALL
red buttons in the game to blink in sync (gamesrc/events/example.py) and one
event which cause the protective glass lid over the button to close
again some time after it was opened.
Create this button with
Note that if you create a test button you must drop it before you can
see its messages!
@create/drop examples.red_button.RedButton
Note that if you must drop the button before you can see its messages!
"""
import random
from game.gamesrc.objects.baseobjects import Object
@ -28,19 +21,17 @@ from game.gamesrc.commands.examples import cmdset_red_button as cmdsetexamples
class RedButton(Object):
"""
This class describes an evil red button.
It will use the script definition in
game/gamesrc/events/example.py to blink
at regular intervals until the lightbulb
breaks. It also use the EventCloselid script to
close the lid and do nasty stuff when pressed.
This class describes an evil red button. It will use the script
definition in game/gamesrc/events/example.py to blink at regular
intervals. It also uses a series of script and commands to handle
pushing the button and causing effects when doing so.
"""
def at_object_creation(self):
"""
This function is called when object is created. Use this
instead of e.g. __init__.
"""
# store desc
# store desc (default, you can change this at creation time)
desc = "This is a large red button, inviting yet evil-looking. "
desc += "A closed glass lid protects it."
self.db.desc = desc
@ -52,9 +43,9 @@ class RedButton(Object):
self.db.lamp_works = True
self.db.lid_locked = False
# set the default cmdset to the object, permanent=True means a
# script will automatically be created to always add this.
self.cmdset.add_default(cmdsetexamples.DefaultCmdSet, permanent=True)
# set the default cmdset to the object. This will by default surivive
# a server reboot (otherwise we could have used permanent=False).
self.cmdset.add_default(cmdsetexamples.DefaultCmdSet)
# since the other cmdsets relevant to the button are added 'on the fly',
# we need to setup custom scripts to do this for us (also, these scripts
@ -68,7 +59,7 @@ class RedButton(Object):
def open_lid(self, feedback=True):
"""
Open the glass lid and start the timer so it will soon close
Opens the glass lid and start the timer so it will soon close
again.
"""

View file

@ -5,99 +5,80 @@ with Evennia's permissions system.
To call these locks, make sure this module is included in the
settings tuple PERMISSION_FUNC_MODULES then define a lock on the form
'<access_type>:func(args)' and add it to the object's lockhandler.
Run the check method of the handler to execute the lock check.
Run the access() method of the handler to execute the lock check.
Note that accessing_obj and accessed_obj can be any object type
with a lock variable/field, so be careful to not expect
a certain object type.
Appendix: MUX locks
Below is a list nicked from the MUX help file on the locks available
in standard MUX. Most of these are not relevant to core Evennia since
locks in Evennia are considerably more flexible and can be implemented
on an individual command/typeclass basis rather than as globally
available like the MUX ones. So many of these are not available in
basic Evennia, but could all be implemented easily if needed for the
individual game.
MUX locks
Below is a list nicked from the MUX docs on the locks available
in MUX. These are not all necessarily relevant to an Evennia game
but to show they are all possible with Evennia, each entry is a
suggestion on how one could implement similar functionality in Evennia.
Name: Affects: Effect:
-------------------------------------------------------------------------
MUX Name: Affects: Effect:
-------------------------------------------------------------------------------
DefaultLock: Exits: controls who may traverse the exit to
its destination.
Evennia: specialized permission key
'traverse' checked in move method
Evennia: "traverse:<lockfunc()>"
Rooms: controls whether the player sees the SUCC
or FAIL message for the room following the
room description when looking at the room.
Evennia: This is better done by implementing
a clever room class ...
Evennia: Custom typeclass
Players/Things: controls who may GET the object.
Evennia: specialized permission key 'get'
defined on object, checked by get command
Evennia: "get:<lockfunc()"
EnterLock: Players/Things: controls who may ENTER the object
Evennia: specialized permission key 'enter'
defined on object, checked by move command
Evennia:
GetFromLock: All but Exits: controls who may gets things from a given
location.
Evennia: Probably done best with a lock function
that searches the database for permitted users
Evennia:
GiveLock: Players/Things: controls who may give the object.
Evennia: specialized permission key 'give'
checked by the give command
Evennia:
LeaveLock: Players/Things: controls who may LEAVE the object.
Evennia: specialized permission key 'leave'
checked by move command
Evennia:
LinkLock: All but Exits: controls who may link to the location if the
location is LINK_OK (for linking exits or
setting drop-tos) or ABODE (for setting
homes)
Evennia: specialized permission key 'link'
set on obj and checked by link command
Evennia:
MailLock: Players: controls who may @mail the player.
Evennia: Lock function that pulls the
config from the player to see if the
calling player is on the blacklist/whitelist
Evennia:
OpenLock: All but Exits: controls who may open an exit.
Evennia: specialized permission key 'open'
set on exit, checked by open command
Evennia:
PageLock: Players: controls who may page the player.
Evennia: see Maillock
Evennia: "send:<lockfunc()>"
ParentLock: All: controls who may make @parent links to the
object.
Evennia: This is handled with typeclasses
and typeclass switching instead.
Evennia: Typeclasses and "puppet:<lockstring()>"
ReceiveLock: Players/Things: controls who may give things to the object.
Evennia: See GiveLock
Evennia:
SpeechLock: All but Exits: controls who may speak in that location
Evennia: Lock function checking if there
is some special restrictions on the room
(game dependent)
Evennia:
TeloutLock: All but Exits: controls who may teleport out of the
location.
Evennia: See LeaveLock
Evennia:
TportLock: Rooms/Things: controls who may teleport there
Evennia: See EnterLock
Evennia:
UseLock: All but Exits: controls who may USE the object, GIVE the
object money and have the PAY attributes
run, have their messages heard and possibly
acted on by LISTEN and AxHEAR, and invoke
$-commands stored on the object.
Evennia: Implemented per game
Evennia: Commands and Cmdsets.
DropLock: All but rooms: controls who may drop that object.
Evennia: specialized permission key 'drop'
set on room, checked by drop command.
Evennia:
VisibleLock: All: Controls object visibility when the object
is not dark and the looker passes the lock.
In DARK locations, the object must also be
set LIGHT and the viewer must pass the
VisibleLock.
Evennia: Better done with Scripts implementing
a dark state/cmdset. For a single object,
use a specialized permission key 'visible'
set on object and checked by look command.
Evennia: Room typeclass with Dark/light script
"""
from django.conf import settings

View file

@ -39,9 +39,11 @@ SERVERNAME = settings.SERVERNAME
VERSION = get_evennia_version()
TELNET_PORTS = settings.TELNET_PORTS
SSH_PORTS = settings.SSH_PORTS
WEBSERVER_PORTS = settings.WEBSERVER_PORTS
TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS
SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
IMC2_ENABLED = settings.IMC2_ENABLED
@ -149,6 +151,8 @@ class Evennia(object):
print ' %s (%s) started on port(s):' % (SERVERNAME, VERSION)
if TELNET_ENABLED:
print " telnet: " + ", ".join([str(port) for port in TELNET_PORTS])
if SSH_ENABLED:
print " ssh: " + ", ".join([str(port) for port in SSH_PORTS])
if WEBSERVER_ENABLED:
clientstring = ""
if WEBCLIENT_ENABLED:
@ -203,6 +207,18 @@ if TELNET_ENABLED:
telnet_service.setName('EvenniaTelnet%s' % port)
EVENNIA.services.addService(telnet_service)
if SSH_ENABLED:
from src.server import ssh
for port in SSH_PORTS:
factory = ssh.makeFactory({'protocolFactory':ssh.SshProtocol,
'protocolArgs':()})
ssh_service = internet.TCPServer(port, factory)
ssh_service.setName('EvenniaSSH%s' % port)
EVENNIA.services.addService(ssh_service)
if WEBSERVER_ENABLED:
# a django-compatible webserver.

332
src/server/ssh.py Normal file
View file

@ -0,0 +1,332 @@
"""
This module implements the ssh (Secure SHell) protocol for encrypted
connections.
This depends on a generic session module that implements
the actual login procedure of the game, tracks
sessions etc.
"""
import os
from twisted.cred.checkers import credentials
from twisted.cred.portal import Portal
from twisted.conch.ssh.keys import Key
from twisted.conch.interfaces import IConchUser
from twisted.conch.ssh.userauth import SSHUserAuthServer
from twisted.conch.ssh import common
from twisted.conch.insults import insults
from twisted.conch.manhole_ssh import TerminalRealm, _Glue, ConchFactory
from twisted.conch.manhole import Manhole, recvline
from twisted.internet import defer
from django.conf import settings
from src.server import session
from src.utils import ansi, utils, logger
ENCODINGS = settings.ENCODINGS
CTRL_C = '\x03'
CTRL_D = '\x04'
CTRL_BACKSLASH = '\x1c'
CTRL_L = '\x0c'
class SshProtocol(Manhole, session.Session):
"""
Each player connecting over ssh gets this protocol assigned to
them. All communication between game and player goes through
here.
"""
def terminalSize(self, width, height):
"""
Initialize the terminal and connect to the new session.
"""
# Clear the previous input line, redraw it at the new
# cursor position
self.terminal.eraseDisplay()
self.terminal.cursorHome()
self.width = width
self.height = height
# initialize the session
self.session_connect(self.getClientAddress())
def connectionMade(self):
"""
This is called when the connection is first
established.
"""
recvline.HistoricRecvLine.connectionMade(self)
self.keyHandlers[CTRL_C] = self.handle_INT
self.keyHandlers[CTRL_D] = self.handle_EOF
self.keyHandlers[CTRL_L] = self.handle_FF
self.keyHandlers[CTRL_BACKSLASH] = self.handle_QUIT
def handle_INT(self):
"""
Handle ^C as an interrupt keystroke by resetting the current input
variables to their initial state.
"""
self.lineBuffer = []
self.lineBufferIndex = 0
self.terminal.nextLine()
self.terminal.write("KeyboardInterrupt")
self.terminal.nextLine()
def handle_EOF(self):
"""
Handles EOF generally used to exit.
"""
if self.lineBuffer:
self.terminal.write('\a')
else:
self.handle_QUIT()
def handle_FF(self):
"""
Handle a 'form feed' byte - generally used to request a screen
refresh/redraw.
"""
self.terminal.eraseDisplay()
self.terminal.cursorHome()
def handle_QUIT(self):
"""
Quit, end, and lose the connection.
"""
self.terminal.loseConnection()
def connectionLost(self, reason=None, step=1):
"""
This is executed when the connection is lost for
whatever reason.
Closing the connection takes two steps
step 1 - is the default and is used when this method is
called automatically. The method should then call self.session_disconnect().
Step 2 - means this method is called from at_disconnect(). At this point
the sessions are assumed to have been handled, and so the transport can close
without further ado.
"""
insults.TerminalProtocol.connectionLost(self, reason)
if step == 1:
self.session_disconnect()
else:
self.terminal.loseConnection()
def getClientAddress(self):
"""
Returns the client's address and port in a tuple. For example
('127.0.0.1', 41917)
"""
return self.terminal.transport.getPeer()
def lineReceived(self, string):
"""
Communication Player -> Evennia. Any line return indicates a
command for the purpose of the MUD. So we take the user input
and pass it on to the game engine.
"""
self.at_data_in(string)
def lineSend(self, string):
"""
Communication Evennia -> Player
Any string sent should already have been
properly formatted and processed
before reaching this point.
"""
for line in string.split('\n'):
self.terminal.write(line) #this is the telnet-specific method for sending
self.terminal.nextLine()
# session-general method hooks
def at_connect(self):
"""
Show the banner screen.
"""
self.telnet_markup = True
# show connection screen
self.execute_cmd('look')
def at_login(self, player):
"""
Called after authentication. self.logged_in=True at this point.
"""
if player.has_attribute('telnet_markup'):
self.telnet_markup = player.get_attribute("telnet_markup")
else:
self.telnet_markup = True
def at_disconnect(self, reason="Connection closed. Goodbye for now."):
"""
Disconnect from server
"""
char = self.get_character()
if char:
char.at_disconnect()
self.at_data_out(reason)
self.connectionLost(step=2)
def at_data_out(self, string, data=None):
"""
Data Evennia -> Player access hook. 'data' argument is ignored.
"""
try:
string = utils.to_str(string, encoding=self.encoding)
except Exception, e:
self.lineSend(str(e))
return
nomarkup = not self.telnet_markup
raw = False
if type(data) == dict:
# check if we want escape codes to go through unparsed.
raw = data.get("raw", self.telnet_markup)
# check if we want to remove all markup
nomarkup = data.get("nomarkup", not self.telnet_markup)
if raw:
self.lineSend(string)
else:
self.lineSend(ansi.parse_ansi(string, strip_ansi=nomarkup))
def at_data_in(self, string, data=None):
"""
Line from Player -> Evennia. 'data' argument is not used.
"""
try:
string = utils.to_unicode(string, encoding=self.encoding)
self.execute_cmd(string)
return
except Exception, e:
logger.log_errmsg(str(e))
class ExtraInfoAuthServer(SSHUserAuthServer):
def auth_password(self, packet):
"""
Password authentication.
Used mostly for setting up the transport so we can query
username and password later.
"""
password = common.getNS(packet[1:])[0]
c = credentials.UsernamePassword(self.user, password)
c.transport = self.transport
return self.portal.login(c, None, IConchUser).addErrback(
self._ebPassword)
class AnyAuth(object):
"""
Special auth method that accepts any credentials.
"""
credentialInterfaces = (credentials.IUsernamePassword,)
def requestAvatarId(self, c):
"Generic credentials"
up = credentials.IUsernamePassword(c, None)
username = up.username
password = up.password
src_ip = str(up.transport.transport.getPeer().host)
return defer.succeed(username)
class TerminalSessionTransport_getPeer:
"""
Taken from twisted's TerminalSessionTransport which doesn't
provide getPeer to the transport. This one does.
"""
def __init__(self, proto, chainedProtocol, avatar, width, height):
self.proto = proto
self.avatar = avatar
self.chainedProtocol = chainedProtocol
session = self.proto.session
self.proto.makeConnection(
_Glue(write=self.chainedProtocol.dataReceived,
loseConnection=lambda: avatar.conn.sendClose(session),
name="SSH Proto Transport"))
def loseConnection():
self.proto.loseConnection()
def getPeer():
session.conn.transport.transport.getPeer()
self.chainedProtocol.makeConnection(
_Glue(getPeer=getPeer, write=self.proto.write,
loseConnection=loseConnection,
name="Chained Proto Transport"))
self.chainedProtocol.terminalProtocol.terminalSize(width, height)
def getKeyPair():
"""
This function looks for RSA keypair files in the current directory. If they
do not exist, the keypair is created.
"""
if not (os.path.exists('ssh-public.key') and os.path.exists('ssh-private.key')):
# No keypair exists. Generate a new RSA keypair
print " Generating SSH RSA keypair (only done once) ...",
from Crypto.PublicKey import RSA
KEY_LENGTH = 1024
rsaKey = Key(RSA.generate(KEY_LENGTH))
publicKeyString = rsaKey.public().toString(type="OPENSSH")
privateKeyString = rsaKey.toString(type="OPENSSH")
# save keys for the future.
file('ssh-public.key', 'w+b').write(publicKeyString)
file('ssh-private.key', 'w+b').write(privateKeyString)
print " done."
else:
publicKeyString = file('ssh-public.key').read()
privateKeyString = file('ssh-private.key').read()
return Key.fromString(publicKeyString), Key.fromString(privateKeyString)
def makeFactory(configdict):
"""
Creates the ssh server factory.
"""
def chainProtocolFactory():
return insults.ServerProtocol(
configdict['protocolFactory'],
*configdict.get('protocolConfigdict', ()),
**configdict.get('protocolKwArgs', {}))
rlm = TerminalRealm()
rlm.transportFactory = TerminalSessionTransport_getPeer
rlm.chainedProtocolFactory = chainProtocolFactory
factory = ConchFactory(Portal(rlm))
# create/get RSA keypair
publicKey, privateKey = getKeyPair()
factory.publicKeys = {'ssh-rsa': publicKey}
factory.privateKeys = {'ssh-rsa': privateKey}
factory.services = factory.services.copy()
factory.services['ssh-userauth'] = ExtraInfoAuthServer
factory.portal.registerChecker(AnyAuth())
return factory

View file

@ -36,6 +36,10 @@ WEBSERVER_PORTS = [8000]
# Start the evennia ajax client on /webclient
# (the webserver must also be running)
WEBCLIENT_ENABLED = True
# Activate SSH protocol
SSH_ENABLED = False
# Ports to use for SSH
SSH_PORTS = [8022]
# Activate full persistence if you want everything in-game to be
# stored to the database. With it set, you can do typeclass.attr=value
# and value will be saved to the database under the name 'attr'.

View file

@ -9,7 +9,7 @@
<link rel='stylesheet' type="text/css" media="screen" href="/media/css/webclient.css">
<!-- Importing the online jQuery javascript library -->
<script src="http://code.jquery.com/jquery-1.4.4.js" type="text/javascript" charset="utf-8"></script>
<script src="http://code.jquery.com/jquery-1.6.1.js" type="text/javascript" charset="utf-8"></script>
<!--for offline testing, download the jquery library from jquery.com-->
<!--script src="/media/javascript/jquery-1.4.4.js" type="text/javascript" charset="utf-8"></script-->