From f48a1104295f4f9ff3a671bb23dc85826865c453 Mon Sep 17 00:00:00 2001 From: Jonathan Hodgson Date: Mon, 27 Dec 2021 21:43:11 +0000 Subject: [PATCH] I have made a start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I have done some work on opening a socket and waiting for a connection. This can be read line by line and I have started a request struct that it will accept. Also started on some docs. Not much is yet working. I am going to start learning µnit for unit tests: https://nemequ.github.io/munit/ --- Makefile | 28 ++++++++++++++++++ README.md | 3 +- docs/config.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++ docs/plugins.md | 28 ++++++++++++++++++ src/config.c | 64 +++++++++++++++++++++++++++++++++++++++ src/config.h | 24 +++++++++++++++ src/database.c | 46 ++++++++++++++++++++++++++++ src/database.h | 13 ++++++++ src/main.c | 76 +++++++++++++++++++++++++++++++++++++++++++++++ src/proxy.c | 42 ++++++++++++++++++++++++++ src/proxy.h | 16 ++++++++++ src/readline.c | 50 +++++++++++++++++++++++++++++++ src/readline.h | 9 ++++++ src/request.c | 69 ++++++++++++++++++++++++++++++++++++++++++ src/request.h | 53 +++++++++++++++++++++++++++++++++ 15 files changed, 599 insertions(+), 1 deletion(-) create mode 100644 Makefile create mode 100644 docs/config.md create mode 100644 docs/plugins.md create mode 100644 src/config.c create mode 100644 src/config.h create mode 100644 src/database.c create mode 100644 src/database.h create mode 100644 src/main.c create mode 100644 src/proxy.c create mode 100644 src/proxy.h create mode 100644 src/readline.c create mode 100644 src/readline.h create mode 100644 src/request.c create mode 100644 src/request.h diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..730a62e --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +# This was stolen from here: https://avikdas.com/2019/12/16/makefiles-for-c-cpp-projects.html + +CFILES = $(wildcard src/*.c) +OBJFILES = $(CFILES:.c=.o) +OUT = proxy +CFLAGS = -Wall +LDLIBS = -lsqlite3 +CC = gcc + + +.PHONY: default +default: $(OUT) + + +.PHONY: run +run: $(OUT) + ./$(OUT) + +$(OUT): $(OBJFILES) + $(CC) -o $@ $^ $(LDLIBS) + +%.o: %.c + $(CC) $(CFLAGS) -c -o $@ $^ + + +.PHONY: clean +clean: + rm -f $(OBJFILES) $(OUT) diff --git a/README.md b/README.md index a3639e1..7172ad4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ -# Intercepting Proxy in C +# YAIP This will hopefully one day be an intercepting proxy written in c. I am using it as a way to learn rather than as a tool I expect other people to use. However, if you wish to, you are welcome to use this. +Check laws in your local area, this tool should only be used for legal purposes. diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 0000000..577e781 --- /dev/null +++ b/docs/config.md @@ -0,0 +1,79 @@ +# Config + +There will be 2 config files available. One at + +$XDG_CONFIG_HOME/yaip/proxy.conf and one at proxy.conf in the directory that the +tool is run from. Either can be overwritten with command line flags. + +The config file is (currently), very simple. Empty lines are ignored, as are +lines that start with a # symbol. This is useful if you wish to make comments +about configuration choices. + +The file in $XDG_CONFIG_HOME is parsed first, followed by the file in the +current working directory meaning that the config file in the current directory +will take precedence. + +To set a configuration option, the line should have the configuration option +followed by a colon, an optional space and the value it should be set to. + +For example: + +``` +port: 8080 +``` + +All options may also be set via a command line switch -c or --config followed by the name +and the value. + +For example: + +``` +yaip --config port 8080 +``` + + +## Available Options + +### port + +The listening port. + +Default: + +``` +port: 8080 +``` + +### database + +The database that requests and responses should be stored in. + +Default: + +``` +database: proxy.sqlite +``` + +### localConfig + +The name of the local config file: + +Default: + +``` +localConfig: proxy.conf +``` + +### userConfig + +The name of the user config file: + +Default: + +``` +$XDG_CONFIG_HOME/yaip/proxy.conf +``` + +**Note:** It only makes sense to change this as a command line argument as it is +the first config file that is sourced. + diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 0000000..65e76e3 --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,28 @@ +# Plugins + +**Note:** This is not implemented yet - it is mearly my plan. + +The tool should be able to load plugins. These should be stand alone scripts / +executables that YAIP can call using `system()` or similar. + +I will use a hook / filter type system. There will be different things that +plugins can do. The order in which plugins run will be dictated in the config +file or via an API if already running. The API still needs to be thought out. + +## Intercept Plugins + +Plugins will be able to intercept requests / responses. This will be blocking. +I haven't decided yet if these plugins will run on stdin or a temporary file. + +I would prefer stdin / stdout although I think that would make it difficult for +a plugin to launch vim and have it be interactive. I'll have to do some trial +and error. + +## Analysis Plugins + +These plugins will hopefully be able to run asynchronously without intercepting +requests / responses. The idea for this is that the plugins could check for +misconfigured headers or sql statements or whatever. These won't be able to +change what the server / browser receives. + + diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..2a46276 --- /dev/null +++ b/src/config.c @@ -0,0 +1,64 @@ +#include "config.h" + +/* + * Checks if the given path exists by calling stat(). + * + */ +static bool path_exists(const char *path) { + struct stat buf; + return (stat(path, &buf) == 0); +} + +/* + * This function resolves ~ in pathnames. + */ +static char* resolveTilde(const char *path) { + static glob_t globbuf; + + int res = glob(path, GLOB_TILDE, NULL, &globbuf); + /* no match, or many wildcard matches are bad */ + if (res == GLOB_NOMATCH) + return strdup(path); + else if (res != 0) { + printf("glob() failed\n"); + return NULL; + } else { + return strdup(globbuf.gl_pathv[0]); + } + globfree(&globbuf); +} + +/* +* Returns a pointer to a config object containing defaults +*/ +Config* configDefaults(){ + Config *conf = malloc( sizeof( Config ) ); + conf->database = "proxy.sqlite"; + conf->port = 8080; + conf->localConfig = "proxy.conf"; + conf->userConfig = getDefaultUserConfigLoc(); + + return conf; +} + +char* getConfigDir(){ + char *xdg_config_home; + if ((xdg_config_home = getenv("XDG_CONFIG_HOME")) == NULL) + xdg_config_home = resolveTilde("~/.config"); + + return xdg_config_home; +} + +char* getDefaultUserConfigLoc(){ + char *configDir = getConfigDir(); + char *configFile[strlen(configDir) + 11]; + + memset(configFile, '\0', strlen(configDir) + 11); + + strcpy( configFile, configDir ); + + strcat( configFile, configDir ); + + return configFile; +} + diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..64f9599 --- /dev/null +++ b/src/config.h @@ -0,0 +1,24 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#include +#include +#include +#include +#include +#include + +typedef struct { + char *database; + char *localConfig; // Project Specific + char *userConfig; // User Specific + unsigned int port; +} Config; + +static bool path_exists(const char *path); +static char* resolveTilde(const char *path); +Config* configDefaults(); +char* getConfigDir(); +char* getDefaultUserConfigLoc(); + +#endif /* ifndef CONFIG_H */ diff --git a/src/database.c b/src/database.c new file mode 100644 index 0000000..b11ff88 --- /dev/null +++ b/src/database.c @@ -0,0 +1,46 @@ +#include "database.h" + +sqlite3 *db_open(char *file){ + sqlite3 *db; + /* Open database */ + int rc = sqlite3_open(file, &db); + + if( rc ) { + fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db)); + return NULL; + } else { + fprintf(stderr, "Opened database successfully\n"); + return db; + } +} + +bool db_file_exists(char *file){ + FILE *fp; + + if ( (fp = fopen(file, "r")) != NULL) { + fclose(fp); + return true; + } else { + return false; + } +} + +bool db_create(char *file){ + FILE *fp = fopen(file, "a"); + if(fp == NULL) { + printf("file can't be opened\n"); + return false; + } + fclose(fp); + + sqlite3 *db = db_open(file); + + // Do stuff here to create the tables. + // Will probably need Requests and Responses - maybe others + + + sqlite3_close(db); + + return true; +} + diff --git a/src/database.h b/src/database.h new file mode 100644 index 0000000..68310e6 --- /dev/null +++ b/src/database.h @@ -0,0 +1,13 @@ +#ifndef DATABASE_H +#define DATABASE_H + +#include +#include +#include + +sqlite3 *db_open(char *file); +bool db_file_exists(char *file); +//Returns true if the file was successfully created, false otherwise +bool db_create(char *file); + +#endif /* ifndef LOG_H */ diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..0f45d8d --- /dev/null +++ b/src/main.c @@ -0,0 +1,76 @@ +#include +#include +#include + +#include "database.h" +#include "proxy.h" +#include "config.h" + +#define PACKAGE_NAME "WICTP" +#define DEFAULT_DATABASE "data.sqlite" +#define DEFAULT_CONFIG "config.json" +#define DEFAULT_PORT 8080 + +unsigned int validatePortNumber(unsigned int portNumber){ + // Given that we are dealing with unsined ints, we don't need to worry about + // checking for values less than 0 + if ( portNumber > 65535 ){ + printf( "Port %i is invalid, using default of %i", portNumber, DEFAULT_PORT ); + return DEFAULT_PORT; + } + return portNumber; +} + +void printHelp(){ + printf("Usage: %s [options]\n", PACKAGE_NAME); + printf( "Options are:\n" ); + printf( "\t --database DATABASE A sqlite3 database file (Defatult: %s)\n", DEFAULT_DATABASE ); + printf( "\t --config CONFIG A config file (Default: %s)\n", DEFAULT_CONFIG ); + printf( "\t --port PORT The listening port (Default: %i)\n", DEFAULT_PORT ); + printf( "\t --help Display version information.\n"); +} + +int main(int argc, char**argv){ + char *database = DEFAULT_DATABASE; + char *config = DEFAULT_CONFIG; + unsigned int port = DEFAULT_PORT; + + Config *defaultconfig = configDefaults(); + + + for ( unsigned int i = 1; i < argc; i++ ){ + if ( strcmp( argv[i], "--database" ) == 0 ){ + if ( i + 1 < argc ){ + database = argv[++i]; + } else { + printf("--database requires a positional argument\n"); + } + } else if ( strcmp( argv[i], "--config" ) == 0 ){ + if ( i + 1 < argc ){ + config = argv[++i]; + } else { + printf("--config requires a positional argument\n"); + } + } else if ( strcmp( argv[i], "--port" ) == 0 ){ + if ( i + 1 < argc ){ + port = validatePortNumber( atoi(argv[++i]) ); + } else { + printf("--port requires a positional argument\n"); + } + } else if ( strcmp( argv[i], "--help" ) == 0 ){ + printHelp(); + return 0; + } else { + printf("Unknown option %s\n", argv[i]); + } + } + + if ( !db_file_exists(database) ){ + printf("Creating DB"); + db_create(database); + } + + proxy_startListener(port); + + return 0; +} diff --git a/src/proxy.c b/src/proxy.c new file mode 100644 index 0000000..2581197 --- /dev/null +++ b/src/proxy.c @@ -0,0 +1,42 @@ +#include "proxy.h" + +void proxy_startListener(unsigned int port){ +// https://www.geeksforgeeks.org/socket-programming-cc/ + int server_fd, new_socket; + struct sockaddr_in address; + int opt = 1; + int addrlen = sizeof(address); + char line[1024] = {0}; + + // Creating socket file descriptor + if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { + perror("socket failed"); + return ; + } + + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_ANY); + address.sin_port = htons( port ); + + // Forcefully attaching socket to the port 8080 + if (bind(server_fd, (struct sockaddr *)&address, sizeof(address))<0) { + perror("bind failed"); + return ; + } + + if (listen(server_fd, 3) < 0) { + perror("listen"); + return ; + } + while ( true ){ + printf("Listening\n"); + if ((new_socket = accept(server_fd, (struct sockaddr *)&address, + (socklen_t*)&addrlen))<0) { + perror("accept"); + return ; + } + + Request *request = newRequestFromSocket(new_socket); + } + +} diff --git a/src/proxy.h b/src/proxy.h new file mode 100644 index 0000000..3de5333 --- /dev/null +++ b/src/proxy.h @@ -0,0 +1,16 @@ +#ifndef PROXY_H +#define PROXY_H + +#include +#include +#include +#include +#include +#include +#include + +#include "request.h" + +void proxy_startListener( unsigned int port ); + +#endif /* ifndef PROXY_H */ diff --git a/src/readline.c b/src/readline.c new file mode 100644 index 0000000..91b3bce --- /dev/null +++ b/src/readline.c @@ -0,0 +1,50 @@ +// https://man7.org/tlpi/code/online/dist/sockets/read_line.c.html +#include "readline.h" + +/* Read characters from 'fd' until a newline is encountered. If a newline + character is not encountered in the first (n - 1) bytes, then the excess + characters are discarded. The returned string placed in 'buf' is + null-terminated and includes the newline character if it was read in the + first (n - 1) bytes. The function return value is the number of bytes + placed in buffer (which includes the newline character if encountered, + but excludes the terminating null byte). */ +ssize_t fdReadLine(int fd, void *buffer, size_t n) { + ssize_t numRead; /* # of bytes fetched by last read() */ + size_t totRead; /* Total bytes read so far */ + char *buf; + char ch; + if (n <= 0 || buffer == NULL) { + errno = EINVAL; + return -1; + } + buf = buffer; /* No pointer arithmetic on "void *" */ + totRead = 0; + for (;;) { + numRead = read(fd, &ch, 1); + if (numRead == -1) { + if (errno == EINTR) /* Interrupted --> restart read() */ + continue; + else + return -1; /* Some other error */ + + } else if (numRead == 0) { /* EOF */ + if (totRead == 0) /* No bytes read; return 0 */ + return 0; + else /* Some bytes read; add '\0' */ + break; + + } else { /* 'numRead' must be 1 if we get here */ + if (totRead < n - 1) { /* Discard > (n - 1) bytes */ + totRead++; + *buf++ = ch; + } + + if (ch == '\n') + break; + } + } + + *buf = '\0'; + return totRead; +} + diff --git a/src/readline.h b/src/readline.h new file mode 100644 index 0000000..88d079c --- /dev/null +++ b/src/readline.h @@ -0,0 +1,9 @@ +#ifndef READLINE_H +#define READLINE_H + +#include +#include + +ssize_t fdReadLine(int fd, void *buffer, size_t n); + +#endif /* ifndef READLINE_H */ diff --git a/src/request.c b/src/request.c new file mode 100644 index 0000000..00452e2 --- /dev/null +++ b/src/request.c @@ -0,0 +1,69 @@ +#include "request.h" + +Header* newHeader(char *str){ + Header *header = malloc(sizeof(Header)); + memset(header, 0, sizeof(Header)); + + int position = -1; + + + for ( unsigned int i = 0; i < strlen(str); i++ ){ + if ( str[i] == ':' ){ + position = i; + break; + } + } + + if ( position == -1 ){ + printf("Header without colon. Not sure what to do\n"); + return NULL; + } + + + // We want to allocate 1 more than the length so we can have a \0 at the end + header->name = malloc( sizeof(char) * ( position + 1 ) ); + memset(header->name, '\0', position+1); + strncpy( header->name, str, position ); + + for ( unsigned int i = position+1; i < strlen(str); i++ ){ + if ( str[i] == '\t' || str[i] == ' ' ) continue; + position = i; + break; + } + + + //Anything left is the value + header->value = malloc( sizeof(char) * ( strlen(str) - position ) ); + memset(header->value, '\0', ( strlen(str) - position ) ); + strcpy( header->value, str + position ); + + + return header; +} + +Request* newRequest(){ + Request *request = malloc(sizeof(Request)); + memset(request, 0, sizeof(Request)); + return request; +} + + +Request* newRequestFromSocket(int socket){ + Request *req = newRequest(); + int valread; + char line[1024] = {0}; + valread = fdReadLine( socket , line, 1024); + char hello[] = "this is a test"; + + // The first line will give us some important information + + //a length of 2 will indicate an empty line which will split the headers + //from the body (if there is a body) + while ( valread > 2 ){ + printf("%s",line ); + valread = fdReadLine( socket , line, 1024); + } + send(socket , hello , strlen(hello) , 0 ); + printf("Hello message sent\n"); + return req; +} diff --git a/src/request.h b/src/request.h new file mode 100644 index 0000000..df065fc --- /dev/null +++ b/src/request.h @@ -0,0 +1,53 @@ +#ifndef REQUEST_H +#define REQUEST_H + +#include +#include +#include +#include "readline.h" +#include + +typedef enum {HTTP,HTTPS} HTTPProtocol; +typedef enum {GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH} HTTPMethod; + +/* +* A struct reperesenting an http header +*/ +typedef struct { + char *name; + // The spec doesn't specify a max size for headers, but nginx doesn't accept headers bigger than 4096 so that's probably ok for us + char *value; +} Header; + +/* +* A linked list wrapper around the headers +*/ +typedef struct HeaderList HeaderList; +struct HeaderList { + Header header; + HeaderList *next; +}; + +/* +* A struct reperesenting an http request +*/ +typedef struct { + // Common versions are: 0.9, 1.0, 1.1, 2.0 + double version; + HTTPMethod method; + HTTPProtocol protocol; + char *host; + char *path; + HeaderList *headers; + char *queryString; + char *body; +} Request; + +Header* newHeader(); +Request* newRequest(); +Request* newRequestFromSocket(int socket); +void* addHeader(Request *req); + + + +#endif /* ifndef REQUEST_H */