mirror of
https://github.com/evennia/evennia.git
synced 2026-03-26 17:56:32 +01:00
Fixing contrib test issues
This commit is contained in:
parent
04a95297b5
commit
7f2b8c81d7
30 changed files with 248 additions and 565 deletions
|
|
@ -59,7 +59,7 @@ Exits: northeast and east
|
|||
|
||||
and
|
||||
|
||||
PROTOTYPE_MODULES += [’evennia.contrib.xyzgrid.prototypes’]
|
||||
PROTOTYPE_MODULES += [’evennia.contrib.grid.xyzgrid.prototypes’]
|
||||
|
||||
This will add the new ability to enter `evennia xyzgrid <option>` on the
|
||||
command line. It will also make the `xyz_room` and `xyz_exit` prototypes
|
||||
|
|
@ -107,7 +107,7 @@ The `evennia xyzgrid` is a custom launch option added only for this contrib.
|
|||
|
||||
The xyzgrid-contrib comes with a full grid example. Let's add it:
|
||||
|
||||
$ evennia xyzgrid add evennia.contrib.xyzgrid.example
|
||||
$ evennia xyzgrid add evennia.contrib.grid.xyzgrid.example
|
||||
|
||||
You can now list the maps on your grid:
|
||||
|
||||
|
|
@ -120,7 +120,7 @@ about each map with the `show` subcommand:
|
|||
$ evennia xyzgrid show "the small cave"
|
||||
|
||||
If you want to peek at how the grid's code, open
|
||||
[evennia/contrib/xyzgrid/example.py](evennia.contrib.xyzgrid.example).
|
||||
[evennia/contrib/grid/xyzgrid/example.py](evennia.contrib.xyzgrid.example).
|
||||
(We'll explain the details in later sections).
|
||||
|
||||
So far the grid is 'abstract' and has no actual in-game presence. Let's
|
||||
|
|
@ -430,9 +430,9 @@ LEGEND = {
|
|||
The legend is optional, and any symbol not explicitly given in your legend will
|
||||
fall back to its value in the default legend [outlined below](#default-legend).
|
||||
|
||||
- [MapNode](evennia.contrib.xyzgrid.xymap_legend.MapNode)
|
||||
- [MapNode](evennia.contrib.grid.xyzgrid.xymap_legend.MapNode)
|
||||
is the base class for all nodes.
|
||||
- [MapLink](evennia.contrib.xyzgrid.xymap_legend.MapLink)
|
||||
- [MapLink](evennia.contrib.grid.xyzgrid.xymap_legend.MapLink)
|
||||
is the base class for all links.
|
||||
|
||||
As the _Map String_ is parsed, each found symbol is looked up in the legend and
|
||||
|
|
@ -445,7 +445,7 @@ with a full set of map elements that use these properties in various ways
|
|||
(described in the next section).
|
||||
|
||||
Some useful properties of the
|
||||
[MapNode](evennia.contrib.xyzgrid.xymap_legend.MapNode)
|
||||
[MapNode](evennia.contrib.grid.xyzgrid.xymap_legend.MapNode)
|
||||
class (see class doc for hook methods):
|
||||
|
||||
- `symbol` (str) - The character to parse from the map into this node. By default this
|
||||
|
|
@ -473,7 +473,7 @@ class (see class doc for hook methods):
|
|||
useful for various reasons, mostly map-transitions).
|
||||
|
||||
Some useful properties of the
|
||||
[MapLink](evennia.contrib.xyzgrid.xymap_legend.MapLink)
|
||||
[MapLink](evennia.contrib.grid.xyzgrid.xymap_legend.MapLink)
|
||||
class (see class doc for hook methods):
|
||||
|
||||
- `symbol` (str) - The character to parse from the map into this node. This must
|
||||
|
|
@ -530,7 +530,7 @@ Below is an example that changes the map's nodes to show up as red
|
|||
(maybe for a lava map?):
|
||||
|
||||
```
|
||||
from evennia.contrib.xyzgrid import xymap_legend
|
||||
from evennia.contrib.grid.xyzgrid import xymap_legend
|
||||
|
||||
class RedMapNode(xymap_legend.MapNode):
|
||||
display_symbol = "|r#|n"
|
||||
|
|
@ -548,7 +548,7 @@ LEGEND = {
|
|||
Below is the default map legend. The `symbol` is what should be put in the Map
|
||||
string. It must always be a single character. The `display-symbol` is what is
|
||||
actually visualized when displaying the map to players in-game. This could have
|
||||
colors etc. All classes are found in `evennia.contrib.xyzgrid.xymap_legend` and
|
||||
colors etc. All classes are found in `evennia.contrib.grid.xyzgrid.xymap_legend` and
|
||||
their names are included to make it easy to know what to override.
|
||||
|
||||
```{eval-rst}
|
||||
|
|
@ -801,7 +801,7 @@ different (unused) unique symbol in your map legend:
|
|||
```python
|
||||
# in your map definition module
|
||||
|
||||
from evennia.contrib.xyzgrid import xymap_legend
|
||||
from evennia.contrib.grid.xyzgrid import xymap_legend
|
||||
|
||||
MAPSTR = r"""
|
||||
|
||||
|
|
@ -851,7 +851,7 @@ added, with different map-legend symbols:
|
|||
```python
|
||||
# in your map definition module (let's say this is mapB)
|
||||
|
||||
from evennia.contrib.xyzgrid import xymap_legend
|
||||
from evennia.contrib.grid.xyzgrid import xymap_legend
|
||||
|
||||
MAPSTR = r"""
|
||||
|
||||
|
|
@ -924,10 +924,10 @@ across the map boundary.
|
|||
[Prototypes](../Components/Prototypes.md) are dicts that describe how to _spawn_ a new instance
|
||||
of an object. Each of the _nodes_ and _links_ above have a default prototype
|
||||
that allows the `evennia xyzgrid spawn` command to convert them to
|
||||
a [XYZRoom](evennia.contrib.xyzgrid.xyzroom.XYZRoom)
|
||||
or an [XYZExit](evennia.contrib.xyzgrid.xyzroom.XYZRoom) respectively.
|
||||
a [XYZRoom](evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom)
|
||||
or an [XYZExit](evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom) respectively.
|
||||
|
||||
The default prototypes are found in `evennia.contrib.xyzgrid.prototypes` (added
|
||||
The default prototypes are found in `evennia.contrib.grid.xyzgrid.prototypes` (added
|
||||
during installation of this contrib), with `prototype_key`s `"xyz_room"` and
|
||||
`"xyz_exit"` - use these as `prototype_parent` to add your own custom prototypes.
|
||||
|
||||
|
|
@ -1012,7 +1012,7 @@ picked up and applied to the existing objects.
|
|||
|
||||
#### Extending the base prototypes
|
||||
|
||||
The default prototypes are found in `evennia.contrib.xyzgrid.prototypes` and
|
||||
The default prototypes are found in `evennia.contrib.grid.xyzgrid.prototypes` and
|
||||
should be included as `prototype_parents` for prototypes on the map. Would it
|
||||
not be nice to be able to change these and have the change apply to all of the
|
||||
grid? You can, by adding the following to your `mygame/server/conf/settings.py`:
|
||||
|
|
@ -1177,9 +1177,9 @@ To access the grid in-code, there are several ways:
|
|||
grid = evennia.search_script("XYZGrid")[0]
|
||||
|
||||
(`search_script` always returns a list)
|
||||
- You can get it with `evennia.contrib.xyzgrid.xyzgrid.get_xyzgrid`
|
||||
- You can get it with `evennia.contrib.grid.xyzgrid.xyzgrid.get_xyzgrid`
|
||||
|
||||
from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid
|
||||
from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid
|
||||
grid = get_xyzgrid()
|
||||
|
||||
This will *always* return a grid, creating an empty grid if one didn't
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib
|
||||
evennia.contrib
|
||||
=======================
|
||||
|
||||
.. automodule:: evennia.contrib
|
||||
|
|
@ -54,6 +54,6 @@ evennia.contrib
|
|||
evennia.contrib.turnbattle
|
||||
evennia.contrib.tutorial_examples
|
||||
evennia.contrib.tutorial_world
|
||||
evennia.contrib.xyzgrid
|
||||
evennia.contrib.grid.xyzgrid
|
||||
|
||||
```
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.xyzgrid.commands
|
||||
evennia.contrib.grid.xyzgrid.commands
|
||||
=======================================
|
||||
|
||||
.. automodule:: evennia.contrib.xyzgrid.commands
|
||||
|
|
@ -7,4 +7,4 @@ evennia.contrib.xyzgrid.commands
|
|||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2162,7 +2162,7 @@ class TestCommsChannel(CommandTest):
|
|||
)
|
||||
|
||||
|
||||
from evennia.comms import comms # noqa
|
||||
from evennia.commands.default import comms # noqa
|
||||
|
||||
|
||||
class TestComms(CommandTest):
|
||||
|
|
@ -2179,13 +2179,13 @@ class TestComms(CommandTest):
|
|||
|
||||
class TestBatchProcess(CommandTest):
|
||||
|
||||
@patch("evennia.contrib.tutorial_examples.red_button.repeat")
|
||||
@patch("evennia.contrib.tutorial_examples.red_button.delay")
|
||||
@patch("evennia.contrib.tutorials.red_button.red_button.repeat")
|
||||
@patch("evennia.contrib.tutorials.red_button.red_button.delay")
|
||||
def test_batch_commands(self, mock_delay, mock_repeat):
|
||||
# cannot test batchcode here, it must run inside the server process
|
||||
self.call(
|
||||
batchprocess.CmdBatchCommands(),
|
||||
"example_batch_cmds",
|
||||
"batchprocessor.example_batch_cmds",
|
||||
"Running Batch-command processor - Automatic mode for example_batch_cmds",
|
||||
)
|
||||
# we make sure to delete the button again here to stop the running reactor
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
Extened Room - Griatch 2012, vincent-lg 2019
|
||||
Contribs related to moving in and manipulating the game world and grid.
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class ForceUTCDatetime(datetime.datetime):
|
|||
return datetime.datetime.utcfromtimestamp(timestamp)
|
||||
|
||||
|
||||
@patch("evennia.contrib.extended_room.datetime.datetime", ForceUTCDatetime)
|
||||
@patch("evennia.contrib.grid.extended_room.extended_room.datetime.datetime", ForceUTCDatetime)
|
||||
# mock gametime to return April 9, 2064, at 21:06 (spring evening)
|
||||
@patch("evennia.utils.gametime.gametime", new=Mock(return_value=2975000766))
|
||||
class TestExtendedRoom(CommandTest):
|
||||
|
|
|
|||
|
|
@ -6,12 +6,193 @@ Test map builder.
|
|||
from evennia.commands.default.tests import CommandTest
|
||||
from . import mapbuilder
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Add the necessary imports for your instructions here.
|
||||
from evennia import create_object
|
||||
from typeclasses import rooms, exits
|
||||
from random import randint
|
||||
import random
|
||||
|
||||
|
||||
# A map with a temple (▲) amongst mountains (n,∩) in a forest (♣,♠) on an
|
||||
# island surrounded by water (≈). By giving no instructions for the water
|
||||
# characters we effectively skip it and create no rooms for those squares.
|
||||
EXAMPLE1_MAP = '''
|
||||
≈≈≈≈≈
|
||||
≈♣n♣≈
|
||||
≈∩▲∩≈
|
||||
≈♠n♠≈
|
||||
≈≈≈≈≈
|
||||
'''
|
||||
|
||||
def example1_build_forest(x, y, **kwargs):
|
||||
'''A basic example of build instructions. Make sure to include **kwargs
|
||||
in the arguments and return an instance of the room for exit generation.'''
|
||||
|
||||
# Create a room and provide a basic description.
|
||||
room = create_object(rooms.Room, key="forest" + str(x) + str(y))
|
||||
room.db.desc = "Basic forest room."
|
||||
|
||||
# Send a message to the account
|
||||
kwargs["caller"].msg(room.key + " " + room.dbref)
|
||||
|
||||
# This is generally mandatory.
|
||||
return room
|
||||
|
||||
|
||||
def example1_build_mountains(x, y, **kwargs):
|
||||
'''A room that is a little more advanced'''
|
||||
|
||||
# Create the room.
|
||||
room = create_object(rooms.Room, key="mountains" + str(x) + str(y))
|
||||
|
||||
# Generate a description by randomly selecting an entry from a list.
|
||||
room_desc = [
|
||||
"Mountains as far as the eye can see",
|
||||
"Your path is surrounded by sheer cliffs",
|
||||
"Haven't you seen that rock before?",
|
||||
]
|
||||
room.db.desc = random.choice(room_desc)
|
||||
|
||||
# Create a random number of objects to populate the room.
|
||||
for i in range(randint(0, 3)):
|
||||
rock = create_object(key="Rock", location=room)
|
||||
rock.db.desc = "An ordinary rock."
|
||||
|
||||
# Send a message to the account
|
||||
kwargs["caller"].msg(room.key + " " + room.dbref)
|
||||
|
||||
# This is generally mandatory.
|
||||
return room
|
||||
|
||||
|
||||
def example1_build_temple(x, y, **kwargs):
|
||||
'''A unique room that does not need to be as general'''
|
||||
|
||||
# Create the room.
|
||||
room = create_object(rooms.Room, key="temple" + str(x) + str(y))
|
||||
|
||||
# Set the description.
|
||||
room.db.desc = (
|
||||
"In what, from the outside, appeared to be a grand and "
|
||||
"ancient temple you've somehow found yourself in the the "
|
||||
"Evennia Inn! It consists of one large room filled with "
|
||||
"tables. The bardisk extends along the east wall, where "
|
||||
"multiple barrels and bottles line the shelves. The "
|
||||
"barkeep seems busy handing out ale and chatting with "
|
||||
"the patrons, which are a rowdy and cheerful lot, "
|
||||
"keeping the sound level only just below thunderous. "
|
||||
"This is a rare spot of mirth on this dread moor."
|
||||
)
|
||||
|
||||
# Send a message to the account
|
||||
kwargs["caller"].msg(room.key + " " + room.dbref)
|
||||
|
||||
# This is generally mandatory.
|
||||
return room
|
||||
|
||||
|
||||
# Include your trigger characters and build functions in a legend dict.
|
||||
EXAMPLE1_LEGEND = {
|
||||
("♣", "♠"): example1_build_forest,
|
||||
("∩", "n"): example1_build_mountains,
|
||||
("▲"): example1_build_temple,
|
||||
}
|
||||
|
||||
# Example two
|
||||
|
||||
# @mapbuilder/two evennia.contrib.mapbuilder.EXAMPLE2_MAP EXAMPLE2_LEGEND
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Add the necessary imports for your instructions here.
|
||||
# from evennia import create_object
|
||||
# from typeclasses import rooms, exits
|
||||
# from evennia.utils import utils
|
||||
# from random import randint
|
||||
# import random
|
||||
|
||||
# This is the same layout as Example 1 but included are characters for exits.
|
||||
# We can use these characters to determine which rooms should be connected.
|
||||
EXAMPLE2_MAP = '''
|
||||
≈ ≈ ≈ ≈ ≈
|
||||
|
||||
≈ ♣-♣-♣ ≈
|
||||
| |
|
||||
≈ ♣ ♣ ♣ ≈
|
||||
| | |
|
||||
≈ ♣-♣-♣ ≈
|
||||
|
||||
≈ ≈ ≈ ≈ ≈
|
||||
'''
|
||||
|
||||
|
||||
def example2_build_forest(x, y, **kwargs):
|
||||
'''A basic room'''
|
||||
# If on anything other than the first iteration - Do nothing.
|
||||
if kwargs["iteration"] > 0:
|
||||
return None
|
||||
|
||||
room = create_object(rooms.Room, key="forest" + str(x) + str(y))
|
||||
room.db.desc = "Basic forest room."
|
||||
|
||||
kwargs["caller"].msg(room.key + " " + room.dbref)
|
||||
|
||||
return room
|
||||
|
||||
|
||||
def example2_build_verticle_exit(x, y, **kwargs):
|
||||
'''Creates two exits to and from the two rooms north and south.'''
|
||||
# If on the first iteration - Do nothing.
|
||||
if kwargs["iteration"] == 0:
|
||||
return
|
||||
|
||||
north_room = kwargs["room_dict"][(x, y - 1)]
|
||||
south_room = kwargs["room_dict"][(x, y + 1)]
|
||||
|
||||
# create exits in the rooms
|
||||
create_object(
|
||||
exits.Exit, key="south", aliases=["s"], location=north_room, destination=south_room
|
||||
)
|
||||
|
||||
create_object(
|
||||
exits.Exit, key="north", aliases=["n"], location=south_room, destination=north_room
|
||||
)
|
||||
|
||||
kwargs["caller"].msg("Connected: " + north_room.key + " & " + south_room.key)
|
||||
|
||||
|
||||
def example2_build_horizontal_exit(x, y, **kwargs):
|
||||
'''Creates two exits to and from the two rooms east and west.'''
|
||||
# If on the first iteration - Do nothing.
|
||||
if kwargs["iteration"] == 0:
|
||||
return
|
||||
|
||||
west_room = kwargs["room_dict"][(x - 1, y)]
|
||||
east_room = kwargs["room_dict"][(x + 1, y)]
|
||||
|
||||
create_object(exits.Exit, key="east", aliases=["e"], location=west_room, destination=east_room)
|
||||
|
||||
create_object(exits.Exit, key="west", aliases=["w"], location=east_room, destination=west_room)
|
||||
|
||||
kwargs["caller"].msg("Connected: " + west_room.key + " & " + east_room.key)
|
||||
|
||||
|
||||
# Include your trigger characters and build functions in a legend dict.
|
||||
EXAMPLE2_LEGEND = {
|
||||
("♣", "♠"): example2_build_forest,
|
||||
("|"): example2_build_verticle_exit,
|
||||
("-"): example2_build_horizontal_exit,
|
||||
}
|
||||
|
||||
|
||||
class TestMapBuilder(CommandTest):
|
||||
def test_cmdmapbuilder(self):
|
||||
self.call(
|
||||
mapbuilder.CmdMapBuilder(),
|
||||
"evennia.contrib.mapbuilder.EXAMPLE1_MAP evennia.contrib.mapbuilder.EXAMPLE1_LEGEND",
|
||||
"evennia.contrib.grid.mapbuilder.tests.EXAMPLE1_MAP "
|
||||
"evennia.contrib.grid.mapbuilder.tests.EXAMPLE1_LEGEND",
|
||||
"""Creating Map...|≈≈≈≈≈
|
||||
≈♣n♣≈
|
||||
≈∩▲∩≈
|
||||
|
|
@ -21,7 +202,8 @@ class TestMapBuilder(CommandTest):
|
|||
)
|
||||
self.call(
|
||||
mapbuilder.CmdMapBuilder(),
|
||||
"evennia.contrib.mapbuilder.EXAMPLE2_MAP evennia.contrib.mapbuilder.EXAMPLE2_LEGEND",
|
||||
"evennia.contrib.grid.mapbuilder.tests.EXAMPLE2_MAP "
|
||||
"evennia.contrib.grid.mapbuilder.tests.EXAMPLE2_LEGEND",
|
||||
"""Creating Map...|≈ ≈ ≈ ≈ ≈
|
||||
|
||||
≈ ♣-♣-♣ ≈
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class TestSimpleDoor(CommandTest):
|
|||
def test_cmdopen(self):
|
||||
self.call(
|
||||
simpledoor.CmdOpen(),
|
||||
"newdoor;door:contrib.simpledoor.SimpleDoor,backdoor;door = Room2",
|
||||
"newdoor;door:contrib.grid.simpledoor.SimpleDoor,backdoor;door = Room2",
|
||||
"Created new Exit 'newdoor' from Room to Room2 (aliases: door).|Note: A door-type exit was "
|
||||
"created - ignored eventual custom return-exit type.|Created new Exit 'newdoor' from Room2 to Room (aliases: door).",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
XYZGrid - Evennia 2021
|
||||
XYZGrid - Griatch 2021
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ the `evennia xyzgrid` launcher command.
|
|||
|
||||
First add the launcher extension in your mygame/server/conf/settings.py:
|
||||
|
||||
EXTRA_LAUNCHER_COMMANDS['xyzgrid'] = 'evennia.contrib.xyzgrid.launchcmd.xyzcommand'
|
||||
EXTRA_LAUNCHER_COMMANDS['xyzgrid'] = 'evennia.contrib.grid.xyzgrid.launchcmd.xyzcommand'
|
||||
|
||||
Then
|
||||
|
||||
evennia xyzgrid init
|
||||
evennia xyzgrid add evennia.contrib.xyzgrid.map_example
|
||||
evennia xyzgrid add evennia.contrib.grid.xyzgrid.map_example
|
||||
evennia xyzgrid build
|
||||
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ from evennia.contrib.grid.xyzgrid import xymap_legend
|
|||
|
||||
# default prototype parent. It's important that
|
||||
# the typeclass inherits from the XYZRoom (or XYZExit)
|
||||
# if adding the evennia.contrib.xyzgrid.prototypes to
|
||||
# if adding the evennia.contrib.grid.xyzgrid.prototypes to
|
||||
# settings.PROTOTYPE_MODULES, one could just set the
|
||||
# prototype_parent to 'xyz_room' and 'xyz_exit' here
|
||||
# instead.
|
||||
|
|
@ -30,14 +30,14 @@ ROOM_PARENT = {
|
|||
"key": "An empty room",
|
||||
"prototype_key": "xyz_exit_prototype",
|
||||
# "prototype_parent": "xyz_room",
|
||||
"typeclass": "evennia.contrib.xyzgrid.xyzroom.XYZRoom",
|
||||
"typeclass": "evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom",
|
||||
"desc": "An empty room.",
|
||||
}
|
||||
|
||||
EXIT_PARENT = {
|
||||
"prototype_key": "xyz_exit_prototype",
|
||||
# "prototype_parent": "xyz_exit",
|
||||
"typeclass": "evennia.contrib.xyzgrid.xyzroom.XYZExit",
|
||||
"typeclass": "evennia.contrib.grid.xyzgrid.xyzroom.XYZExit",
|
||||
"desc": "A path to the next location.",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ server (since this can be slow).
|
|||
To use, add to the settings:
|
||||
::
|
||||
|
||||
EXTRA_LAUNCHER_COMMANDS.update({'xyzgrid': 'evennia.contrib.xyzgrid.launchcmd.xyzcommand'})
|
||||
EXTRA_LAUNCHER_COMMANDS.update({'xyzgrid': 'evennia.contrib.grid.xyzgrid.launchcmd.xyzcommand'})
|
||||
|
||||
You should now be able to do
|
||||
::
|
||||
|
|
@ -80,7 +80,7 @@ add <path.to.xymap.module> [<path> <path>,...]
|
|||
{"map": mapstring, "zcoord": mapname/zcoord, "legend": dict, "prototypes": dict}
|
||||
describing one single XYmap, or
|
||||
- a XYMAP_DATA_LIST - a list of multiple dicts on the XYMAP_DATA form. This allows for
|
||||
embedding multiple maps in the same module. See evennia/contrib/xyzgrid/example.py
|
||||
embedding multiple maps in the same module. See evennia/contrib/grid/xyzgrid/example.py
|
||||
for an example of how this looks.
|
||||
|
||||
Note that adding a map does *not* spawn it. If maps are linked to one another, you should
|
||||
|
|
@ -89,7 +89,7 @@ add <path.to.xymap.module> [<path> <path>,...]
|
|||
|
||||
Examples:
|
||||
|
||||
evennia xyzgrid add evennia.contrib.xyzgrid.example
|
||||
evennia xyzgrid add evennia.contrib.grid.xyzgrid.example
|
||||
evennia xyzgrid add world.mymap1 world.mymap2 world.mymap3
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ Default prototypes for building the XYZ-grid into actual game-rooms.
|
|||
|
||||
Add this to mygame/conf/settings/settings.py:
|
||||
|
||||
PROTOTYPE_MODULES += ['evennia.contrib.xyzgrid.prototypes']
|
||||
PROTOTYPE_MODULES += ['evennia.contrib.grid.xyzgrid.prototypes']
|
||||
|
||||
The prototypes can then be used in mapping prototypes as
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ except AttributeError:
|
|||
|
||||
room_prototype = {
|
||||
'prototype_key': 'xyz_room',
|
||||
'typeclass': 'evennia.contrib.xyzgrid.xyzroom.XYZRoom',
|
||||
'typeclass': 'evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom',
|
||||
'prototype_tags': ("xyzroom", ),
|
||||
'key': "A room",
|
||||
'desc': "An empty room."
|
||||
|
|
@ -37,7 +37,7 @@ room_prototype.update(room_override)
|
|||
|
||||
exit_prototype = {
|
||||
'prototype_key': 'xyz_exit',
|
||||
'typeclass': 'evennia.contrib.xyzgrid.xyzroom.XYZExit',
|
||||
'typeclass': 'evennia.contrib.grid.xyzgrid.xyzroom.XYZExit',
|
||||
'prototype_tags': ("xyzexit", ),
|
||||
'desc': "An exit."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -854,8 +854,6 @@ class TestMap8(_MapTest):
|
|||
target=target, target_path_style="",
|
||||
character='@',
|
||||
max_size=max_size)
|
||||
self.assertEqual(expected, mapstr.replace("||", "|"))
|
||||
|
||||
def test_spawn(self):
|
||||
"""
|
||||
Spawn the map into actual objects.
|
||||
|
|
@ -1018,6 +1016,8 @@ class TestMap11(_MapTest):
|
|||
target=target, target_path_style="",
|
||||
character='@',
|
||||
max_size=max_size)
|
||||
print(f"\n\n{coord}-{target}\n{expected}\n\n{mapstr}")
|
||||
|
||||
self.assertEqual(expected, mapstr)
|
||||
|
||||
def test_spawn(self):
|
||||
|
|
@ -1252,6 +1252,7 @@ class TestXYZGridTransition(EvenniaTest):
|
|||
self.assertEqual(east_exit.db_destination, room2)
|
||||
self.assertEqual(west_exit.db_destination, room1)
|
||||
|
||||
|
||||
class TestBuildExampleGrid(EvenniaTest):
|
||||
"""
|
||||
Test building the map-example (this takes about 30s)
|
||||
|
|
@ -1274,7 +1275,7 @@ class TestBuildExampleGrid(EvenniaTest):
|
|||
Build the map example.
|
||||
|
||||
"""
|
||||
mapdatas = self.grid.maps_from_module("evennia.contrib.xyzgrid.example")
|
||||
mapdatas = self.grid.maps_from_module("evennia.contrib.grid.xyzgrid.example")
|
||||
self.assertEqual(len(mapdatas), 2)
|
||||
|
||||
self.grid.add_maps(*mapdatas)
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ class XYMap:
|
|||
if not _LOADED_PROTOTYPES:
|
||||
# inject default prototypes, but don't override prototype-keys loaded from
|
||||
# settings, if they exist (that means the user wants to replace the defaults)
|
||||
protlib.load_module_prototypes("evennia.contrib.xyzgrid.prototypes", override=False)
|
||||
protlib.load_module_prototypes("evennia.contrib.grid.xyzgrid.prototypes", override=False)
|
||||
_LOADED_PROTOTYPES = True
|
||||
|
||||
self.Z = Z
|
||||
|
|
@ -636,7 +636,7 @@ class XYMap:
|
|||
"""
|
||||
global _XYZROOMCLASS
|
||||
if not _XYZROOMCLASS:
|
||||
from evennia.contrib.xyzgrid.xyzroom import XYZRoom as _XYZROOMCLASS
|
||||
from evennia.contrib.grid.xyzgrid.xyzroom import XYZRoom as _XYZROOMCLASS
|
||||
x, y = xy
|
||||
wildcard = '*'
|
||||
spawned = []
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ class MapNode:
|
|||
# create a new entity with proper coordinates etc
|
||||
tclass = self.prototype['typeclass']
|
||||
tclass = (f' ({tclass})'
|
||||
if tclass != 'evennia.contrib.xyzgrid.xyzroom.XYZRoom'
|
||||
if tclass != 'evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom'
|
||||
else '')
|
||||
self.log(f" spawning room at xyz={xyz}{tclass}")
|
||||
nodeobj, err = NodeTypeclass.create(
|
||||
|
|
@ -413,7 +413,7 @@ class MapNode:
|
|||
prot = maplinks[key.lower()][3].prototype
|
||||
tclass = prot['typeclass']
|
||||
tclass = (f' ({tclass})'
|
||||
if tclass != 'evennia.contrib.xyzgrid.xyzroom.XYZExit'
|
||||
if tclass != 'evennia.contrib.grid.xyzgrid.xyzroom.XYZExit'
|
||||
else '')
|
||||
self.log(f" spawning/updating exit xyz={xyz}, direction={key}{tclass}")
|
||||
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ class XYZRoom(DefaultRoom):
|
|||
def xyzgrid(self):
|
||||
global GET_XYZGRID
|
||||
if not GET_XYZGRID:
|
||||
from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID
|
||||
from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID
|
||||
return GET_XYZGRID()
|
||||
|
||||
@property
|
||||
|
|
@ -493,7 +493,7 @@ class XYZExit(DefaultExit):
|
|||
def xyzgrid(self):
|
||||
global GET_XYZGRID
|
||||
if not GET_XYZGRID:
|
||||
from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID
|
||||
from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid as GET_XYZGRID
|
||||
return GET_XYZGRID()
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
# It allows batch processing of normal Evennia commands.
|
||||
# Test it by loading it with @batchcommand:
|
||||
#
|
||||
# @batchcommand[/interactive] examples.batch_example
|
||||
# batchcommand[/interactive] examples.batch_example
|
||||
#
|
||||
# A # as the first symbol on a line begins a comment and
|
||||
# marks the end of a previous command definition (important!).
|
||||
|
|
@ -17,12 +17,12 @@
|
|||
|
||||
# This creates a red button
|
||||
|
||||
@create button:tutorial_examples.red_button.RedButton
|
||||
create button:red_button.RedButton
|
||||
|
||||
# This comment ends input for @create
|
||||
# Next command:
|
||||
|
||||
@set button/desc =
|
||||
set button/desc =
|
||||
This is a large red button. Now and then
|
||||
it flashes in an evil, yet strangely tantalizing way.
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ know you want to!
|
|||
# Now let's place the button where it belongs (let's say limbo #2 is
|
||||
# the evil lair in our example).
|
||||
|
||||
@teleport #2
|
||||
teleport #2
|
||||
|
||||
#... and drop it (remember, this comment ends input to @teleport, so don't
|
||||
#forget it!) The very last command in the file need not be ended with #.
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
# all other #CODE blocks when they are executed.
|
||||
|
||||
from evennia import create_object, search_object
|
||||
from evennia.contrib.tutorial_examples import red_button
|
||||
from evennia.contrib.tutorials import red_button
|
||||
from evennia import DefaultObject
|
||||
|
||||
limbo = search_object("Limbo")[0]
|
||||
|
|
|
|||
|
|
@ -32,14 +32,14 @@ Files included in this module:
|
|||
Deployment is completed by configuring a few settings in server.conf. This line
|
||||
is required:
|
||||
|
||||
SERVER_SESSION_CLASS = 'evennia.contrib.security.auditing.server.AuditedServerSession'
|
||||
SERVER_SESSION_CLASS = 'evennia.contrib.utils.auditing.server.AuditedServerSession'
|
||||
|
||||
This tells Evennia to use this ServerSession instead of its own. Below are the
|
||||
other possible options along with the default value that will be used if unset.
|
||||
|
||||
# Where to send logs? Define the path to a module containing your callback
|
||||
# function. It should take a single dict argument as input
|
||||
AUDIT_CALLBACK = 'evennia.contrib.security.auditing.outputs.to_file'
|
||||
AUDIT_CALLBACK = 'evennia.contrib.utils.auditing.outputs.to_file'
|
||||
|
||||
# Log user input? Be ethical about this; it will log all private and
|
||||
# public communications between players and/or admins (default: False).
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from evennia.server.serversession import ServerSession
|
|||
|
||||
# Attributes governing auditing of commands and where to send log objects
|
||||
AUDIT_CALLBACK = getattr(
|
||||
ev_settings, "AUDIT_CALLBACK", "evennia.contrib.security.auditing.outputs.to_file"
|
||||
ev_settings, "AUDIT_CALLBACK", "evennia.contrib.utils.auditing.outputs.to_file"
|
||||
)
|
||||
AUDIT_IN = getattr(ev_settings, "AUDIT_IN", False)
|
||||
AUDIT_OUT = getattr(ev_settings, "AUDIT_OUT", False)
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ from evennia.utils.test_resources import EvenniaTest
|
|||
import re
|
||||
|
||||
# Configure session auditing settings - TODO: This is bad practice that leaks over to other tests
|
||||
settings.AUDIT_CALLBACK = "evennia.security.contrib.auditing.outputs.to_syslog"
|
||||
settings.AUDIT_CALLBACK = "evennia.contrib.utils.auditing.outputs.to_syslog"
|
||||
settings.AUDIT_IN = True
|
||||
settings.AUDIT_OUT = True
|
||||
settings.AUDIT_ALLOW_SPARSE = True
|
||||
|
||||
# Configure settings to use custom session - TODO: This is bad practice, changing global settings
|
||||
settings.SERVER_SESSION_CLASS = "evennia.contrib.security.auditing.server.AuditedServerSession"
|
||||
settings.SERVER_SESSION_CLASS = "evennia.contrib.utils.auditing.server.AuditedServerSession"
|
||||
|
||||
|
||||
class AuditingTest(EvenniaTest):
|
||||
|
|
|
|||
|
|
@ -399,7 +399,7 @@ class ServerSessionHandler(SessionHandler):
|
|||
self.server.at_post_portal_sync(mode)
|
||||
# announce the reconnection
|
||||
if _BROADCAST_SERVER_RESTART_MESSAGES:
|
||||
self.announce_all(_(" ... Server restarted."))
|
||||
self.announce_all(_(" ... Server restarted."))
|
||||
|
||||
def portal_disconnect(self, session):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -508,7 +508,7 @@ TYPECLASS_PATHS = [
|
|||
"typeclasses",
|
||||
"evennia",
|
||||
"evennia.contrib",
|
||||
"evennia.contrib.tutorial_examples",
|
||||
"evennia.contrib.tutorials",
|
||||
]
|
||||
|
||||
# Typeclass for account objects (linked to a character) (fallback)
|
||||
|
|
@ -602,7 +602,7 @@ VALIDATOR_FUNC_MODULES = ["evennia.utils.validatorfuncs"]
|
|||
BASE_BATCHPROCESS_PATHS = [
|
||||
"world",
|
||||
"evennia.contrib",
|
||||
"evennia.contrib.tutorial_examples",
|
||||
"evennia.contrib.tutorials",
|
||||
]
|
||||
|
||||
######################################################################
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
# Security
|
||||
|
||||
This directory contains security-related contribs
|
||||
|
||||
- Auditing (Johnny 2018) - Allow for optional security logging of user input/output.
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
# Input/Output Auditing
|
||||
|
||||
Contrib - Johnny 2017
|
||||
|
||||
This is a tap that optionally intercepts all data sent to/from clients and the
|
||||
server and passes it to a callback of your choosing.
|
||||
|
||||
It is intended for quality assurance, post-incident investigations and debugging
|
||||
but obviously can be abused. All data is recorded in cleartext. Please
|
||||
be ethical, and if you are unwilling to properly deal with the implications of
|
||||
recording user passwords or private communications, please do not enable
|
||||
this module.
|
||||
|
||||
Some checks have been implemented to protect the privacy of users.
|
||||
|
||||
|
||||
Files included in this module:
|
||||
|
||||
outputs.py - Example callback methods. This module ships with examples of
|
||||
callbacks that send data as JSON to a file in your game/server/logs
|
||||
dir or to your native Linux syslog daemon. You can of course write
|
||||
your own to do other things like post them to Kafka topics.
|
||||
|
||||
server.py - Extends the Evennia ServerSession object to pipe data to the
|
||||
callback upon receipt.
|
||||
|
||||
tests.py - Unit tests that check to make sure commands with sensitive
|
||||
arguments are having their PII scrubbed.
|
||||
|
||||
|
||||
Installation/Configuration:
|
||||
|
||||
Deployment is completed by configuring a few settings in server.conf. This line
|
||||
is required:
|
||||
|
||||
SERVER_SESSION_CLASS = 'evennia.contrib.security.auditing.server.AuditedServerSession'
|
||||
|
||||
This tells Evennia to use this ServerSession instead of its own. Below are the
|
||||
other possible options along with the default value that will be used if unset.
|
||||
|
||||
# Where to send logs? Define the path to a module containing your callback
|
||||
# function. It should take a single dict argument as input
|
||||
AUDIT_CALLBACK = 'evennia.contrib.security.auditing.outputs.to_file'
|
||||
|
||||
# Log user input? Be ethical about this; it will log all private and
|
||||
# public communications between players and/or admins (default: False).
|
||||
AUDIT_IN = False
|
||||
|
||||
# Log server output? This will result in logging of ALL system
|
||||
# messages and ALL broadcasts to connected players, so on a busy game any
|
||||
# broadcast to all users will yield a single event for every connected user!
|
||||
AUDIT_OUT = False
|
||||
|
||||
# The default output is a dict. Do you want to allow key:value pairs with
|
||||
# null/blank values? If you're just writing to disk, disabling this saves
|
||||
# some disk space, but whether you *want* sparse values or not is more of a
|
||||
# consideration if you're shipping logs to a NoSQL/schemaless database.
|
||||
# (default: False)
|
||||
AUDIT_ALLOW_SPARSE = False
|
||||
|
||||
# If you write custom commands that handle sensitive data like passwords,
|
||||
# you must write a regular expression to remove that before writing to log.
|
||||
# AUDIT_MASKS is a list of dictionaries that define the names of commands
|
||||
# and the regexes needed to scrub them.
|
||||
# The system already has defaults to filter out sensitive login/creation
|
||||
# commands in the default command set. Your list of AUDIT_MASKS will be appended
|
||||
# to those defaults.
|
||||
#
|
||||
# In the regex, the sensitive data itself must be captured in a named group with a
|
||||
# label of 'secret' (see the Python docs on the `re` module for more info). For
|
||||
# example: `{'authentication': r"^@auth\s+(?P<secret>[\w]+)"}`
|
||||
AUDIT_MASKS = []
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
"""
|
||||
Auditable Server Sessions - Example Outputs
|
||||
Example methods demonstrating output destinations for logs generated by
|
||||
audited server sessions.
|
||||
|
||||
This is designed to be a single source of events for developers to customize
|
||||
and add any additional enhancements before events are written out-- i.e. if you
|
||||
want to keep a running list of what IPs a user logs in from on account/character
|
||||
objects, or if you want to perform geoip or ASN lookups on IPs before committing,
|
||||
or tag certain events with the results of a reputational lookup, this should be
|
||||
the easiest place to do it. Write a method and invoke it via
|
||||
`settings.AUDIT_CALLBACK` to have log data objects passed to it.
|
||||
|
||||
Evennia contribution - Johnny 2017
|
||||
"""
|
||||
from evennia.utils.logger import log_file
|
||||
import json
|
||||
import syslog
|
||||
|
||||
|
||||
def to_file(data):
|
||||
"""
|
||||
Writes dictionaries of data generated by an AuditedServerSession to files
|
||||
in JSON format, bucketed by date.
|
||||
|
||||
Uses Evennia's native logger and writes to the default
|
||||
log directory (~/yourgame/server/logs/ or settings.LOG_DIR)
|
||||
|
||||
Args:
|
||||
data (dict): Parsed session transmission data.
|
||||
|
||||
"""
|
||||
# Bucket logs by day and remove objects before serialization
|
||||
bucket = data.pop("objects")["time"].strftime("%Y-%m-%d")
|
||||
|
||||
# Write it
|
||||
log_file(json.dumps(data), filename="audit_%s.log" % bucket)
|
||||
|
||||
|
||||
def to_syslog(data):
|
||||
"""
|
||||
Writes dictionaries of data generated by an AuditedServerSession to syslog.
|
||||
|
||||
Takes advantage of your system's native logger and writes to wherever
|
||||
you have it configured, which is independent of Evennia.
|
||||
Linux systems tend to write to /var/log/syslog.
|
||||
|
||||
If you're running rsyslog, you can configure it to dump and/or forward logs
|
||||
to disk and/or an external data warehouse (recommended-- if your server is
|
||||
compromised or taken down, losing your logs along with it is no help!).
|
||||
|
||||
Args:
|
||||
data (dict): Parsed session transmission data.
|
||||
|
||||
"""
|
||||
# Remove objects before serialization
|
||||
data.pop("objects")
|
||||
|
||||
# Write it out
|
||||
syslog.syslog(json.dumps(data))
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
"""
|
||||
Auditable Server Sessions:
|
||||
Extension of the stock ServerSession that yields objects representing
|
||||
user inputs and system outputs.
|
||||
|
||||
Evennia contribution - Johnny 2017
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
|
||||
from django.utils import timezone
|
||||
from django.conf import settings as ev_settings
|
||||
from evennia.utils import utils, logger, mod_import, get_evennia_version
|
||||
from evennia.server.serversession import ServerSession
|
||||
|
||||
# Attributes governing auditing of commands and where to send log objects
|
||||
AUDIT_CALLBACK = getattr(
|
||||
ev_settings, "AUDIT_CALLBACK", "evennia.contrib.security.auditing.outputs.to_file"
|
||||
)
|
||||
AUDIT_IN = getattr(ev_settings, "AUDIT_IN", False)
|
||||
AUDIT_OUT = getattr(ev_settings, "AUDIT_OUT", False)
|
||||
AUDIT_ALLOW_SPARSE = getattr(ev_settings, "AUDIT_ALLOW_SPARSE", False)
|
||||
AUDIT_MASKS = [
|
||||
{"connect": r"^[@\s]*[connect]{5,8}\s+(\".+?\"|[^\s]+)\s+(?P<secret>.+)"},
|
||||
{"connect": r"^[@\s]*[connect]{5,8}\s+(?P<secret>[\w]+)"},
|
||||
{"create": r"^[^@]?[create]{5,6}\s+(\w+|\".+?\")\s+(?P<secret>[\w]+)"},
|
||||
{"create": r"^[^@]?[create]{5,6}\s+(?P<secret>[\w]+)"},
|
||||
{"userpassword": r"^[@\s]*[userpassword]{11,14}\s+(\w+|\".+?\")\s+=*\s*(?P<secret>[\w]+)"},
|
||||
{"userpassword": r"^.*new password set to '(?P<secret>[^']+)'\."},
|
||||
{"userpassword": r"^.* has changed your password to '(?P<secret>[^']+)'\."},
|
||||
{"password": r"^[@\s]*[password]{6,9}\s+(?P<secret>.*)"},
|
||||
] + getattr(ev_settings, "AUDIT_MASKS", [])
|
||||
|
||||
|
||||
if AUDIT_CALLBACK:
|
||||
try:
|
||||
AUDIT_CALLBACK = getattr(
|
||||
mod_import(".".join(AUDIT_CALLBACK.split(".")[:-1])), AUDIT_CALLBACK.split(".")[-1]
|
||||
)
|
||||
logger.log_sec("Auditing module online.")
|
||||
logger.log_sec(
|
||||
"Audit record User input: {}, output: {}.\n"
|
||||
"Audit sparse recording: {}, Log callback: {}".format(
|
||||
AUDIT_IN, AUDIT_OUT, AUDIT_ALLOW_SPARSE, AUDIT_CALLBACK
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.log_err("Failed to activate Auditing module. %s" % e)
|
||||
|
||||
|
||||
class AuditedServerSession(ServerSession):
|
||||
"""
|
||||
This particular implementation parses all server inputs and/or outputs and
|
||||
passes a dict containing the parsed metadata to a callback method of your
|
||||
creation. This is useful for recording player activity where necessary for
|
||||
security auditing, usage analysis or post-incident forensic discovery.
|
||||
|
||||
*** WARNING ***
|
||||
All strings are recorded and stored in plaintext. This includes those strings
|
||||
which might contain sensitive data (create, connect, @password). These commands
|
||||
have their arguments masked by default, but you must mask or mask any
|
||||
custom commands of your own that handle sensitive information.
|
||||
|
||||
See README.md for installation/configuration instructions.
|
||||
"""
|
||||
|
||||
def audit(self, **kwargs):
|
||||
"""
|
||||
Extracts messages and system data from a Session object upon message
|
||||
send or receive.
|
||||
|
||||
Keyword Args:
|
||||
src (str): Source of data; 'client' or 'server'. Indicates direction.
|
||||
text (str or list): Client sends messages to server in the form of
|
||||
lists. Server sends messages to client as string.
|
||||
|
||||
Returns:
|
||||
log (dict): Dictionary object containing parsed system and user data
|
||||
related to this message.
|
||||
|
||||
"""
|
||||
# Get time at start of processing
|
||||
time_obj = timezone.now()
|
||||
time_str = str(time_obj)
|
||||
|
||||
session = self
|
||||
src = kwargs.pop("src", "?")
|
||||
bytecount = 0
|
||||
|
||||
# Do not log empty lines
|
||||
if not kwargs:
|
||||
return {}
|
||||
|
||||
# Get current session's IP address
|
||||
client_ip = session.address
|
||||
|
||||
# Capture Account name and dbref together
|
||||
account = session.get_account()
|
||||
account_token = ""
|
||||
if account:
|
||||
account_token = "%s%s" % (account.key, account.dbref)
|
||||
|
||||
# Capture Character name and dbref together
|
||||
char = session.get_puppet()
|
||||
char_token = ""
|
||||
if char:
|
||||
char_token = "%s%s" % (char.key, char.dbref)
|
||||
|
||||
# Capture Room name and dbref together
|
||||
room = None
|
||||
room_token = ""
|
||||
if char:
|
||||
room = char.location
|
||||
room_token = "%s%s" % (room.key, room.dbref)
|
||||
|
||||
# Try to compile an input/output string
|
||||
def drill(obj, bucket):
|
||||
if isinstance(obj, dict):
|
||||
return bucket
|
||||
elif utils.is_iter(obj):
|
||||
for sub_obj in obj:
|
||||
bucket.extend(drill(sub_obj, []))
|
||||
else:
|
||||
bucket.append(obj)
|
||||
return bucket
|
||||
|
||||
text = kwargs.pop("text", "")
|
||||
if utils.is_iter(text):
|
||||
text = "|".join(drill(text, []))
|
||||
|
||||
# Mask any PII in message, where possible
|
||||
bytecount = len(text.encode("utf-8"))
|
||||
text = self.mask(text)
|
||||
|
||||
# Compile the IP, Account, Character, Room, and the message.
|
||||
log = {
|
||||
"time": time_str,
|
||||
"hostname": socket.getfqdn(),
|
||||
"application": "%s" % ev_settings.SERVERNAME,
|
||||
"version": get_evennia_version(),
|
||||
"pid": os.getpid(),
|
||||
"direction": "SND" if src == "server" else "RCV",
|
||||
"protocol": self.protocol_key,
|
||||
"ip": client_ip,
|
||||
"session": "session#%s" % self.sessid,
|
||||
"account": account_token,
|
||||
"character": char_token,
|
||||
"room": room_token,
|
||||
"text": text.strip(),
|
||||
"bytes": bytecount,
|
||||
"data": kwargs,
|
||||
"objects": {
|
||||
"time": time_obj,
|
||||
"session": self,
|
||||
"account": account,
|
||||
"character": char,
|
||||
"room": room,
|
||||
},
|
||||
}
|
||||
|
||||
# Remove any keys with blank values
|
||||
if AUDIT_ALLOW_SPARSE is False:
|
||||
log["data"] = {k: v for k, v in log["data"].items() if v}
|
||||
log["objects"] = {k: v for k, v in log["objects"].items() if v}
|
||||
log = {k: v for k, v in log.items() if v}
|
||||
|
||||
return log
|
||||
|
||||
def mask(self, msg):
|
||||
"""
|
||||
Masks potentially sensitive user information within messages before
|
||||
writing to log. Recording cleartext password attempts is bad policy.
|
||||
|
||||
Args:
|
||||
msg (str): Raw text string sent from client <-> server
|
||||
|
||||
Returns:
|
||||
msg (str): Text string with sensitive information masked out.
|
||||
|
||||
"""
|
||||
# Check to see if the command is embedded within server output
|
||||
_msg = msg
|
||||
is_embedded = False
|
||||
match = re.match(".*Command.*'(.+)'.*is not available.*", msg, flags=re.IGNORECASE)
|
||||
if match:
|
||||
msg = match.group(1).replace("\\", "")
|
||||
submsg = msg
|
||||
is_embedded = True
|
||||
|
||||
for mask in AUDIT_MASKS:
|
||||
for command, regex in mask.items():
|
||||
try:
|
||||
match = re.match(regex, msg, flags=re.IGNORECASE)
|
||||
except Exception as e:
|
||||
logger.log_err(regex)
|
||||
logger.log_err(e)
|
||||
continue
|
||||
|
||||
if match:
|
||||
term = match.group("secret")
|
||||
masked = re.sub(term, "*" * len(term.zfill(8)), msg)
|
||||
|
||||
if is_embedded:
|
||||
msg = re.sub(
|
||||
submsg, "%s <Masked: %s>" % (masked, command), _msg, flags=re.IGNORECASE
|
||||
)
|
||||
else:
|
||||
msg = masked
|
||||
|
||||
return msg
|
||||
|
||||
return _msg
|
||||
|
||||
def data_out(self, **kwargs):
|
||||
"""
|
||||
Generic hook for sending data out through the protocol.
|
||||
|
||||
Keyword Args:
|
||||
kwargs (any): Other data to the protocol.
|
||||
|
||||
"""
|
||||
if AUDIT_CALLBACK and AUDIT_OUT:
|
||||
try:
|
||||
log = self.audit(src="server", **kwargs)
|
||||
if log:
|
||||
AUDIT_CALLBACK(log)
|
||||
except Exception as e:
|
||||
logger.log_err(e)
|
||||
|
||||
super(AuditedServerSession, self).data_out(**kwargs)
|
||||
|
||||
def data_in(self, **kwargs):
|
||||
"""
|
||||
Hook for protocols to send incoming data to the engine.
|
||||
|
||||
Keyword Args:
|
||||
kwargs (any): Other data from the protocol.
|
||||
|
||||
"""
|
||||
if AUDIT_CALLBACK and AUDIT_IN:
|
||||
try:
|
||||
log = self.audit(src="client", **kwargs)
|
||||
if log:
|
||||
AUDIT_CALLBACK(log)
|
||||
except Exception as e:
|
||||
logger.log_err(e)
|
||||
|
||||
super(AuditedServerSession, self).data_in(**kwargs)
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
"""
|
||||
Module containing the test cases for the Audit system.
|
||||
"""
|
||||
|
||||
from anything import Anything
|
||||
from django.test import override_settings
|
||||
from django.conf import settings
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
import re
|
||||
|
||||
# Configure session auditing settings - TODO: This is bad practice that leaks over to other tests
|
||||
settings.AUDIT_CALLBACK = "evennia.security.contrib.auditing.outputs.to_syslog"
|
||||
settings.AUDIT_IN = True
|
||||
settings.AUDIT_OUT = True
|
||||
settings.AUDIT_ALLOW_SPARSE = True
|
||||
|
||||
# Configure settings to use custom session - TODO: This is bad practice, changing global settings
|
||||
settings.SERVER_SESSION_CLASS = "evennia.contrib.security.auditing.server.AuditedServerSession"
|
||||
|
||||
|
||||
class AuditingTest(EvenniaTest):
|
||||
def test_mask(self):
|
||||
"""
|
||||
Make sure the 'mask' function is properly masking potentially sensitive
|
||||
information from strings.
|
||||
"""
|
||||
safe_cmds = (
|
||||
"/say hello to my little friend",
|
||||
"@ccreate channel = for channeling",
|
||||
"@create/drop some stuff",
|
||||
"@create rock",
|
||||
"@create a pretty shirt : evennia.contrib.clothing.Clothing",
|
||||
"@charcreate johnnyefhiwuhefwhef",
|
||||
'Command "@logout" is not available. Maybe you meant "@color" or "@cboot"?',
|
||||
'/me says, "what is the password?"',
|
||||
"say the password is plugh",
|
||||
# Unfortunately given the syntax, there is no way to discern the
|
||||
# latter of these as sensitive
|
||||
"@create pretty sunset" "@create johnny password123",
|
||||
'{"text": "Command \'do stuff\' is not available. Type "help" for help."}',
|
||||
)
|
||||
|
||||
for cmd in safe_cmds:
|
||||
self.assertEqual(self.session.mask(cmd), cmd)
|
||||
|
||||
unsafe_cmds = (
|
||||
(
|
||||
"something - new password set to 'asdfghjk'.",
|
||||
"something - new password set to '********'.",
|
||||
),
|
||||
(
|
||||
"someone has changed your password to 'something'.",
|
||||
"someone has changed your password to '*********'.",
|
||||
),
|
||||
("connect johnny password123", "connect johnny ***********"),
|
||||
("concnct johnny password123", "concnct johnny ***********"),
|
||||
("concnct johnnypassword123", "concnct *****************"),
|
||||
('connect "johnny five" "password 123"', 'connect "johnny five" **************'),
|
||||
('connect johnny "password 123"', "connect johnny **************"),
|
||||
("create johnny password123", "create johnny ***********"),
|
||||
("@password password1234 = password2345", "@password ***************************"),
|
||||
("@password password1234 password2345", "@password *************************"),
|
||||
("@passwd password1234 = password2345", "@passwd ***************************"),
|
||||
("@userpassword johnny = password234", "@userpassword johnny = ***********"),
|
||||
("craete johnnypassword123", "craete *****************"),
|
||||
(
|
||||
"Command 'conncect teddy teddy' is not available. Maybe you meant \"@encode\"?",
|
||||
"Command 'conncect ******** ********' is not available. Maybe you meant \"@encode\"?",
|
||||
),
|
||||
(
|
||||
"{'text': u'Command \\'conncect jsis dfiidf\\' is not available. Type \"help\" for help.'}",
|
||||
"{'text': u'Command \\'conncect jsis ********\\' is not available. Type \"help\" for help.'}",
|
||||
),
|
||||
)
|
||||
|
||||
for index, (unsafe, safe) in enumerate(unsafe_cmds):
|
||||
self.assertEqual(re.sub(" <Masked: .+>", "", self.session.mask(unsafe)).strip(), safe)
|
||||
|
||||
# Make sure scrubbing is not being abused to evade monitoring
|
||||
secrets = [
|
||||
"say password password password; ive got a secret that i cant explain",
|
||||
"whisper johnny = password\n let's lynch the landlord",
|
||||
"say connect johnny password1234|the secret life of arabia",
|
||||
"@password eval(\"__import__('os').system('clear')\", {'__builtins__':{}})",
|
||||
]
|
||||
for secret in secrets:
|
||||
self.assertEqual(self.session.mask(secret), secret)
|
||||
|
||||
def test_audit(self):
|
||||
"""
|
||||
Make sure the 'audit' function is returning a dictionary based on values
|
||||
parsed from the Session object.
|
||||
"""
|
||||
log = self.session.audit(src="client", text=[["hello"]])
|
||||
obj = {
|
||||
k: v for k, v in log.items() if k in ("direction", "protocol", "application", "text")
|
||||
}
|
||||
self.assertEqual(
|
||||
obj,
|
||||
{
|
||||
"direction": "RCV",
|
||||
"protocol": "telnet",
|
||||
"application": Anything, # this will change if running tests from the game dir
|
||||
"text": "hello",
|
||||
},
|
||||
)
|
||||
|
||||
# Make sure OOB data is being recorded
|
||||
log = self.session.audit(
|
||||
src="client", text="connect johnny password123", prompt="hp=20|st=10|ma=15", pane=2
|
||||
)
|
||||
self.assertEqual(log["text"], "connect johnny ***********")
|
||||
self.assertEqual(log["data"]["prompt"], "hp=20|st=10|ma=15")
|
||||
self.assertEqual(log["data"]["pane"], 2)
|
||||
Loading…
Add table
Add a link
Reference in a new issue