Move the auth handler out of JS. #7

This commit is contained in:
Cory McWilliams 2024-03-31 16:15:50 -04:00
parent 9ce30dee70
commit b04eccdbda
11 changed files with 872 additions and 242 deletions

View File

@ -1,8 +1,6 @@
import * as core from './core.js';
import * as form from './form.js';
let gDatabase = new Database('auth');
const kRefreshInterval = 1 * 7 * 24 * 60 * 60 * 1000;
/**
@ -106,60 +104,6 @@ function readSession(session) {
}
}
/**
* Check the provided password matches the hash
* @param {string} password
* @param {string} hash bcrypt hash
* @returns true if the password matches the hash
*/
function verifyPassword(password, hash) {
return bCrypt.hashpw(password, hash) === hash;
}
/**
* Hashes a password
* @param {string} password
* @returns {string} TODOC
*/
function hashPassword(password) {
let salt = bCrypt.gensalt(12);
return bCrypt.hashpw(password, salt);
}
/**
* Check if there is an administrator on the instance
* @returns TODOC
*/
function noAdministrator() {
return (
!core.globalSettings ||
!core.globalSettings.permissions ||
!Object.keys(core.globalSettings.permissions).some(function (name) {
return (
core.globalSettings.permissions[name].indexOf('administration') != -1
);
})
);
}
/**
* Makes a user an administrator
* @param {string} name the user's name
*/
function makeAdministrator(name) {
if (!core.globalSettings.permissions) {
core.globalSettings.permissions = {};
}
if (!core.globalSettings.permissions[name]) {
core.globalSettings.permissions[name] = [];
}
if (core.globalSettings.permissions[name].indexOf('administration') == -1) {
core.globalSettings.permissions[name].push('administration');
}
core.setGlobalSettings(core.globalSettings);
}
/**
* TODOC
* @param {*} headers most likely an object
@ -181,175 +125,6 @@ function getCookies(headers) {
return cookies;
}
/**
* Validates a username
* @param {string} name
* @returns false | boolean[] ?
*/
function isNameValid(name) {
// TODO(tasiaiso): convert this into a regex
let c = name.charAt(0);
return (
((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) &&
name
.split()
.map(
(x) =>
x >= ('a' && x <= 'z') ||
x >= ('A' && x <= 'Z') ||
x >= ('0' && x <= '9')
)
);
}
/**
* Request handler ?
* @param {*} request TODOC
* @param {*} response
* @returns
*/
function handler(request, response) {
// TODO(tasiaiso): split this function
let session = getCookies(request.headers).session;
if (request.uri == '/login') {
let formData = form.decodeForm(request.query);
if (query(request.headers)?.permissions?.authenticated) {
if (formData.return) {
response.writeHead(303, {Location: formData.return});
} else {
response.writeHead(303, {
Location:
(request.client.tls ? 'https://' : 'http://') +
request.headers.host +
'/',
'Content-Length': '0',
});
}
response.end();
return;
}
let sessionIsNew = false;
let loginError;
if (request.method == 'POST' || formData.submit) {
sessionIsNew = true;
formData = form.decodeForm(utf8Decode(request.body), formData);
if (formData.submit == 'Login') {
let account = gDatabase.get('user:' + formData.name);
account = account ? JSON.parse(account) : account;
if (formData.register == '1') {
if (
!account &&
isNameValid(formData.name) &&
formData.password == formData.confirm
) {
let users = new Set();
let users_original = gDatabase.get('users');
try {
users = new Set(JSON.parse(users_original));
} catch {}
if (!users.has(formData.name)) {
users.add(formData.name);
}
users = JSON.stringify([...users].sort());
if (users !== users_original) {
gDatabase.set('users', users);
}
session = makeJwt({name: formData.name});
account = {password: hashPassword(formData.password)};
gDatabase.set('user:' + formData.name, JSON.stringify(account));
if (noAdministrator()) {
makeAdministrator(formData.name);
}
} else {
loginError = 'Error registering account.';
}
} else if (formData.change == '1') {
if (
account &&
isNameValid(formData.name) &&
formData.new_password == formData.confirm &&
verifyPassword(formData.password, account.password)
) {
session = makeJwt({name: formData.name});
account = {password: hashPassword(formData.new_password)};
gDatabase.set('user:' + formData.name, JSON.stringify(account));
} else {
loginError = 'Error changing password.';
}
} else {
if (
account &&
account.password &&
verifyPassword(formData.password, account.password)
) {
session = makeJwt({name: formData.name});
if (noAdministrator()) {
makeAdministrator(formData.name);
}
} else {
loginError = 'Invalid username or password.';
}
}
} else {
// Proceed as Guest
session = makeJwt({name: 'guest'});
}
}
let cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; HttpOnly`;
let entry = readSession(session);
if (entry && formData.return) {
response.writeHead(303, {
Location: formData.return,
'Set-Cookie': cookie,
});
response.end();
} else {
File.readFile('core/auth.html')
.then(function (data) {
let html = utf8Decode(data);
let auth_data = {
session_is_new: sessionIsNew,
name: entry?.name,
error: loginError,
code_of_conduct: core.globalSettings.code_of_conduct,
have_administrator: !noAdministrator(),
};
html = utf8Encode(
html.replace('$AUTH_DATA', JSON.stringify(auth_data))
);
response.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
'Set-Cookie': cookie,
'Content-Length': html.length,
});
response.end(html);
})
.catch(function (error) {
response.writeHead(404, {
'Content-Type': 'text/plain; charset=utf-8',
Connection: 'close',
});
response.end('404 File not found');
});
}
} else if (request.uri == '/login/logout') {
response.writeHead(303, {
'Set-Cookie': `session=; path=/; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly`,
Location: '/login' + (request.query ? '?' + request.query : ''),
});
response.end();
} else {
response.writeHead(200, {
'Content-Type': 'text/plain; charset=utf-8',
Connection: 'close',
});
response.end('Hello, ' + request.client.peerName + '.');
}
}
/**
* Gets a user's permissions based on it's session ?
* @param {*} session TODOC
@ -417,4 +192,4 @@ function makeRefresh(credentials) {
}
}
export {handler, query, makeRefresh};
export {query, makeRefresh};

View File

@ -1334,8 +1334,6 @@ loadSettings()
if (tildefriends.https_port && gGlobalSettings.http_redirect) {
httpd.set_http_redirect(gGlobalSettings.http_redirect);
}
httpd.all('/login', auth.handler);
httpd.all('/login/logout', auth.handler);
httpd.all('/app/socket', app.socket);
httpd.all('', function default_http_handler(request, response) {
let match;

View File

@ -1030,3 +1030,48 @@ void* tf_http_get_user_data(tf_http_t* http)
{
return http->user_data;
}
const char* tf_http_get_cookie(const char* cookie_header, const char* name)
{
if (!cookie_header)
{
return NULL;
}
int name_start = 0;
int equals = 0;
for (int i = 0; ; i++)
{
if (cookie_header[i] == '=')
{
equals = i;
}
else if (cookie_header[i] == ',' || cookie_header[i] == ';' || cookie_header[i] == '\0')
{
if (equals > name_start &&
strncmp(cookie_header + name_start, name, equals - name_start) == 0 &&
(int)strlen(name) == equals - name_start)
{
int length = i - equals - 1;
char* result = tf_malloc(length + 1);
memcpy(result, cookie_header + equals + 1, length);
result[length] = '\0';
return result;
}
if (cookie_header[i] == '\0')
{
break;
}
else
{
name_start = i + 1;
while (cookie_header[name_start] == ' ')
{
name_start++;
}
}
}
}
return NULL;
}

View File

@ -196,6 +196,15 @@ void tf_http_request_unref(tf_http_request_t* request);
*/
const char* tf_http_request_get_header(tf_http_request_t* request, const char* name);
/**
** Get a cookie value from request headers.
** @param cookie_header The value of the "Cookie" header of the form
** "name1=value1; name2=value2".
** @param name The cookie name.
** @return The cookie value, if found, or NULL. Must be freed with tf_free().
*/
const char* tf_http_get_cookie(const char* cookie_header, const char* name);
/**
** Send a websocket message.
** @param request The HTTP request which was previously updated to a websocket

View File

@ -4,16 +4,26 @@
#include "http.h"
#include "log.h"
#include "mem.h"
#include "ssb.h"
#include "ssb.db.h"
#include "task.h"
#include "tlscontext.js.h"
#include "trace.h"
#include "util.js.h"
#include "ow-crypt.h"
#include "picohttpparser.h"
#include "sodium/crypto_sign.h"
#include "sodium/utils.h"
#include "sqlite3.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <openssl/sha.h>
@ -23,6 +33,8 @@
#define tf_countof(a) ((int)(sizeof((a)) / sizeof(*(a))))
const int64_t k_refresh_interval = 1ULL * 7 * 24 * 60 * 60 * 1000;
static JSValue _httpd_websocket_upgrade(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static JSClassID _httpd_class_id;
@ -732,6 +744,746 @@ static void _httpd_endpoint_debug(tf_http_request_t* request)
tf_free(response);
}
const char** _form_data_decode(const char* data, int length)
{
int key_max = 1;
for (int i = 0; i < length; i++)
{
if (data[i] == '&')
{
key_max++;
}
}
int write_length = length + 1;
char** result = tf_malloc(sizeof(const char*) * (key_max + 1) * 2 + write_length);
char* result_buffer = ((char*)result) + sizeof(const char*) * (key_max + 1) * 2;
char* write_pos = result_buffer;
int count = 0;
int i = 0;
while (i < length)
{
result[count++] = write_pos;
while (i < length)
{
if (data[i] == '+')
{
*write_pos++ = ' ';
i++;
}
else if (data[i] == '%' && i + 2 < length)
{
*write_pos++ = (char)strtoul((const char[]) { data[i + 1], data[i + 2], 0 }, NULL, 16);
i += 3;
}
else if (data[i] == '=')
{
if (count % 2 == 0)
{
result[count++] = "";
}
i++;
break;
}
else if (data[i] == '&')
{
if (count % 2 != 0)
{
result[count++] = "";
}
i++;
break;
}
else
{
*write_pos++ = data[i++];
}
}
*write_pos++ = '\0';
}
result[count++] = NULL;
result[count++] = NULL;
return (const char**)result;
}
const char* _form_data_get(const char** form_data, const char* key)
{
for (int i = 0; form_data[i]; i += 2)
{
if (form_data[i] && strcmp(form_data[i], key) == 0)
{
return form_data[i + 1];
}
}
return NULL;
}
typedef struct _login_request_t
{
tf_http_request_t* request;
const char* session_cookie;
JSValue jwt;
const char* name;
const char* error;
const char* code_of_conduct;
bool have_administrator;
bool session_is_new;
} login_request_t;
static void _httpd_endpoint_login_file_read_callback(tf_task_t* task, const char* path, int result, const void* data, void* user_data)
{
login_request_t* login = user_data;
tf_http_request_t* request = login->request;
if (result >= 0)
{
const char* k_pattern = "session=%s; path=/; Max-Age=%" PRId64 "; %sSameSite=Strict; HttpOnly";
int length = login->session_cookie ? snprintf(NULL, 0, k_pattern, login->session_cookie, k_refresh_interval, request->is_tls ? "Secure; " : "") : 0;
char* cookie = length ? tf_malloc(length + 1) : NULL;
if (cookie)
{
snprintf(cookie, length + 1, k_pattern, login->session_cookie, k_refresh_interval, request->is_tls ? "Secure; " : "");
}
const char* headers[] =
{
"Content-Type", "text/html; charset=utf-8",
"Set-Cookie", cookie ? cookie : "",
};
const char* replace_me = "$AUTH_DATA";
const char* auth = strstr(data, replace_me);
if (auth)
{
JSContext* context = tf_task_get_context(task);
JSValue object = JS_NewObject(context);
JS_SetPropertyStr(context, object, "session_is_new", JS_NewBool(context, login->session_is_new));
JS_SetPropertyStr(context, object, "name", login->name ? JS_NewString(context, login->name) : JS_UNDEFINED);
JS_SetPropertyStr(context, object, "error", login->error ? JS_NewString(context, login->error) : JS_UNDEFINED);
JS_SetPropertyStr(context, object, "code_of_conduct", login->code_of_conduct ? JS_NewString(context, login->code_of_conduct) : JS_UNDEFINED);
JS_SetPropertyStr(context, object, "have_administrator", JS_NewBool(context, login->have_administrator));
JSValue object_json = JS_JSONStringify(context, object, JS_NULL, JS_NULL);
size_t json_length = 0;
const char* json = JS_ToCStringLen(context, &json_length, object_json);
char* copy = tf_malloc(result + json_length);
int replace_start = (auth - (const char*)data);
int replace_end = (auth - (const char*)data) + (int)strlen(replace_me);
memcpy(copy, data, replace_start);
memcpy(copy + replace_start, json, json_length);
memcpy(copy + replace_start + json_length, ((const char*)data) + replace_end, result - replace_end);
tf_http_respond(request, 200, headers, tf_countof(headers) / 2, copy, replace_start + json_length + (result - replace_end));
tf_free(copy);
JS_FreeCString(context, json);
JS_FreeValue(context, object_json);
JS_FreeValue(context, object);
}
else
{
tf_http_respond(request, 200, headers, tf_countof(headers) / 2, data, result);
}
tf_free(cookie);
}
else
{
const char* k_payload = tf_http_status_text(404);
tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
}
tf_http_request_unref(request);
tf_free((void*)login->name);
tf_free((void*)login->code_of_conduct);
tf_free((void*)login->session_cookie);
tf_free(login);
}
static bool _string_property_equals(JSContext* context, JSValue object, const char* name, const char* value)
{
JSValue object_value = JS_GetPropertyStr(context, object, name);
const char* object_value_string = JS_ToCString(context, object_value);
bool equals = object_value_string && strcmp(object_value_string, value) == 0;
JS_FreeCString(context, object_value_string);
JS_FreeValue(context, object_value);
return equals;
}
static void _public_key_visit(const char* identity, void* user_data)
{
snprintf(user_data, k_id_base64_len, "%s", identity);
}
static JSValue _authenticate_jwt(JSContext* context, const char* jwt)
{
if (!jwt)
{
return JS_UNDEFINED;
}
int dot[2] = { 0 };
int dot_count = 0;
for (int i = 0; jwt[i]; i++)
{
if (jwt[i] == '.')
{
if (dot_count >= tf_countof(dot))
{
return JS_UNDEFINED;
}
dot[dot_count++] = i;
}
}
if (dot_count != 2)
{
return JS_UNDEFINED;
}
uint8_t header[256];
size_t actual_length = 0;
if (sodium_base642bin(header, sizeof(header), jwt, dot[0], NULL, &actual_length, NULL, sodium_base64_VARIANT_URLSAFE_NO_PADDING) != 0)
{
return JS_UNDEFINED;
}
JSValue header_value = JS_ParseJSON(context, (const char*)header, actual_length, NULL);
bool header_valid =
_string_property_equals(context, header_value, "typ", "JWT") &&
_string_property_equals(context, header_value, "alg", "HS256");
JS_FreeValue(context, header_value);
if (!header_valid)
{
return JS_UNDEFINED;
}
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
char public_key_b64[k_id_base64_len] = { 0 };
tf_ssb_db_identity_visit(ssb, ":auth", _public_key_visit, public_key_b64);
const char* payload = jwt + dot[0] + 1;
size_t payload_length = dot[1] - dot[0];
if (!tf_ssb_hmacsha256_verify(public_key_b64, payload, payload_length, jwt + dot[1] + 1))
{
return JS_UNDEFINED;
}
uint8_t payload_bin[256];
size_t actual_payload_length = 0;
if (sodium_base642bin(payload_bin, sizeof(payload_bin), payload, payload_length, NULL, &actual_payload_length, NULL, sodium_base64_VARIANT_URLSAFE_NO_PADDING) != 0)
{
return JS_UNDEFINED;
}
JSValue parsed = JS_ParseJSON(context, (const char*)payload_bin, actual_payload_length, NULL);
JSValue exp = JS_GetPropertyStr(context, parsed, "exp");
int64_t exp_value = 0;
JS_ToInt64(context, &exp_value, exp);
if (time(NULL) >= exp_value)
{
JS_FreeValue(context, parsed);
return JS_UNDEFINED;
}
return parsed;
}
static bool _session_is_authenticated_as_user(JSContext* context, JSValue session)
{
bool result = false;
JSValue user = JS_GetPropertyStr(context, session, "user");
const char* user_string = JS_ToCString(context, user);
result = user_string && strcmp(user_string, "guest") != 0;
JS_FreeCString(context, user_string);
JS_FreeValue(context, user);
return result;
}
static bool _is_name_valid(const char* name)
{
if (!name ||
!((*name >= 'a' && *name <= 'z') || (*name >= 'A' && *name <= 'Z')))
{
return false;
}
for (const char* p = name; *p; p++)
{
bool in_range =
(*p >= 'a' && *p <= 'z') ||
(*p >= 'A' && *p <= 'Z') ||
(*p >= '0' && *p <= '9');
if (!in_range)
{
return false;
}
}
return true;
}
static bool _read_account(tf_ssb_t* ssb, const char* name, char* out_passwd, size_t passwd_size)
{
bool result = false;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare(db, "SELECT value ->> '$.password' FROM properties WHERE id = 'auth' AND key = 'user:' || ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW)
{
snprintf(out_passwd, passwd_size, "%s", (const char*)sqlite3_column_text(statement, 0));
result = true;
}
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_reader(ssb, db);
return result;
}
static bool _set_account_password(JSContext* context, sqlite3* db, const char* name, const char* password)
{
bool result = false;
static const int k_salt_length = 12;
char buffer[16];
tf_task_t* task = tf_task_get(context);
size_t bytes = uv_random(tf_task_get_loop(task), &(uv_random_t) { 0 }, buffer, sizeof(buffer), 0, NULL) == 0 ? sizeof(buffer) : 0;
char output[7 + 22 + 1];
char* salt = crypt_gensalt_rn("$2b$", k_salt_length, buffer, bytes, output, sizeof(output));
char hash_output[7 + 22 + 31 + 1];
char* hash = crypt_rn(password, salt, hash_output, sizeof(hash_output));
JSValue user_entry = JS_NewObject(context);
JS_SetPropertyStr(context, user_entry, "password", JS_NewString(context, hash));
JSValue user_json = JS_JSONStringify(context, user_entry, JS_NULL, JS_NULL);
size_t user_length = 0;
const char* user_string = JS_ToCStringLen(context, &user_length, user_json);
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES ('auth', 'user:' || ?, ?)", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 2, user_string, user_length, NULL) == SQLITE_OK)
{
result = sqlite3_step(statement) == SQLITE_DONE;
}
sqlite3_finalize(statement);
}
JS_FreeCString(context, user_string);
JS_FreeValue(context, user_json);
JS_FreeValue(context, user_entry);
return result;
}
static bool _register_account(tf_ssb_t* ssb, const char* name, const char* password)
{
bool result = false;
JSContext* context = tf_ssb_get_context(ssb);
JSValue users_array = JS_UNDEFINED;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = 'auth' AND key = 'users'", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW)
{
users_array = JS_ParseJSON(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0), NULL);
}
sqlite3_finalize(statement);
}
if (JS_IsUndefined(users_array))
{
users_array = JS_NewArray(context);
}
int length = tf_util_get_length(context, users_array);
JS_SetPropertyUint32(context, users_array, length, JS_NewString(context, name));
JSValue json = JS_JSONStringify(context, users_array, JS_NULL, JS_NULL);
JS_FreeValue(context, users_array);
size_t value_length = 0;
const char* value = JS_ToCStringLen(context, &value_length, json);
if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES ('auth', 'users', ?)", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, value, value_length, NULL) == SQLITE_OK)
{
result = sqlite3_step(statement) == SQLITE_DONE;
}
sqlite3_finalize(statement);
}
JS_FreeCString(context, value);
JS_FreeValue(context, json);
result = result && _set_account_password(context, db, name, password);
tf_ssb_release_db_writer(ssb, db);
return result;
}
static void _visit_auth_identity(const char* identity, void* user_data)
{
if (!*(char*)user_data)
{
snprintf((char*)user_data, k_id_base64_len, "%s", identity);
}
}
static const char* _make_session_jwt(tf_ssb_t* ssb, const char* name)
{
char id[k_id_base64_len] = { 0 };
tf_ssb_db_identity_visit(ssb, ":auth", _visit_auth_identity, id);
if (!*id)
{
uint8_t public_key[crypto_sign_PUBLICKEYBYTES];
uint8_t private_key[crypto_sign_SECRETKEYBYTES];
if (tf_ssb_db_identity_create(ssb, ":auth", public_key, private_key))
{
tf_ssb_id_bin_to_str(id, sizeof(id), public_key);
}
}
if (!*id)
{
return NULL;
}
uint8_t private_key[crypto_sign_SECRETKEYBYTES];
if (!tf_ssb_db_identity_get_private_key(ssb, ":auth", id, private_key, sizeof(private_key)))
{
return NULL;
}
uv_timespec64_t now = { 0 };
uv_clock_gettime(UV_CLOCK_REALTIME, &now);
JSContext* context = tf_ssb_get_context(ssb);
const char* header_json = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
char header_base64[256];
sodium_bin2base64(header_base64, sizeof(header_base64), (uint8_t*)header_json, strlen(header_json), sodium_base64_VARIANT_URLSAFE_NO_PADDING);
JSValue payload = JS_NewObject(context);
JS_SetPropertyStr(context, payload, "name", JS_NewString(context, name));
JS_SetPropertyStr(context, payload, "exp", JS_NewInt64(context, now.tv_sec * 1000 + now.tv_nsec / 1000000LL + k_refresh_interval));
JSValue payload_json = JS_JSONStringify(context, payload, JS_NULL, JS_NULL);
size_t payload_length = 0;
const char* payload_string = JS_ToCStringLen(context, &payload_length, payload_json);
char payload_base64[256];
sodium_bin2base64(payload_base64, sizeof(payload_base64), (uint8_t*)payload_string, payload_length, sodium_base64_VARIANT_URLSAFE_NO_PADDING);
char* result = NULL;
uint8_t signature[crypto_sign_BYTES];
unsigned long long signature_length = 0;
char signature_base64[256] = { 0 };
if (crypto_sign_detached(signature, &signature_length, (const uint8_t*)payload_base64, strlen(payload_base64), private_key) == 0)
{
sodium_bin2base64(signature_base64, sizeof(signature_base64), signature, sizeof(signature), sodium_base64_VARIANT_URLSAFE_NO_PADDING);
size_t size = strlen(header_base64) + 1 + strlen(payload_base64) + 1 + strlen(signature_base64) + 1;
result = tf_malloc(size);
snprintf(result, size, "%s.%s.%s", header_base64, payload_base64, signature_base64);
}
JS_FreeCString(context, payload_string);
JS_FreeValue(context, payload_json);
JS_FreeValue(context, payload);
return result;
}
static bool _verify_password(const char* password, const char* hash)
{
char buffer[7 + 22 + 31 + 1];
const char* out_hash = crypt_rn(password, hash, buffer, sizeof(buffer));
return out_hash && strcmp(hash, out_hash) == 0;
}
static const char* _get_property(tf_ssb_t* ssb, const char* id, const char* key)
{
char* result = NULL;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = ? AND key = ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW)
{
size_t length = sqlite3_column_bytes(statement, 0);
result = tf_malloc(length + 1);
memcpy(result, sqlite3_column_text(statement, 0), length);
result[length] = '\0';
}
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_reader(ssb, db);
return result;
}
static bool _set_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value)
{
bool result = false;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES (?, ?, ?)", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, value, -1, NULL) == SQLITE_OK)
{
result = sqlite3_step(statement) == SQLITE_DONE;
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
return result;
}
static void _httpd_endpoint_login(tf_http_request_t* request)
{
tf_task_t* task = request->user_data;
JSContext* context = tf_task_get_context(task);
tf_ssb_t* ssb = tf_task_get_ssb(task);
const char* session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session");
const char** form_data = _form_data_decode(request->query, request->query ? strlen(request->query) : 0);
const char* account_name_copy = NULL;
JSValue jwt = _authenticate_jwt(context, session);
if (_session_is_authenticated_as_user(context, jwt))
{
const char* return_url = _form_data_get(form_data, "return");
char url[1024];
if (!return_url)
{
snprintf(url, sizeof(url), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host"));
return_url = url;
}
const char* headers[] =
{
"Location", return_url,
};
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
goto done;
}
const char* send_session = tf_strdup(session);
bool session_is_new = false;
const char* login_error = NULL;
bool may_become_first_admin = false;
if (strcmp(request->method, "POST") == 0)
{
session_is_new = true;
const char** post_form_data = _form_data_decode(request->body, request->content_length);
const char* submit = _form_data_get(post_form_data, "submit");
if (submit && strcmp(submit, "Login") == 0)
{
const char* account_name = _form_data_get(post_form_data, "name");
account_name_copy = tf_strdup(account_name);
const char* password = _form_data_get(post_form_data, "password");
const char* new_password = _form_data_get(post_form_data, "new_password");
const char* confirm = _form_data_get(post_form_data, "confirm");
const char* change = _form_data_get(post_form_data, "change");
const char* form_register = _form_data_get(post_form_data, "register");
char account_passwd[256] = { 0 };
bool have_account = _read_account(
ssb,
_form_data_get(post_form_data, "name"),
account_passwd,
sizeof(account_passwd));
if (form_register && strcmp(form_register, "1") == 0)
{
if (!have_account &&
_is_name_valid(account_name) &&
password &&
confirm &&
strcmp(password, confirm) == 0 &&
_register_account(ssb, account_name, password))
{
send_session = _make_session_jwt(ssb, account_name);
may_become_first_admin = true;
}
else
{
login_error = "Error registering account.";
}
}
else if (change && strcmp(change, "1") == 0)
{
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
if (have_account &&
_is_name_valid(account_name) &&
new_password &&
confirm &&
strcmp(new_password, confirm) == 0 &&
_verify_password(password, account_passwd) &&
_set_account_password(context, db, account_name, new_password))
{
send_session = _make_session_jwt(ssb, account_name);
}
else
{
login_error = "Error changing password.";
}
tf_ssb_release_db_writer(ssb, db);
}
else
{
if (have_account && *account_passwd && _verify_password(password, account_passwd))
{
send_session = _make_session_jwt(ssb, account_name);
may_become_first_admin = true;
}
else
{
login_error = "Invalid username or password.";
}
}
}
else
{
send_session = _make_session_jwt(ssb, "guest");
}
tf_free(post_form_data);
}
if (session_is_new && _form_data_get(form_data, "return") && !login_error)
{
const char* return_url = _form_data_get(form_data, "return");
char url[1024];
if (!return_url)
{
snprintf(url, sizeof(url), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host"));
return_url = url;
}
const char* k_pattern = "session=%s; path=/; Max-Age=%" PRId64 "; %sSameSite=Strict; HttpOnly";
int length = send_session ? snprintf(NULL, 0, k_pattern, send_session, k_refresh_interval, request->is_tls ? "Secure; " : "") : 0;
char* cookie = length ? tf_malloc(length + 1) : NULL;
if (cookie)
{
snprintf(cookie, length + 1, k_pattern, send_session, k_refresh_interval, request->is_tls ? "Secure; " : "");
}
const char* headers[] =
{
"Location", return_url,
"Set-Cookie", cookie ? cookie : "",
};
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
tf_free(cookie);
tf_free((void*)send_session);
}
else
{
tf_http_request_ref(request);
const char* settings = _get_property(ssb, "core", "settings");
JSValue settings_value = settings ? JS_ParseJSON(context, settings, strlen(settings), NULL) : JS_UNDEFINED;
JSValue code_of_conduct_value = JS_GetPropertyStr(context, settings_value, "code_of_conduct");
const char* code_of_conduct = JS_ToCString(context, code_of_conduct_value);
bool have_administrator = false;
JSValue permissions = JS_GetPropertyStr(context, settings_value, "permissions");
JSPropertyEnum* ptab = NULL;
uint32_t plen = 0;
JS_GetOwnPropertyNames(context, &ptab, &plen, permissions, JS_GPN_STRING_MASK);
for (int i = 0; i < (int)plen; i++)
{
JSPropertyDescriptor desc = { 0 };
if (JS_GetOwnProperty(context, &desc, permissions, ptab[i].atom) == 1)
{
int permission_length = tf_util_get_length(context, desc.value);
for (int i = 0; i < permission_length; i++)
{
JSValue entry = JS_GetPropertyUint32(context, desc.value, i);
const char* permission = JS_ToCString(context, entry);
if (permission && strcmp(permission, "administration") == 0)
{
have_administrator = true;
}
JS_FreeCString(context, permission);
JS_FreeValue(context, entry);
}
JS_FreeValue(context, desc.setter);
JS_FreeValue(context, desc.getter);
JS_FreeValue(context, desc.value);
}
}
for (uint32_t i = 0; i < plen; ++i)
{
JS_FreeAtom(context, ptab[i].atom);
}
js_free(context, ptab);
if (!have_administrator && may_become_first_admin)
{
if (JS_IsUndefined(permissions))
{
permissions = JS_NewObject(context);
JS_SetPropertyStr(context, settings_value, "permissions", permissions);
}
JSValue user = JS_GetPropertyStr(context, permissions, account_name_copy);
if (JS_IsUndefined(user))
{
user = JS_NewArray(context);
JS_SetPropertyStr(context, permissions, account_name_copy, user);
}
JS_SetPropertyUint32(context, user, tf_util_get_length(context, user), JS_NewString(context, "administration"));
JSValue settings_json = JS_JSONStringify(context, settings_value, JS_NULL, JS_NULL);
const char* settings_string = JS_ToCString(context, settings_json);
_set_property(ssb, "core", "settings", settings_string);
JS_FreeCString(context, settings_string);
JS_FreeValue(context, settings_json);
}
JS_FreeValue(context, permissions);
login_request_t* login = tf_malloc(sizeof(login_request_t));
*login = (login_request_t)
{
.request = request,
.name = account_name_copy,
.jwt = jwt,
.error = login_error,
.session_cookie = send_session,
.session_is_new = session_is_new,
.code_of_conduct = tf_strdup(code_of_conduct),
.have_administrator = have_administrator,
};
JS_FreeCString(context, code_of_conduct);
JS_FreeValue(context, code_of_conduct_value);
JS_FreeValue(context, settings_value);
tf_free((void*)settings);
tf_file_read(request->user_data, "core/auth.html", _httpd_endpoint_login_file_read_callback, login);
jwt = JS_UNDEFINED;
account_name_copy = NULL;
}
done:
tf_free((void*)session);
tf_free(form_data);
tf_free((void*)account_name_copy);
JS_FreeValue(context, jwt);
}
static void _httpd_endpoint_logout(tf_http_request_t* request)
{
const char* k_set_cookie = request->is_tls ?
"session=; path=/; Secure; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly" :
"session=; path=/; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly";
const char* k_location_format = "/login%s%s";
int length = snprintf(NULL, 0, k_location_format, request->query ? "?" : "", request->query);
char* location = alloca(length + 1);
snprintf(location, length + 1, k_location_format, request->query ? "?" : "", request->query ? request->query : "");
const char* headers[] =
{
"Set-Cookie", k_set_cookie,
"Location", location,
};
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
}
void tf_httpd_register(JSContext* context)
{
JS_NewClassID(&_httpd_class_id);
@ -775,6 +1527,9 @@ void tf_httpd_register(JSContext* context)
tf_http_add_handler(http, "/mem", _httpd_endpoint_mem, NULL, task);
tf_http_add_handler(http, "/trace", _httpd_endpoint_trace, NULL, task);
tf_http_add_handler(http, "/login/logout", _httpd_endpoint_logout, NULL, task);
tf_http_add_handler(http, "/login", _httpd_endpoint_login, NULL, task);
JS_SetPropertyStr(context, httpd, "handlers", JS_NewObject(context));
JS_SetPropertyStr(context, httpd, "all", JS_NewCFunction(context, _httpd_endpoint_all, "all", 2));
JS_SetPropertyStr(context, httpd, "start", JS_NewCFunction(context, _httpd_endpoint_start, "start", 2));

View File

@ -3910,3 +3910,29 @@ void tf_ssb_schedule_work(tf_ssb_t* ssb, int delay_ms, void (*callback)(tf_ssb_t
uv_timer_start(&timer->timer, _tf_ssb_scheduled_timer, delay_ms, 0);
uv_unref((uv_handle_t*)&timer->timer);
}
bool tf_ssb_hmacsha256_verify(const char* public_key, const void* payload, size_t payload_length, const char* signature)
{
bool result = false;
const char* public_key_start = public_key && *public_key == '@' ? public_key + 1 : public_key;
const char* public_key_end = public_key_start ? strstr(public_key_start, ".ed25519") : NULL;
if (public_key_start && !public_key_end)
{
public_key_end = public_key_start + strlen(public_key_start);
}
uint8_t bin_public_key[crypto_sign_PUBLICKEYBYTES] = { 0 };
if (tf_base64_decode(public_key_start, public_key_end - public_key_start, bin_public_key, sizeof(bin_public_key)) > 0)
{
uint8_t bin_signature[crypto_sign_BYTES] = { 0 };
if (tf_base64_decode(signature, strlen(signature), bin_signature, sizeof(bin_signature)) > 0)
{
if (crypto_sign_verify_detached(bin_signature, (const uint8_t*)payload, payload_length, bin_public_key) == 0)
{
result = true;
}
}
}
return result;
}

View File

@ -172,7 +172,7 @@ int tf_ssb_db_identity_get_count_for_user(tf_ssb_t* ssb, const char* user);
** @param ssb The SSB instance.
** @param user The user's username.
** @param[out] out_public_key A buffer populated with the new public key.
** @param[out] out_private_key A buffer populated with the new privatee key.
** @param[out] out_private_key A buffer populated with the new private key.
** @return True if the identity was created.
*/
bool tf_ssb_db_identity_create(tf_ssb_t* ssb, const char* user, uint8_t* out_public_key, uint8_t* out_private_key);

View File

@ -957,4 +957,6 @@ void tf_ssb_set_room_name(tf_ssb_t* ssb, const char* room_name);
*/
void tf_ssb_schedule_work(tf_ssb_t* ssb, int delay_ms, void (*callback)(tf_ssb_t* ssb, void* user_data), void* user_data);
bool tf_ssb_hmacsha256_verify(const char* public_key, const void* payload, size_t payload_length, const char* signature);
/** @} */

View File

@ -443,7 +443,7 @@ JSValue tf_taskstub_kill(tf_taskstub_t* stub)
JSValue result = JS_UNDEFINED;
if (!tf_task_get_one_proc(stub->_owner))
{
uv_process_kill(&stub->_process, SIGTERM);
uv_process_kill(&stub->_process, SIGKILL);
}
else
{

View File

@ -701,6 +701,14 @@ static void _test_http_async(uv_async_t* async)
static void _test_http_thread(void* data)
{
test_http_t* test = data;
const char* value = tf_http_get_cookie("a=foo; b=bar", "a");
assert(strcmp(value, "foo") == 0);
tf_free((void*)value);
value = tf_http_get_cookie("a=foo; b=bar", "b");
assert(strcmp(value, "bar") == 0);
tf_free((void*)value);
assert(tf_http_get_cookie("a=foo; b=bar", "c") == NULL);
int r = system("curl -v http://localhost:23456/404");
assert(WEXITSTATUS(r) == 0);
tf_printf("curl returned %d\n", WEXITSTATUS(r));

View File

@ -28,7 +28,7 @@ try:
driver.get('http://localhost:8888')
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'login').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'register_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('test_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
@ -39,7 +39,13 @@ try:
driver.switch_to.default_content()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content')))
# StaleElementReferenceException
while True:
try:
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
break
except:
pass
wait.until(expected_conditions.presence_of_element_located((By.ID, 'create_id'))).click()
driver.switch_to.alert.accept()
@ -71,7 +77,13 @@ try:
driver.switch_to.default_content()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content')))
# StaleElementReferenceException
while True:
try:
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
break
except:
pass
# NoSuchShadowRootException
while True:
try:
@ -89,15 +101,15 @@ try:
driver.switch_to.default_content()
driver.find_element(By.ID, 'allow').click()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout test_user').click()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout testuser').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content')))
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout test_user').click()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout testuser').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'guest_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'guestButton').click()
@ -109,7 +121,7 @@ try:
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout guest').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('wrong_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'error')
@ -120,7 +132,7 @@ try:
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'error')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'register_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('wrong_test_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('wrong_test_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
@ -141,20 +153,20 @@ try:
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'error')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'change_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'new_password').send_keys('new_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('new_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout test_user').click()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout testuser').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'error')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('test_user')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('new_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))