1
0
Files
BSc-Thesis/proj/exam-2020W-2A/check.py

328 lines
13 KiB
Python

import contextlib
import logging
import multiprocessing
import os
import random
import signal
import socket
import string
import struct
import subprocess
import sys
import tempfile
import traceback
from libosue import *
from libosue import CSnippet as C
from libosue import FunctionPrototype as FP
################################################################################
# INITIALIZE THE EXAM OBJECT
################################################################################
# These functions are not allowed.
not_allowed = [
"DEMO_setup_connection",
"DEMO_server_request_handler",
"DEMO_execute_command",
]
prototypes = {
"parse_arguments": FP("void")("int argc", "char **argv", "struct arguments *args"),
"DEMO_setup_connection": FP("int")("const char *port_str"),
"DEMO_server_request_handler": FP("void")("int sockfd", "const char *command", "int flags"),
"DEMO_execute_command": FP("FILE *")("const char *command", "const char *argument"),
}
PORT = 692
exam.initialize(
"./server",
server_port=7220,
not_allowed=not_allowed + exam.DEFAULT_RESTRICTED_FUNCTIONS,
call_limit=200,
prototypes=prototypes,
report_file="report.txt",
result_file="result.txt",
debug_file="libosue.dbg",
make_targets=["clean", "all"],
initialize_stack=["setup_connection", "server_request_handler", "execute_command"],
)
class FunctionAborted(EvalError):
"""Raised when a function should be aborted."""
pass
# Kill programs on port 692.
# This is slow, so we only do it once every deliver call and not for every session.
utils.free_port(PORT)
################################################################################
# TASK 1: SETUP CONNECTION
################################################################################
def bind_cb(s, args):
"""Call setsockopt(...) before binding."""
reuse = Value.from_type("char")
reuse.value = 1
s.c.setsockopt(args["sockfd"], socket.SOL_SOCKET, socket.SO_REUSEADDR, reuse.address, 1)
with exam.task("Task 1: setup connection", 10) as task:
with task.subtask("initialize server socket", 8) as st:
with exam.session([]) as s:
# Setup tracing for hints.
s.c.socket.tracing = True
s.c.bind.tracing = True
s.c.listen.tracing = True
# Make sure setsockopt(...) is called before bind().
s.c.bind.call_cb = bind_cb
# Call the student's setup_connection connection function
sockfd = s.c.setup_connection(str(PORT).encode())
# Check if the server socket works and creates valid connection sockets.
utils.check_server_socket(s, sockfd, PORT)
# Give the students some hints for their implementation
if st.points != 8:
if "socket" not in s.trace:
exam.log("[INFO] socket() was never called")
if "bind" not in s.trace:
exam.log("[INFO] bind() was never called")
if "listen" not in s.trace:
exam.log("[INFO] listen() was never called")
exam.log("The following subtasks are skipped because of errors:")
exam.log("* error handling (2 points)")
task.skip_subtasks()
with task.subtask("error handling", 2) as st:
with st.subtask("bind()", 1):
with exam.session([]) as s:
s.c.listen.tracing = True
s.c.bind.override_return = C("((int) -1)")
with exam.raises(ExitError, "program did not exit after bind() failed") as excinfo:
sockfd = s.c.setup_connection(str(PORT).encode())
assert excinfo.value.exitcode == 1, "wrong exit code"
assert "listen" not in s.trace, "listen() called after bind() failed"
with st.subtask("listen()", 1):
with exam.session([]) as s:
s.c.listen.override_return = C("((int) -1)")
with exam.raises(ExitError, "program did not exit after listen() failed") as excinfo:
sockfd = s.c.setup_connection(str(PORT).encode())
assert excinfo.value.exitcode == 1, "wrong exit code"
with exam.task("Task 2: handle requests", 20) as task:
with task.subtask("read argument and execute command", 7) as st:
# Check if the program correctly accepts a connection and reads the argument
# from the connection socket. Execution stops when task 3 is called.
def accept_return_cb(s, rv):
rv = rv.convert()
if rv > 0:
s["connfd"] = rv
def read_call_cb(s, args):
connfd = args.get("sockfd") or args.get("fd")
s["read"] = connfd.convert() == s["connfd"]
s["buffer"] = args.get("buf")
def read_return_cb(s, rv):
if s["read"]:
n = rv.convert()
s["result"] += s["buffer"].convert(shape=n, cast="char *")
def execute_command_cb(s, args):
s["ec_called"] = True
try:
s["command"] = args["command"].convert()
s["argument"] = args["argument"].convert()
except InvalidMemoryError:
pass
raise FunctionAborted("This is where execution ends.")
with exam.session([]) as s:
s.c.accept.return_cb = accept_return_cb
s.c.read.call_cb = read_call_cb
s.c.read.return_cb = read_return_cb
s.c.recv.call_cb = read_call_cb
s.c.recv.return_cb = read_return_cb
s.c.execute_command.call_cb = execute_command_cb
s.c.DEMO_execute_command.call_cb = execute_command_cb
s.c.DEMO_execute_command.allowed = True
# Setup the tracing variables.
s["connfd"] = None
s["result"] = b""
s["ec_called"] = False
s["command"] = None
s["argument"] = None
argument = "".join(random.choices(string.ascii_letters, k=24)).encode()
sockfd = s.c.DEMO_setup_connection(str(PORT).encode())
with utils.SingleShotClient(PORT, argument) as client:
with contextlib.suppress(FunctionAborted):
s.c.server_request_handler(sockfd, b"./osue-tool")
assert s["result"], "no argument read from client"
assert s["result"] == argument, "read wrong argument from client"
assert s["ec_called"], "execute_command() not called"
assert s["command"] == b"./osue-tool", "invalid command passt to execute_command()"
assert (
s["argument"] == argument
), "Invalid argument passed to execute_command()\nDid you terminate it?"
with task.subtask("response", 8) as st:
# Execute the full function and check if the client receives the correct result.
def execute_command_cb(s, args):
with s.c.fork.save():
s.c.fork.allowed = True
return s.c.DEMO_execute_command(args["command"], args["argument"])
with st.subtask("single line output", 4) as pt:
with exam.session([]) as s:
s.c.execute_command.call_cb = execute_command_cb
s.c.DEMO_execute_command.call_cb = execute_command_cb
s.c.DEMO_execute_command.allowed = True
sockfd = s.c.DEMO_setup_connection(str(PORT).encode())
argument = "".join(random.choices(string.ascii_letters, k=24)).encode()
expected = argument + b"\n"
with utils.SingleShotClient(PORT, argument) as client:
s.c.server_request_handler(sockfd, b"./osue-tool")
assert client.response, "didn't receive a response for single-line output"
assert client.response == expected, "received wrong response for single-line output"
with st.subtask("multi line output", 4) as pt:
with exam.session([]) as s:
s.c.execute_command.call_cb = execute_command_cb
s.c.DEMO_execute_command.call_cb = execute_command_cb
s.c.DEMO_execute_command.allowed = True
sockfd = s.c.DEMO_setup_connection(str(PORT).encode())
argument = "".join(random.choices(string.ascii_letters, k=128)).encode()
expected = b"\n".join(argument[i : i + 32] for i in range(0, len(argument), 32)) + b"\n"
with utils.SingleShotClient(PORT, argument) as client:
s.c.server_request_handler(sockfd, b"./osue-tool")
assert client.response, "didn't receive a response for multi-line output"
assert client.response == expected, "received wrong response for multi-line output"
if task.points != 15:
exam.log("The following subtasks are skipped because of errors:")
exam.log("* repeated write/send calls (5 points)")
task.skip_subtasks()
with task.subtask("repeated write/send calls", 5) as st:
def accept_return_cb(s, rv):
s["connfd"] = rv.convert()
def write_cb(s, args):
fd = (args.get("fd") or args.get("sockfd")).convert()
if fd == s["connfd"]:
n = args.get("count") or args.get("len")
buf = args.get("buf")
s["messages"].append(buf.convert(shape=n.convert(), cast="char *"))
return 1
with exam.session([]) as s:
s.c.execute_command.call_cb = execute_command_cb
s.c.DEMO_execute_command.call_cb = execute_command_cb
s.c.DEMO_execute_command.allowed = True
s.c.accept.return_cb = accept_return_cb
s.c.write.call_cb = write_cb
s.c.send.call_cb = write_cb
s["connfd"] = None
s["messages"] = []
argument = "".join(random.choices(string.ascii_letters, k=24)).encode()
expected = argument + b"\n"
sockfd = s.c.DEMO_setup_connection(str(PORT).encode())
with utils.SingleShotClient(PORT, argument) as client:
s.c.server_request_handler(sockfd, b"./osue-tool")
assert len(s["messages"]) > 1, "write/send not called multiple times"
message = b"".join(m[:1] for m in s["messages"])
assert message == expected, "received wrong response"
with exam.task("Task 3: execute command", 20) as task:
with task.subtask("fork/exec pattern", 5) as st:
pattern = utils.ForkNode(parent=None, child=utils.ExecNode("./osue-tool"))
with utils.ForkExecTraces(pattern) as tracegen:
for fork_exec_run in tracegen:
with exam.session() as s:
with fork_exec_run(s):
s.c.execute_command(b"./osue-tool", b"just a random string")
assert tracegen.correct, tracegen.default_message
if task.points != 5:
exam.log("The following subtasks are skipped because of errors:")
exam.log("* wait for child process to finish (5 points)")
exam.log("* result (10 points)")
task.skip_subtasks()
with task.subtask("wait for child process to finish", 5) as st:
# Correctly wait for the child process to finish.
# Follow the parent process and trace the return value of fork() and wait().
def fork_return_cb(s, rv):
s["fork_result"] = rv.convert()
def wait_return_cb(s, rv):
s["wait_result"] = rv.convert()
with exam.session([]) as s:
s.c.fork.allowed = True
s.c.fork.return_cb = fork_return_cb
s.c.wait.return_cb = wait_return_cb
s.c.waitpid.return_cb = wait_return_cb
s.c.execute_command(b"./osue-tool", b"just a random string")
assert "wait_result" in s, "wait never called"
assert s.get("wait_result") == s.get("fork_result"), "program did not wait for child process"
if task.points != 10:
exam.log("The following subtasks are skipped because of errors:")
exam.log("* result (10 points)")
task.skip_subtasks()
with task.subtask("result", 10) as st:
# Handle a single request and check the result.
with exam.session([]) as s:
s.c.fork.allowed = True
argument = "".join(random.choices(string.ascii_letters, k=24)).encode()
expected = argument + b"\n"
fp = s.c.execute_command(b"./osue-tool", argument)
assert fp.intaddr(), "returned file pointer not valid"
bufsize = 8192
try:
buf = s.c.malloc(bufsize, cast="char *")
n = s.c.fread(buf, 1, bufsize, fp)
except FunctionKilledError:
st.fail("reading from returned file pointer never terminated")
except SignalError:
st.fail("reading from returned file pointer failed")
result = buf.convert(shape=n.convert())
assert result == expected, "invalid response"