mirror of
https://github.com/pi-hole/FTL.git
synced 2024-10-26 16:52:18 +02:00
64144a960c
Signed-off-by: DL6ER <dl6er@dl6er.de>
713 lines
22 KiB
C
713 lines
22 KiB
C
/* Pi-hole: A black hole for Internet advertisements
|
|
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
|
|
* Network-wide ad blocking via your own hardware.
|
|
*
|
|
* FTL Engine
|
|
* API Implementation /api/auth
|
|
*
|
|
* This file is copyright under the latest version of the EUPL.
|
|
* Please see LICENSE file for your rights under this license. */
|
|
|
|
#include "FTL.h"
|
|
#include "api/auth.h"
|
|
#include "webserver/http-common.h"
|
|
#include "webserver/json_macros.h"
|
|
#include "api/api.h"
|
|
#include "log.h"
|
|
#include "config/config.h"
|
|
// get_password_hash()
|
|
#include "config/setupVars.h"
|
|
// (un)lock_shm()
|
|
#include "shmem.h"
|
|
// getrandom()
|
|
#include "daemon.h"
|
|
// sha256_raw_to_hex()
|
|
#include "config/password.h"
|
|
// database session functions
|
|
#include "database/session-table.h"
|
|
|
|
static uint16_t max_sessions = 0;
|
|
static struct session *auth_data = NULL;
|
|
|
|
static void add_request_info(struct ftl_conn *api, const char *csrf)
|
|
{
|
|
// Copy CSRF token into request
|
|
if(csrf != NULL)
|
|
strncpy((char*)api->request->csrf_token, csrf, sizeof(api->request->csrf_token) - 1);
|
|
|
|
// Store that this client is authenticated
|
|
// We use memset() with the size of an int here to avoid a
|
|
// compiler warning about modifying a variable in a const struct
|
|
memset((int*)&api->request->is_authenticated, 1, sizeof(api->request->is_authenticated));
|
|
}
|
|
|
|
void init_api(void)
|
|
{
|
|
// Restore sessions from database
|
|
max_sessions = config.webserver.api.max_sessions.v.u16;
|
|
auth_data = calloc(max_sessions, sizeof(struct session));
|
|
if(auth_data == NULL)
|
|
{
|
|
log_crit("Could not allocate memory for API sessions, check config value of webserver.api.max_sessions");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
restore_db_sessions(auth_data, max_sessions);
|
|
}
|
|
|
|
void free_api(void)
|
|
{
|
|
if(auth_data == NULL)
|
|
return;
|
|
|
|
// Store sessions in database
|
|
backup_db_sessions(auth_data, max_sessions);
|
|
max_sessions = 0;
|
|
free(auth_data);
|
|
auth_data = NULL;
|
|
}
|
|
|
|
// Is this client connecting from localhost?
|
|
bool __attribute__((pure)) is_local_api_user(const char *remote_addr)
|
|
{
|
|
return strcmp(remote_addr, LOCALHOSTv4) == 0 ||
|
|
strcmp(remote_addr, LOCALHOSTv6) == 0;
|
|
}
|
|
|
|
// Can we validate this client?
|
|
// Returns -1 if not authenticated or expired
|
|
// Returns >= 0 for any valid authentication
|
|
int check_client_auth(struct ftl_conn *api, const bool is_api)
|
|
{
|
|
// When the pwhash is unset, authentication is disabled
|
|
if(config.webserver.api.pwhash.v.s[0] == '\0')
|
|
{
|
|
api->message = "no password set";
|
|
add_request_info(api, NULL);
|
|
return API_AUTH_EMPTYPASS;
|
|
}
|
|
|
|
// Does the client provide a session ID?
|
|
char sid[SID_SIZE];
|
|
const char *sid_source = "-";
|
|
// Try to extract SID from cookie
|
|
bool sid_avail = false;
|
|
|
|
// If not, does the client provide a session ID via GET/POST?
|
|
if(api->payload.avail)
|
|
{
|
|
// Try to extract SID from form-encoded payload
|
|
if(GET_VAR("sid", sid, api->payload.raw) > 0)
|
|
{
|
|
// "+" may have been replaced by " ", undo this here
|
|
for(unsigned int i = 0; i < SID_SIZE; i++)
|
|
if(sid[i] == ' ')
|
|
sid[i] = '+';
|
|
|
|
// Zero terminate SID string
|
|
sid[SID_SIZE-1] = '\0';
|
|
// Mention source of SID
|
|
sid_source = "payload (form-data)";
|
|
// Mark SID as available
|
|
sid_avail = true;
|
|
}
|
|
// Try to extract SID from root of a possibly included JSON payload
|
|
else if(api->payload.json != NULL)
|
|
{
|
|
cJSON *sid_obj = cJSON_GetObjectItem(api->payload.json, "sid");
|
|
if(cJSON_IsString(sid_obj))
|
|
{
|
|
// Copy SID string
|
|
strncpy(sid, sid_obj->valuestring, SID_SIZE - 1u);
|
|
// Zero terminate SID string
|
|
sid[SID_SIZE-1] = '\0';
|
|
// Mention source of SID
|
|
sid_source = "payload (JSON)";
|
|
// Mark SID as available
|
|
sid_avail = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If not, does the client provide a session ID via HEADER?
|
|
if(!sid_avail)
|
|
{
|
|
const char *sid_header = NULL;
|
|
// Try to extract SID from header
|
|
if((sid_header = mg_get_header(api->conn, "sid")) != NULL ||
|
|
(sid_header = mg_get_header(api->conn, "X-FTL-SID")) != NULL)
|
|
{
|
|
// Copy SID string
|
|
strncpy(sid, sid_header, SID_SIZE - 1u);
|
|
// Zero terminate SID string
|
|
sid[SID_SIZE-1] = '\0';
|
|
// Mention source of SID
|
|
sid_source = "header";
|
|
// Mark SID as available
|
|
sid_avail = true;
|
|
}
|
|
}
|
|
|
|
// If not, does the client provide a session ID via COOKIE?
|
|
bool cookie_auth = false;
|
|
if(!sid_avail)
|
|
{
|
|
cookie_auth = http_get_cookie_str(api, "sid", sid, SID_SIZE);
|
|
if(cookie_auth)
|
|
{
|
|
// Mention source of SID
|
|
sid_source = "cookie";
|
|
// Mark SID as available
|
|
sid_avail = true;
|
|
}
|
|
}
|
|
|
|
// If not, does the client provide a session ID via URI?
|
|
if(!sid_avail && api->request->query_string && GET_VAR("sid", sid, api->request->query_string) > 0)
|
|
{
|
|
// "+" may have been replaced by " ", undo this here
|
|
for(unsigned int i = 0; i < SID_SIZE; i++)
|
|
if(sid[i] == ' ')
|
|
sid[i] = '+';
|
|
|
|
// Zero terminate SID string
|
|
sid[SID_SIZE-1] = '\0';
|
|
// Mention source of SID
|
|
sid_source = "URI";
|
|
// Mark SID as available
|
|
sid_avail = true;
|
|
}
|
|
|
|
if(!sid_avail)
|
|
{
|
|
api->message = "no SID provided";
|
|
log_debug(DEBUG_API, "API Authentication: FAIL (%s)", api->message);
|
|
return API_AUTH_UNAUTHORIZED;
|
|
}
|
|
|
|
// else: Analyze SID
|
|
int user_id = API_AUTH_UNAUTHORIZED;
|
|
const time_t now = time(NULL);
|
|
log_debug(DEBUG_API, "Read sid=\"%s\" from %s", sid, sid_source);
|
|
|
|
// If the SID has been sent through a cookie, we require a CSRF token in
|
|
// the header to be sent along with the request for any API requests
|
|
char csrf[SID_SIZE];
|
|
const bool need_csrf = cookie_auth && is_api;
|
|
if(need_csrf)
|
|
{
|
|
const char *csrf_header = NULL;
|
|
// Try to extract CSRF token from header
|
|
if((csrf_header = mg_get_header(api->conn, "X-CSRF-TOKEN")) != NULL)
|
|
{
|
|
// Copy CSRF string
|
|
strncpy(csrf, csrf_header, SID_SIZE - 1u);
|
|
// Zero terminate CSRF string
|
|
csrf[SID_SIZE-1] = '\0';
|
|
}
|
|
else
|
|
{
|
|
api->message = "Cookie authentication without CSRF token";
|
|
log_debug(DEBUG_API, "API Authentication: FAIL (%s)", api->message);
|
|
return API_AUTH_UNAUTHORIZED;
|
|
}
|
|
}
|
|
|
|
bool expired = false;
|
|
for(unsigned int i = 0; i < max_sessions; i++)
|
|
{
|
|
if(auth_data[i].used &&
|
|
strcmp(auth_data[i].sid, sid) == 0)
|
|
{
|
|
// Check if session is known but expired
|
|
if(auth_data[i].valid_until < now)
|
|
expired = true;
|
|
|
|
// Check CSRF if authentiating via cookie
|
|
if(need_csrf && strcmp(auth_data[i].csrf, csrf) != 0)
|
|
{
|
|
api->message = "CSRF token mismatch";
|
|
log_debug(DEBUG_API, "API Authentication: FAIL (%s, received \"%s\", expected \"%s\")",
|
|
api->message, csrf, auth_data[i].csrf);
|
|
return API_AUTH_UNAUTHORIZED;
|
|
}
|
|
user_id = i;
|
|
break;
|
|
}
|
|
}
|
|
if(user_id > API_AUTH_UNAUTHORIZED)
|
|
{
|
|
// Authentication successful: valid session
|
|
|
|
// Update timestamp of this client to extend
|
|
// the validity of their API authentication
|
|
auth_data[user_id].valid_until = now + config.webserver.session.timeout.v.ui;
|
|
|
|
// Set strict_tls permanently to false if the client connected via HTTP
|
|
auth_data[user_id].tls.mixed |= api->request->is_ssl != auth_data[user_id].tls.login;
|
|
|
|
// Update user cookie
|
|
if(snprintf(pi_hole_extra_headers, sizeof(pi_hole_extra_headers),
|
|
FTL_SET_COOKIE,
|
|
auth_data[user_id].sid, config.webserver.session.timeout.v.ui) < 0)
|
|
{
|
|
return send_json_error(api, 500, "internal_error", "Internal server error", NULL);
|
|
}
|
|
|
|
// Add CSRF token to request
|
|
add_request_info(api, auth_data[user_id].csrf);
|
|
|
|
// Debug logging
|
|
if(config.debug.api.v.b)
|
|
{
|
|
char timestr[TIMESTR_SIZE];
|
|
get_timestr(timestr, auth_data[user_id].valid_until, false, false);
|
|
log_debug(DEBUG_API, "Recognized known user: user_id %i, valid_until: %s, remote_addr %s (%s at login)",
|
|
user_id, timestr, api->request->remote_addr, auth_data[user_id].remote_addr);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
api->message = expired ? "session expired" : "session unknown";
|
|
log_debug(DEBUG_API, "API Authentication: FAIL (%s)", api->message);
|
|
return API_AUTH_UNAUTHORIZED;
|
|
}
|
|
|
|
api->user_id = user_id;
|
|
api->session = &auth_data[user_id];
|
|
|
|
api->message = "correct password";
|
|
return user_id;
|
|
}
|
|
|
|
static int get_all_sessions(struct ftl_conn *api, cJSON *json)
|
|
{
|
|
const time_t now = time(NULL);
|
|
cJSON *sessions = JSON_NEW_ARRAY();
|
|
for(int i = 0; i < max_sessions; i++)
|
|
{
|
|
if(!auth_data[i].used)
|
|
continue;
|
|
cJSON *session = JSON_NEW_OBJECT();
|
|
JSON_ADD_NUMBER_TO_OBJECT(session, "id", i);
|
|
JSON_ADD_BOOL_TO_OBJECT(session, "current_session", i == api->user_id);
|
|
JSON_ADD_BOOL_TO_OBJECT(session, "valid", auth_data[i].valid_until >= now);
|
|
cJSON *tls = JSON_NEW_OBJECT();
|
|
JSON_ADD_BOOL_TO_OBJECT(tls, "login", auth_data[i].tls.login);
|
|
JSON_ADD_BOOL_TO_OBJECT(tls, "mixed", auth_data[i].tls.mixed);
|
|
JSON_ADD_ITEM_TO_OBJECT(session, "tls", tls);
|
|
JSON_ADD_NUMBER_TO_OBJECT(session, "login_at", auth_data[i].login_at);
|
|
JSON_ADD_NUMBER_TO_OBJECT(session, "last_active", auth_data[i].valid_until - config.webserver.session.timeout.v.ui);
|
|
JSON_ADD_NUMBER_TO_OBJECT(session, "valid_until", auth_data[i].valid_until);
|
|
JSON_REF_STR_IN_OBJECT(session, "remote_addr", auth_data[i].remote_addr);
|
|
if(auth_data[i].user_agent[0] != '\0')
|
|
JSON_REF_STR_IN_OBJECT(session, "user_agent", auth_data[i].user_agent);
|
|
else
|
|
JSON_ADD_NULL_TO_OBJECT(session, "user_agent");
|
|
if(auth_data[i].x_forwarded_for[0] != '\0')
|
|
JSON_REF_STR_IN_OBJECT(session, "x_forwarded_for", auth_data[i].x_forwarded_for);
|
|
else
|
|
JSON_ADD_NULL_TO_OBJECT(session, "x_forwarded_for");
|
|
JSON_ADD_BOOL_TO_OBJECT(session, "app", auth_data[i].app);
|
|
JSON_ADD_BOOL_TO_OBJECT(session, "cli", auth_data[i].cli);
|
|
JSON_ADD_ITEM_TO_ARRAY(sessions, session);
|
|
}
|
|
JSON_ADD_ITEM_TO_OBJECT(json, "sessions", sessions);
|
|
return 0;
|
|
}
|
|
|
|
static int get_session_object(struct ftl_conn *api, cJSON *json, const int user_id, const time_t now)
|
|
{
|
|
cJSON *session = JSON_NEW_OBJECT();
|
|
|
|
// Authentication not needed
|
|
if(user_id == API_AUTH_EMPTYPASS)
|
|
{
|
|
JSON_ADD_BOOL_TO_OBJECT(session, "valid", true);
|
|
JSON_ADD_BOOL_TO_OBJECT(session, "totp", strlen(config.webserver.api.totp_secret.v.s) > 0);
|
|
JSON_ADD_NULL_TO_OBJECT(session, "sid");
|
|
JSON_ADD_NUMBER_TO_OBJECT(session, "validity", -1);
|
|
JSON_REF_STR_IN_OBJECT(session, "message", api->message);
|
|
JSON_ADD_ITEM_TO_OBJECT(json, "session", session);
|
|
return 0;
|
|
}
|
|
|
|
// Valid session
|
|
if(user_id > API_AUTH_UNAUTHORIZED && auth_data[user_id].used)
|
|
{
|
|
JSON_ADD_BOOL_TO_OBJECT(session, "valid", true);
|
|
JSON_ADD_BOOL_TO_OBJECT(session, "totp", strlen(config.webserver.api.totp_secret.v.s) > 0);
|
|
JSON_REF_STR_IN_OBJECT(session, "sid", auth_data[user_id].sid);
|
|
JSON_REF_STR_IN_OBJECT(session, "csrf", auth_data[user_id].csrf);
|
|
JSON_ADD_NUMBER_TO_OBJECT(session, "validity", auth_data[user_id].valid_until - now);
|
|
JSON_REF_STR_IN_OBJECT(session, "message", api->message);
|
|
JSON_ADD_ITEM_TO_OBJECT(json, "session", session);
|
|
return 0;
|
|
}
|
|
|
|
// No valid session
|
|
JSON_ADD_BOOL_TO_OBJECT(session, "valid", false);
|
|
JSON_ADD_BOOL_TO_OBJECT(session, "totp", strlen(config.webserver.api.totp_secret.v.s) > 0);
|
|
JSON_ADD_NULL_TO_OBJECT(session, "sid");
|
|
JSON_ADD_NUMBER_TO_OBJECT(session, "validity", -1);
|
|
JSON_REF_STR_IN_OBJECT(session, "message", api->message);
|
|
JSON_ADD_ITEM_TO_OBJECT(json, "session", session);
|
|
return 0;
|
|
}
|
|
|
|
static bool delete_session(const int user_id)
|
|
{
|
|
// Skip if nothing to be done here
|
|
if(user_id < 0 || user_id >= max_sessions)
|
|
return false;
|
|
|
|
const bool was_valid = auth_data[user_id].used;
|
|
|
|
// Zero out this session (also sets valid to false == 0)
|
|
memset(&auth_data[user_id], 0, sizeof(auth_data[user_id]));
|
|
|
|
return was_valid;
|
|
}
|
|
|
|
void delete_all_sessions(void)
|
|
{
|
|
// Zero out all sessions without looping
|
|
memset(auth_data, 0, max_sessions*sizeof(*auth_data));
|
|
}
|
|
|
|
static int send_api_auth_status(struct ftl_conn *api, const int user_id, const time_t now)
|
|
{
|
|
if(user_id > API_AUTH_UNAUTHORIZED && (api->method == HTTP_GET || api->method == HTTP_POST))
|
|
{
|
|
log_debug(DEBUG_API, "API Auth status: OK");
|
|
|
|
// Ten minutes validity
|
|
if(snprintf(pi_hole_extra_headers, sizeof(pi_hole_extra_headers),
|
|
FTL_SET_COOKIE,
|
|
auth_data[user_id].sid, config.webserver.session.timeout.d.ui) < 0)
|
|
{
|
|
return send_json_error(api, 500, "internal_error", "Internal server error", NULL);
|
|
}
|
|
|
|
cJSON *json = JSON_NEW_OBJECT();
|
|
get_session_object(api, json, user_id, now);
|
|
JSON_SEND_OBJECT(json);
|
|
}
|
|
else if(api->method == HTTP_DELETE)
|
|
{
|
|
if(user_id > API_AUTH_UNAUTHORIZED)
|
|
{
|
|
log_debug(DEBUG_API, "API Auth status: Logout, asking to delete cookie");
|
|
|
|
strncpy(pi_hole_extra_headers, FTL_DELETE_COOKIE, sizeof(pi_hole_extra_headers));
|
|
|
|
// Revoke client authentication. This slot can be used by a new client afterwards.
|
|
const int code = delete_session(user_id) ? 204 : 404;
|
|
|
|
// Send empty reply with appropriate HTTP status code
|
|
send_http_code(api, "application/json; charset=utf-8", code, "");
|
|
return code;
|
|
}
|
|
else
|
|
{
|
|
log_debug(DEBUG_API, "API Auth status: Logout, but not authenticated");
|
|
|
|
cJSON *json = JSON_NEW_OBJECT();
|
|
get_session_object(api, json, user_id, now);
|
|
JSON_SEND_OBJECT_CODE(json, 401); // 401 Unauthorized
|
|
}
|
|
}
|
|
else if(user_id == API_AUTH_EMPTYPASS)
|
|
{
|
|
log_debug(DEBUG_API, "API Auth status: OK (empty password)");
|
|
|
|
cJSON *json = JSON_NEW_OBJECT();
|
|
get_session_object(api, json, user_id, now);
|
|
JSON_SEND_OBJECT(json);
|
|
}
|
|
else
|
|
{
|
|
log_debug(DEBUG_API, "API Auth status: Invalid, asking to delete cookie");
|
|
|
|
strncpy(pi_hole_extra_headers, FTL_DELETE_COOKIE, sizeof(pi_hole_extra_headers));
|
|
cJSON *json = JSON_NEW_OBJECT();
|
|
get_session_object(api, json, user_id, now);
|
|
JSON_SEND_OBJECT_CODE(json, 401); // 401 Unauthorized
|
|
}
|
|
}
|
|
|
|
static void generateSID(char *sid)
|
|
{
|
|
uint8_t raw_sid[SID_SIZE];
|
|
if(getrandom(raw_sid, sizeof(raw_sid), 0) < 0)
|
|
{
|
|
log_err("getrandom() failed in generateSID()");
|
|
return;
|
|
}
|
|
base64_encode_raw(NETTLE_SIGN sid, SID_BITSIZE/8, raw_sid);
|
|
sid[SID_SIZE-1] = '\0';
|
|
}
|
|
|
|
// api/auth
|
|
// GET: Check authentication
|
|
// POST: Login
|
|
// DELETE: Logout
|
|
int api_auth(struct ftl_conn *api)
|
|
{
|
|
// Check HTTP method
|
|
char *password = NULL;
|
|
const time_t now = time(NULL);
|
|
const bool empty_password = config.webserver.api.pwhash.v.s[0] == '\0';
|
|
|
|
if(api->item != NULL && strlen(api->item) > 0)
|
|
{
|
|
// Sub-paths are not allowed
|
|
return 0;
|
|
}
|
|
|
|
// Login attempt, check password
|
|
if(api->method == HTTP_POST)
|
|
{
|
|
// Try to extract response from payload
|
|
const int ret = check_json_payload(api);
|
|
if(ret != 0)
|
|
return ret;
|
|
|
|
// Check if password is available
|
|
cJSON *json_password;
|
|
if((json_password = cJSON_GetObjectItemCaseSensitive(api->payload.json, "password")) == NULL)
|
|
{
|
|
const char *message = "No password found in JSON payload";
|
|
log_debug(DEBUG_API, "API auth error: %s", message);
|
|
return send_json_error(api, 400,
|
|
"bad_request",
|
|
message,
|
|
NULL);
|
|
}
|
|
|
|
// Check password type
|
|
if(!cJSON_IsString(json_password))
|
|
{
|
|
const char *message = "Field password has to be of type 'string'";
|
|
log_debug(DEBUG_API, "API auth error: %s", message);
|
|
return send_json_error(api, 400,
|
|
"bad_request",
|
|
message,
|
|
NULL);
|
|
}
|
|
|
|
// password is already null-terminated
|
|
password = json_password->valuestring;
|
|
}
|
|
|
|
// Did the client authenticate before and we can validate this?
|
|
int user_id = check_client_auth(api, false);
|
|
|
|
// If this is a valid session, we can exit early at this point if no password is supplied
|
|
if(user_id != API_AUTH_UNAUTHORIZED && (password == NULL || strlen(password) == 0))
|
|
return send_api_auth_status(api, user_id, now);
|
|
|
|
// Logout attempt
|
|
if(api->method == HTTP_DELETE)
|
|
{
|
|
log_debug(DEBUG_API, "API Auth: User with ID %i wants to log out", user_id);
|
|
return send_api_auth_status(api, user_id, now);
|
|
}
|
|
|
|
// If this is not a login attempt, we can exit early at this point
|
|
if(password == NULL && !empty_password)
|
|
return send_api_auth_status(api, user_id, now);
|
|
|
|
// else: Login attempt
|
|
// - Client tries to authenticate using a password, or
|
|
// - There no password on this machine
|
|
enum password_result result = PASSWORD_INCORRECT;
|
|
|
|
// If there is no password (or empty), check if there is any password at all
|
|
if(empty_password && (password == NULL || strlen(password) == 0))
|
|
result = PASSWORD_CORRECT;
|
|
else
|
|
result = verify_login(password);
|
|
|
|
if(result == PASSWORD_CORRECT ||
|
|
result == APPPASSWORD_CORRECT ||
|
|
result == CLIPASSWORD_CORRECT)
|
|
{
|
|
// Accepted
|
|
|
|
// Zero-out password in memory to avoid leaking it when it is
|
|
// freed at the end of the current API request
|
|
if(password != NULL)
|
|
memset(password, 0, strlen(password));
|
|
|
|
// Check possible 2FA token
|
|
// Successful login with empty password does not require 2FA
|
|
if(strlen(config.webserver.api.totp_secret.v.s) > 0 && result == PASSWORD_CORRECT)
|
|
{
|
|
// Get 2FA token from payload
|
|
cJSON *json_totp;
|
|
if((json_totp = cJSON_GetObjectItemCaseSensitive(api->payload.json, "totp")) == NULL)
|
|
{
|
|
const char *message = "No 2FA token found in JSON payload";
|
|
log_debug(DEBUG_API, "API auth error: %s", message);
|
|
return send_json_error(api, 400,
|
|
"bad_request",
|
|
message,
|
|
NULL);
|
|
}
|
|
|
|
enum totp_status totp = verifyTOTP(json_totp->valueint);
|
|
if(totp == TOTP_REUSED)
|
|
{
|
|
// 2FA token has been reused
|
|
return send_json_error(api, 401,
|
|
"unauthorized",
|
|
"Reused 2FA token",
|
|
"wait for new token");
|
|
}
|
|
else if(totp != TOTP_CORRECT)
|
|
{
|
|
// 2FA token is invalid
|
|
return send_json_error(api, 401,
|
|
"unauthorized",
|
|
"Invalid 2FA token",
|
|
NULL);
|
|
}
|
|
}
|
|
|
|
// Find unused authentication slot
|
|
for(unsigned int i = 0; i < max_sessions; i++)
|
|
{
|
|
// Expired slow, mark as unused
|
|
if(auth_data[i].used &&
|
|
auth_data[i].valid_until < now)
|
|
{
|
|
log_debug(DEBUG_API, "API: Session of client %u (%s) expired, freeing...",
|
|
i, auth_data[i].remote_addr);
|
|
delete_session(i);
|
|
}
|
|
|
|
// Found unused authentication slot (might have been freed before)
|
|
if(!auth_data[i].used)
|
|
{
|
|
// Mark as used
|
|
auth_data[i].used = true;
|
|
// Set validitiy to now + timeout
|
|
auth_data[i].login_at = now;
|
|
auth_data[i].valid_until = now + config.webserver.session.timeout.v.ui;
|
|
// Set remote address
|
|
strncpy(auth_data[i].remote_addr, api->request->remote_addr, sizeof(auth_data[i].remote_addr));
|
|
auth_data[i].remote_addr[sizeof(auth_data[i].remote_addr)-1] = '\0';
|
|
// Store user-agent (if available)
|
|
const char *user_agent = mg_get_header(api->conn, "user-agent");
|
|
if(user_agent != NULL)
|
|
{
|
|
strncpy(auth_data[i].user_agent, user_agent, sizeof(auth_data[i].user_agent));
|
|
auth_data[i].user_agent[sizeof(auth_data[i].user_agent)-1] = '\0';
|
|
}
|
|
else
|
|
{
|
|
auth_data[i].user_agent[0] = '\0';
|
|
}
|
|
// Store X-Forwarded-For (if available)
|
|
const char *x_forwarded_for = mg_get_header(api->conn, "X-Forwarded-For");
|
|
if(x_forwarded_for != NULL)
|
|
{
|
|
strncpy(auth_data[i].x_forwarded_for, x_forwarded_for, sizeof(auth_data[i].x_forwarded_for));
|
|
auth_data[i].x_forwarded_for[sizeof(auth_data[i].x_forwarded_for)-1] = '\0';
|
|
}
|
|
else
|
|
{
|
|
auth_data[i].x_forwarded_for[0] = '\0';
|
|
}
|
|
|
|
auth_data[i].tls.login = api->request->is_ssl;
|
|
auth_data[i].tls.mixed = false;
|
|
auth_data[i].app = result == APPPASSWORD_CORRECT;
|
|
auth_data[i].cli = result == CLIPASSWORD_CORRECT;
|
|
|
|
// Generate new SID and CSRF token
|
|
generateSID(auth_data[i].sid);
|
|
generateSID(auth_data[i].csrf);
|
|
|
|
user_id = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Debug logging
|
|
if(config.debug.api.v.b && user_id > API_AUTH_UNAUTHORIZED)
|
|
{
|
|
char timestr[TIMESTR_SIZE];
|
|
get_timestr(timestr, auth_data[user_id].valid_until, false, false);
|
|
log_debug(DEBUG_API, "API: Registered new user: user_id %i valid_until: %s remote_addr %s (accepted due to %s)",
|
|
user_id, timestr, auth_data[user_id].remote_addr,
|
|
empty_password ? "empty password" : "correct response");
|
|
}
|
|
if(user_id == API_AUTH_UNAUTHORIZED)
|
|
{
|
|
log_warn("No free API seats available (webserver.api.max_sessions = %u), not authenticating client",
|
|
max_sessions);
|
|
|
|
return send_json_error(api, 429,
|
|
"api_seats_exceeded",
|
|
"API seats exceeded",
|
|
"increase webserver.api.max_sessions");
|
|
}
|
|
|
|
api->message = result == APPPASSWORD_CORRECT ? "app-password correct" : "password correct";
|
|
}
|
|
else if(result == PASSWORD_RATE_LIMITED)
|
|
{
|
|
// Rate limited
|
|
return send_json_error(api, 429,
|
|
"rate_limiting",
|
|
"Rate-limiting login attempts",
|
|
NULL);
|
|
}
|
|
else if(result == NO_PASSWORD_SET)
|
|
{
|
|
// No password set
|
|
api->message = "password incorrect";
|
|
log_debug(DEBUG_API, "API: Trying to auth with password but none set: '%s'", password);
|
|
}
|
|
else
|
|
{
|
|
api->message = "password incorrect";
|
|
log_debug(DEBUG_API, "API: Password incorrect: '%s'", password);
|
|
}
|
|
|
|
// Free allocated memory
|
|
return send_api_auth_status(api, user_id, now);
|
|
}
|
|
|
|
int api_auth_sessions(struct ftl_conn *api)
|
|
{
|
|
// Get session object
|
|
cJSON *json = JSON_NEW_OBJECT();
|
|
get_all_sessions(api, json);
|
|
JSON_SEND_OBJECT(json);
|
|
}
|
|
|
|
int api_auth_session_delete(struct ftl_conn *api)
|
|
{
|
|
// Get user ID
|
|
int uid;
|
|
if(sscanf(api->item, "%i", &uid) != 1)
|
|
return send_json_error(api, 400, "bad_request", "Missing or invalid session ID", NULL);
|
|
|
|
// Check if session ID is valid
|
|
if(uid <= API_AUTH_UNAUTHORIZED || uid >= max_sessions)
|
|
return send_json_error(api, 400, "bad_request", "Session ID out of bounds", NULL);
|
|
|
|
// Check if session is used
|
|
if(!auth_data[uid].used)
|
|
return send_json_error(api, 400, "bad_request", "Session ID not in use", NULL);
|
|
|
|
// Delete session
|
|
const int code = delete_session(uid) ? 204 : 404;
|
|
|
|
// Send empty reply with appropriate HTTP status code
|
|
send_http_code(api, "application/json; charset=utf-8", code, "");
|
|
return code;
|
|
}
|