Implement authorization #1

Merged
ThoNohT merged 1 commits from userauth into master 2026-06-12 06:46:30 +00:00
5 changed files with 189 additions and 125 deletions

View File

@@ -1,5 +1,3 @@
#include "path.c"
typedef struct { typedef struct {
bool is_dir; bool is_dir;
SV name; SV name;

View File

@@ -11,7 +11,7 @@ static Noh_String build_path_(bool url, Noh_String_View *first, ...) {
Noh_String_View elem = *elem_; Noh_String_View elem = *elem_;
while (elem.count > 0) { while (elem.count > 0) {
Noh_String_View component = noh_sv_chop_by_delim(&elem, '/'); Noh_String_View component = noh_sv_chop_by_delim(&elem, '/');
if (noh_sv_eq(component, sv(".."))) continue; if (noh_sv_eq(component, noh_sv_from_cstr(".."))) continue;
if (component.count > 0) { if (component.count > 0) {
if (start) start = false; else noh_string_append_cstr(&result, "/"); if (start) start = false; else noh_string_append_cstr(&result, "/");

View File

@@ -5,25 +5,82 @@
#define NOH_IMPLEMENTATION #define NOH_IMPLEMENTATION
#include "noh.h" #include "noh.h"
#define NO_AUTH Noh_Arena temp;
#include "path.c"
#include "users.c"
#define SV Noh_String_View
#define sv noh_sv_from_cstr
/* To disable Basic auth for testing: /* To disable Basic auth for testing:
#define NO_AUTH #define NO_AUTH
This will ignore any auth headers and always log in with username demo. This will ignore any auth headers and always log in with username demo.
*/ */
#define SV Noh_String_View
#define sv noh_sv_from_cstr
Noh_Arena temp;
#include "id_store.c"
#include "library.c"
typedef struct { typedef struct {
SV base_path; SV base_path;
SV library_name; SV library_name;
} Settings; } Settings;
// Authorizes the username from the Basic authorization info in the provided connection.
// The username data is guaranteed to live as long as the connection lives.
static bool authorize_user(struct MHD_Connection *connection, SV *username, Settings *settings) {
#ifdef NO_AUTH
(void)connection;
*username = sv("demo");
return false;
#else
bool result = true;
struct MHD_BasicAuthInfo *auth = MHD_basic_auth_get_username_password3(connection);
if (!auth) return false;
SV provided_username = { .elems = auth->username, .count = auth->username_len };
SV provided_password = { .elems = auth->password, .count = auth->password_len };
// Read the users file.
noh_arena_save(&temp);
Noh_String_View file_name = noh_sv_from_cstr("users.conf");
Noh_String users_file_path = build_path(false, &settings->base_path, &file_name);
noh_string_append_null(&users_file_path);
Noh_String hashed = {0};
Users users = {0};
Noh_String users_file = {0};
if (!read_users_file(users_file_path.elems, &users_file, &users)) noh_return_defer(false);
// Find the user in the users list.
bool found_user = false;
User *user = NULL;
for (size_t i = 0; i < users.count && !found_user; i++) {
user = &users.elems[i];
if (noh_sv_eq(provided_username, user->username)) found_user = true;
}
if (!found_user) noh_return_defer(false);
// Hash the provided password.
hashed = hash_password(provided_username, provided_password);
// Check the hash.
if (!noh_sv_eq(noh_sv_from_string(hashed), user->hash)) noh_return_defer(false);
*username = provided_username;
defer:
noh_string_free(&hashed);
noh_da_free(&users);
noh_string_free(&users_file_path);
noh_string_free(&users_file);
noh_arena_reset(&temp);
return result;
#endif
}
static enum MHD_Result send_response(struct MHD_Connection *connection, int status, SV message, bool xml) { static enum MHD_Result send_response(struct MHD_Connection *connection, int status, SV message, bool xml) {
struct MHD_Response *response = MHD_create_response_from_buffer( struct MHD_Response *response = MHD_create_response_from_buffer(
message.count, (void*)message.elems, MHD_RESPMEM_MUST_COPY); message.count, (void*)message.elems, MHD_RESPMEM_MUST_COPY);
@@ -47,6 +104,8 @@ static enum MHD_Result send_epub_file(struct MHD_Connection *connection, int fil
return ret; return ret;
} }
#include "id_store.c"
#include "library.c"
#include "opds_handler.c" #include "opds_handler.c"
#include "get_handler.c" #include "get_handler.c"
@@ -65,20 +124,7 @@ static enum MHD_Result request_handler(
noh_log(NOH_INFO, "Request: %s %s %s", version, method, _url); noh_log(NOH_INFO, "Request: %s %s %s", version, method, _url);
SV username; SV username;
#ifdef NO_AUTH if (!authorize_user(connection, &username, &settings)) return send_response(connection, 401, sv(""), false);
username = sv("demo");
#else
struct MHD_BasicAuthInfo *auth = MHD_basic_auth_get_username_password3(connection);
if (!auth) return send_response(connection, 401, sv(""), false);
username = (SV){ .elems = auth->username, .count = auth->username_len };
SV password = { .elems = auth->password, .count = auth->password_len };
// TODO: Read username from user config.
if (!noh_sv_eq(password, sv("jaja"))) return send_response(connection, 401, sv(""), false);
#endif
noh_log(NOH_INFO, "Authenticated as "Nsv_Fmt, Nsv_Arg(username));
if (strcmp(method, "GET") != 0) return send_response(connection, 405, sv(""), false); if (strcmp(method, "GET") != 0) return send_response(connection, 405, sv(""), false);
@@ -87,7 +133,7 @@ static enum MHD_Result request_handler(
SV controller = noh_sv_chop_by_delim(&url, '/'); SV controller = noh_sv_chop_by_delim(&url, '/');
if (noh_sv_eq_ci(controller, sv("opds"))) return handle_opds(connection, url, settings, username); if (noh_sv_eq_ci(controller, sv("opds"))) return handle_opds(connection, url, settings, username);
if (noh_sv_eq_ci(controller, sv("get"))) return handle_get(connection, url, settings, username); if ( noh_sv_eq_ci(controller, sv("get"))) return handle_get(connection, url, settings, username);
return send_response(connection, 404, sv(""), false); return send_response(connection, 404, sv(""), false);
} }

View File

@@ -1,15 +1,15 @@
#include <stdio.h> #include <stdio.h>
#include <sha256.h>
#include <dirent.h> #include <dirent.h>
#define NOH_IMPLEMENTATION #define NOH_IMPLEMENTATION
#include "noh.h" #include "noh.h"
#define SV Noh_String_View Noh_Arena temp;
#define sv noh_sv_from_cstr
#include "path.c" #include "path.c"
#include "users.c"
Noh_Arena temp; #define SV Noh_String_View
#define sv noh_sv_from_cstr
static void show_usage(char *program_name, char *error) { static void show_usage(char *program_name, char *error) {
noh_log(NOH_INFO, "Usage:"); noh_log(NOH_INFO, "Usage:");
@@ -35,42 +35,6 @@ static void show_usage(char *program_name, char *error) {
exit(1); exit(1);
} }
Noh_String hash_password(Noh_String_View username, Noh_String_View password) {
// Build the sha input string.
Noh_String input = {0};
noh_string_append_sv(&input, username);
noh_string_append_cstr(&input, "<><>");
noh_string_append_sv(&input, password);
// Calculate the hash.
SHA256_CTX ctx = {0};
BYTE hash[32];
sha256_init(&ctx);
sha256_update(&ctx, (BYTE*)input.elems, input.count);
sha256_final(&ctx, hash);
// Build the result string.
noh_arena_save(&temp);
Noh_String result = {0};
for (int i = 0; i < 32; i++) {
noh_string_append_cstr(&result, noh_arena_sprintf(&temp, "%02x", hash[i]));
}
noh_arena_reset(&temp);
return result;
}
typedef struct {
Noh_String_View username;
Noh_String_View hash;
} User;
typedef struct {
User *elems;
size_t count;
size_t capacity;
} Users;
bool remove_directory(char *path) { bool remove_directory(char *path) {
noh_arena_save(&temp); noh_arena_save(&temp);
bool result = true; bool result = true;
@@ -103,55 +67,6 @@ bool remove_directory(char *path) {
return result; return result;
} }
bool read_users_file(char *file_path, Users *users) {
Noh_String users_file = {0};
noh_log(NOH_INFO, "Reading users file: %s", file_path);
if (!noh_file_exists(file_path)) return true;
if (!noh_string_read_file(&users_file, file_path)) return false;
Noh_String_View users_file_sv = noh_sv_from_string(users_file);
Noh_String_View line = {0};
for (size_t i = 0; users_file_sv.count > 0; i++) {
line = noh_sv_chop_line(&users_file_sv);
// Ignore empty lines.
noh_sv_trim_space(&line);
if (line.count == 0) continue;
Noh_String_View un = noh_sv_chop_by_delim(&line, ':');
if (line.count == 0) {
noh_log(NOH_ERROR, "%s %zu: Line not in username:hash format.", file_path, i);
noh_string_free(&users_file);
return false;
}
noh_da_append(users, ((User){ .username = un, .hash = line }));
}
return true;
}
bool write_users_file(char *file_path, Users *users, User *user_to_skip) {
Noh_String users_file = {0};
for (size_t i = 0; i < users->count; i++) {
User *u = &users->elems[i];
if (u == user_to_skip) continue;
noh_string_append_sv(&users_file, u->username);
noh_string_append_cstr(&users_file, ":");
noh_string_append_sv(&users_file, u->hash);
noh_string_append_cstr(&users_file, "\n");
}
bool result = noh_string_write_file(&users_file, file_path);
noh_string_free(&users_file);
return result;
}
/// Prompts a user with a question and returns their response. /// Prompts a user with a question and returns their response.
bool prompt_user(char *question) { bool prompt_user(char *question) {
noh_arena_save(&temp); noh_arena_save(&temp);
@@ -168,18 +83,22 @@ bool prompt_user(char *question) {
} }
bool register_user(Noh_String_View library_path, Noh_String_View username, Noh_String_View password) { bool register_user(Noh_String_View library_path, Noh_String_View username, Noh_String_View password) {
bool result = true;
// Read the file. // Read the file.
Noh_String_View file_name = noh_sv_from_cstr("users.conf"); Noh_String_View file_name = noh_sv_from_cstr("users.conf");
Noh_String users_file_path = build_path(false, &library_path, &file_name); Noh_String users_file_path = build_path(false, &library_path, &file_name);
noh_string_append_null(&users_file_path); noh_string_append_null(&users_file_path);
// Build users map. // Build users map.
Noh_String hashed = {0};
Users users = {0}; Users users = {0};
if (!read_users_file(users_file_path.elems, &users)) return false; Noh_String users_file = {0};
bool result = true; Noh_String user_folder_path = {0};
if (!read_users_file(users_file_path.elems, &users_file, &users)) noh_return_defer(false);
// Hash the password. // Hash the password.
Noh_String hashed = hash_password(username, password); hashed = hash_password(username, password);
// Find the user in the map of users. // Find the user in the map of users.
for (size_t i = 0; i < users.count; i++) { for (size_t i = 0; i < users.count; i++) {
@@ -197,7 +116,7 @@ bool register_user(Noh_String_View library_path, Noh_String_View username, Noh_S
if (!write_users_file(users_file_path.elems, &users, NULL)) noh_return_defer(false); if (!write_users_file(users_file_path.elems, &users, NULL)) noh_return_defer(false);
// Create user folder if needed. // Create user folder if needed.
Noh_String user_folder_path = build_path(false, &library_path, &username); user_folder_path = build_path(false, &library_path, &username);
noh_string_append_null(&user_folder_path); noh_string_append_null(&user_folder_path);
if (!noh_mkdir_if_needed(user_folder_path.elems)) noh_return_defer(false); if (!noh_mkdir_if_needed(user_folder_path.elems)) noh_return_defer(false);
@@ -210,25 +129,32 @@ bool register_user(Noh_String_View library_path, Noh_String_View username, Noh_S
if (!write_users_file(users_file_path.elems, &users, NULL)) noh_return_defer(false); if (!write_users_file(users_file_path.elems, &users, NULL)) noh_return_defer(false);
// Create user folder if needed. // Create user folder if needed.
Noh_String user_folder_path = build_path(false, &library_path, &username); user_folder_path = build_path(false, &library_path, &username);
noh_string_append_null(&user_folder_path); noh_string_append_null(&user_folder_path);
if (!noh_mkdir_if_needed(user_folder_path.elems)) noh_return_defer(false); if (!noh_mkdir_if_needed(user_folder_path.elems)) noh_return_defer(false);
defer: defer:
noh_string_free(&users_file_path);
noh_string_free(&user_folder_path);
noh_da_free(&users); noh_da_free(&users);
noh_string_free(&users_file);
noh_string_free(&hashed);
return result; return result;
} }
bool delete_user(Noh_String_View library_path, Noh_String_View username) { bool delete_user(Noh_String_View library_path, Noh_String_View username) {
bool result = true;
// Read the file. // Read the file.
Noh_String_View file_name = noh_sv_from_cstr("users.conf"); Noh_String_View file_name = noh_sv_from_cstr("users.conf");
Noh_String users_file_path = build_path(false, &library_path, &file_name); Noh_String users_file_path = build_path(false, &library_path, &file_name);
noh_string_append_null(&users_file_path); noh_string_append_null(&users_file_path);
// Build users map. // Build users map.
Noh_String user_folder_path = {0};
Users users = {0}; Users users = {0};
if (!read_users_file(users_file_path.elems, &users)) return false; Noh_String users_file = {0};
bool result = true; if (!read_users_file(users_file_path.elems, &users_file, &users)) noh_return_defer(false);
// Check if the user exists. // Check if the user exists.
User *user_to_delete = NULL; User *user_to_delete = NULL;
@@ -256,7 +182,7 @@ bool delete_user(Noh_String_View library_path, Noh_String_View username) {
if (!write_users_file(users_file_path.elems, &users, user_to_delete)) noh_return_defer(false); if (!write_users_file(users_file_path.elems, &users, user_to_delete)) noh_return_defer(false);
// Prompt to delete the user's library folder. // Prompt to delete the user's library folder.
Noh_String user_folder_path = build_path(false, &library_path, &username); user_folder_path = build_path(false, &library_path, &username);
noh_string_append_null(&user_folder_path); noh_string_append_null(&user_folder_path);
if (!noh_file_exists(user_folder_path.elems)) noh_return_defer(true); if (!noh_file_exists(user_folder_path.elems)) noh_return_defer(true);
@@ -266,7 +192,10 @@ bool delete_user(Noh_String_View library_path, Noh_String_View username) {
} }
defer: defer:
noh_string_free(&user_folder_path);
noh_string_free(&users_file_path);
noh_da_free(&users); noh_da_free(&users);
noh_string_free(&users_file);
return result; return result;
} }
@@ -278,11 +207,13 @@ bool list_users(Noh_String_View library_path) {
// Build users map. // Build users map.
Users users = {0}; Users users = {0};
if (!read_users_file(users_file_path.elems, &users)) return false; Noh_String users_file = {0};
if (!read_users_file(users_file_path.elems, &users_file, &users)) return false;
if (users.count == 0) { if (users.count == 0) {
noh_log(NOH_INFO, "No users registered."); noh_log(NOH_INFO, "No users registered.");
noh_da_free(&users); noh_da_free(&users);
noh_string_free(&users_file);
return true; return true;
} }
@@ -294,7 +225,9 @@ bool list_users(Noh_String_View library_path) {
} }
noh_arena_reset(&temp); noh_arena_reset(&temp);
noh_string_free(&users_file_path);
noh_da_free(&users); noh_da_free(&users);
noh_string_free(&users_file);
return true; return true;
} }

87
src/users.c Normal file
View File

@@ -0,0 +1,87 @@
#include <sha256.h>
typedef struct {
Noh_String_View username;
Noh_String_View hash;
} User;
typedef struct {
User *elems;
size_t count;
size_t capacity;
} Users;
Noh_String hash_password(Noh_String_View username, Noh_String_View password) {
// Build the sha input string.
Noh_String input = {0};
noh_string_append_sv(&input, username);
noh_string_append_cstr(&input, "<><>");
noh_string_append_sv(&input, password);
// Calculate the hash.
SHA256_CTX ctx = {0};
BYTE hash[32];
sha256_init(&ctx);
sha256_update(&ctx, (BYTE*)input.elems, input.count);
sha256_final(&ctx, hash);
// Build the result string.
noh_arena_save(&temp);
Noh_String result = {0};
for (int i = 0; i < 32; i++) {
noh_string_append_cstr(&result, noh_arena_sprintf(&temp, "%02x", hash[i]));
}
noh_arena_reset(&temp);
return result;
}
// Reads the users file into users_file. Free this string when the data is no longer needed.
// Note that the string needs to be initialized and empty before calling this function.
// The users struct will contain string views referencing the data in this string.
bool read_users_file(char *file_path, Noh_String *users_file, Users *users) {
noh_log(NOH_INFO, "Reading users file: %s", file_path);
if (!noh_file_exists(file_path)) return true;
if (!noh_string_read_file(users_file, file_path)) return false;
Noh_String_View users_file_sv = noh_sv_from_string(*users_file);
Noh_String_View line = {0};
for (size_t i = 0; users_file_sv.count > 0; i++) {
line = noh_sv_chop_line(&users_file_sv);
// Ignore empty lines.
noh_sv_trim_space(&line);
if (line.count == 0) continue;
Noh_String_View un = noh_sv_chop_by_delim(&line, ':');
if (line.count == 0) {
noh_log(NOH_ERROR, "%s %zu: Line not in username:hash format.", file_path, i);
noh_string_free(users_file);
return false;
}
noh_da_append(users, ((User){ .username = un, .hash = line }));
}
return true;
}
bool write_users_file(char *file_path, Users *users, User *user_to_skip) {
Noh_String users_file = {0};
for (size_t i = 0; i < users->count; i++) {
User *u = &users->elems[i];
if (u == user_to_skip) continue;
noh_string_append_sv(&users_file, u->username);
noh_string_append_cstr(&users_file, ":");
noh_string_append_sv(&users_file, u->hash);
noh_string_append_cstr(&users_file, "\n");
}
bool result = noh_string_write_file(&users_file, file_path);
noh_string_free(&users_file);
return result;
}