Compare commits

..
11 changed files with 58 additions and 44 deletions
+6 -5
View File
@@ -11,7 +11,8 @@ RUN curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
libzmq5 \
nginx \
gettext-base &&\
rm -rf /var/lib/apt/lists/*
rm -rf /var/lib/apt/lists/* &&\
ln -sf /bin/bash /bin/sh
COPY requirements.txt /tmp
RUN pip3 install -r /tmp/requirements.txt
@@ -26,7 +27,7 @@ ENV TFW_PUBLIC_PORT=8888 \
EXPOSE ${TFW_PUBLIC_PORT}
ENV PYTHONPATH="/usr/local/lib/" \
ENV PYTHONPATH="/usr/local/lib" \
TFW_SUPERVISOR_HTTP_URI="http://localhost:${TFW_SUPERVISOR_HTTP_PORT}" \
TFW_SUPERVISORD_CONF="/etc/supervisor/supervisord.conf" \
TFW_SUPERVISORD_COMPONENTS="/etc/supervisor/conf" \
@@ -40,7 +41,7 @@ ENV PYTHONPATH="/usr/local/lib/" \
PROMPT_COMMAND="history -a"
COPY bashrc /tmp
RUN echo "export HISTFILE=${TFW_HISTFILE}\n" >> /tmp/bashrc &&\
RUN echo "export HISTFILE=${TFW_HISTFILE}" >> /tmp/bashrc &&\
cat /tmp/bashrc >> /home/${AVATAO_USER}/.bashrc
COPY supervisor/supervisord.conf ${TFW_SUPERVISORD_CONF}
@@ -49,8 +50,8 @@ COPY nginx/default.conf ${TFW_NGINX_DEFAULT}
COPY nginx/components/ ${TFW_NGINX_COMPONENTS}
COPY lib LICENSE ${TFW_LIB_DIR}
RUN for dir in "${TFW_LIB_DIR}" "/etc/nginx" "/etc/supervisor"; do \
chown -R root:root "$dir" && chmod -R 700 "$dir"; \
RUN for dir in "${TFW_LIB_DIR}"/{tfw,tao,envvars.py} "/etc/nginx" "/etc/supervisor"; do \
chown -R root:root "$dir" && chmod -R 700 "$dir"; \
done
ONBUILD ARG BUILD_CONTEXT="solvable"
+10 -10
View File
@@ -3,11 +3,11 @@
This is the beating heart of TFW the Docker baseimage containing the internals of the framework.
Every tutorial-framework based challenge has a `solvable` Docker image based on this one: their `Dockerfile`s begin with `FROM eu.gcr.io/avatao-challengestore/tutorial-framework`.
Note that TFW is not avaliable on Docker Hub due to legal reasons and is only accessible through local builds (dont't worry, we've got you covered with build scripts in the `test-tutorial-framework` repo).
Note that TFW is not avaliable on Docker Hub due to legal reasons and is only accessible through local builds (don't worry, we've got you covered with build scripts in the [test-tutorial-framework](https://github.com/avatao-content/test-tutorial-framework) repo).
This document explains the general concepts of TFW and should be the first thing you read before getting started with development.
For more on building and running you should consult the `test-tutorial-framework` repo.
For more on building and running you should check the [test-tutorial-framework](https://github.com/avatao-content/test-tutorial-framework) repo.
## The framework
@@ -22,9 +22,9 @@ Frontend components use websockets to connect to the TFW server, to which you ca
### Event handlers
Imagine event handlers as callbacks that are invoked when TFW receives a specific type of message. For instance you could send a message to the framework when the user does something of note.
Imagine event handlers as callbacks that are invoked when TFW receives a specific type of message. For instance, you could send a message to the framework when the user does something of note.
This allows you to define actions triggered on the backend when the user presses a button on the frontend, moves the cursor to a specific area or anything like that.
Event handler allow you to define actions triggered on the backend when the user presses a button on the frontend or moves the cursor to a specific area, etc.
Event handlers use ZeroMQ to connect to the framework. Due to this they are as loosely-coupled as possible: usually they are running in separate processes and only communicate with TFW through ZMQ.
@@ -38,9 +38,9 @@ Inside Avatao this means that any of the content teams can use the framework wit
Another unique feature of the framework is the FSM finite state machine representing the state of your challenge.
This allows you to track users progressing with the tasks you've defined for them to complete.
For instance you could represent whether the user managed to create a malicious user with a state called `user_registered` and subscribe callbacks to events regarding that state (like entering or leaving).
For instance, you could represent whether the user managed to create a malicious user with a state called `user_registered` and subscribe callbacks to events regarding that state (like entering or leaving).
You could create challenges that can be completed in several different ways: imagine a state called `challenge_complete`, which represents when the challenge is completed. Several series of actions (triggers) could lead to this state.
You could create challenges that can be completed in several different ways: imagine a state called `challenge_complete`, which indicates if the challenge is completed. Several series of actions (triggers) could lead to this state.
This enables you to guide your users through the experience you've envisioned with your tutorial.
We can provide a whole new level of interactivity in our challenges because we know what the user is doing.
@@ -48,17 +48,17 @@ This includes context-dependent hints and the automatic typing of commands to a
### Frontend
Note that our frontend implementation is written in Angular. It is maintained and documented in the `frontend-tutorial-framework` repository.
Note that our frontend implementation is written in Angular. It is maintained and documented in the [frontend-tutorial-framework](https://github.com/avatao-content/frontend-tutorial-framework) repository.
### Messaging format
The framework uses JSON messages internally and in exposed APIs as well.
These messages must comply some rules.
These messages must comply with some rules.
Don't worry, we are not too fond of rules around these parts.
The TFW message format:
```json
```text
{
"key: "some identifier used for addressing",
"data":
@@ -80,4 +80,4 @@ The TFW message format:
Most of the components you need have docstrings included (hang on tight, this is work in progress) refer to them for usage info.
To get started you should take a look at the `test-tutorial-framework` repository, which serves as an example project as well.
To get started you should take a look at the [test-tutorial-framework](https://github.com/avatao-content/test-tutorial-framework) repository, which serves as an example project as well.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 40 KiB

+2 -2
View File
@@ -3,7 +3,7 @@
from .directory_monitoring_event_handler import DirectoryMonitoringEventHandler
from .process_managing_event_handler import ProcessManagingEventHandler
from .terminado_event_handler import TerminadoEventHandler
from .webide_event_handler import WebideEventHandler
from .terminal_event_handler import TerminalEventHandler
from .ide_event_handler import IdeEventHandler
from .history_monitor import HistoryMonitor, BashMonitor, GDBMonitor
from .terminal_commands import TerminalCommands
+3 -3
View File
@@ -17,7 +17,7 @@ LOG = logging.getLogger(__name__)
class DirectoryMonitor(ObserverMixin):
def __init__(self, directory):
ObserverMixin.__init__(self)
self.eventhandler = WebideReloadWatchdogEventHandler()
self.eventhandler = IdeReloadWatchdogEventHandler()
self.observer.schedule(self.eventhandler, directory, recursive=True)
self.pause, self.resume = self.eventhandler.pause, self.eventhandler.resume
@@ -42,7 +42,7 @@ class DirectoryMonitor(ObserverMixin):
self.directorymonitor.resume()
class WebideReloadWatchdogEventHandler(FileSystemWatchdogEventHandler):
class IdeReloadWatchdogEventHandler(FileSystemWatchdogEventHandler):
def __init__(self):
super().__init__()
self.uplink = ServerUplinkConnector()
@@ -63,7 +63,7 @@ class WebideReloadWatchdogEventHandler(FileSystemWatchdogEventHandler):
self.ignore = self.ignore - 1
return
LOG.debug(event)
self.uplink.send({'key': 'webide',
self.uplink.send({'key': 'ide',
'data': {'command': 'reload'}})
@@ -40,9 +40,9 @@ class FileManager: # pylint: disable=too-many-instance-attributes
@workdir.setter
def workdir(self, directory):
if not exists(directory) or not isdir(directory):
raise EnvironmentError('"{}" is not a directory!'.format(directory))
raise EnvironmentError(f'"{directory}" is not a directory!')
if not self._is_in_whitelisted_dir(directory):
raise EnvironmentError('Directory "{}" is not in whitelist!'.format(directory))
raise EnvironmentError(f'Directory "{directory}" is not in whitelist!')
self._workdir = directory
@property
@@ -91,7 +91,7 @@ class FileManager: # pylint: disable=too-many-instance-attributes
return relpath(self._filepath(filename), start=self._workdir)
class WebideEventHandler(EventHandlerBase, MonitorManagerMixin):
class IdeEventHandler(EventHandlerBase, MonitorManagerMixin):
# pylint: disable=too-many-arguments
"""
Event handler implementing the backend of our browser based IDE.
@@ -111,8 +111,11 @@ class WebideEventHandler(EventHandlerBase, MonitorManagerMixin):
:param exclude: list of filenames that should not appear between files (for *.o, *.pyc, etc.)
"""
super().__init__(key)
self.filemanager = FileManager(allowed_directories=allowed_directories, working_directory=directory,
selected_file=selected_file, exclude=exclude)
try:
self.filemanager = FileManager(allowed_directories=allowed_directories, working_directory=directory,
selected_file=selected_file, exclude=exclude)
except IndexError:
raise EnvironmentError(f'No file(s) in IdeEventHandler working_directory "{directory}"!')
MonitorManagerMixin.__init__(self, DirectoryMonitor, self.filemanager.workdir)
self.commands = {'read': self.read,
@@ -141,7 +144,8 @@ class WebideEventHandler(EventHandlerBase, MonitorManagerMixin):
"""
Overwrites a file with the desired string.
:param data['content']: string containing the desired file contents
:param data: TFW message data containing keys:
|-string: containing the desired file contents
"""
self.monitor.ignore = self.monitor.ignore + 1
try:
@@ -155,7 +159,8 @@ class WebideEventHandler(EventHandlerBase, MonitorManagerMixin):
"""
Selects a file from the current directory.
:param data['filename']: name of file to select relative to the current directory
:param data: TFW message data containing keys:
|-filename: name of file to select relative to the current directory
"""
try:
self.filemanager.filename = data['filename']
@@ -167,9 +172,10 @@ class WebideEventHandler(EventHandlerBase, MonitorManagerMixin):
"""
Select a new working directory to display files from.
:param data['directory']: absolute path of diretory to select.
must be a path whitelisted in
self.allowed_directories
:param data: TFW message data containing keys:
|-directory: absolute path of diretory to select.
must be a path whitelisted in
self.allowed_directories
"""
try:
self.filemanager.workdir = data['directory']
@@ -187,7 +193,8 @@ class WebideEventHandler(EventHandlerBase, MonitorManagerMixin):
"""
Overwrite list of excluded files
:param data['exclude']: list of filename patterns to be excluded, e.g.: ["*.pyc", "*.o"]
:param data: TFW message data containing keys:
|-exclude: list of filename patterns to be excluded, e.g.: ["*.pyc", "*.o"]
"""
try:
self.filemanager.exclude = list(data['exclude'])
@@ -10,7 +10,7 @@ from .terminado_mini_server import TerminadoMiniServer
LOG = logging.getLogger(__name__)
class TerminadoEventHandler(EventHandlerBase):
class TerminalEventHandler(EventHandlerBase):
"""
Event handler responsible for managing terminal sessions for frontend xterm
sessions to connect to. You need to instanciate this in order for frontend
@@ -54,7 +54,8 @@ class TerminadoEventHandler(EventHandlerBase):
Writes a string to the terminal session (on the pty level).
Useful for pre-typing and executing commands for the user.
:param data['shellcmd']: command to be written to the pty
:param data: TFW message data containing keys:
|-shellcmd: command to be written to the pty
"""
self.terminado_server.pty.write(data['shellcmd'])
@@ -62,7 +63,8 @@ class TerminadoEventHandler(EventHandlerBase):
"""
Reads the history of commands executed.
:param data['count']: the number of history elements to return
:param data: TFW message data containing keys:
|-count: the number of history elements to return
:return: message with list of commands in data['history']
"""
data['count'] = int(data.get('count', 1))
+9 -5
View File
@@ -5,14 +5,18 @@ from .fsm_base import FSMBase
class LinearFSM(FSMBase):
# pylint: disable=anomalous-backslash-in-string
"""
This is a state machine for challenges with linear progression, consisting of
a number of steps specified in the constructor. It automatically sets up a single
action between states as such:
0 ==step_1==> 1 ==step_2==> 2 ==step_3==> 3 ... and so on
a number of steps specified in the constructor. It automatically sets up 2
actions (triggers) between states as such:
(0) -- step_1 --> (1) -- step_2 --> (2) -- step_3 --> (3) ... and so on
\-step_next-/ \-step_next-/ \-step_next-/
"""
def __init__(self, number_of_steps):
self.states = list(map(str, range(number_of_steps)))
self.transitions = [{'trigger': 'step_{}'.format(int(index)+1), 'source': index, 'dest': str(int(index)+1)}
for index in self.states[:-1]]
self.transitions = []
for index in self.states[:-1]:
self.transitions.append({'trigger': f'step_{int(index)+1}', 'source': index, 'dest': str(int(index)+1)})
self.transitions.append({'trigger': 'step_next', 'source': index, 'dest': str(int(index)+1)})
super(LinearFSM, self).__init__()
+1 -1
View File
@@ -12,7 +12,7 @@ class ControllerConnector(ZMQConnectorBase):
def __init__(self, zmq_context=None):
super(ControllerConnector, self).__init__(zmq_context)
self._zmq_rep_socket = self._zmq_context.socket(zmq.REP)
self._zmq_rep_socket.connect('tcp://localhost:{}'.format(TFWENV.CONTROLLER_PORT))
self._zmq_rep_socket.connect(f'tcp://localhost:{TFWENV.CONTROLLER_PORT}')
self._zmq_rep_stream = ZMQStream(self._zmq_rep_socket)
self.register_callback = self._zmq_rep_stream.on_recv_stream
@@ -15,7 +15,7 @@ class ServerDownlinkConnector(ZMQConnectorBase):
def __init__(self, zmq_context=None):
super(ServerDownlinkConnector, self).__init__(zmq_context)
self._zmq_sub_socket = self._zmq_context.socket(zmq.SUB)
self._zmq_sub_socket.connect('tcp://localhost:{}'.format(TFWENV.PUBLISHER_PORT))
self._zmq_sub_socket.connect(f'tcp://localhost:{TFWENV.PUBLISHER_PORT}')
self._zmq_sub_stream = ZMQStream(self._zmq_sub_socket)
self.subscribe = partial(self._zmq_sub_socket.setsockopt_string, zmq.SUBSCRIBE)
@@ -30,7 +30,7 @@ class ServerUplinkConnector(ZMQConnectorBase):
def __init__(self, zmq_context=None):
super(ServerUplinkConnector, self).__init__(zmq_context)
self._zmq_push_socket = self._zmq_context.socket(zmq.PUSH)
self._zmq_push_socket.connect('tcp://localhost:{}'.format(TFWENV.RECEIVER_PORT))
self._zmq_push_socket.connect(f'tcp://localhost:{TFWENV.RECEIVER_PORT}')
def send_to_eventhandler(self, message):
"""
@@ -16,7 +16,7 @@ class EventHandlerDownlinkConnector(ZMQConnectorBase):
super(EventHandlerDownlinkConnector, self).__init__(zmq_context)
self._zmq_pull_socket = self._zmq_context.socket(zmq.PULL)
self._zmq_pull_stream = ZMQStream(self._zmq_pull_socket)
address = 'tcp://*:{}'.format(TFWENV.RECEIVER_PORT)
address = f'tcp://*:{TFWENV.RECEIVER_PORT}'
self._zmq_pull_socket.bind(address)
LOG.debug('Pull socket bound to %s', address)
@@ -25,7 +25,7 @@ class EventHandlerUplinkConnector(ZMQConnectorBase):
def __init__(self, zmq_context=None):
super(EventHandlerUplinkConnector, self).__init__(zmq_context)
self._zmq_pub_socket = self._zmq_context.socket(zmq.PUB)
address = 'tcp://*:{}'.format(TFWENV.PUBLISHER_PORT)
address = f'tcp://*:{TFWENV.PUBLISHER_PORT}'
self._zmq_pub_socket.bind(address)
LOG.debug('Pub socket bound to %s', address)