1
0
mirror of https://github.com/avatao-content/test-tutorial-framework synced 2024-12-04 18:41:33 +00:00

Merge branch 'chausie'

This commit is contained in:
Kristóf Tóth 2019-09-12 13:48:56 -04:00
commit e5aaf29c17
22 changed files with 360 additions and 529 deletions

View File

@ -1,5 +1,12 @@
[TYPECHECK]
ignored-modules = zmq
max-line-length = 150
max-line-length = 120
disable = missing-docstring, too-few-public-methods, invalid-name
[SIMILARITIES]
min-similarity-lines=7
ignore-comments=yes
ignore-docstrings=yes
ignore-imports=yes

View File

@ -30,6 +30,9 @@ Dependencies:
- yarn
- Angular CLI
- GNU coreutils
- GNU findutils
- GNU sed
- GNU grep
Just copy and paste the following command in a terminal:
@ -180,7 +183,7 @@ Refer to the example in this repo.
### src
This folder contains the source code of a server running TFW and an other server running our event handlers.
This folder contains the source code of our pre-written event handlers and example FSMs.
Note that this is not a part of the framework by any means, these are just simple examples.
```
@ -207,7 +210,7 @@ A good state machine is the backbone of a good TFW challenge.
There are two ways to define a state machine:
- Using a YAML configuration file
- Implementing it in Python by hand
- Implementing it in Python
The first option allows you to handle FSM callbacks and custom logic in any programming language (not just Python) and is generally really easy to work with (you can execute arbitrary shell commands on events).
You should choose this method unless you have good reason not to.
@ -230,6 +233,8 @@ It is also possible to add preconditions to transitions.
This is done by adding a `predicates` key with a list of shell commands to run.
If you do this, the transition will only succeed if the return code of all predicates was `0` (as per unix convention for success).
Our `YamlFSM` implementation also supports jinja2 templates inside the `YAML` config file (examples in `test_fsm.yml`).
## Baby steps
When creating your own challenge the process should be the following:

View File

@ -2,12 +2,11 @@ FROM avatao/controller:debian-buster
USER root
ENV PYTHONPATH="/usr/local/lib" \
TFW_PUBLISHER_PORT=7654 \
TFW_RECEIVER_PORT=8765 \
TFW_PUB_PORT=7654 \
TFW_PULL_PORT=8765 \
TFW_AUTH_KEY="/tmp/tfw-auth.key" \
CONTROLLER_PORT=5555
RUN pip3 install watchdog transitions
COPY ./controller/ /
CMD ["python3", "/opt/server.py"]

View File

@ -4,36 +4,35 @@ import json
from tornado.ioloop import IOLoop
from tornado.web import RequestHandler, Application
from tfw import FSMAwareEventHandler
from tfw.event_handlers import FSMAwareEventHandler
from tfw.main import EventHandlerFactory, setup_signal_handlers
class ControllerPostHandler(RequestHandler):
# pylint: disable=abstract-method
def initialize(self, **kwargs): # pylint: disable=arguments-differ
# pylint: disable=abstract-method,attribute-defined-outside-init,unused-argument
def initialize(self, **kwargs):
self.controller = kwargs['controller']
def post(self, *args, **kwargs):
self.set_header('Content-Type', 'application/json')
self.write(json.dumps({
'solved': self.controller.in_accepted_state
'solved': self.controller.fsm_in_accepted_state
}))
class ControllerEventHandler(FSMAwareEventHandler):
def handle_event(self, message):
pass
if __name__ == '__main__':
controller = ControllerEventHandler('controller')
controller_eh = EventHandlerFactory().build(
lambda *_: None,
event_handler_type=FSMAwareEventHandler
)
route = os.environ['SECRET'].rstrip('/')
application = Application([(
f'/{os.environ["SECRET"]}',
f'/{route}/?',
ControllerPostHandler,
{'controller': controller}
{'controller': controller_eh}
)])
application.listen(os.environ['CONTROLLER_PORT'])
try:
IOLoop.instance().start()
finally:
controller.cleanup()
setup_signal_handlers()
IOLoop.current().start()

View File

@ -3,7 +3,7 @@ set -eu
set -o pipefail
set -o errtrace
shopt -s expand_aliases
[ "$(uname)" == "Darwin" ] && alias sed="gsed" || :
[ "$(uname)" == "Darwin" ] && alias sed="gsed" && alias grep="ggrep" || :
HERE="$(pwd)"
CHALLENGE=${CHALLENGE:-test-tutorial-framework}

View File

@ -38,9 +38,9 @@ challenge::run() {
local mount_volumes
if [[ "${HOTRELOAD:-0}" == "1" ]]; then
if [[ -d "${BASEIMAGE_PATH}" ]]; then
mount_baseimage="-v ${BASEIMAGE_PATH}/lib/tfw:/usr/local/lib/tfw"
mount_baseimage="-e HOTRELOAD=1 -v ${BASEIMAGE_PATH}/tfw:/usr/local/lib/tfw"
fi
mount_challenge="-v ${CHALLENGE_PATH}/solvable/src:/srv/.tfw_builtin_ehs"
mount_challenge="-v ${CHALLENGE_PATH}/solvable/src:/.tfw/builtin_event_handlers"
mount_volumes="${mount_baseimage:-} ${mount_challenge}"
fi
popd

View File

@ -3,7 +3,7 @@ set -eu
set -o pipefail
set -o errtrace
shopt -s expand_aliases
[ "$(uname)" == "Darwin" ] && alias readlink="greadlink" || :
[ "$(uname)" == "Darwin" ] && alias readlink="greadlink" && alias grep="ggrep" || :
SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
TFW_PATH="${TFW_PATH:-$SCRIPT_DIR/../..}"

View File

@ -1,15 +1,9 @@
FROM eu.gcr.io/avatao-challengestore/tutorial-framework
# Install webservice dependencies
RUN pip3 install Flask==1.0 \
SQLAlchemy==1.2.7 \
passlib==1.7.1 \
git+https://github.com/avatao-content/tfwconnector.git#subdirectory=python3
# Define variables to use later
ENV TFW_EHMAIN_DIR="/srv/.tfw_builtin_ehs" \
TFW_WEBSERVICE_DIR="/srv/webservice" \
TFW_IDE_WD="/home/${AVATAO_USER}/workdir" \
ENV TFW_EHMAIN_DIR="${TFW_DIR}/builtin_event_handlers" \
TFW_WEBSERVICE_DIR="/srv/webservice" \
TFW_IDE_WD="/home/${AVATAO_USER}/workdir" \
TFW_TERMINADO_WD="/home/${AVATAO_USER}/workdir"
# Copy TFW related stuff to a dedicated directory
@ -21,12 +15,12 @@ ADD solvable/src/webservice/frontend-deps.tar ${TFW_WEBSERVICE_DIR}/static
# Create IDE directory, symlink server source and give proper permissions to AVATAO_USER
RUN mkdir -p ${TFW_IDE_WD} &&\
ln -s ${TFW_WEBSERVICE_DIR}/user_ops.py ${TFW_IDE_WD} &&\
chown -R ${AVATAO_USER}: "${TFW_IDE_WD}" "${TFW_WEBSERVICE_DIR}" &&\
chmod -R 755 "${TFW_IDE_WD}" "${TFW_WEBSERVICE_DIR}"
# Hide TFW related code from user
RUN chown -R root:root ${TFW_SERVER_DIR} && chmod -R 700 ${TFW_SERVER_DIR}
RUN chown -R root:root ${TFW_SERVER_DIR} ${TFW_DIR} &&\
chmod -R 700 ${TFW_SERVER_DIR} ${TFW_DIR}
# Make AVATAO_USER's home writeable and set it as WORKDIR
# Make webservice directory writable

View File

@ -0,0 +1,45 @@
import logging
from json import loads, JSONDecodeError
from tfw.components.frontend import MessageSender
from tfw.components.terminal import TerminalCommandsHandler
from tfw.main import TFWUplinkConnector
LOG = logging.getLogger(__name__)
class CenatorHandler:
keys = ['history.bash']
def handle_event(self, message, connector): # pylint: disable=no-self-use
command = message['command']
LOG.debug('User executed command: "%s"', command)
MessageSender(connector).send(f'You\'ve executed "{command}"', originator='JOHN CENA')
class TestCommandsHandler(TerminalCommandsHandler):
# pylint: disable=unused-argument,attribute-defined-outside-init,no-self-use
def command_sendmessage(self, *args):
if not args:
message_template = """'{"key": ""}'"""
TFWUplinkConnector().send_message({
'key': 'terminal.write',
'command': f'sendmessage {message_template}'
})
else:
try:
TFWUplinkConnector().send_message(loads(args[0]))
except JSONDecodeError:
LOG.error('IGNORING MESSAGE: Invalid message received: %s', args[0])
def messageFSMStepsHandler(message, connector):
"""
When the FSM steps this method is invoked.
Receives a 'data' field from an fsm_update message as kwargs.
"""
MessageSender(connector).send(
f'FSM has stepped from state "{message["last_event"]["from_state"]}" '
f'to state "{message["current_state"]}" in response to trigger "{message["last_event"]["trigger"]}"',
originator='FSM info'
)

View File

@ -1,148 +1,94 @@
from ast import literal_eval
import logging
from os.path import dirname, realpath, join
from functools import partial
from signal import signal, SIGTERM, SIGINT
from tornado.ioloop import IOLoop
from tfw import YamlFSM, FSMAwareEventHandler, EventHandlerBase
from tfw.components import IdeEventHandler, TerminalEventHandler
from tfw.components import ProcessManagingEventHandler, BashMonitor
from tfw.components import TerminalCommands, LogMonitoringEventHandler
from tfw.components import FSMManagingEventHandler
from tfw.networking import MessageSender, TFWServerConnector
from tfw.config import TFWENV
from tfw.config.logs import logging
from tao.config import TAOENV
from tfw.fsm import YamlFSM
from tfw.event_handlers import FSMAwareEventHandler
from tfw.components.ide import IdeHandler, DeployHandler
from tfw.components.terminal import TerminalHandler
from tfw.components.frontend import FrontendProxyHandler, FrontendReadyHandler
from tfw.components.frontend import ConsoleLogsHandler, MessageQueueHandler, FrontendConfigHandler
from tfw.components.process_management import ProcessHandler, ProcessLogHandler
from tfw.components.fsm import FSMHandler
from tfw.main import EventHandlerFactory, setup_logger, setup_signal_handlers
from tfw.config import TFWENV, TAOENV
from custom_handlers import CenatorHandler, TestCommandsHandler, messageFSMStepsHandler
LOG = logging.getLogger(__name__)
here = dirname(realpath(__file__))
def cenator(history):
"""
Logs commands executed in terminal to messages.
!! Please remove from production code. !!
"""
LOG.debug('User executed command: "%s"', history[-1])
MessageSender().send('JOHN CENA', f'You\'ve executed "{history[-1]}"')
def main():
# pylint: disable=unused-variable,too-many-locals
setup_logger(__file__)
class TestCommands(TerminalCommands):
"""
Some example commands useful for debugging.
!! Please remove from production code !! and inherit your own
class from TerminalCommands if you need to define custom
commands in your challenge.
"""
# pylint: disable=unused-argument, attribute-defined-outside-init, no-self-use
def command_sendmessage(self, *args):
"""
Insert TFW message template as first argument if executed without args.
Evaluate first argumen as a dict and send it to the frontend.
This is useful for playing around with frontend APIs.
"""
if not args:
message_template = """'{"key": "", "data": {"command": ""}}'"""
TFWServerConnector().send_to_eventhandler({
'key': 'shell',
'data': {
'command': 'write',
'value': f'sendmessage {message_template}'
}
})
else:
TFWServerConnector().send(literal_eval(args[0]))
def command_seppuku_tfw(self, *args):
"""
Restart tfw_server.py and event_handler_main.py.
This can speed up development when combined with mounting
volumes from host to container.
"""
seppuku = (
'nohup sh -c "supervisorctl restart tfwserver event_handler_main" &> /dev/null & '
'clear && echo "Committed seppuku! :)" && sleep infinity'
eh_factory = EventHandlerFactory()
# TFW builtin EventHandlers (required for their respective functionalities)
# TFW FSM
fsm_eh = eh_factory.build(FSMHandler(
fsm_type=partial(
YamlFSM,
'test_fsm.yml',
{} # jinja2 variables, empty dict enables jinja2 without any variables
)
uplink = TFWServerConnector()
uplink.send_to_eventhandler({
'key': 'shell',
'data': {
'command': 'write',
'value': f'{seppuku}\n'
}
})
uplink.send({
'key': 'dashboard',
'data': {
'command': 'reloadFrontend'
}
})
))
# Web IDE backend
ide_eh = eh_factory.build(IdeHandler(
patterns=['/home/user/workdir/*', '/srv/webservice/user_ops.py']
))
deploy_eh = eh_factory.build(DeployHandler())
# Web shell backend
terminal_eh = eh_factory.build(TerminalHandler(
port=TFWENV.TERMINADO_PORT,
user=TAOENV.USER,
working_directory=TFWENV.TERMINADO_WD,
histfile=TFWENV.HISTFILE
))
# Handles 'deploy' button clicks
process_eh = eh_factory.build(ProcessHandler(
supervisor_uri=TFWENV.SUPERVISOR_HTTP_URI
))
# Sends live logs of webservice process to frontend
processlog_eh = eh_factory.build(ProcessLogHandler(
process_name='webservice',
supervisor_uri=TFWENV.SUPERVISOR_HTTP_URI,
log_tail=2000
))
# Proxies frontend API calls to frontend
frontendproxy_eh = eh_factory.build(FrontendProxyHandler())
# Initiates first FSM step
frontendready = FrontendReadyHandler('step_1')
frontendready_eh = eh_factory.build(frontendready)
frontendready.stop = frontendready_eh.stop
# Configures frontend
frontendconfig_eh = eh_factory.build(
FrontendConfigHandler(join(here, 'frontend_config.yaml'))
)
# Manages message queues
messagequeue_eh = eh_factory.build(MessageQueueHandler(25))
# Writes live logs to console on frontend
console_logs_eh = eh_factory.build(ConsoleLogsHandler(stream='stdout'))
# Replace these with your custom event handlers
# Echoes executed commands to messages
cenator_eh = eh_factory.build(CenatorHandler())
# Echoes FSM steps
message_fsm_steps_eh = eh_factory.build(
messageFSMStepsHandler,
event_handler_type=FSMAwareEventHandler
)
# Catches special commands
commands_eh = eh_factory.build(TestCommandsHandler(
bashrc=f'/home/{TAOENV.USER}/.bashrc'
))
class MessageFSMStepsEventHandler(FSMAwareEventHandler):
"""
This example EventHandler is capable of detecting FSM state.
!! Please remove from production code !!
"""
def handle_event(self, message):
pass
def handle_fsm_step(self, from_state, to_state, trigger):
"""
When the FSM steps this method is invoked.
"""
MessageSender().send(
'FSM info',
f'FSM has stepped from state "{from_state}" '
f'to state "{to_state}" in response to trigger "{trigger}"'
)
setup_signal_handlers()
IOLoop.current().start()
if __name__ == '__main__':
# TFW component EventHandlers (builtins, required for their respective functionalities)
fsm = FSMManagingEventHandler( # TFW FSM
key='fsm',
fsm_type=partial(YamlFSM, 'test_fsm.yml')
)
ide = IdeEventHandler( # Web IDE backend
key='ide',
allowed_directories=[TFWENV.IDE_WD, TFWENV.WEBSERVICE_DIR],
directory=TFWENV.IDE_WD,
exclude=['*.pyc']
)
terminal = TerminalEventHandler( # Web shell backend
key='shell',
monitor=BashMonitor(TFWENV.HISTFILE)
)
processmanager = ProcessManagingEventHandler( # Handles 'deploy' button clicks
key='processmanager',
dirmonitor=ide.monitor,
log_tail=2000
)
logmonitor = LogMonitoringEventHandler( # Sends live logs of webservice process to frontend
key='logmonitor',
process_name='webservice',
log_tail=2000
)
# Your custom event handlers
message_fsm_steps = MessageFSMStepsEventHandler(
key='test'
)
# Terminal command handlers
commands = TestCommands(bashrc=f'/home/{TAOENV.USER}/.bashrc')
terminal.historymonitor.subscribe_callback(commands.callback)
# Example terminal command callback
terminal.historymonitor.subscribe_callback(cenator)
event_handlers = EventHandlerBase.get_local_instances()
def cleanup(sig, frame):
for eh in event_handlers:
eh.cleanup()
exit(0)
signal(SIGTERM, cleanup)
signal(SIGINT, cleanup)
IOLoop.instance().start()
main()

View File

@ -0,0 +1,27 @@
dashboard:
layout: terminal-ide-web
hideMessages: false
iframeUrl: /webservice
showUrlBar: false
terminalMenuItem: terminal
reloadIframeOnDeploy: false
enabledLayouts:
- terminal-ide-web
- terminal-ide-vertical
- terminal-web
- ide-web-vertical
- terminal-ide-horizontal
- terminal-only
- ide-only
- web-only
ide:
autoSaveInterval: 444
showDeployButton: true
deployButtonText:
TODEPLOY: Deploy
DEPLOYED: Deployed
DEPLOYING: Reloading app...
FAILED: Deployment failed
site:
askReloadSite: false
documentTitle: Avatao Tutorials

View File

@ -1,235 +0,0 @@
from json import dumps, loads
from tfw.crypto import KeyManager, sign_message, verify_message
from tfw.components import PipeIOEventHandlerBase
from tfw.components.pipe_io_event_handler import DEFAULT_PERMISSIONS
class SignMessagePipeIOEventHandler(PipeIOEventHandlerBase):
"""
Signs a valid TFW message with HMAC.
Note that the running process needs root permissions in order to read
the authentication key.
When forwarding is true, it will send the signed message to the TFW
server before writing it into the output pipe.
"""
def __init__(
self, in_pipe_path, out_pipe_path,
permissions=DEFAULT_PERMISSIONS,
forwarding=True
):
self.forwarding = forwarding
self.auth_key = KeyManager().auth_key
super().__init__(None, in_pipe_path, out_pipe_path, permissions)
def handle_event(self, message):
pass
def handle_pipe_event(self, message_bytes):
message = loads(message_bytes)
sign_message(self.auth_key, message)
if self.forwarding:
self.server_connector.send(message)
self.pipe_io.send_message(dumps(message).encode())
class VerifyMessagePipeIOEventHandler(PipeIOEventHandlerBase):
"""
Verifies a signed TFW message.
This pipe also needs root permissions. Send the serialized JSON object
to the pipe, then wait for its boolean response.
"""
def __init__(self, in_pipe_path, out_pipe_path, permissions=DEFAULT_PERMISSIONS):
self.auth_key = KeyManager().auth_key
super().__init__(None, in_pipe_path, out_pipe_path, permissions)
def handle_event(self, message):
pass
def handle_pipe_event(self, message_bytes):
message = loads(message_bytes)
validity = verify_message(self.auth_key, message)
self.pipe_io.send_message(str(validity).lower().encode())
class BotPipeIOEventHandler(PipeIOEventHandlerBase):
"""
Sends bot messages to the frontend.
If you assign @originator, it will be the default message sender.
When you write a line to the pipe, it will be considered as a single
message and gets appended to the queue until an empty line is received,
which triggers forwarding the messages to the TFW server.
"""
def __init__(
self, in_pipe_path, out_pipe_path, permissions=DEFAULT_PERMISSIONS,
originator='avataobot'
):
self.queue = []
self.originator = originator
super().__init__(None, in_pipe_path, out_pipe_path, permissions)
def handle_event(self, message):
pass
def handle_pipe_event(self, message_bytes):
if message_bytes == b"":
if self.queue:
self.server_connector.send({
'key': 'queueMessages',
'data': {
'messages': self.queue
}
})
self.queue = []
else:
self.queue.append({
'originator': self.originator,
'message': message_bytes.decode().replace('\\n', '\n')
})
class DeployPipeIOEventHandler(PipeIOEventHandlerBase):
"""
Manages deployment in the IDE.
When you receive "deploy", then you have to answer with a "true" or
"false" depending whether you are satisfied with the result or not.
The @process parameter is the name of the supervised service.
"""
# pylint: disable=too-many-arguments
def __init__(
self, in_pipe_path, out_pipe_path, process,
permissions=DEFAULT_PERMISSIONS
):
self.expected = False
self.process = process
self.onsuccess = {
'key': 'processmanager',
'data': {
'process_name': process,
'command': 'restart'
}
}
self.onerror = {
'key': 'processmanager',
'data': {
'process_name': process,
'error': True
}
}
super().__init__('processmanager', in_pipe_path, out_pipe_path, permissions)
def handle_event(self, message):
if message == self.onsuccess:
self.expected = True
self.pipe_io.send_message(b'deploy')
def handle_pipe_event(self, message_bytes):
if not self.expected:
raise ValueError(
f'{self.pipe_io.in_pipe}: There is nothing to deploy.'
)
self.expected = False
if message_bytes == b'true':
self.server_connector.send(self.onsuccess)
elif message_bytes == b'false':
self.server_connector.send(self.onerror)
else:
raise ValueError(
f'{self.pipe_io.in_pipe}: Expected "true" or "false".'
)
class IdePipeIOEventHandler(PipeIOEventHandlerBase):
"""
Manipulates the content of the IDE.
You can observe a file, and when the user edits it, you will receive
the new contents where newlines are escaped as "\\n".
In order to overwrite the file, send an escaped text back to the pipe.
Since the pipe doesn't know if the file is selected initially in the IDE,
you have to provide this information by yourself with @selected,
but it will track it later on.
"""
# pylint: disable=too-many-arguments
def __init__(
self, in_pipe_path, out_pipe_path, filename,
permissions=DEFAULT_PERMISSIONS,
selected=True
):
self.selected = selected
self.filename = filename
super().__init__('ide', in_pipe_path, out_pipe_path, permissions)
def handle_event(self, message):
data = message['data']
if data['command'] == 'select':
self.selected = data['filename'] == self.filename
elif data['command'] == 'write' and self.selected:
clean = data['content'].replace('\n', '\\n')
self.pipe_io.send_message(clean.encode())
def handle_pipe_event(self, message_bytes):
if not self.selected:
self.server_connector.send({
'key': 'mirror',
'data': {
'key': 'ide',
'data': {
'command': 'select',
'filename': self.filename
}
}
})
self.server_connector.send({
'key': 'mirror',
'data': {
'key': 'ide',
'data': {
'command': 'write',
'content': message_bytes.decode().replace('\\n', '\n')
}
}
})
self.server_connector.send({
'key': 'mirror',
'data': {
'key': 'ide',
'data': {
'command': 'read'
}
}
})
class FSMPipeIOEventHandler(PipeIOEventHandlerBase):
"""
Handles FSM steps.
When the FSM enters the next state, you will receive a line containing
its name. To trigger a state change, send the name of the transition to
the pipe.
"""
def __init__(self, in_pipe_path, out_pipe_path, permissions=DEFAULT_PERMISSIONS):
super().__init__(
['fsm', 'fsm_update'],
in_pipe_path, out_pipe_path, permissions
)
def handle_event(self, message):
if 'current_state' in message['data']:
self.pipe_io.send_message(message['data']['current_state'].encode())
def handle_pipe_event(self, message_bytes):
self.server_connector.send({
'key': '',
'trigger': message_bytes.decode()
})

View File

@ -1,77 +1,30 @@
from signal import signal, SIGTERM, SIGINT
import logging
from tornado.ioloop import IOLoop
from tfw import EventHandlerBase
from tfw.components import PipeIOEventHandler
from tfw.config import TFWENV
from tfw.components.pipe_io import PipeIOHandler, ProxyPipeConnectorHandler
from tfw.main import EventHandlerFactory, setup_logger, setup_signal_handlers
from pipe_io_auxlib import (
SignMessagePipeIOEventHandler, VerifyMessagePipeIOEventHandler,
BotPipeIOEventHandler,
DeployPipeIOEventHandler, IdePipeIOEventHandler,
FSMPipeIOEventHandler
)
LOG = logging.getLogger(__name__)
def main():
# pylint: disable=unused-variable
setup_logger(__file__)
eh_factory = EventHandlerFactory()
json_pipe_eh = eh_factory.build(PipeIOHandler(
'/tmp/tfw_send',
'/tmp/tfw_recv',
permissions=0o666
))
proxy_pipe_eh = eh_factory.build(ProxyPipeConnectorHandler(TFWENV.PIPES_DIR))
setup_signal_handlers()
IOLoop.current().start()
if __name__ == '__main__':
"""
Creates general purpose pipes.
The first parameter associates the receiving pipe with a key, which is
an empty string in this case. It has a special meaning, you can
subscribe to every kind of message with this key.
If you wish to filter incoming data, specify a single or more keys in
a list, eg.: processmanager, ide, key...
You can send/receive JSON messages to/from the TFW server as any user,
because we gave read+write permissions, without that parameter, only
the owner has access to the pipes.
"""
json_pipe = PipeIOEventHandler(
'',
'/tmp/tfw_json_send',
'/tmp/tfw_json_recv',
permissions=0o666
)
sign_pipe = SignMessagePipeIOEventHandler(
'/tmp/tfw_sign_send',
'/tmp/tfw_sign_recv',
forwarding=True
)
verify_pipe = VerifyMessagePipeIOEventHandler(
'/tmp/tfw_verify_send',
'/tmp/tfw_verify_recv'
)
bot_pipe = BotPipeIOEventHandler(
'/tmp/tfw_bot_send',
'/tmp/tfw_bot_recv'
)
deploy_pipe = DeployPipeIOEventHandler(
'/tmp/tfw_deploy_send',
'/tmp/tfw_deploy_recv',
'webservice'
)
ide_pipe = IdePipeIOEventHandler(
'/tmp/tfw_ide_send',
'/tmp/tfw_ide_recv',
'user_ops.py',
selected=True
)
fsm_pipe = FSMPipeIOEventHandler(
'/tmp/tfw_fsm_send',
'/tmp/tfw_fsm_recv'
)
event_handlers = EventHandlerBase.get_local_instances()
def cleanup(sig, frame):
for eh in event_handlers:
eh.cleanup()
exit(0)
signal(SIGTERM, cleanup)
signal(SIGINT, cleanup)
IOLoop.instance().start()
main()

View File

@ -2,8 +2,9 @@
from os.path import exists
from tfw import LinearFSM
from tfw.networking import MessageSender
from tfw.fsm import LinearFSM
from tfw.components.frontend import MessageSender
from tfw.main import TFWUplinkConnector
class TestFSM(LinearFSM):
@ -11,7 +12,8 @@ class TestFSM(LinearFSM):
def __init__(self):
super().__init__(6)
self.message_sender = MessageSender()
self.uplink = TFWUplinkConnector()
self.message_sender = MessageSender(self.uplink)
self.subscribe_predicate('step_3', self.step_3_allowed)
@staticmethod

View File

@ -4,25 +4,21 @@ states:
- name: '0'
- name: '1'
on_enter: |
python3 -c "from tfwconnector import MessageSender; MessageSender().send('FSM', 'Entered state 1!')"
printf "Entered state 1!\n\n" > /tmp/tfw_bot_send
- name: '2'
on_enter: |
file=/home/user/workdir/cat.txt
echo "As you can see it is possible to execute arbitrary shell commands here." >> $file
python3 -c \
"
from tfwconnector import MessageSender
MessageSender().send('FSM', 'Entered state 2! Written stuff to $file')
"
printf "Entered state 2! Written stuff to $file\n\n" > /tmp/tfw_bot_send
- name: '3'
on_enter: |
python3 -c "from tfwconnector import MessageSender; MessageSender().send('FSM', 'Entered state 3!')"
printf "Entered state 3!\n\n" > /tmp/tfw_bot_send
- name: '4'
on_enter: |
python3 -c "from tfwconnector import MessageSender; MessageSender().send('FSM', 'Entered state 4!')"
printf "Entered state 4!\n\n" > /tmp/tfw_bot_send
- name: '5'
on_enter: |
python3 -c "from tfwconnector import MessageSender; MessageSender().send('FSM', 'Entered state 5!')"
printf "Entered state 5!\n\n" > /tmp/tfw_bot_send
transitions:
- trigger: step_1
source: '0'
@ -41,3 +37,8 @@ transitions:
- trigger: step_5
source: '4'
dest: '5'
{% for i in range(5) %} # you can also use jinja2 in this config file
- trigger: 'step_next'
source: '{{i}}'
dest: '{{i+1}}'
{% endfor %}

View File

@ -1,11 +1,24 @@
from passlib.hash import pbkdf2_sha256
from os import urandom
from hashlib import scrypt
class PasswordHasher:
@staticmethod
def hash(password):
return pbkdf2_sha256.hash(password)
n = 16384
r = 8
p = 1
dklen = 32
@staticmethod
def verify(password, hashdigest):
return pbkdf2_sha256.verify(password, hashdigest)
@classmethod
def hash(cls, password):
salt = urandom(32)
return cls.scrypt(password, salt).hex() + salt.hex()
@classmethod
def verify(cls, password, salted_hash):
salt = bytes.fromhex(salted_hash)[32:]
hashdigest = bytes.fromhex(salted_hash)[:32]
return cls.scrypt(password, salt) == hashdigest
@classmethod
def scrypt(cls, password, salt):
return scrypt(password.encode(), salt=salt, n=cls.n, r=cls.r, p=cls.p, dklen=cls.dklen)

View File

@ -13,18 +13,29 @@ session_factory = sessionmaker(
)
@contextmanager
def Session(factory=session_factory):
session = factory()
try:
yield session
session.commit()
except:
session.rollback()
raise
# session is closed by flask
# finally:
# session.close()
class SessionWrapper:
def __init__(self):
self._session_factory = session_factory
self._session_handle = None
@contextmanager
def session(self):
try:
yield self._session
self._session.commit()
except:
self._session.rollback()
raise
@property
def _session(self):
if self._session_handle is None:
self._session_handle = self._session_factory()
return self._session_handle
def teardown(self):
if self._session_handle is not None:
self._session_handle.close()
Base = declarative_base()

View File

@ -0,0 +1,69 @@
from typing import Callable
class PipeReader:
def __init__(self, pipe_path):
self._pipe = open(pipe_path, 'rb')
self._message_handler = lambda msg: None
def __enter__(self):
return self
def __exit__(self, type_, value, traceback):
self.close()
def close(self):
self._pipe.close()
@property
def message_handler(self):
return self._message_handler
@message_handler.setter
def message_handler(self, value):
if not isinstance(value, Callable):
raise ValueError("message_handler must be callable!")
self._message_handler = value
def run(self):
msg = self.recv_message()
while msg:
self._message_handler(msg)
msg = self.recv_message()
def recv_message(self):
return self._pipe.readline()[:-1]
class PipeWriter:
def __init__(self, pipe_path):
self._pipe = open(pipe_path, 'wb')
def __enter__(self):
return self
def __exit__(self, type_, value, traceback):
self.close()
def close(self):
self._pipe.close()
def send_message(self, message):
self._pipe.write(message + b'\n')
self._pipe.flush()
class PipeIO:
def __init__(self, in_pipe_path, out_pipe_path):
self.reader = PipeReader(in_pipe_path)
self.writer = PipeWriter(out_pipe_path)
def __enter__(self):
return self
def __exit__(self, type_, value, traceback):
self.close()
def close(self):
self.reader.close()
self.writer.close()

View File

@ -1,9 +1,8 @@
from os import urandom, getenv
from functools import partial
from flask import Flask, render_template, request, session, url_for, g
from model import init_db, session_factory, Session
from model import init_db, SessionWrapper
from user_ops import UserOps
from errors import InvalidCredentialsError, UserExistsError
@ -16,25 +15,24 @@ app.jinja_env.globals.update( # pylint: disable=no-member
)
def get_db_session():
if not hasattr(g, 'db_session'):
g.db_session = session_factory()
return g.db_session
Session = partial(Session, get_db_session)
@app.before_request
def setup_db():
# pylint: disable=protected-access
g._db_session_wrapper = SessionWrapper()
g.db_session = g._db_session_wrapper.session
@app.teardown_appcontext
def close_db_session(err): # pylint: disable=unused-argument
if hasattr(g, 'db_session'):
g.db_session.close()
def close_db_session(_):
# pylint: disable=protected-access
g._db_session_wrapper.teardown()
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
try:
with Session() as db_session:
with g.db_session() as db_session:
UserOps(
request.form.get('username'),
request.form.get('password'),
@ -67,7 +65,7 @@ def register():
return render_template('register.html', alert='Passwords do not match! Please try again.')
try:
with Session() as db_session:
with g.db_session() as db_session:
UserOps(
request.form.get('username'),
request.form.get('password'),

View File

@ -1,7 +1,4 @@
from functools import partial
from tfwconnector import MessageSender
from pipe_io import PipeWriter
from crypto import PasswordHasher
from model import User
from errors import InvalidCredentialsError, UserExistsError
@ -12,11 +9,8 @@ class UserOps:
self.username = username
self.password = password
self.db_session = db_session
self.message_sender = MessageSender()
self.log = partial(
self.message_sender.send,
'Authenticator'
)
self.pipe = PipeWriter('/tmp/tfw_bot_send')
self.log = lambda message: self.pipe.send_message(message.encode()+b"\n")
def authenticate(self):
"""

View File

@ -1,4 +1,7 @@
[program:event_handler_main]
user=root
directory=%(ENV_TFW_EHMAIN_DIR)s
command=python3 event_handler_main.py
command=python3 -u event_handler_main.py
[supervisord]
strip_ansi=false

View File

@ -1,4 +1,4 @@
[program:pipe_io_main]
user=root
directory=%(ENV_TFW_EHMAIN_DIR)s
command=python3 pipe_io_main.py
command=python3 -u pipe_io_main.py