commit d197d53ca108fb0bb6c049035a39c38ff73eabbb Author: gome Date: Sat Sep 16 15:39:43 2023 -0500 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e660fd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bin/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9fa7b87 --- /dev/null +++ b/Makefile @@ -0,0 +1,60 @@ +flags = -Wall -D_GNU_SOURCE -Iinclude +lib-flags = $(flags) -fPIC -shared +obj-flags = $(flags) -c +exe-flags = $(flags) +exe-src := $(wildcard src/*.c) +exe := $(exe-src:src/%.c=bin/%) +lib-src := $(wildcard lib/*.c) +lib := $(lib-src:lib/%.c=bin/%.so) +obj := $(lib-src:lib/%.c=bin/%.o) +server-src := $(wildcard src/*.c) +client-src := $(wildcard src/client/*.c) +shared-src := $(wildcard src/shared/*.c) + +all: exe lib + +exe: $(exe) + +lib: % : server-% client-% + +server-lib client-lib: %-lib : bin/%.so + +# note: $^ is all prereqs listed on the effective line, $< is first prereq listed + +$(exe): bin/client.o bin/server.o +$(exe): bin/% : src/%.c + @mkdir -p $(@D) + @printf "Compiling executable $@ ...\n" + gcc $(exe-flags) $^ -o $@ + @printf "Done.\n" + +bin/%.o : include/%.h +bin/%.o : lib/%.c + @mkdir -p $(@D) + @printf "Compiling object for $< ...\n" + gcc $(obj-flags) $< -o $@ + @printf "Done.\n" + +bin/%.so : include/%.h +bin/%.so : lib/%.c + @mkdir -p $(@D) + @printf "Compiling DLL for $< ... \n" + @gcc $(lib-flags) $< -o $@ + @printf "Done.\n" + +clean: + @rm -rf bin/* + @printf "Object files and executable removed.\n" + +# for debugging the Makefile. say e.g. "make print-shared-src" +print-% : ; @echo $* = $($*) + +# client-test server-test: % : bin/% + +# bin/client-test bin/server-test: c-flags += -g +# bin/client-test bin/server-test: bin/%-test : src/c/%.h +# bin/client-test bin/server-test: bin/%-test : src/c/%.c +# @printf "Compiling test executable for $< ... \n" +# @gcc $(c-flags) -o $@ $< && printf "Done.\n" + +.PHONY: clean exe server-lib client-lib all diff --git a/include/client.h b/include/client.h new file mode 100644 index 0000000..689138b --- /dev/null +++ b/include/client.h @@ -0,0 +1,18 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +// run this before using anything else +// path: filesystem path to server's unix socket +int client_init(char* path); +// once successfully connected, send a string request +int client_request(char* request); +// if your request is successful, get the response +// the memory will be freed next time you request, +// so copy it if you need it +char* client_get_response(); diff --git a/include/server.h b/include/server.h new file mode 100644 index 0000000..7c3dc6c --- /dev/null +++ b/include/server.h @@ -0,0 +1,30 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// TODO: I looked at https://man7.org/linux/man-pages/man3/cmsg.3.html to get this value, but I really don't know if it's right... +#define CONTROL_BUFFER_SIZE CMSG_SPACE(sizeof(struct ucred)) +// expect clients' paths to be 6 chars long (auto-assigned sockets are a null first byte plus 5 [0-9a-f] characters) +#define CLIENT_PATH_LENGTH 6 + +// run this before using anything else +// path: filesystem path to create a unix socket at (can't be in use) +int server_init(char* path); +// blocks until a request is received +int server_listen(); +// once a request is received, get it +// the memory will be freed next time you listen, +// so copy it if you need it +char* server_get_request(); +// get the uid of the last successful request +uid_t server_get_uid(); +// get the username associated with a uid +char* server_get_username(uid_t uid); +// send back a response +int server_respond(char* response); diff --git a/lib/client.c b/lib/client.c new file mode 100644 index 0000000..78d75ab --- /dev/null +++ b/lib/client.c @@ -0,0 +1,103 @@ +#include "client.h" + +struct { + struct sockaddr_un server_addr; + socklen_t server_addr_len; + int sock_fd; + char* response; +} client_globals = { + .response = NULL, +}; + +int client_init(char* path) { + struct sockaddr_un client_addr; + // if we specify this as the size, the socket will get a unique abstract autobind name + socklen_t client_addr_len = sizeof(sa_family_t); + + client_globals.sock_fd = socket(AF_UNIX, SOCK_DGRAM, 0); + if (client_globals.sock_fd < 0) { + return errno; // TODO make an error code enum + } + + memset(&client_addr, 0, sizeof(client_addr)); + client_addr.sun_family = AF_UNIX; + + if (bind(client_globals.sock_fd, (struct sockaddr *)&client_addr, client_addr_len) != 0) { + return errno; // TODO make an error code enum + } + + // set up server address for all sockets to use + memset(&client_globals.server_addr, 0, sizeof(client_globals.server_addr)); + client_globals.server_addr.sun_family = AF_UNIX; + // copy in the provided directory path for the socket + memcpy(client_globals.server_addr.sun_path, path, strlen(path)); + + client_globals.server_addr_len = sizeof(client_globals.server_addr.sun_family) + strlen(client_globals.server_addr.sun_path); + + return 0; +} + +int client_request(char* request) { + sendto(client_globals.sock_fd, request, strlen(request) + 1, 0, (struct sockaddr *) &client_globals.server_addr, client_globals.server_addr_len); + + // RECEIVE + ssize_t msg_size; + + { + // make copy of server address struct + struct sockaddr_un server_addr = server_addr; + // recvfrom requires a pointer to server_addr_len, so make a copy just in case it does something to it + socklen_t server_addr_len_copy = client_globals.server_addr_len; + + msg_size = recvfrom( + client_globals.sock_fd, + NULL, + 0, + MSG_PEEK | MSG_TRUNC, + (struct sockaddr *) &server_addr, + &server_addr_len_copy + ); + if (msg_size < 0) { + return errno; + } + } + + { + free(client_globals.response); + client_globals.response = malloc(msg_size); + + // make a new copy of server address struct + struct sockaddr_un server_addr = server_addr; + // recvfrom requires a pointer to server_addr_len, so make a copy just in case it does something to it + socklen_t server_addr_len_copy = client_globals.server_addr_len; + + ssize_t recv_size; + recv_size = recvfrom( + client_globals.sock_fd, + client_globals.response, + msg_size, + 0, + (struct sockaddr *) &server_addr, + &server_addr_len_copy + ); + + if (recv_size < 0) { + return errno; + } + } + + return 0; +} + +char* client_get_response() { + return client_globals.response; +} + +// void __attribute__ ((constructor)) init(void) { + +// } + +void __attribute__ ((destructor)) client_clean_up(void) { + free(client_globals.response); + close(client_globals.sock_fd); +} diff --git a/lib/server.c b/lib/server.c new file mode 100644 index 0000000..a37d76b --- /dev/null +++ b/lib/server.c @@ -0,0 +1,149 @@ +#include "server.h" + +struct { + int sock_fd; + char* request; + uid_t uid; + char* client_path; +} server_globals = { + .request = NULL, + .uid = 0, + .client_path = NULL, +}; + +int server_init(char* path) { + umask(0); + server_globals.sock_fd = socket(AF_UNIX, SOCK_DGRAM, 0); + if (server_globals.sock_fd < 0) { + return errno; + } + + struct sockaddr_un server_addr; + + memset(&server_addr, 0, sizeof(server_addr)); + server_addr.sun_family = AF_UNIX; + // copy in the provided directory path for the socket + memcpy(server_addr.sun_path, path, strlen(path)); + + // // is this needed? + // signal(SIGPIPE, SIG_IGN); + + socklen_t server_addr_len = sizeof(server_addr.sun_family) + strlen(server_addr.sun_path); + + unlink(server_addr.sun_path); + if (bind(server_globals.sock_fd, (struct sockaddr *)&server_addr, server_addr_len) != 0) { + return errno; + } + + int optval = 1; + if (setsockopt(server_globals.sock_fd, SOL_SOCKET, SO_PASSCRED, &optval, sizeof(optval)) != 0) { + return errno; + } + + server_globals.client_path = malloc(CLIENT_PATH_LENGTH); + + return 0; +} + +int server_listen() { + struct sockaddr_un client_addr; + struct msghdr message_hdr; + // tells recvmsg where to put the client address name + message_hdr.msg_name = (struct sockaddr *)&client_addr; + // only ever expect the abstract address to be 5 chars long (plus one for null at beginning) + // sockaddr_un is big enough to handle up to 108 characters but we don't want more than 6 + message_hdr.msg_namelen = sizeof(sa_family_t) + CLIENT_PATH_LENGTH; + + // this is where the message (the client's request) will be stored, it's weird cause it can support multiple buffers + struct iovec iovec; + // the buffer to receive into is just the memory for the request in the return result, this way no copying is required + iovec.iov_base = NULL; + iovec.iov_len = 0; + message_hdr.msg_iov = &iovec; + // there's only one element in the iovec cause we only use one buffer + message_hdr.msg_iovlen = 1; + + // set after call, not checked so it shouldn't matter + message_hdr.msg_flags = 0; + + // this is the buffer for getting ancillary data (namely, the UID of the requesting client) + char message_control_buffer[CONTROL_BUFFER_SIZE]; + message_hdr.msg_control = &message_control_buffer; + message_hdr.msg_controllen = CONTROL_BUFFER_SIZE; + + // call gets info about waiting message and sender (address and uid) + ssize_t msg_size = recvmsg(server_globals.sock_fd, &message_hdr, MSG_PEEK | MSG_TRUNC); + + // for now, just output how big it was, later, we will check to make sure it's the correct size + if (msg_size < 0) { + return errno; + } + + // extract credentials from the message + struct ucred credentials; + memcpy(&credentials, CMSG_DATA((struct cmsghdr *)message_hdr.msg_control), sizeof(credentials)); + + // recvmsg populated msg_name with the client's actual path info. + // copy that into the client path + // client_addr.sun_path should be updated by recvmsg + memcpy(server_globals.client_path, client_addr.sun_path, CLIENT_PATH_LENGTH); + + server_globals.uid = credentials.uid; + // username = getpwuid(credentials.uid)->pw_name; + + // allocate buffer to receive the actual message + free(server_globals.request); + server_globals.request = malloc(msg_size); + + ssize_t recv_size = recv(server_globals.sock_fd, server_globals.request, msg_size, 0); + + if (recv_size < 0) { + return errno; + } + + // request was already added to result, so it's ready to go + return 0; +} + +int server_respond(char* response) { + // set up client address + struct sockaddr_un client_addr; + client_addr.sun_family = AF_UNIX; + memcpy(client_addr.sun_path, server_globals.client_path, CLIENT_PATH_LENGTH); + + // send stream + // NOTE: I added a 1 here to include the null byte at the end of the string, because it didn't seem to be included otherwise + // but it appears to work without that when the client sends the request? Probably needs more investigation + ssize_t sent = sendto(server_globals.sock_fd, response, strlen(response) + 1, 0, (struct sockaddr *)&client_addr, sizeof(sa_family_t) + CLIENT_PATH_LENGTH); + + if (sent < 0) { + return errno; + } + + return 0; +} + +char* server_get_request() { + return server_globals.request; +} + +char* server_get_username(uid_t uid) { + // if this doesn't work sometime you can try storing the uid instead and then calling getpwuid in here + return getpwuid(uid)->pw_name; +} + +uid_t server_get_uid() { + return server_globals.uid; +} + +// void __attribute__ ((constructor)) init(void) { + +// } + +void __attribute__ ((destructor)) server_clean_up(void) { + // TODO: error handling? + close(server_globals.sock_fd); + free(server_globals.request); + free(server_globals.client_path); + // don't need to free username since it's provided by getpwuid +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..ea818f1 --- /dev/null +++ b/readme.md @@ -0,0 +1,61 @@ +# Local Server + +You could use this to make a little game on your server or something. + +The server library listens on a [Unix domain socket](https://en.wikipedia.org/wiki/Unix_domain_socket) you specify (like a file path), and then other users can send requests to the server using the client library. + +The server library can tell you what user sent a request, giving you some nice OS-level authentication. + +There are also WIP command line utilities that should make it easier to work with the libraries from scripts. + +## Building + +#### Make the libraries + +``` +> make lib +``` + +#### Make the command line util + +``` +> make exe +``` + +## Usage + +### Using the libraries + +To use the libraries, you'll need the library file created from the `make lib` step and the corresponding header file. + +So, for example, to use the server, you'd need: + - `./bin/server.so` + - `./include/server.h` + +You should be able to use these libraries in a variety of languages that support a C [foreign function interface (FFI)](https://en.wikipedia.org/wiki/Foreign_function_interface). + +### API + +I'll write up API documentation soon, but for now, look at the header files in `./include` for an idea of the API. + +### Running the command line util + +There are subcommands for the server and the client respectively. +This part is still a WIP, so don't expect much yet! + +##### Server + +``` +> localserv server +``` + +The `` can be any file path that you have permissions to that isn't already occupied, e.g., `~/example.sock` + +##### Client + +``` +> localserv client +``` + +The `` is the same one you specified when starting your server. +The `` is just any string you want to send to the server. diff --git a/src/localserv.c b/src/localserv.c new file mode 100644 index 0000000..cc37e91 --- /dev/null +++ b/src/localserv.c @@ -0,0 +1,84 @@ +#include "server.h" +#include "client.h" +#include +#include +#include + +char *exe_name; + +int server(char *path) { + int error; + error = server_init(path); + if (error) { + fprintf(stderr, "%s: server init failed: %s\n", exe_name, strerror(error)); + return error; + } + + error = server_listen(); + if (error) { + fprintf(stderr, "%s: listen failed: %s\n", exe_name, strerror(error)); + return error; + } + + char* request = server_get_request(); + printf("Request: %s\n", request); + printf("Input response: "); + + char* response = NULL; + size_t len = 0; + if (getline(&response, &len, stdin) == -1) { + fprintf(stderr, "%s: failed getting input: %s\n", exe_name, strerror(errno)); + } + error = server_respond(response); + if (error) { + fprintf(stderr, "%s: respond failed: %s\n", exe_name, strerror(error)); + return error; + } + + return 0; +} + +int client(char *path, char *message) { + int error; + error = client_init(path); + if (error) { + fprintf(stderr, "%s: client init failed: %s\n", exe_name, strerror(error)); + return error; + } + + error = client_request(message); + if (error) { + fprintf(stderr, "%s: request failed: %s\n", exe_name, strerror(error)); + return error; + } + + printf("%s\n", client_get_response()); + return 0; +} + +int main(int argc, char *argv[]) { + exe_name = argv[0]; + if (argc <= 1) { + // TODO: full help + fprintf(stderr, "%s: provide subcommand \"server\" or \"client\"", exe_name); + return -1; + } else if (strcmp(argv[1], "server") != 0 && strcmp(argv[1], "client") != 0) { + // TODO: full help + fprintf(stderr, "%s: command not recognized; provide subcommand \"server\" or \"client\"", exe_name); + return -1; + } else if (argc < 3) { + fprintf(stderr, "%s: provide a path", exe_name); + return -1; + } + if (strcmp(argv[1], "server") == 0) { + return server(argv[2]); + } else if (strcmp(argv[1], "client") == 0) { + if (argc < 4) { + fprintf(stderr, "%s client: provide a message", exe_name); + return -1; + } + return client(argv[2], argv[3]); + } else { + return -1; + } +}