initial commit

This commit is contained in:
gome 2023-09-16 15:39:43 -05:00
commit d197d53ca1
8 changed files with 506 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
bin/

60
Makefile Normal file
View File

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

18
include/client.h Normal file
View File

@ -0,0 +1,18 @@
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
// 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();

30
include/server.h Normal file
View File

@ -0,0 +1,30 @@
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <pwd.h>
// 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);

103
lib/client.c Normal file
View File

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

149
lib/server.c Normal file
View File

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

61
readme.md Normal file
View File

@ -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 <socket-path>
```
The `<socket-path>` can be any file path that you have permissions to that isn't already occupied, e.g., `~/example.sock`
##### Client
```
> localserv client <socket-path> <request>
```
The `<socket-path>` is the same one you specified when starting your server.
The `<request>` is just any string you want to send to the server.

84
src/localserv.c Normal file
View File

@ -0,0 +1,84 @@
#include "server.h"
#include "client.h"
#include <unistd.h>
#include <stdio.h>
#include <limits.h>
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;
}
}