commit 8aac44bb98af5442e29c8cb9a5a4acbe40d96bb2
parent 53156862fa68b130c9a57f2824275f99017929ac
Author: Louis Burda <quent.burda@gmail.com>
Date: Wed, 28 Apr 2021 10:51:50 +0200
added sample service templates, basic service outline and moved service info to documentation dir
Diffstat:
17 files changed, 727 insertions(+), 69 deletions(-)
diff --git a/README.md b/README.md
@@ -1,72 +1,5 @@
-enowars-5 printdoc
-==================
+Enowars5 PrintDoc
+=================
An stl file info service.
-
-setup
------
-
-The service is hosted with ynetd or similar, one process per client.
-
-You submit an stl file and the service gives you details about the file:
-
-- how many triangles
-- file type (bin/ascii)
-- name
-- attributes (binary header parsing)
-
-The file upload size has to be below a certain limit (4kB?).
-
-The files are simply stored in a directory and cleaned up
-via a crontab which checks their *last modified* date.
-
-The model name is used to create hash / id which also
-acts as directory name for the actual stl and parsed info.
-
-Error msg if too many verticies for one loop.. see vulnerability.
-
-Error msg if invalid format.
-
-
-countermeasures
----------------
-
-Countermeasures against malicious players, who via an
-unintended vulnerability gain remote code execution:
-
-
-checker
--------
-
-The flag is saved as a 3d model of the flag text. One needs
-to orient it, take a screenshot and decode the text from the
-image for automated exploitation.
-
-
-vulnerability
--------------
-
-If there are > 3 verticies in a `loop` in the stl, a warning
-message is returned by preparing and `printf`ing a buffer,
-however, WITHOUT a terminating null byte. As such, when
-processing the string, we read into the stack-adjacent integer
-that holds the file's attribute byte count. This value
-is zero by default so the buffer overflow will go unnoticed.
-
-We can set this value to 0x6e25 (= 28197), which corresponds
-to the string '%n' on a little endian system.
-
-When the warning prints, it will write the size of the
-format string (which can be controlled via the model name)
-to the address of the next value on the stack: the hash str.
-By varying this value to write 256 aka 0x100 we terminate
-the string with a null byte, making it an empty.
-
-Next, the program will return the info of all scans that match
-the hash prefix (files are saved in a directory <hash>-<timestamp>).
-Since the hash is not empty the information for each scan will be
-returned, including the id, which can be used to request the flag file.
-
-
-
diff --git a/checker/Dockerfile b/checker/Dockerfile
@@ -0,0 +1,17 @@
+FROM python:3.9
+
+# Create user
+RUN useradd -ms /bin/bash -u 1000 checker
+
+USER checker
+WORKDIR /checker
+
+# Install all required dependencies for the checker.
+COPY ./src/requirements.txt /checker/requirements.txt
+RUN pip3 install -r requirements.txt
+
+# Copy all files into the container.
+COPY ./src/ /checker/
+
+
+ENTRYPOINT [ "/home/checker/.local/bin/gunicorn", "-c", "gunicorn.conf.py", "checker:app" ]
diff --git a/checker/data/.gitkeep b/checker/data/.gitkeep
diff --git a/checker/docker-compose.yml b/checker/docker-compose.yml
@@ -0,0 +1,23 @@
+version: '3'
+services:
+ # Give your container proper names!
+ n0t3b00k-checker:
+ build: .
+ # The checker runs a HTTP interfaces, so we need to map port 3031 to the outside (port 8000).
+ ports:
+ - 8000:3031
+ environment:
+ - MONGO_ENABLED=1
+ - MONGO_HOST=n0t3b00k-mongo
+ - MONGO_PORT=27017
+ - MONGO_USER=n0t3b00k_checker
+ - MONGO_PASSWORD=n0t3b00k_checker
+
+ # The python checkerlib requires a mongo db!
+ n0t3b00k-mongo:
+ image: mongo
+ volumes:
+ - ./data:/data/db
+ environment:
+ MONGO_INITDB_ROOT_USERNAME: n0t3b00k_checker
+ MONGO_INITDB_ROOT_PASSWORD: n0t3b00k_checker
+\ No newline at end of file
diff --git a/checker/src/checker.py b/checker/src/checker.py
@@ -0,0 +1,387 @@
+#!/usr/bin/env python3
+from enochecker import BaseChecker, BrokenServiceException, EnoException, run
+from enochecker.utils import SimpleSocket, assert_equals, assert_in
+import random
+import string
+
+#### Checker Tenets
+# A checker SHOULD not be easily identified by the examination of network traffic => This one is not satisfied, because our usernames and notes are simple too random and easily identifiable.
+# A checker SHOULD use unusual, incorrect or pseudomalicious input to detect network filters => This tenet is not satisfied, because we do not send common attack strings (i.e. for SQL injection, RCE, etc.) in our notes or usernames.
+####
+
+
+class N0t3b00kChecker(BaseChecker):
+ """
+ Change the methods given here, then simply create the class and .run() it.
+ Magic.
+ A few convenient methods and helpers are provided in the BaseChecker.
+ ensure_bytes ans ensure_unicode to make sure strings are always equal.
+ As well as methods:
+ self.connect() connects to the remote server.
+ self.get and self.post request from http.
+ self.chain_db is a dict that stores its contents to a mongodb or filesystem.
+ conn.readline_expect(): fails if it's not read correctly
+ To read the whole docu and find more goodies, run python -m pydoc enochecker
+ (Or read the source, Luke)
+ """
+
+ ##### EDIT YOUR CHECKER PARAMETERS
+ flag_variants = 1
+ noise_variants = 1
+ havoc_variants = 3
+ service_name = "n0t3b00k"
+ port = 2323 # The port will automatically be picked up as default by self.connect and self.http.
+ ##### END CHECKER PARAMETERS
+
+ def register_user(self, conn: SimpleSocket, username: str, password: str):
+ self.debug(
+ f"Sending command to register user: {username} with password: {password}"
+ )
+ conn.write(f"reg {username} {password}\n")
+ conn.readline_expect(
+ b"User successfully registered",
+ read_until=b">",
+ exception_message="Failed to register user",
+ )
+
+ def login_user(self, conn: SimpleSocket, username: str, password: str):
+ self.debug(f"Sending command to login.")
+ conn.write(f"log {username} {password}\n")
+ conn.readline_expect(
+ b"Successfully logged in!",
+ read_until=b">",
+ exception_message="Failed to log in",
+ )
+
+ def putflag(self): # type: () -> None
+ """
+ This method stores a flag in the service.
+ In case multiple flags are provided, self.variant_id gives the appropriate index.
+ The flag itself can be retrieved from self.flag.
+ On error, raise an Eno Exception.
+ :raises EnoException on error
+ :return this function can return a result if it wants
+ if nothing is returned, the service status is considered okay.
+ the preferred way to report errors in the service is by raising an appropriate enoexception
+ """
+ if self.variant_id == 0:
+ # First we need to register a user. So let's create some random strings. (Your real checker should use some funny usernames or so)
+ username: str = "".join(
+ random.choices(string.ascii_uppercase + string.digits, k=12)
+ )
+ password: str = "".join(
+ random.choices(string.ascii_uppercase + string.digits, k=12)
+ )
+
+ # Log a message before any critical action that could raise an error.
+ self.debug(f"Connecting to service")
+ # Create a TCP connection to the service.
+ conn = self.connect()
+ welcome = conn.read_until(">")
+
+ # Register a new user
+ self.register_user(conn, username, password)
+
+ # Now we need to login
+ self.login_user(conn, username, password)
+
+ # Finally, we can post our note!
+ self.debug(f"Sending command to set the flag")
+ conn.write(f"set {self.flag}\n")
+ conn.read_until(b"Note saved! ID is ")
+
+ try:
+ # Try to retrieve the resulting noteId. Using rstrip() is hacky, you should probably want to use regular expressions or something more robust.
+ noteId = conn.read_until(b"!\n>").rstrip(b"!\n>").decode()
+ except Exception as ex:
+ self.debug(f"Failed to retrieve note: {ex}")
+ raise BrokenServiceException("Could not retrieve NoteId")
+
+ assert_equals(len(noteId) > 0, True, message="Empty noteId received")
+
+ self.debug(f"Got noteId {noteId}")
+
+ # Exit!
+ self.debug(f"Sending exit command")
+ conn.write(f"exit\n")
+ conn.close()
+
+ # Save the generated values for the associated getflag() call.
+ # This is not a real dictionary! You cannot update it (i.e., self.chain_db["foo"] = bar) and some types are converted (i.e., bool -> str.). See: https://github.com/enowars/enochecker/issues/27
+ self.chain_db = {
+ "username": username,
+ "password": password,
+ "noteId": noteId,
+ }
+
+ else:
+ raise EnoException("Wrong variant_id provided")
+
+ def getflag(self): # type: () -> None
+ """
+ This method retrieves a flag from the service.
+ Use self.flag to get the flag that needs to be recovered and self.round to get the round the flag was placed in.
+ On error, raise an EnoException.
+ :raises EnoException on error
+ :return this function can return a result if it wants
+ if nothing is returned, the service status is considered okay.
+ the preferred way to report errors in the service is by raising an appropriate enoexception
+ """
+ if self.variant_id == 0:
+ # First we check if the previous putflag succeeded!
+ try:
+ username: str = self.chain_db["username"]
+ password: str = self.chain_db["password"]
+ noteId: str = self.chain_db["noteId"]
+ except IndexError as ex:
+ self.debug(f"error getting notes from db: {ex}")
+ raise BrokenServiceException("Previous putflag failed.")
+
+ self.debug(f"Connecting to the service")
+ conn = self.connect()
+ welcome = conn.read_until(">")
+
+ # Let's login to the service
+ self.login_user(conn, username, password)
+
+ # Let´s obtain our note.
+ self.debug(f"Sending command to retrieve note: {noteId}")
+ conn.write(f"get {noteId}\n")
+ note = conn.read_until(">")
+ assert_in(
+ self.flag.encode(), note, "Resulting flag was found to be incorrect"
+ )
+
+ # Exit!
+ self.debug(f"Sending exit command")
+ conn.write(f"exit\n")
+ conn.close()
+ else:
+ raise EnoException("Wrong variant_id provided")
+
+
+ def putnoise(self): # type: () -> None
+ """
+ This method stores noise in the service. The noise should later be recoverable.
+ The difference between noise and flag is, that noise does not have to remain secret for other teams.
+ This method can be called many times per round. Check how often using self.variant_id.
+ On error, raise an EnoException.
+ :raises EnoException on error
+ :return this function can return a result if it wants
+ if nothing is returned, the service status is considered okay.
+ the preferred way to report errors in the service is by raising an appropriate enoexception
+ """
+ if self.variant_id == 0:
+ self.debug(f"Connecting to the service")
+ conn = self.connect()
+ welcome = conn.read_until(">")
+
+ # First we need to register a user. So let's create some random strings. (Your real checker should use some better usernames or so [i.e., use the "faker¨ lib])
+ username = "".join(
+ random.choices(string.ascii_uppercase + string.digits, k=12)
+ )
+ password = "".join(
+ random.choices(string.ascii_uppercase + string.digits, k=12)
+ )
+ randomNote = "".join(
+ random.choices(string.ascii_uppercase + string.digits, k=36)
+ )
+
+ # Register another user
+ self.register_user(conn, username, password)
+
+ # Now we need to login
+ self.login_user(conn, username, password)
+
+ # Finally, we can post our note!
+ self.debug(f"Sending command to save a note")
+ conn.write(f"set {randomNote}\n")
+ conn.read_until(b"Note saved! ID is ")
+
+ try:
+ noteId = conn.read_until(b"!\n>").rstrip(b"!\n>").decode()
+ except Exception as ex:
+ self.debug(f"Failed to retrieve note: {ex}")
+ raise BrokenServiceException("Could not retrieve NoteId")
+
+ assert_equals(len(noteId) > 0, True, message="Empty noteId received")
+
+ self.debug(f"{noteId}")
+
+ # Exit!
+ self.debug(f"Sending exit command")
+ conn.write(f"exit\n")
+ conn.close()
+
+ self.chain_db = {
+ "username": username,
+ "password": password,
+ "noteId": noteId,
+ "note": randomNote,
+ }
+ else:
+ raise EnoException("Wrong variant_id provided")
+
+ def getnoise(self): # type: () -> None
+ """
+ This method retrieves noise in the service.
+ The noise to be retrieved is inside self.flag
+ The difference between noise and flag is, that noise does not have to remain secret for other teams.
+ This method can be called many times per round. Check how often using variant_id.
+ On error, raise an EnoException.
+ :raises EnoException on error
+ :return this function can return a result if it wants
+ if nothing is returned, the service status is considered okay.
+ the preferred way to report errors in the service is by raising an appropriate enoexception
+ """
+ if self.variant_id == 0:
+ try:
+ username: str = self.chain_db["username"]
+ password: str = self.chain_db["password"]
+ noteId: str = self.chain_db["noteId"]
+ randomNote: str = self.chain_db["note"]
+ except Exception as ex:
+ self.debug("Failed to read db {ex}")
+ raise BrokenServiceException("Previous putnoise failed.")
+
+ self.debug(f"Connecting to service")
+ conn = self.connect()
+ welcome = conn.read_until(">")
+
+ # Let's login to the service
+ self.login_user(conn, username, password)
+
+ # Let´s obtain our note.
+ self.debug(f"Sending command to retrieve note: {noteId}")
+ conn.write(f"get {noteId}\n")
+ conn.readline_expect(
+ randomNote.encode(),
+ read_until=b">",
+ exception_message="Resulting flag was found to be incorrect"
+ )
+
+ # Exit!
+ self.debug(f"Sending exit command")
+ conn.write(f"exit\n")
+ conn.close()
+ else:
+ raise EnoException("Wrong variant_id provided")
+
+ def havoc(self): # type: () -> None
+ """
+ This method unleashes havoc on the app -> Do whatever you must to prove the service still works. Or not.
+ On error, raise an EnoException.
+ :raises EnoException on Error
+ :return This function can return a result if it wants
+ If nothing is returned, the service status is considered okay.
+ The preferred way to report Errors in the service is by raising an appropriate EnoException
+ """
+ self.debug(f"Connecting to service")
+ conn = self.connect()
+ welcome = conn.read_until(">")
+
+ if self.variant_id == 0:
+ # In variant 1, we'll check if the help text is available
+ self.debug(f"Sending help command")
+ conn.write(f"help\n")
+ is_ok = conn.read_until(">")
+
+ for line in [
+ "This is a notebook service. Commands:",
+ "reg USER PW - Register new account",
+ "log USER PW - Login to account",
+ "set TEXT..... - Set a note",
+ "user - List all users",
+ "list - List all notes",
+ "exit - Exit!",
+ "dump - Dump the database",
+ "get ID",
+ ]:
+ assert_in(line.encode(), is_ok, "Received incomplete response.")
+
+ elif self.variant_id == 1:
+ # In variant 2, we'll check if the `user` command still works.
+ username = "".join(
+ random.choices(string.ascii_uppercase + string.digits, k=12)
+ )
+ password = "".join(
+ random.choices(string.ascii_uppercase + string.digits, k=12)
+ )
+
+ # Register and login a dummy user
+ self.register_user(conn, username, password)
+ self.login_user(conn, username, password)
+
+ self.debug(f"Sending user command")
+ conn.write(f"user\n")
+ ret = conn.readline_expect(
+ "User 0: ",
+ read_until=b">",
+ exception_message="User command does not return any users",
+ )
+
+ if username:
+ assert_in(username.encode(), ret, "Flag username not in user output")
+
+ elif self.variant_id == 2:
+ # In variant 2, we'll check if the `list` command still works.
+ username = "".join(
+ random.choices(string.ascii_uppercase + string.digits, k=12)
+ )
+ password = "".join(
+ random.choices(string.ascii_uppercase + string.digits, k=12)
+ )
+ randomNote = "".join(
+ random.choices(string.ascii_uppercase + string.digits, k=36)
+ )
+
+ # Register and login a dummy user
+ self.register_user(conn, username, password)
+ self.login_user(conn, username, password)
+
+ self.debug(f"Sending command to save a note")
+ conn.write(f"set {randomNote}\n")
+ conn.read_until(b"Note saved! ID is ")
+
+ try:
+ noteId = conn.read_until(b"!\n>").rstrip(b"!\n>").decode()
+ except Exception as ex:
+ self.debug(f"Failed to retrieve note: {ex}")
+ raise BrokenServiceException("Could not retrieve NoteId")
+
+ assert_equals(len(noteId) > 0, True, message="Empty noteId received")
+
+ self.debug(f"{noteId}")
+
+ self.debug(f"Sending list command")
+ conn.write(f"list\n")
+ conn.readline_expect(
+ noteId.encode(),
+ read_until=b'>',
+ exception_message="List command does not work as intended"
+ )
+
+ else:
+ raise EnoException("Wrong variant_id provided")
+
+ # Exit!
+ self.debug(f"Sending exit command")
+ conn.write(f"exit\n")
+ conn.close()
+
+ def exploit(self):
+ """
+ This method was added for CI purposes for exploits to be tested.
+ Will (hopefully) not be called during actual CTF.
+ :raises EnoException on Error
+ :return This function can return a result if it wants
+ If nothing is returned, the service status is considered okay.
+ The preferred way to report Errors in the service is by raising an appropriate EnoException
+ """
+ # TODO: We still haven't decided if we want to use this function or not. TBA
+ pass
+
+
+app = N0t3b00kChecker.service # This can be used for uswgi.
+if __name__ == "__main__":
+ run(N0t3b00kChecker)
diff --git a/checker/src/gunicorn.conf.py b/checker/src/gunicorn.conf.py
@@ -0,0 +1,7 @@
+# This is a configuration file required by the checker.
+import multiprocessing
+worker_class = "eventlet"
+workers = multiprocessing.cpu_count()*2+1
+bind = "0.0.0.0:3031"
+timeout = 90
+keepalive = 3600
+\ No newline at end of file
diff --git a/checker/src/requirements.txt b/checker/src/requirements.txt
@@ -0,0 +1,23 @@
+certifi==2020.12.5
+chardet==4.0.0
+click==7.1.2
+dnspython==1.16.0
+enochecker==0.3.2
+enochecker-cli==0.6.0
+enochecker-core==0.8.1
+eventlet==0.30.2
+Flask==1.1.2
+greenlet==1.0.0
+gunicorn==20.1.0
+idna==2.10
+itsdangerous==1.1.0
+Jinja2==2.11.3
+jsons==1.4.2
+MarkupSafe==1.1.1
+pycryptodome==3.10.1
+pymongo==3.11.3
+requests==2.25.1
+six==1.15.0
+typish==1.9.2
+urllib3==1.26.4
+Werkzeug==1.0.1
+\ No newline at end of file
diff --git a/documentation/README.md b/documentation/README.md
@@ -0,0 +1,68 @@
+PrintDoc
+========
+
+Setup
+-----
+
+The service is hosted with ynetd or similar, one process per client.
+
+You submit an stl file and the service gives you details about the file:
+
+- how many triangles
+- file type (bin/ascii)
+- name
+- attributes (binary header parsing)
+
+The file upload size has to be below a certain limit (4kB?).
+
+The files are simply stored in a directory and cleaned up
+via a crontab which checks their *last modified* date.
+
+The model name is used to create hash / id which also
+acts as directory name for the actual stl and parsed info.
+
+Error msg if too many verticies for one loop.. see vulnerability.
+
+Error msg if invalid format.
+
+
+Countermeasures
+---------------
+
+Countermeasures against malicious players, who via an
+unintended vulnerability gain remote code execution:
+
+
+Checker
+-------
+
+The flag is saved as a 3d model of the flag text. One needs
+to orient it, take a screenshot and decode the text from the
+image for automated exploitation.
+
+
+Vulnerability
+-------------
+
+If there are > 3 verticies in a `loop` in the stl, a warning
+message is returned by preparing and `printf`ing a buffer,
+however, WITHOUT a terminating null byte. As such, when
+processing the string, we read into the stack-adjacent integer
+that holds the file's attribute byte count. This value
+is zero by default so the buffer overflow will go unnoticed.
+
+We can set this value to 0x6e25 (= 28197), which corresponds
+to the string '%n' on a little endian system.
+
+When the warning prints, it will write the size of the
+format string (which can be controlled via the model name)
+to the address of the next value on the stack: the hash str.
+By varying this value to write 256 aka 0x100 we terminate
+the string with a null byte, making it an empty.
+
+Next, the program will return the info of all scans that match
+the hash prefix (files are saved in a directory <hash>-<timestamp>).
+Since the hash is not empty the information for each scan will be
+returned, including the id, which can be used to request the flag file.
+
+
diff --git a/service/Dockerfile b/service/Dockerfile
@@ -0,0 +1,18 @@
+FROM ubuntu:18.04
+
+RUN apt update && apt install -y --no-install-recommends socat
+
+RUN addgroup --system service
+RUN adduser --system --ingroup service --uid 1000 service
+
+RUN mkdir /data
+
+COPY entrypoint.sh /
+RUN chmod +x /entrypoint.sh
+
+COPY src/ /service/
+
+WORKDIR /service/
+EXPOSE 9000
+
+ENTRYPOINT ["/entrypoint.sh"]
diff --git a/service/docker-compose.yml b/service/docker-compose.yml
@@ -0,0 +1,8 @@
+version: '1'
+services:
+ printdoc:
+ build: .
+ volumes:
+ - ./data/:/data:rw
+ ports:
+ - "2323:8000"
diff --git a/service/entrypoint.sh b/service/entrypoint.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -e
+set -x
+
+# Chown the mounted data volume
+chown -R service:service "/data/"
+
+# Launch our service as user 'service'
+exec su -s /bin/sh -c 'PYTHONUNBUFFERED=1 python3 n0t3b00k.py' service
+\ No newline at end of file
diff --git a/service/src/.gitignore b/service/src/.gitignore
@@ -0,0 +1,2 @@
+printdoc
+*.o
diff --git a/service/src/Makefile b/service/src/Makefile
@@ -0,0 +1,6 @@
+
+%.o: %.c %.h
+ $(CC) -c -o $@ $< -I .
+
+printdoc: printdoc.c stlfile.o
+ $(CC) -o $@ $< $(CFLAGS) $(LDLIBS)
diff --git a/service/src/cat b/service/src/cat
@@ -0,0 +1,5 @@
+
+ /\_/\ [ENO] _
+ = u u =_______| \\ < GET THE FLAG! ;)
+ _ w __( \__))
+ c_____>__(_____)__,
diff --git a/service/src/printdoc.c b/service/src/printdoc.c
@@ -0,0 +1,134 @@
+#include <stdio.h>
+#include <string.h>
+#include <stdarg.h>
+#include <unistd.h>
+
+#include "stlfile.h"
+
+#define ARRSIZE(x) (sizeof(x)/sizeof((x)[0]))
+
+struct command {
+ const char *name;
+ void (*func)();
+};
+
+void request_info();
+void parse_file();
+void cat_flag();
+void list_commands();
+
+struct command commands[] = {
+ { "request", request_info },
+ { "info", parse_file },
+ { "cat", cat_flag },
+ { "ls", list_commands },
+};
+
+const char*
+ask(const char *fmtstr, ...)
+{
+ static char linebuf[256];
+ va_list ap;
+
+ va_start(ap, fmtstr);
+ vprintf(fmtstr, ap);
+ va_end(ap);
+
+ fgets(linebuf, sizeof(linebuf), stdin);
+
+ return linebuf;
+}
+
+void*
+die(const char *fmtstr, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmtstr);
+ vfprintf(stderr, fmtstr, ap);
+ va_end(ap);
+
+ exit(EXIT_FAILURE);
+}
+
+void
+request_info()
+{
+ const char *bufp;
+ char *end, *contents;
+ int len;
+
+ bufp = ask("How large is your file?\n");
+ len = strtoul(bufp, &end, 10);
+ if (!len || *end || len < 7000)
+ die("Invalid file length!\n");
+
+ printf("Ok! Im listening..\n");
+ contents = malloc(len);
+ read(STDIN_FILENO, contents, len);
+
+ printf("I got: %s\n", contents);
+
+ free(contents);
+}
+
+void
+parse_file()
+{
+ printf("hi\n");
+}
+
+void
+cat_flag()
+{
+ FILE *f;
+ char catbuf[256];
+ int nb;
+
+ f = fopen("cat", "r");
+ nb = fread(catbuf, 1, sizeof(catbuf), f);
+ fclose(f);
+
+ printf("%.*s\n", nb, catbuf);
+}
+
+void
+list_commands()
+{
+ int i;
+
+ printf("Available commands:\n");
+ for (i = 0; i < ARRSIZE(commands); i++)
+ printf("%s%s", i ? " " : "", commands[i].name);
+ printf("\n");
+}
+
+int
+main()
+{
+ char linebuf[256], *cp;
+ int exit, i;
+
+ exit = 0;
+ while (!exit) {
+ memset(linebuf, ' ', sizeof(linebuf));
+
+ printf("$ ");
+ exit = !fgets(linebuf, sizeof(linebuf), stdin);
+ if (exit || !*linebuf) break;
+
+ if (*linebuf == '\n') continue;
+ linebuf[strlen(linebuf) - 1] = '\0';
+
+ cp = strchr(linebuf, ' ');
+ if (cp) *cp = 0;
+ for (i = 0; i < ARRSIZE(commands); i++) {
+ if (!strcmp(commands[i].name, linebuf)) {
+ commands[i].func();
+ break;
+ }
+ }
+ }
+
+ printf("see you later!\n");
+}
diff --git a/service/src/stlfile.c b/service/src/stlfile.c
@@ -0,0 +1,4 @@
+#include "stlfile.h"
+
+
+
diff --git a/service/src/stlfile.h b/service/src/stlfile.h
@@ -0,0 +1,10 @@
+#ifndef STLFILE_H
+#define STLFILE_H
+
+#include <stdlib.h>
+#include <string.h>
+
+
+
+
+#endif // STLFILE_H