add auth mode protocol

This commit is contained in:
gome 2023-09-23 22:19:49 -05:00
parent 3899af7b10
commit 12a71b794d
17 changed files with 517 additions and 248 deletions

View File

@ -1,9 +1,12 @@
flags = -Wall -D_GNU_SOURCE -Iinclude
lib-flags = $(flags) -fPIC -shared
obj-flags = $(flags) -c
exe-flags = $(flags) -ljwt
# the --no-as-needed flag is passed to the linker
# if I was building this correctly it wouldn't be necessary
# but I'm not going to figure out how to do it right now (TODO: figure out)
exe-flags = $(flags) -Wl,--no-as-needed -ljwt
exe := bin/localserv
src := $(wildcard src/*.c)
src := $(wildcard src/*.c) $(wildcard src/**/*.c)
obj := $(src:src/%.c=bin/%.o)
lib-src := $(wildcard lib/*.c)
lib-obj := $(lib-src:lib/%.c=bin/%.o)
@ -21,21 +24,21 @@ server-lib client-lib: %-lib : bin/%.so
$(exe): $(lib-obj) $(obj)
@mkdir -p $(@D)
@printf "Compiling executable $@ ...\n"
gcc $(exe-flags) $^ -o $@
@gcc $(exe-flags) $^ -o $@
@printf "Done.\n"
$(lib-obj) : bin/%.o : include/%.h
$(lib-obj) : bin/%.o : lib/%.c
@mkdir -p $(@D)
@printf "Compiling object for $< ...\n"
gcc $(obj-flags) $< -o $@
@gcc $(obj-flags) $< -o $@
@printf "Done.\n"
$(obj) : bin/%.o : src/%.h
$(obj) : bin/%.o : src/%.c
@mkdir -p $(@D)
@printf "Compiling object for $< ...\n"
gcc $(obj-flags) $< -o $@
@gcc $(obj-flags) $< -o $@
@printf "Done.\n"
bin/%.so : include/%.h

View File

@ -7,6 +7,7 @@
#include <stdlib.h>
#include <stdbool.h>
#include <pwd.h>
#include <signal.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))

View File

@ -42,29 +42,68 @@ You should be able to use these libraries in a variety of languages that support
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
## 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`
### Server
The server listens for strings from clients, writes them to `stdout`, and waits for a line from `stdin` to send back to the client. This can be handled interactively, but it can also be hooked into with a script, as in the example `./server.sh`.
#### Client
```
> localserv client <socket-path> <request>
> localserv server [-u] [-a] <socket-path>
```
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.
- `<socket-path>`: any file path that you have permissions to that isn't already occupied, e.g., `~/example.sock`
- `-u, --username`: flag to include username in request output.
- `-a, --auth`: flag for auth mode (implies `--username`)
#### Auth mode
The server features an auth mode, in which it implements an additional protocol layer for enabling a simple form of authorization. In this mode, servers will accept three types of messages from clients.
##### Issue
**Request**:
```
ISSUE
```
**Response**: The server issues a JSON web token (JWT) which identifies the username of the client. This JWT has a short lifespan (15 seconds), and is primarily used to get a longer-lasting token via the "refresh" action. This action does not write anything to `stdout` or read anything from `stdin`.
##### Refresh
**Request**:
```
REFRESH
<JSON web token>
```
**Response**: If the token is valid, server issues a new JSON web token (JWT) with the same username identified. This JWT has a long lifespan (10 minutes), and can be used with the "authorize" action to send messages to the server. This action does not write anything to `stdout` or read anything from `stdin`.
##### Issue
Message format:
```
AUTHORIZE
<JSON web token>
<message>
```
**Response**: If the token is valid, server conveys the username identified in the JWT and the corresponding message to `stdin`, and waits for a response from `stdout`, conveying it back to the client.
### Client
```
> localserv client <socket-path> [<request>]
```
- `<socket-path>`: the same path you specified when starting your server.
- `<request>`: just any string you want to send to the server.
### Server script example

View File

@ -9,9 +9,10 @@ if [[ -z "${1-}" ]]; then
fi
# set up named pipes in same place as the socket
recv="$1.recvpipe"
resp="$1.resppipe"
mkfifo $recv $resp
recv="$1.in.pipe"
resp="$1.out.pipe"
# -m=600: make the pipes user permissions only
mkfifo -m=600 $recv $resp
# they will be removed when the script exits
trap "rm -f $recv $resp" EXIT
@ -27,5 +28,4 @@ while read -r line; do
done <$recv >$resp &
# run the server, hooked up to the pipes and at the socket address the user gave
bin/localserv server $1 >$recv <$resp
bin/localserv server -a $1 >$recv <$resp

31
src/interrupt.c Normal file
View File

@ -0,0 +1,31 @@
#include "interrupt.h"
volatile sig_atomic_t exit_flag = 0;
void interrupt_register_sighandler() {
struct sigaction action;
memset( &action, 0, sizeof(action) );
action.sa_handler = interrupt_onsignal;
sigfillset(&action.sa_mask);
sigaction(SIGINT, &action, NULL);
}
void interrupt_onsignal(int) {
exit_flag = 1;
}
int interrupt_return_value() {
if (exit_flag == 1) {
// print message if we exited through sigint
fprintf(stderr, "\nExiting\n");
}
return exit_flag >= 0 ? 0 : exit_flag;
}
int interrupt_should_exit() {
return exit_flag;
}
void interrupt_set_exit_flag() {
exit_flag = -1;
}

14
src/interrupt.h Normal file
View File

@ -0,0 +1,14 @@
#include <signal.h>
#include <string.h>
#include <stdio.h>
#ifndef __INTERRUPT_H
#define __INTERRUPT_H
void interrupt_register_sighandler();
void interrupt_onsignal(int);
int interrupt_return_value();
int interrupt_should_exit();
void interrupt_set_exit_flag();
#endif

View File

@ -1,218 +1,83 @@
#include "localserv.h"
char *exe_name;
int server(char *path, int jwt_mode) {
int error;
error = server_init(path);
if (error) {
fprintf(stderr, "server init failed: %s\n", strerror(error));
return error;
}
jwt_t *jwt = NULL;
if (jwt_mode) {
error = jwt_new(&jwt);
if (error) {
fprintf(stderr, "JWT initialization failed: %s\n", strerror(error));
return error;
}
jwt_set_alg(jwt, JWT_ALG_HS256, (unsigned char *)"THIS IS NOT A SAFE KEY! -- TODO: use a user-provided key instead", 64);
if (error) {
fprintf(stderr, "JWT initialization failed: %s\n", strerror(error));
return error;
}
}
int listen_fails = 0;
while (true) {
error = server_listen();
if (error) {
fprintf(stderr, "listen failed: %s\n", strerror(error));
if (++listen_fails > 5) {
fprintf(stderr, "listen failed 5 times, exiting\n");
if (jwt != NULL) jwt_free(jwt);
return error;
} else {
continue;
}
}
char* request = server_get_request();
printf("%s\n", request);
fflush(stdout);
char* response = NULL;
if (jwt_mode) {
char *username = server_get_username(server_get_uid());
if (username == NULL) {
fprintf(stderr, "failed getting username\n");
if (jwt != NULL) jwt_free(jwt);
return -1;
}
jwt_del_grants(jwt, NULL);
jwt_add_grant(jwt, "sub", username);
jwt_add_grant_int(jwt, "iat", time(NULL));
response = jwt_encode_str(jwt);
if (response == NULL) {
fprintf(stderr, "failed emitting JWT\n");
if (jwt != NULL) jwt_free(jwt);
return -1;
}
}
else {
size_t len = 0;
ssize_t length = getline(&response, &len, stdin);
if (length == -1) {
fprintf(stderr, "failed getting input\n");
if (jwt != NULL) jwt_free(jwt);
return -1;
}
response[length - 1] = '\0';
}
error = server_respond(response);
if (error) {
fprintf(stderr, "respond failed (will retry): %s\n", strerror(error));
error = server_respond(response);
if (error) {
fprintf(stderr, "respond retry failed: %s\nfailed retry, exiting\n", strerror(error));
free(response);
if (jwt != NULL) jwt_free(jwt);
return error;
}
}
else {
free(response);
}
}
return 0;
}
int client(char *path, char *message) {
int error;
error = client_init(path);
if (error) {
fprintf(stderr, "client init failed: %s\n", strerror(error));
return error;
}
error = client_request(message);
if (error) {
fprintf(stderr, "request failed: %s\n", strerror(error));
return error;
}
printf("%s\n", client_get_response());
return 0;
}
int server_subcommand(opts opts, int argc, char *argv[]) {
if (opts.help) {
show_usage(stdout, server_usage);
return 0;
}
else if (argc <= 1) {
fprintf(stderr, "server: please provide a path\n\n");
show_usage(stderr, server_usage);
return -1;
}
char *path = argv[1];
return server(path, opts.jwt);
}
int client_subcommand(opts opts, int argc, char *argv[]) {
if (opts.help) {
show_usage(stdout, client_usage);
return 0;
}
else if (opts.jwt) {
fprintf(stderr, "client: invalid flag: “-j/--jwt” only applies to server subcommand\n\n");
show_usage(stderr, server_usage);
return -1;
}
else if (argc <= 1) {
fprintf(stderr, "client: please provide a path\n\n");
show_usage(stderr, client_usage);
return -1;
}
else if (argc <= 2) {
fprintf(stderr, "client: please provide a message\n\n");
show_usage(stderr, client_usage);
return -1;
}
return client(argv[1], argv[2]);
}
int main(int argc, char *argv[]) {
interrupt_register_sighandler();
set_exe_name(argv[0]);
// OSSL_DECODER_CTX_new_for_pkey()
// BIO_new_file()
// OSSL_DECODER_from_bio()
if (argc <= 1) { // no subcommand!
fprintf(stderr, "Please provide a subcommand.\n\n");
show_usage(stderr, both_usage);
return -1;
}
opts opts = {
.help = 0,
.jwt = 0,
};
static struct option help_opt[] = {
{"help", no_argument, NULL, 'h'},
{"jwt", no_argument, NULL, 'j'},
{0, 0, 0, 0},
};
int option_index = 0;
// // Configures getopt to not print its own error message
// opterr = 0;
int optchar;
while ((optchar = getopt_long(argc, argv, "hj", help_opt, &option_index)) != -1) {
switch (optchar) {
case 'h':
opts.help = 1;
return 0;
case 'j':
opts.jwt = 1;
default:
break;
}
}
if (optind < argc) {
char *subcommand = argv[optind];
if (strcmp(subcommand, "server") == 0) {
return server_subcommand(opts, argc - optind, argv + optind);
}
else if (strcmp(subcommand, "client") == 0) {
return client_subcommand(opts, argc - optind, argv + optind);
}
else if (opts.help) {
show_usage(stdout, both_usage);
return 0;
}
fprintf(stderr, "Subcommand “%s” not recognized; provide a valid subcommand as the first argument.\n\n", subcommand);
}
else if (opts.help) {
show_usage(stdout, both_usage);
return 0;
interrupt_set_exit_flag();
}
else {
// if optind is equal to (or greater than, which probably won't happen) the number of args we got,
// it means we only got options, no positional args
fprintf(stderr, "Please provide a subcommand.\n\n");
opts opts = {
.help = 0,
.username = 0,
.auth = 0,
.delims = { '\t', '\n', '\n' },
};
static struct option help_opt[] = {
{"help", no_argument, NULL, 'h'},
{"username", no_argument, NULL, 'u'},
{"auth", no_argument, NULL, 'a'},
// TODO: args for delims
{0, 0, 0, 0},
};
int option_index = 0;
// // Configures getopt to not print its own error message
// opterr = 0;
int optchar;
while ((optchar = getopt_long(argc, argv, "hua", help_opt, &option_index)) != -1) {
switch (optchar) {
case 'h':
opts.help = 1;
break;
case 'u':
opts.username = 1;
break;
case 'a':
opts.auth = 1;
// authmode implies username
opts.username = 1;
break;
// TODO: delims
default:
interrupt_set_exit_flag();
break;
}
}
if (!interrupt_should_exit()) {
if (optind < argc) {
char *subcommand = argv[optind];
if (strcmp(subcommand, "server") == 0) {
server_subcommand(opts, argc - optind, argv + optind);
}
else if (strcmp(subcommand, "client") == 0) {
client_subcommand(opts, argc - optind, argv + optind);
}
else {
fprintf(stderr, "Subcommand “%s” not recognized; provide a valid subcommand as the first argument.\n\n", subcommand);
show_usage(true);
interrupt_set_exit_flag();
}
}
else if (opts.help) {
show_usage(false);
}
else {
// if optind is equal to (or greater than, which probably won't happen) the number of args we got,
// it means we only got options, no positional args
fprintf(stderr, "Please provide a subcommand.\n\n");
show_usage(true);
interrupt_set_exit_flag();
}
}
}
show_usage(stderr, both_usage);
return -1;
return interrupt_return_value();
}

View File

@ -1,14 +1,11 @@
#include "server.h"
#include "client.h"
#include "subcommand/server.h"
#include "subcommand/client.h"
#include "usage.h"
#include "interrupt.h"
#include "opts.h"
#include <unistd.h>
#include <getopt.h>
#include <stdio.h>
#include <limits.h>
#include <time.h>
#include <jwt.h>
typedef struct {
int help;
int jwt;
} opts;

11
src/opts.h Normal file
View File

@ -0,0 +1,11 @@
#ifndef __OPTS_H
#define __OPTS_H
typedef struct opts {
int help;
int username;
int auth;
char delims[3];
} opts;
#endif

126
src/subcommand/auth.c Normal file
View File

@ -0,0 +1,126 @@
#include "auth.h"
#define ISSUE "ISSUE"
#define REFRESH "REFRESH\n"
#define REFRESH_OFFSET sizeof(REFRESH) - 1
#define AUTHORIZE "AUTHORIZE\n"
#define AUTHORIZE_OFFSET sizeof(AUTHORIZE) - 1
#define ISSUE_EXPIRATION 15
#define REFRESH_EXPIRATION 1200
#define grant_failure(grant) fprintf(stderr, "JWT %s set failed: %s\n", grant, strerror(error))
unsigned char *key = (unsigned char *)"THIS IS NOT A SAFE KEY! -- TODO: use a user-provided key instead";
int keylen = 64;
int set_jwt_grants(jwt_t *jwt, const char *sub, const time_t exp) {
int error = jwt_set_alg(jwt, JWT_ALG_HS256, key, keylen);
if (error) {
grant_failure("alg");
return error;
}
error = jwt_add_grant(jwt, "sub", sub);
if (error) {
grant_failure("sub");
return error;
}
time_t iat = time(NULL);
error = jwt_add_grant_int(jwt, "iat", iat);
if (error) {
grant_failure("iat");
return error;
}
error = jwt_add_grant_int(jwt, "exp", iat + exp);
if (error) {
grant_failure("exp");
return error;
}
return 0;
}
char* auth_issue_jwt(const char *sub, const time_t exp) {
jwt_t *jwt = NULL;
int error = jwt_new(&jwt);
char *response = NULL;
if (error) {
fprintf(stderr, "JWT initialization failed: %s\n", strerror(error));
return NULL;
}
error = set_jwt_grants(jwt, sub, exp);
if (error) {
jwt_free(jwt);
return NULL;
}
response = jwt_encode_str(jwt);
jwt_free(jwt);
if (response == NULL) {
fprintf(stderr, "failed emitting JWT: %s\n", strerror(errno));
}
return response;
}
char* auth_verify_jwt(const char *jwt_str, time_t now, char **sub) {
jwt_t *jwt;
int error = jwt_decode(&jwt, jwt_str, key, keylen);
if (error) {
fprintf(stderr, "JWT validation failed: %s\n", strerror(error));
// TODO: determine which errnos correspond to invalid credentials
// and which correspond to server errors
// and then return the right thing here
return strdup("Bad Credential");
}
time_t exp = jwt_get_grant_int(jwt, "exp");
if (errno == ENOENT) {
return strdup("Bad Credential");
}
else if (now >= exp) {
return strdup("Token Expired");
}
*sub = jwt_get_grant(jwt, "sub");
if (sub == NULL) {
return strdup("Bad Credential");
}
return NULL;
}
char* auth(char **request, char **username) {
time_t now = time(NULL);
if (strcasecmp(*request, ISSUE) == 0) {
char *response = auth_issue_jwt(*username, ISSUE_EXPIRATION);
return response == NULL ? strdup("Internal Server Error") : response;
}
else if (strncasecmp(*request, REFRESH, REFRESH_OFFSET) == 0) {
char *response = auth_verify_jwt(*request + REFRESH_OFFSET, now, username);
if (response == NULL) {
if (*username == NULL) {
return strdup("Internal Server Error");
}
response = auth_issue_jwt(*username, REFRESH_EXPIRATION);
}
return response == NULL ? strdup("Internal Server Error") : response;
}
else if (strncasecmp(*request, AUTHORIZE, AUTHORIZE_OFFSET) == 0) {
char *newline = strchr(*request + AUTHORIZE_OFFSET, '\n');
if (newline != NULL) {
// usually there should be a message after the token.
// if so, we replace this newline with a null char so JWT parsing works
*newline = '\0';
}
char *response = auth_verify_jwt(*request + AUTHORIZE_OFFSET, now, username);
if (*username == NULL) {
return strdup("Internal Server Error");
}
// response NULL means we verified!
// pass along request by moving past auth stuff
if (response == NULL) {
*request = newline == NULL ? "" : newline + 1;
}
return response;
}
return strdup("Bad Request");
}

6
src/subcommand/auth.h Normal file
View File

@ -0,0 +1,6 @@
#include <errno.h>
#include <string.h>
#include <time.h>
#include <jwt.h>
char* auth(char **request, char **username);

46
src/subcommand/client.c Normal file
View File

@ -0,0 +1,46 @@
#include "client.h"
void client(char *path, char *message, opts *opts) {
int error;
error = client_init(path);
if (error) {
fprintf(stderr, "client init failed: %s\n", strerror(error));
interrupt_set_exit_flag();
return;
}
error = client_request(message);
if (error) {
fprintf(stderr, "request failed: %s\n", strerror(error));
interrupt_set_exit_flag();
return;
}
printf("%s\n", client_get_response());
}
void client_subcommand(opts opts, int argc, char *argv[]) {
set_usage_context(usage_client);
char *message = "";
if (opts.help) {
show_usage(false);
}
else if (opts.username) {
fprintf(stderr, "client: invalid flag: “-u/--username” only applies to server subcommand\n\n");
show_usage(true);
interrupt_set_exit_flag();
}
else if (argc <= 1) {
fprintf(stderr, "client: please provide a path\n\n");
show_usage(true);
interrupt_set_exit_flag();
}
else {
if (argc > 2) {
// message provided
message = argv[2];
}
client(argv[1], message, &opts);
}
}

6
src/subcommand/client.h Normal file
View File

@ -0,0 +1,6 @@
#include "../../include/client.h"
#include "../opts.h"
#include "../usage.h"
#include "../interrupt.h"
void client_subcommand(opts opts, int argc, char *argv[]);

100
src/subcommand/server.c Normal file
View File

@ -0,0 +1,100 @@
#include "server.h"
void server(char *path, opts *opts) {
int error;
error = server_init(path);
if (error) {
fprintf(stderr, "server init failed: %s\n", strerror(error));
interrupt_set_exit_flag();
}
int listen_fails = 0;
char* response = NULL;
while (!interrupt_should_exit()) {
error = server_listen();
if (error) {
if (interrupt_should_exit()) {
// no message necessary
break;
}
fprintf(stderr, "listen failed: %s\n", strerror(error));
if (++listen_fails > 5) {
fprintf(stderr, "listen failed 5 times, exiting\n");
interrupt_set_exit_flag();
break;
}
continue;
}
// figure out username
char *username = NULL;
if ((*opts).username) {
username = server_get_username(server_get_uid());
if (username == NULL) {
fprintf(stderr, "failed getting username\n");
interrupt_set_exit_flag();
break;
}
}
char *request = server_get_request();
if ((*opts).auth) {
response = auth(&request, &username);
}
// if auth supplied us with a response,
// no need to do the interactive loop
if (response == NULL) {
// write request to stdout
if (username) {
printf("%s%c", username, (*opts).delims[0]);
}
printf("%s%c", request, (*opts).delims[1]);
fflush(stdout);
// read user input
size_t len = 0;
ssize_t length = getdelim(&response, &len, (*opts).delims[2], stdin);
if (length == -1) {
fprintf(stderr, "failed getting input\n");
interrupt_set_exit_flag();
break;
}
response[length - 1] = '\0';
}
error = server_respond(response);
if (error) {
fprintf(stderr, "respond failed (will retry): %s\n", strerror(error));
error = server_respond(response);
if (error) {
fprintf(stderr, "respond retry failed: %s\nfailed retry, exiting\n", strerror(error));
interrupt_set_exit_flag();
break;
}
}
free(response);
response = NULL;
}
free(response);
}
void server_subcommand(opts opts, int argc, char *argv[]) {
set_usage_context(usage_server);
if (opts.help) {
show_usage(false);
}
else if (argc <= 1) {
fprintf(stderr, "server: please provide a path\n\n");
show_usage(true);
interrupt_set_exit_flag();
}
else {
char *path = argv[1];
server(path, &opts);
}
}

8
src/subcommand/server.h Normal file
View File

@ -0,0 +1,8 @@
#include "../../include/server.h"
#include "../opts.h"
#include "../usage.h"
#include "../interrupt.h"
#include "auth.h"
#include <stdint.h>
void server_subcommand(opts opts, int argc, char *argv[]);

View File

@ -1,14 +1,17 @@
#include "usage.h"
const char server_callstring[] = "server [-j] <path>";
const char server_callstring[] = "server [-h] [-u] [-a] <path>";
const char server_helpstring[] =
" <path>: filesystem path at which to create server Unix domain socket\n"
" -j, --jwt: run server in JWT mode\n";
" -u, --username: include username in request output\n"
" -a, --auth: run server in auth mode (implies --username)\n"
" -h, --help: show usage message\n";
const char client_callstring[] = "client <path> <message>";
const char client_callstring[] = "client [-h] <path> [<message>]";
const char client_helpstring[] =
" <path>: filesystem path of target servers socket\n"
" <message>: string to send to server\n";
" <message>: string to send to server\n"
" -h, --help: show usage message\n";
const char *my_name = NULL;
@ -19,12 +22,20 @@ void set_exe_name(char *argv0) {
}
}
void show_usage(FILE *file, usage_type usage) {
usage_type usage_context = usage_both;
void set_usage_context(usage_type usage) {
usage_context = usage;
}
void show_usage(int is_error) {
if (my_name == NULL) {
my_name = "localserv";
}
if (usage == server_usage) {
FILE *file = is_error ? stderr : stdout;
if (usage_context == usage_server) {
fprintf(
file,
"usage: %s %s\n\n%s\n",
@ -32,7 +43,7 @@ void show_usage(FILE *file, usage_type usage) {
server_callstring,
server_helpstring
);
} else if (usage == client_usage) {
} else if (usage_context == usage_client) {
fprintf(
file,
"usage: %s %s\n\n%s\n",
@ -41,7 +52,7 @@ void show_usage(FILE *file, usage_type usage) {
client_helpstring
);
} else { // usage == both_usage
} else { // usage_context == usage_both
fprintf(
file,
"usage:\n"
@ -58,5 +69,4 @@ void show_usage(FILE *file, usage_type usage) {
client_helpstring
);
}
}

View File

@ -1,7 +1,13 @@
#include <libgen.h>
#include <stdio.h>
typedef enum { server_usage, client_usage, both_usage } usage_type;
#ifndef __USAGE_H
#define __USAGE_H
typedef enum { usage_server, usage_client, usage_both } usage_type;
void set_exe_name(char *argv0);
void show_usage(FILE *file, usage_type usage);
void set_usage_context(usage_type usage);
void show_usage(int is_error);
#endif