I have made a start

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/
master
Jonathan Hodgson 2 years ago
parent 4e17e706fa
commit f48a110429
  1. 28
      Makefile
  2. 3
      README.md
  3. 79
      docs/config.md
  4. 28
      docs/plugins.md
  5. 64
      src/config.c
  6. 24
      src/config.h
  7. 46
      src/database.c
  8. 13
      src/database.h
  9. 76
      src/main.c
  10. 42
      src/proxy.c
  11. 16
      src/proxy.h
  12. 50
      src/readline.c
  13. 9
      src/readline.h
  14. 69
      src/request.c
  15. 53
      src/request.h

@ -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)

@ -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.

@ -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.

@ -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.

@ -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;
}

@ -0,0 +1,24 @@
#ifndef CONFIG_H
#define CONFIG_H
#include <glob.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
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 */

@ -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;
}

@ -0,0 +1,13 @@
#ifndef DATABASE_H
#define DATABASE_H
#include <stdio.h>
#include <sqlite3.h>
#include <stdbool.h>
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 */

@ -0,0 +1,76 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}

@ -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);
}
}

@ -0,0 +1,16 @@
#ifndef PROXY_H
#define PROXY_H
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>
#include <stdbool.h>
#include "request.h"
void proxy_startListener( unsigned int port );
#endif /* ifndef PROXY_H */

@ -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;
}

@ -0,0 +1,9 @@
#ifndef READLINE_H
#define READLINE_H
#include <unistd.h>
#include <errno.h>
ssize_t fdReadLine(int fd, void *buffer, size_t n);
#endif /* ifndef READLINE_H */

@ -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;
}

@ -0,0 +1,53 @@
#ifndef REQUEST_H
#define REQUEST_H
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "readline.h"
#include <netinet/in.h>
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 */
Loading…
Cancel
Save