memed/memed.py

241 lines
6.5 KiB
Python
Raw Normal View History

2017-12-01 21:02:23 +00:00
"""
MemeD - the MEMEwork Daemon
- This program manages the memework vps'
command logging
"""
import asyncio
import struct
import json
import logging
2017-12-03 04:55:40 +00:00
import sys
2017-12-01 21:02:23 +00:00
import asyncpg
import config
2017-12-02 01:04:35 +00:00
from bot import schedule_bot
2017-12-01 21:02:23 +00:00
2017-12-08 00:54:46 +00:00
logging.basicConfig(level=logging.DEBUG)
2017-12-01 21:02:23 +00:00
log = logging.getLogger(__name__)
# globals are bad, but who cares
2017-12-01 21:02:23 +00:00
db = None
2017-12-02 14:24:55 +00:00
bot = None
# for writer:
# - op 0 : hello
# - op 1 : request response
# for reader:
# - op 1 : log
# - op 2 : rsudo
2017-12-08 00:54:46 +00:00
# - op 3 : rsudo with steroids
2017-12-01 21:02:23 +00:00
2017-12-04 01:30:23 +00:00
async def wrap(coro):
try:
await coro()
2018-01-20 20:46:55 +00:00
except ConnectionError as err:
log.warning(f'connection err: {err!r}')
except Exception:
2017-12-04 01:30:23 +00:00
log.exception('error inside wrapped')
2017-12-01 21:02:23 +00:00
def parse_logstr(string):
# '2015-02-11T19:05:10+00:00 labrat-1 snoopy[896]: [uid:0 sid:11679
# tty:/dev/pts/2 cwd:/root filename:/usr/bin/cat]: cat /etc/fstab.BAK'
# I really need to parse the uid, cwd and the command out of that.
2017-12-03 20:23:01 +00:00
# '[uid:123 sid:440 tty:/dev/pts/4 cwd:/root filename:/bin/chmod]: AAAA BBBBBB CCCCCCCC'
# THIS IS WHAT WE PARSE NOW.
2017-12-01 21:02:23 +00:00
splitted = string.split(':')
command = splitted[-1].strip()
k = string.find('[')
2017-12-03 20:23:01 +00:00
important = string[k:]
2017-12-01 21:02:23 +00:00
lst = important.replace('[', '').replace(']', '').split()
# filder uid and cwd
2018-01-20 20:46:55 +00:00
spl = [s.split(':') for s in lst if 'uid' in s or 'cwd' in s]
2017-12-01 21:02:23 +00:00
2018-01-20 20:46:55 +00:00
uid = [e[1] for e in spl if e[0] == 'uid'][0]
cwd = [e[1] for e in spl if e[0] == 'cwd'][0]
2017-12-03 20:23:01 +00:00
return int(uid), cwd, command
2017-12-01 21:02:23 +00:00
2017-12-02 14:24:55 +00:00
2017-12-08 00:54:46 +00:00
class MemeClient:
"""MemeD client handler."""
def __init__(self, reader, writer):
self.reader = reader
self.writer = writer
2017-12-08 01:35:10 +00:00
self.loop = asyncio.get_event_loop()
2017-12-08 00:54:46 +00:00
async def read_msg(self) -> str:
"""Read one message from the socket."""
2017-12-08 00:54:46 +00:00
header = await self.reader.read(8)
log.debug('[recv] %r', header)
length, opcode = struct.unpack('Ii', header)
2017-12-08 00:54:46 +00:00
data = await self.reader.read(length)
data = data.decode()
log.debug('[recv] %d %d %s', length, opcode, data)
return opcode, data
2017-12-08 00:54:46 +00:00
async def read_payload(self) -> dict:
2018-01-20 20:46:55 +00:00
"""Read a payload from the socket."""
opcode, message = await self.read_msg()
# NOTE: this is kinda unused
if opcode > 10:
return opcode, json.loads(message)
2017-12-08 00:54:46 +00:00
return opcode, message
async def send_msg(self, op: int, data: str):
2017-12-08 00:54:46 +00:00
"""Send a message.
2017-12-08 00:54:46 +00:00
This does not wait for the receiving end
to properly flush their buffers.
2017-12-08 00:54:46 +00:00
Arguments
---------
op: int
OP code to be sent.
data: str
Message to be sent with the op code.
"""
# create header, pack message, yadda yadda
2017-12-08 00:54:46 +00:00
header = struct.pack('Ii', len(data), op).decode()
msg = f'{header}{data}'.encode()
log.debug('[send] %d, %s -> %r', op, data, msg)
self.writer.write(msg)
# clients can close this early
2017-12-08 00:54:46 +00:00
# and make writer.drain kill itself
# so we wrap on a task which is isolated
asyncio.get_event_loop().create_task(wrap(self.writer.drain))
async def process(self, op: int, message: str) -> 'None':
"""Process a message given through the socket"""
handler = getattr(self, f'handle_{op}', None)
if not handler:
# Ignore unknown OP codes.
return
await handler(message)
async def handle_1(self, message: str):
2018-02-25 01:31:57 +00:00
global db
uid, cwd, command = parse_logstr(message)
log.info('[process] Logging command '
f'uid={uid} cwd={cwd} cmd={command}')
await db.execute("""
INSERT INTO logs (uid, cwd, cmd) VALUES ($1, $2, $3)
""", uid, cwd, command)
async def handle_2(self, message: str):
"""Handle an OP 2 packet.
This is the RSudo handling, but without waiting for
the approval of a mod (the 'nowait' behavior).
"""
if not bot:
return await self.send_msg(1, 'no bot up')
rsudo = bot.get_cog('Rsudo')
if not rsudo:
return await self.send_msg(1, 'no rsudo cog')
log.info('[rsudo:nowait] %r', message)
# this doesnt wait for the thing
self.loop.create_task(rsudo.request(message))
return await self.send_msg(1, "true")
async def handle_3(self, message: str):
"""Handle an OP 3 packet.
This follows the same logic as OP 2, however
the client gets a response back, a string ('true' or 'false'),
depending on the request.
NO COMMANDS WILL BE EXECUTED SERVER-SIDE FROM THIS OP.
"""
if not bot:
2018-02-24 15:00:56 +00:00
return await self.send_msg(1, 'no bot started')
rsudo = bot.get_cog('Rsudo')
if not rsudo:
return await self.send_msg(1, 'no rsudo cog')
log.info('[rsudo:wait] %r', message)
success = await rsudo.request(message, True)
# the original idea was sending an int back
# but we had no idea how to do that since
# we were already working with strings all the time
# so yeah, this is *another* string
ok_str = 'true' if success else 'false'
return await self.send_msg(1, ok_str)
2017-12-08 00:54:46 +00:00
2017-12-08 01:35:10 +00:00
async def client_loop(self):
"""Enter a loop waiting for messages from the client."""
2017-12-08 00:54:46 +00:00
try:
while True:
op, message = await self.read_msg()
await self.process(op, message)
except ConnectionError as e:
log.warning('conn err: %r', e)
except Exception:
log.exception('error at loop')
self.writer.close()
2017-12-02 14:24:55 +00:00
2017-12-01 21:02:23 +00:00
2017-12-02 01:04:35 +00:00
async def handle_client(reader, writer):
"""Handle clients coming in the socket, spawn a loop for them."""
2017-12-08 00:54:46 +00:00
client = MemeClient(reader, writer)
2017-12-01 21:02:23 +00:00
2017-12-08 00:54:46 +00:00
await client.send_msg(0, 'hello')
2017-12-08 01:35:10 +00:00
await client.client_loop()
2017-12-01 21:02:23 +00:00
def main():
2018-02-24 15:00:56 +00:00
# YES GLOBALS ARE BAD I KNOW
global bot
global db
2017-12-01 21:02:23 +00:00
loop = asyncio.get_event_loop()
2017-12-03 04:55:40 +00:00
coro = asyncio.start_unix_server(handle_client, sys.argv[1],
2017-12-01 21:02:23 +00:00
loop=loop)
2018-02-25 01:10:52 +00:00
pool = loop.run_until_complete(asyncpg.create_pool(**config.db))
2018-02-24 15:00:56 +00:00
db = pool
2018-02-25 01:10:52 +00:00
2017-12-01 21:02:23 +00:00
server = loop.run_until_complete(coro)
2017-12-02 14:24:55 +00:00
if config.bot_token:
bot = schedule_bot(loop, config, pool)
2017-12-02 15:53:47 +00:00
if bot:
loop.create_task(bot.start(config.bot_token))
2017-12-02 01:04:35 +00:00
log.info(f'memed serving at {server.sockets[0].getsockname()}')
2017-12-01 21:02:23 +00:00
try:
loop.run_forever()
finally:
log.info('Closing server')
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()
2017-12-01 21:02:23 +00:00
if __name__ == '__main__':
main()