mirror of
https://github.com/allinurl/goaccess.git
synced 2025-06-18 14:35:34 -04:00
Added WebSocket authentication options --ws-auth and --ws-auth-expire.
Some checks are pending
C build and Test / build (, macos-14, brew update && brew install ncurses gettext autoconf automake openssl@3 libmaxminddb jq) (push) Waiting to run
C build and Test / build (, macos-latest, brew install ncurses gettext autoconf automake libmaxminddb openssl@3 jq) (push) Waiting to run
C build and Test / build (, ubuntu-latest, sudo apt-get update && sudo apt-get install -y build-essential autoconf gettext autopoint libncursesw5-dev libssl-dev git libmaxminddb-dev jq) (push) Waiting to run
C build and Test / build (--enable-debug, macos-14, brew update && brew install ncurses gettext autoconf automake openssl@3 libmaxminddb jq) (push) Waiting to run
C build and Test / build (--enable-debug, macos-latest, brew install ncurses gettext autoconf automake libmaxminddb openssl@3 jq) (push) Waiting to run
C build and Test / build (--enable-debug, ubuntu-latest, sudo apt-get update && sudo apt-get install -y build-essential autoconf gettext autopoint libncursesw5-dev libssl-dev git libmaxminddb-dev jq) (push) Waiting to run
C build and Test / build (--enable-utf8 --enable-debug --with-getline, macos-14, brew update && brew install ncurses gettext autoconf automake openssl@3 libmaxminddb jq) (push) Waiting to run
C build and Test / build (--enable-utf8 --enable-debug --with-getline, macos-latest, brew install ncurses gettext autoconf automake libmaxminddb openssl@3 jq) (push) Waiting to run
C build and Test / build (--enable-utf8 --enable-debug --with-getline, ubuntu-latest, sudo apt-get update && sudo apt-get install -y build-essential autoconf gettext autopoint libncursesw5-dev libssl-dev git libmaxminddb-dev jq) (push) Waiting to run
C build and Test / build (--enable-utf8 --with-getline --enable-asan, macos-14, brew update && brew install ncurses gettext autoconf automake openssl@3 libmaxminddb jq) (push) Waiting to run
C build and Test / build (--enable-utf8 --with-getline --enable-asan, macos-latest, brew install ncurses gettext autoconf automake libmaxminddb openssl@3 jq) (push) Waiting to run
C build and Test / build (--enable-utf8 --with-getline --enable-asan, ubuntu-latest, sudo apt-get update && sudo apt-get install -y build-essential autoconf gettext autopoint libncursesw5-dev libssl-dev git libmaxminddb-dev jq) (push) Waiting to run
C build and Test / build (--with-getline --enable-asan, macos-14, brew update && brew install ncurses gettext autoconf automake openssl@3 libmaxminddb jq) (push) Waiting to run
C build and Test / build (--with-getline --enable-asan, macos-latest, brew install ncurses gettext autoconf automake libmaxminddb openssl@3 jq) (push) Waiting to run
C build and Test / build (--with-getline --enable-asan, ubuntu-latest, sudo apt-get update && sudo apt-get install -y build-essential autoconf gettext autopoint libncursesw5-dev libssl-dev git libmaxminddb-dev jq) (push) Waiting to run
Docker / test (push) Waiting to run
Docker / push (push) Blocked by required conditions
Some checks are pending
C build and Test / build (, macos-14, brew update && brew install ncurses gettext autoconf automake openssl@3 libmaxminddb jq) (push) Waiting to run
C build and Test / build (, macos-latest, brew install ncurses gettext autoconf automake libmaxminddb openssl@3 jq) (push) Waiting to run
C build and Test / build (, ubuntu-latest, sudo apt-get update && sudo apt-get install -y build-essential autoconf gettext autopoint libncursesw5-dev libssl-dev git libmaxminddb-dev jq) (push) Waiting to run
C build and Test / build (--enable-debug, macos-14, brew update && brew install ncurses gettext autoconf automake openssl@3 libmaxminddb jq) (push) Waiting to run
C build and Test / build (--enable-debug, macos-latest, brew install ncurses gettext autoconf automake libmaxminddb openssl@3 jq) (push) Waiting to run
C build and Test / build (--enable-debug, ubuntu-latest, sudo apt-get update && sudo apt-get install -y build-essential autoconf gettext autopoint libncursesw5-dev libssl-dev git libmaxminddb-dev jq) (push) Waiting to run
C build and Test / build (--enable-utf8 --enable-debug --with-getline, macos-14, brew update && brew install ncurses gettext autoconf automake openssl@3 libmaxminddb jq) (push) Waiting to run
C build and Test / build (--enable-utf8 --enable-debug --with-getline, macos-latest, brew install ncurses gettext autoconf automake libmaxminddb openssl@3 jq) (push) Waiting to run
C build and Test / build (--enable-utf8 --enable-debug --with-getline, ubuntu-latest, sudo apt-get update && sudo apt-get install -y build-essential autoconf gettext autopoint libncursesw5-dev libssl-dev git libmaxminddb-dev jq) (push) Waiting to run
C build and Test / build (--enable-utf8 --with-getline --enable-asan, macos-14, brew update && brew install ncurses gettext autoconf automake openssl@3 libmaxminddb jq) (push) Waiting to run
C build and Test / build (--enable-utf8 --with-getline --enable-asan, macos-latest, brew install ncurses gettext autoconf automake libmaxminddb openssl@3 jq) (push) Waiting to run
C build and Test / build (--enable-utf8 --with-getline --enable-asan, ubuntu-latest, sudo apt-get update && sudo apt-get install -y build-essential autoconf gettext autopoint libncursesw5-dev libssl-dev git libmaxminddb-dev jq) (push) Waiting to run
C build and Test / build (--with-getline --enable-asan, macos-14, brew update && brew install ncurses gettext autoconf automake openssl@3 libmaxminddb jq) (push) Waiting to run
C build and Test / build (--with-getline --enable-asan, macos-latest, brew install ncurses gettext autoconf automake libmaxminddb openssl@3 jq) (push) Waiting to run
C build and Test / build (--with-getline --enable-asan, ubuntu-latest, sudo apt-get update && sudo apt-get install -y build-essential autoconf gettext autopoint libncursesw5-dev libssl-dev git libmaxminddb-dev jq) (push) Waiting to run
Docker / test (push) Waiting to run
Docker / push (push) Blocked by required conditions
This commit introduces two new command-line options to enhance WebSocket security in GoAccess: --ws-auth=<jwt[:secret]>: Enables JWT-based authentication for WebSocket connections. Supports an optional secret (as a string or file path) for token verification. Without a secret, it falls back to the GOACCESS_WSAUTH_SECRET environment variable or generates an HS256-compatible secret. When enabled, the HTML report delays bootstrapping initial data until authentication succeeds. --ws-auth-expire=<secs>: Sets the JWT expiration time, defaulting to 3600 seconds (1 hour). Supports flexible formats like "24h", "10m", or "10d" for user convenience. These options strengthen real-time HTML output security by ensuring only authenticated clients access the WebSocket feed. Closes #2794, #1133, #2411
This commit is contained in:
parent
8056805314
commit
95ca855183
@ -207,6 +207,12 @@ goaccess_SOURCES = \
|
||||
src/xmalloc.c \
|
||||
src/xmalloc.h
|
||||
|
||||
if WITH_SSL
|
||||
goaccess_SOURCES += \
|
||||
src/wsauth.c \
|
||||
src/wsauth.h
|
||||
endif
|
||||
|
||||
if USE_SHA1
|
||||
goaccess_SOURCES += \
|
||||
src/sha1.c \
|
||||
|
@ -74,6 +74,7 @@ if test "$openssl" = 'yes'; then
|
||||
AC_CHECK_LIB([crypto], [CRYPTO_free],,[AC_MSG_ERROR([crypto library missing])])
|
||||
AC_CHECK_LIB([ssl], [SSL_CIPHER_standard_name], [AC_DEFINE([HAVE_CIPHER_STD_NAME], 1, [HAVE_CIPHER_STD_NAME])])
|
||||
fi
|
||||
AM_CONDITIONAL([WITH_SSL], [test "x$with_openssl" = "xyes"])
|
||||
|
||||
# GeoIP
|
||||
AC_ARG_ENABLE([geoip],[AS_HELP_STRING([--enable-geoip],[Enable GeoIP country lookup. Supported types: mmdb, legacy. Default is disabled])],[geoip="$enableval"],[geoip=no])
|
||||
|
33
goaccess.1
33
goaccess.1
@ -1,4 +1,4 @@
|
||||
.TH goaccess 1 "MAY 2024" GNU+Linux "User Manuals"
|
||||
.TH goaccess 1 "MAR 2025" GNU+Linux "User Manuals"
|
||||
.SH NAME
|
||||
goaccess \- fast web log analyzer and interactive viewer.
|
||||
.SH SYNOPSIS
|
||||
@ -460,6 +460,37 @@ Enable real-time HTML output.
|
||||
GoAccess uses its own WebSocket server to push the data from the server to the
|
||||
client. See http://gwsocket.io for more details how the WebSocket server works.
|
||||
.TP
|
||||
\fB\-\-ws-auth=<jwt[:secret]>
|
||||
|
||||
Enable WebSocket authentication using a JSON Web Token (JWT). Optionally, a secret key can be provided for verification.
|
||||
|
||||
.IP
|
||||
When this option is used, the HTML report will not bootstrap the initial parsed data. Instead, it will only display the report if authentication succeeds.
|
||||
.IP
|
||||
The system processes this option as follows: if the argument starts with "jwt:", the part after the colon is treated as either a file path (if it exists, the secret is read from the file) or a direct secret string. If only "jwt" is provided, the secret is sourced from the environment variable \fBGOACCESS_WSAUTH_SECRET\fR, or a default HS256-compatible secret is generated if the variable is unset. See http://gwsocket.io for more details on the underlying WebSocket server.
|
||||
|
||||
.TP
|
||||
\fB\-\-ws-auth-expire=<secs>
|
||||
|
||||
Set the time after which the JWT expires. Defaults to 8 hours (28800 seconds) if not specified.
|
||||
|
||||
.IP
|
||||
Users can specify the expiration time in various formats. The value is converted to seconds for JWT expiration validation. Supported formats:
|
||||
|
||||
.RS
|
||||
.IP \(bu 4
|
||||
"3600" -> 3600 seconds
|
||||
.IP \(bu 4
|
||||
"24h" -> 24 hours = 86,400 seconds
|
||||
.IP \(bu 4
|
||||
"10m" -> 10 minutes = 600 seconds
|
||||
.IP \(bu 4
|
||||
"10d" -> 10 days = 864,000 seconds
|
||||
.RE
|
||||
|
||||
.IP
|
||||
The expiration time controls how long the JWT remains valid after issuance, ensuring secure WebSocket connections.
|
||||
.TP
|
||||
\fB\-\-ws-url=<[scheme://]url[:port]>
|
||||
URL to which the WebSocket server responds. This is the URL supplied to the
|
||||
WebSocket constructor on the client side.
|
||||
|
@ -30,11 +30,26 @@ h3 {
|
||||
.expandable>td {
|
||||
cursor: pointer;
|
||||
}
|
||||
.loading-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.spinner {
|
||||
color: #999;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-bottom: 10px; /* Space between spinner and text */
|
||||
}
|
||||
.app-loading-status {
|
||||
color: #999;
|
||||
text-align: center;
|
||||
text-shadow: 1px 1px 0 #FFF;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.powered {
|
||||
bottom: 190px;
|
||||
|
@ -66,9 +66,20 @@ window.GoAccess = window.GoAccess || {
|
||||
var ls = JSON.parse(localStorage.getItem('AppPrefs'));
|
||||
this.AppPrefs = GoAccess.Util.merge(this.AppPrefs, ls);
|
||||
}
|
||||
if (Object.keys(this.AppWSConn).length)
|
||||
this.setWebSocket(this.AppWSConn);
|
||||
|
||||
// Track if the app has been initialized
|
||||
this.isAppInitialized = false;
|
||||
|
||||
// If window.goaccessJWT is set and WebSocket is configured, set it up
|
||||
if (window.goaccessJWT && Object.keys(this.AppWSConn).length) {
|
||||
this.setWebSocket(this.AppWSConn);
|
||||
} else {
|
||||
// No JWT or no WebSocket: initialize immediately
|
||||
GoAccess.App.initialize();
|
||||
if (Object.keys(this.AppWSConn).length) {
|
||||
this.setWebSocket(this.AppWSConn);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getPanelUI: function (panel) {
|
||||
@ -110,31 +121,69 @@ window.GoAccess = window.GoAccess || {
|
||||
setWebSocket: function (wsConn) {
|
||||
var host = null, pingId = null, uri = null, defURI = null, str = null;
|
||||
|
||||
const messages = [
|
||||
'Validating WebSocket tokens... Please wait.',
|
||||
'Authenticating WebSocket connection... Please wait.',
|
||||
'Verifying WebSocket credentials... Please wait.',
|
||||
'Authorizing WebSocket session... Please wait.'
|
||||
];
|
||||
let currentMessageIndex = 0;
|
||||
let messageInterval;
|
||||
|
||||
function displayNextMessage() {
|
||||
if (currentMessageIndex < messages.length) {
|
||||
document.querySelector('.app-loading-status > small').innerHTML = messages[currentMessageIndex];
|
||||
currentMessageIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// Start message rotation
|
||||
messageInterval = setInterval(displayNextMessage, 100);
|
||||
|
||||
defURI = window.location.hostname ? window.location.hostname + ':' + wsConn.port : "localhost" + ':' + wsConn.port;
|
||||
uri = wsConn.url && /^(wss?:\/\/)?[^\/]+:[0-9]{1,5}/.test(wsConn.url) ? wsConn.url : this.buildWSURI(wsConn);
|
||||
|
||||
str = uri || defURI;
|
||||
str = !/^wss?:\/\//i.test(str) ? (window.location.protocol === "https:" ? 'wss://' : 'ws://') + str : str;
|
||||
|
||||
if (window.goaccessJWT) {
|
||||
const separator = str.includes('?') ? '&' : '?';
|
||||
str = str + separator + 'token=' + encodeURIComponent(window.goaccessJWT);
|
||||
}
|
||||
|
||||
var socket = new WebSocket(str);
|
||||
|
||||
socket.onopen = function (event) {
|
||||
clearInterval(messageInterval);
|
||||
document.querySelector('.app-loading-status > small').innerHTML = 'Authentication successful.';
|
||||
|
||||
this.currDelay = this.wsDelay;
|
||||
this.retries = 0;
|
||||
|
||||
// attempt to keep connection alive (e.g., ping/pong)
|
||||
if (wsConn.ping_interval)
|
||||
if (wsConn.ping_interval) {
|
||||
pingId = setInterval(() => { socket.send('ping'); }, wsConn.ping_interval * 1E3);
|
||||
|
||||
}
|
||||
GoAccess.Nav.WSOpen(str);
|
||||
}.bind(this);
|
||||
|
||||
socket.onmessage = function (event) {
|
||||
this.AppState['updated'] = true;
|
||||
this.AppData = JSON.parse(event.data);
|
||||
|
||||
if (window.goaccessJWT && !this.isAppInitialized) {
|
||||
GoAccess.App.initialize();
|
||||
GoAccess.Nav.WSOpen(str);
|
||||
this.isAppInitialized = true;
|
||||
}
|
||||
|
||||
this.App.renderData();
|
||||
}.bind(this);
|
||||
|
||||
socket.onclose = function (event) {
|
||||
clearInterval(messageInterval);
|
||||
document.querySelector('.app-loading-status > small').innerHTML = 'Unable to authenticate WebSocket.';
|
||||
document.querySelector('.loading-container > .spinner').style.display = 'none';
|
||||
|
||||
GoAccess.Nav.WSClose();
|
||||
window.clearInterval(pingId);
|
||||
socket = null;
|
||||
@ -395,7 +444,7 @@ GoAccess.OverallStats = {
|
||||
'from': data.start_date,
|
||||
'to': data.end_date,
|
||||
}));
|
||||
$('#overall').setAttribute('aria-labelledby', 'overall-heading');
|
||||
$('#overall').setAttribute('aria-labelledby', 'overall-heading');
|
||||
|
||||
// Iterate over general data object
|
||||
for (var x in data) {
|
||||
@ -703,11 +752,12 @@ GoAccess.Nav = {
|
||||
},
|
||||
|
||||
WSOpen: function (str) {
|
||||
const baseUrl = str.split('?')[0].split('#')[0];
|
||||
$$('.nav-ws-status', function (item) {
|
||||
item.classList.remove('fa-stop');
|
||||
item.classList.add('fa-circle');
|
||||
item.setAttribute('aria-label', `${GoAccess.i18n.websocket_connected} (${str})`);
|
||||
item.setAttribute('title', `${GoAccess.i18n.websocket_connected} (${str})`);
|
||||
item.setAttribute('aria-label', `${GoAccess.i18n.websocket_connected} (${baseUrl})`);
|
||||
item.setAttribute('title', `${GoAccess.i18n.websocket_connected} (${baseUrl})`);
|
||||
});
|
||||
},
|
||||
|
||||
@ -1926,16 +1976,20 @@ GoAccess.App = {
|
||||
GoAccess.Tables.reloadTables();
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.setInitSort();
|
||||
this.setTpls();
|
||||
renderPanels: function () {
|
||||
GoAccess.Nav.initialize();
|
||||
this.initDom();
|
||||
GoAccess.OverallStats.initialize();
|
||||
GoAccess.Panels.initialize();
|
||||
GoAccess.Charts.initialize();
|
||||
GoAccess.Tables.initialize();
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.setInitSort();
|
||||
this.setTpls();
|
||||
this.initDom();
|
||||
this.renderPanels();
|
||||
},
|
||||
};
|
||||
|
||||
// Adds the visibilitychange EventListener
|
||||
@ -1961,6 +2015,5 @@ window.onload = function () {
|
||||
'wsConnection': window.connection || null,
|
||||
'prefs': window.html_prefs || {},
|
||||
});
|
||||
GoAccess.App.initialize();
|
||||
};
|
||||
}());
|
||||
|
85
src/base64.c
85
src/base64.c
@ -29,6 +29,7 @@
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "base64.h"
|
||||
#include "xmalloc.h"
|
||||
@ -76,3 +77,87 @@ base64_encode (const void *buf, size_t size) {
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
/*
|
||||
* base64_decode
|
||||
*
|
||||
* Given a Base64 encoded string in 'data', this function decodes it into
|
||||
* a newly allocated binary buffer. The length of the decoded data is stored
|
||||
* in *out_len. The caller is responsible for freeing the returned buffer.
|
||||
*
|
||||
* Returns NULL on error (for example, if the data's length is not a multiple
|
||||
* of 4).
|
||||
*/
|
||||
char *
|
||||
base64_decode (const char *data, size_t *out_len) {
|
||||
size_t decoded_len = 0, i = 0, j = 0, pad = 0, len = 0;
|
||||
char *out = NULL;
|
||||
uint32_t triple = 0;
|
||||
|
||||
/* Create a lookup table for decoding.
|
||||
* For valid Base64 characters 'A'-'Z', 'a'-'z', '0'-'9', '+', '/',
|
||||
* we place their value; for '=' we simply return 0.
|
||||
* All other characters are marked as 0x80 (an invalid marker).
|
||||
*/
|
||||
static const unsigned char dtable[256] = {
|
||||
['A'] = 0,['B'] = 1,['C'] = 2,['D'] = 3,
|
||||
['E'] = 4,['F'] = 5,['G'] = 6,['H'] = 7,
|
||||
['I'] = 8,['J'] = 9,['K'] = 10,['L'] = 11,
|
||||
['M'] = 12,['N'] = 13,['O'] = 14,['P'] = 15,
|
||||
['Q'] = 16,['R'] = 17,['S'] = 18,['T'] = 19,
|
||||
['U'] = 20,['V'] = 21,['W'] = 22,['X'] = 23,
|
||||
['Y'] = 24,['Z'] = 25,
|
||||
['a'] = 26,['b'] = 27,['c'] = 28,['d'] = 29,
|
||||
['e'] = 30,['f'] = 31,['g'] = 32,['h'] = 33,
|
||||
['i'] = 34,['j'] = 35,['k'] = 36,['l'] = 37,
|
||||
['m'] = 38,['n'] = 39,['o'] = 40,['p'] = 41,
|
||||
['q'] = 42,['r'] = 43,['s'] = 44,['t'] = 45,
|
||||
['u'] = 46,['v'] = 47,['w'] = 48,['x'] = 49,
|
||||
['y'] = 50,['z'] = 51,
|
||||
['0'] = 52,['1'] = 53,['2'] = 54,['3'] = 55,
|
||||
['4'] = 56,['5'] = 57,['6'] = 58,['7'] = 59,
|
||||
['8'] = 60,['9'] = 61,
|
||||
['+'] = 62,['/'] = 63,
|
||||
['='] = 0,
|
||||
/* All other values are implicitly 0 (or you can mark them as invalid
|
||||
by setting them to 0x80 if you prefer stricter checking). */
|
||||
};
|
||||
|
||||
len = strlen (data);
|
||||
/* Validate length: Base64 encoded data must be a multiple of 4 */
|
||||
if (len % 4 != 0)
|
||||
return NULL;
|
||||
|
||||
/* Count padding characters at the end */
|
||||
if (len) {
|
||||
if (data[len - 1] == '=')
|
||||
pad++;
|
||||
if (len > 1 && data[len - 2] == '=')
|
||||
pad++;
|
||||
}
|
||||
|
||||
/* Calculate the length of the decoded data */
|
||||
decoded_len = (len / 4) * 3 - pad;
|
||||
out = (char *) xmalloc (decoded_len + 1); /* +1 for a null terminator if needed */
|
||||
|
||||
for (i = 0, j = 0; i < len;) {
|
||||
unsigned int sextet_a = data[i] == '=' ? 0 : dtable[(unsigned char) data[i]];
|
||||
unsigned int sextet_b = data[i + 1] == '=' ? 0 : dtable[(unsigned char) data[i + 1]];
|
||||
unsigned int sextet_c = data[i + 2] == '=' ? 0 : dtable[(unsigned char) data[i + 2]];
|
||||
unsigned int sextet_d = data[i + 3] == '=' ? 0 : dtable[(unsigned char) data[i + 3]];
|
||||
i += 4;
|
||||
|
||||
triple = (sextet_a << 18) | (sextet_b << 12) | (sextet_c << 6) | sextet_d;
|
||||
if (j < decoded_len)
|
||||
out[j++] = (triple >> 16) & 0xFF;
|
||||
if (j < decoded_len)
|
||||
out[j++] = (triple >> 8) & 0xFF;
|
||||
if (j < decoded_len)
|
||||
out[j++] = triple & 0xFF;
|
||||
}
|
||||
|
||||
out[decoded_len] = '\0'; /* Null-terminate the output buffer */
|
||||
if (out_len)
|
||||
*out_len = decoded_len;
|
||||
return out;
|
||||
}
|
||||
|
@ -33,5 +33,6 @@
|
||||
#include <stddef.h>
|
||||
|
||||
char *base64_encode (const void *buf, size_t size);
|
||||
char *base64_decode (const char *data, size_t *out_len);
|
||||
|
||||
#endif // for #ifndef BASE64_H
|
||||
|
@ -43,6 +43,7 @@
|
||||
#include "json.h"
|
||||
#include "settings.h"
|
||||
#include "websocket.h"
|
||||
#include "wsauth.h"
|
||||
#include "xmalloc.h"
|
||||
|
||||
/* Allocate memory for a new GWSReader instance.
|
||||
@ -356,6 +357,12 @@ set_ws_opts (void) {
|
||||
ws_set_config_sslcert (conf.sslcert);
|
||||
if (conf.sslkey)
|
||||
ws_set_config_sslkey (conf.sslkey);
|
||||
#ifdef HAVE_LIBSSL
|
||||
if (conf.ws_auth_secret) {
|
||||
ws_set_config_auth_secret (conf.ws_auth_secret);
|
||||
ws_set_config_auth_cb (verify_jwt_token);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Setup and start the WebSocket threads. */
|
||||
|
131
src/options.c
131
src/options.c
@ -33,7 +33,9 @@
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <stdlib.h>
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
#include <getopt.h>
|
||||
#include <errno.h>
|
||||
@ -47,6 +49,7 @@
|
||||
#include "error.h"
|
||||
#include "labels.h"
|
||||
#include "util.h"
|
||||
#include "wsauth.h"
|
||||
|
||||
#include "xmalloc.h"
|
||||
|
||||
@ -153,6 +156,10 @@ static const struct option long_opts[] = {
|
||||
#endif
|
||||
{"time-format" , required_argument , 0 , 0 } ,
|
||||
{"ws-url" , required_argument , 0 , 0 } ,
|
||||
#ifdef HAVE_LIBSSL
|
||||
{"ws-auth" , required_argument , 0 , 0 } ,
|
||||
{"ws-auth-expire" , required_argument , 0 , 0 } ,
|
||||
#endif
|
||||
{"ping-interval" , required_argument , 0 , 0 } ,
|
||||
#ifdef HAVE_GEOLOCATION
|
||||
{"geoip-database" , required_argument , 0 , 0 } ,
|
||||
@ -234,6 +241,12 @@ cmd_help (void)
|
||||
" --ssl-key=<priv.key> - Path to TLS/SSL private key.\n"
|
||||
" --user-name=<username> - Run as the specified user.\n"
|
||||
" --ws-url=<url> - URL to which the WebSocket server responds.\n"
|
||||
#ifdef HAVE_LIBSSL
|
||||
" --ws-auth=<jwt[:secret]> - Enables WebSocket authentication using a\n"
|
||||
" JSON Web Token (JWT). Optionally, a secret key\n"
|
||||
" can be provided for verification.\n"
|
||||
" --ws-auth-expire=<secs> - Time after which the JWT expires.\n"
|
||||
#endif
|
||||
" --ping-interval=<secs> - Enable WebSocket ping with specified\n"
|
||||
" interval in seconds.\n"
|
||||
"\n"
|
||||
@ -365,6 +378,113 @@ set_array_opt (const char *oarg, const char *arr[], int *size, int max) {
|
||||
arr[(*size)++] = oarg;
|
||||
}
|
||||
|
||||
#ifdef HAVE_LIBSSL
|
||||
/*
|
||||
* parse_ws_auth_expire_option:
|
||||
* Parses a time duration string and converts it to seconds.
|
||||
*
|
||||
* Supported formats:
|
||||
* "3600" -> 3600 seconds
|
||||
* "24h" -> 24 hours = 24 * 3600 seconds
|
||||
* "10m" -> 10 minutes = 10 * 60 seconds
|
||||
* "10d" -> 10 days = 10 * 86400 seconds
|
||||
*
|
||||
* Returns 0 on success, nonzero on failure.
|
||||
*/
|
||||
static int
|
||||
parse_ws_auth_expire_option (const char *input) {
|
||||
char *endptr = NULL;
|
||||
double value = 0, multiplier = 0, total_seconds = 0;
|
||||
|
||||
if (!input || *input == '\0')
|
||||
return -1; // Empty or NULL string
|
||||
|
||||
value = strtod (input, &endptr);
|
||||
if (endptr == input) {
|
||||
// No conversion could be performed
|
||||
return -1;
|
||||
}
|
||||
|
||||
multiplier = 1.0;
|
||||
/* Skip any whitespace after the number */
|
||||
while (isspace ((unsigned char) *endptr))
|
||||
endptr++;
|
||||
|
||||
if (*endptr != '\0') {
|
||||
/* Expect a single unit character; any extra characters are not allowed */
|
||||
char unit = *endptr;
|
||||
endptr++;
|
||||
while (isspace ((unsigned char) *endptr))
|
||||
endptr++;
|
||||
|
||||
if (*endptr != '\0') {
|
||||
// Extra unexpected characters
|
||||
return -1;
|
||||
}
|
||||
|
||||
switch (unit) {
|
||||
case 's':
|
||||
case 'S':
|
||||
multiplier = 1.0;
|
||||
break;
|
||||
case 'm':
|
||||
case 'M':
|
||||
multiplier = 60.0;
|
||||
break;
|
||||
case 'h':
|
||||
case 'H':
|
||||
multiplier = 3600.0;
|
||||
break;
|
||||
case 'd':
|
||||
case 'D':
|
||||
multiplier = 86400.0;
|
||||
break;
|
||||
default:
|
||||
// Unknown unit
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
total_seconds = value * multiplier;
|
||||
if (total_seconds < 0)
|
||||
return -1; // Negative durations are not allowed
|
||||
|
||||
/* Store the result in your global configuration */
|
||||
conf.ws_auth_expire = (long) total_seconds;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
parse_ws_auth_option (const char *optarg) {
|
||||
/* Check if the option starts with "jwt:" */
|
||||
if (strncmp (optarg, "jwt:", 4) == 0) {
|
||||
const char *secret_part = optarg + 4;
|
||||
|
||||
/* Check if the secret is a file path */
|
||||
if (access (secret_part, F_OK) == 0) {
|
||||
/* File exists: read the file content */
|
||||
conf.ws_auth_secret = read_secret_from_file (secret_part);
|
||||
} else {
|
||||
/* Not a file: use the secret directly */
|
||||
conf.ws_auth_secret = strdup (secret_part);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
/* Check for plain "jwt" option */
|
||||
if (strcmp (optarg, "jwt") == 0) {
|
||||
/* Try to get secret from environment variable */
|
||||
const char *env_secret = getenv ("GOACCESS_WSAUTH_SECRET");
|
||||
if (env_secret != NULL)
|
||||
conf.ws_auth_secret = strdup (env_secret);
|
||||
else
|
||||
conf.ws_auth_secret = generate_ws_auth_secret ();
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Parse command line long options. */
|
||||
static void
|
||||
parse_long_opt (const char *name, const char *oarg) {
|
||||
@ -540,6 +660,17 @@ parse_long_opt (const char *name, const char *oarg) {
|
||||
if (!strcmp ("ws-url", name))
|
||||
conf.ws_url = oarg;
|
||||
|
||||
#ifdef HAVE_LIBSSL
|
||||
/* WebSocket auth */
|
||||
if (!strcmp ("ws-auth", name) && parse_ws_auth_option (oarg) != 0)
|
||||
FATAL ("Invalid --ws-auth option.");
|
||||
|
||||
/* WebSocket auth JWT expires */
|
||||
if (!strcmp ("ws-auth-expire", name) && parse_ws_auth_expire_option (oarg) != 0)
|
||||
FATAL ("Invalid --ws-auth-expire option.");
|
||||
|
||||
#endif
|
||||
|
||||
/* WebSocket ping interval in seconds */
|
||||
if (!strcmp ("ping-interval", name))
|
||||
conf.ping_interval = oarg;
|
||||
|
24
src/output.c
24
src/output.c
@ -46,6 +46,7 @@
|
||||
#include "settings.h"
|
||||
#include "ui.h"
|
||||
#include "util.h"
|
||||
#include "wsauth.h"
|
||||
#include "xmalloc.h"
|
||||
|
||||
#include "tpls.h"
|
||||
@ -289,7 +290,10 @@ print_html_body (FILE * fp, const char *now)
|
||||
fprintf (fp,
|
||||
"<nav class='hidden-xs hidden-sm hide' aria-label='Main navigation'>"
|
||||
"</nav>"
|
||||
"<div class='loading-container'>"
|
||||
"<i class='spinner fa fa-circle-o-notch fa-spin fa-3x fa-fw' aria-hidden='true' aria-label='Loading'></i>"
|
||||
"<div class='app-loading-status'><small class='muted'></small></div>"
|
||||
"</div>"
|
||||
"<div class='container hide' role='document'>"
|
||||
"<header class='page-header'>"
|
||||
"<h1 class='h-dashboard'>"
|
||||
@ -501,14 +505,14 @@ hits_bw_req_plot (FILE *fp, GHTMLPlot plot, int sp) {
|
||||
|
||||
/* Output JSON data definitions. */
|
||||
static void
|
||||
print_json_data (FILE *fp, GHolder *holder) {
|
||||
print_json_data (FILE *fp, GHolder *holder, const char *jwt) {
|
||||
char *json = NULL;
|
||||
|
||||
if ((json = get_json (holder, 1)) == NULL)
|
||||
return;
|
||||
|
||||
fprintf (fp, external_assets ? "" : "<script type='text/javascript'>");
|
||||
fprintf (fp, "var json_data=%s", json);
|
||||
fprintf (fp, "var json_data=%s", (conf.ws_auth_secret && jwt ? "{}" : json));
|
||||
fprintf (fp, external_assets ? "\n" : "</script>");
|
||||
|
||||
free (json);
|
||||
@ -516,7 +520,7 @@ print_json_data (FILE *fp, GHolder *holder) {
|
||||
|
||||
/* Output WebSocket connection definition. */
|
||||
static void
|
||||
print_conn_def (FILE *fp) {
|
||||
print_conn_def (FILE *fp, const char *jwt) {
|
||||
int sp = 0;
|
||||
/* use tabs to prettify output */
|
||||
if (conf.json_pretty_print)
|
||||
@ -527,6 +531,9 @@ print_conn_def (FILE *fp) {
|
||||
|
||||
fprintf (fp, external_assets ? "" : "<script type='text/javascript'>");
|
||||
|
||||
if (conf.ws_auth_secret && jwt)
|
||||
fprintf (fp, "window.goaccessJWT=\"%s\";", jwt);
|
||||
|
||||
fprintf (fp, "var connection = ");
|
||||
fpopen_obj (fp, sp);
|
||||
fpskeysval (fp, "url", (conf.ws_url ? conf.ws_url : ""), sp, 0);
|
||||
@ -1279,6 +1286,7 @@ void
|
||||
output_html (GHolder *holder, const char *filename) {
|
||||
FILE *fp, *fjs = NULL, *fcs = NULL;
|
||||
char now[DATE_TIME] = { 0 };
|
||||
char *jwt = NULL;
|
||||
|
||||
if (filename != NULL)
|
||||
fp = fopen (filename, "w");
|
||||
@ -1302,15 +1310,21 @@ output_html (GHolder *holder, const char *filename) {
|
||||
generate_time ();
|
||||
strftime (now, DATE_TIME, "%Y-%m-%d %H:%M:%S %z", &now_tm);
|
||||
|
||||
#ifdef HAVE_LIBSSL
|
||||
jwt = create_jwt_token ();
|
||||
#endif
|
||||
|
||||
print_html_header (fp, fcs);
|
||||
|
||||
print_html_body (fp, now);
|
||||
print_json_defs ((fjs ? fjs : fp));
|
||||
print_json_data ((fjs ? fjs : fp), holder);
|
||||
print_conn_def ((fjs ? fjs : fp));
|
||||
print_json_data ((fjs ? fjs : fp), holder, jwt);
|
||||
print_conn_def ((fjs ? fjs : fp), jwt);
|
||||
|
||||
print_html_footer (fp, fjs);
|
||||
|
||||
free (jwt);
|
||||
|
||||
if (fjs)
|
||||
fclose (fjs);
|
||||
if (fcs)
|
||||
|
24
src/parser.c
24
src/parser.c
@ -358,26 +358,6 @@ free_glog (GLogItem *logitem) {
|
||||
free (logitem);
|
||||
}
|
||||
|
||||
/* Decodes the given URL-encoded string.
|
||||
*
|
||||
* On success, the decoded string is assigned to the output buffer. */
|
||||
#define B16210(x) (((x) >= '0' && (x) <= '9') ? ((x) - '0') : (toupper((unsigned char) (x)) - 'A' + 10))
|
||||
static void
|
||||
decode_hex (char *url, char *out) {
|
||||
char *ptr;
|
||||
const char *c;
|
||||
|
||||
for (c = url, ptr = out; *c; c++) {
|
||||
if (*c != '%' || !isxdigit ((unsigned char) c[1]) || !isxdigit ((unsigned char) c[2])) {
|
||||
*ptr++ = *c;
|
||||
} else {
|
||||
*ptr++ = (char) ((B16210 (c[1]) * 16) + (B16210 (c[2])));
|
||||
c += 2;
|
||||
}
|
||||
}
|
||||
*ptr = 0;
|
||||
}
|
||||
|
||||
/* Entry point to decode the given URL-encoded string.
|
||||
*
|
||||
* On success, the decoded trimmed string is assigned to the output
|
||||
@ -390,10 +370,10 @@ decode_url (char *url) {
|
||||
return NULL;
|
||||
|
||||
out = decoded = xstrdup (url);
|
||||
decode_hex (url, out);
|
||||
decode_hex (url, out, 0);
|
||||
/* double encoded URL? */
|
||||
if (conf.double_decode)
|
||||
decode_hex (decoded, out);
|
||||
decode_hex (decoded, out, 0);
|
||||
strip_newlines (out);
|
||||
|
||||
return trim_str (out);
|
||||
|
@ -249,6 +249,7 @@ free_cmd_args (void) {
|
||||
free (nargv[i]);
|
||||
free (nargv);
|
||||
free (conf.iconfigfile);
|
||||
free (conf.ws_auth_secret);
|
||||
}
|
||||
|
||||
/* Append extra value to argv */
|
||||
|
@ -146,6 +146,8 @@ typedef struct GConf_
|
||||
const char *ws_url; /* WebSocket URL */
|
||||
const char *ping_interval; /* WebSocket ping interval in seconds */
|
||||
const char *unix_socket; /* unix socket to bind to */
|
||||
char *ws_auth_secret; /* WebSocket AUTH */
|
||||
long ws_auth_expire; /* WebSocket AUTH JWT expire in seconds */
|
||||
|
||||
/* User flags */
|
||||
int all_static_files; /* parse all static files */
|
||||
|
23
src/util.c
23
src/util.c
@ -973,6 +973,29 @@ u642str (uint64_t d, int width) {
|
||||
return s;
|
||||
}
|
||||
|
||||
/* Decodes the given URL-encoded string.
|
||||
*
|
||||
* On success, the decoded string is assigned to the output buffer. */
|
||||
#define B16TOD(x) (((x) >= '0' && (x) <= '9') ? ((x) - '0') : (toupper((unsigned char) (x)) - 'A' + 10))
|
||||
void
|
||||
decode_hex (char *url, char *out, int decode_plus) {
|
||||
char *ptr;
|
||||
const char *c;
|
||||
for (c = url, ptr = out; *c; c++) {
|
||||
if (*c != '%' || !isxdigit ((unsigned char) c[1]) || !isxdigit ((unsigned char) c[2])) {
|
||||
if (decode_plus && *c == '+') {
|
||||
*ptr++ = ' ';
|
||||
} else {
|
||||
*ptr++ = *c;
|
||||
}
|
||||
} else {
|
||||
*ptr++ = (char) ((B16TOD (c[1]) * 16) + (B16TOD (c[2])));
|
||||
c += 2;
|
||||
}
|
||||
}
|
||||
*ptr = 0;
|
||||
}
|
||||
|
||||
/* Convert the given float to a string with the ability to add some
|
||||
* padding.
|
||||
*
|
||||
|
@ -105,6 +105,7 @@ off_t file_size (const char *filename);
|
||||
size_t append_str (char **dest, const char *src);
|
||||
uint32_t djb2 (const unsigned char *str);
|
||||
uint64_t u64encode (uint32_t x, uint32_t y);
|
||||
void decode_hex(char *url, char *out, int decode_plus);
|
||||
void genstr (char *dest, size_t len);
|
||||
void set_tz (void);
|
||||
void strip_newlines (char *str);
|
||||
|
@ -437,6 +437,8 @@ ws_free_header_fields (WSHeaders *headers) {
|
||||
free (headers->path);
|
||||
if (headers->protocol)
|
||||
free (headers->protocol);
|
||||
if (headers->jwt)
|
||||
free (headers->jwt);
|
||||
if (headers->upgrade)
|
||||
free (headers->upgrade);
|
||||
if (headers->ws_accept)
|
||||
@ -1129,13 +1131,48 @@ ws_verify_req_headers (WSHeaders *headers) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Extract JWT token from query string */
|
||||
static char *
|
||||
ws_extract_jwt_token (char *path) {
|
||||
char decoded_token[8192] = { 0 }; // Adjust size as needed
|
||||
char *query = NULL, *tokenParam = NULL, *end = NULL;
|
||||
if (!path)
|
||||
return NULL;
|
||||
|
||||
// Look for a query string in the path
|
||||
query = strchr (path, '?');
|
||||
if (!query)
|
||||
return NULL;
|
||||
|
||||
query++; // Skip the '?'
|
||||
|
||||
// Look for the token parameter
|
||||
tokenParam = strstr (query, "token=");
|
||||
if (!tokenParam)
|
||||
return NULL;
|
||||
|
||||
tokenParam += strlen ("token="); // Move pointer past "token="
|
||||
|
||||
// Decode the token
|
||||
decode_hex (tokenParam, decoded_token, 1);
|
||||
|
||||
// Find the end of the token (next '&' or end of string)
|
||||
end = strpbrk (decoded_token, "& ");
|
||||
if (end)
|
||||
*end = '\0';
|
||||
|
||||
// Return a dynamically allocated copy of the decoded token
|
||||
return strdup (decoded_token);
|
||||
}
|
||||
|
||||
/* From RFC2616, each header field consists of a name followed by a
|
||||
* colon (":") and the field value. Field names are case-insensitive.
|
||||
* The field value MAY be preceded by any amount of LWS, though a
|
||||
* single SP is preferred */
|
||||
static int
|
||||
ws_set_header_fields (char *line, WSHeaders *headers) {
|
||||
char *path = NULL, *method = NULL, *proto = NULL, *p, *value;
|
||||
char *path = NULL, *method = NULL, *proto = NULL;
|
||||
char *p = NULL, *value = NULL;
|
||||
|
||||
if (line[0] == '\n' || line[0] == '\r')
|
||||
return 1;
|
||||
@ -1143,10 +1180,14 @@ ws_set_header_fields (char *line, WSHeaders *headers) {
|
||||
if ((strstr (line, "GET ")) || (strstr (line, "get "))) {
|
||||
if ((path = ws_parse_request (line, &method, &proto)) == NULL)
|
||||
return 1;
|
||||
|
||||
headers->path = path;
|
||||
headers->method = method;
|
||||
headers->protocol = proto;
|
||||
|
||||
/* Extract JWT token from path */
|
||||
headers->jwt = ws_extract_jwt_token (path);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -1161,15 +1202,16 @@ ws_set_header_fields (char *line, WSHeaders *headers) {
|
||||
return 1;
|
||||
|
||||
*p = '\0';
|
||||
|
||||
if (strpbrk (line, " \t") != NULL) {
|
||||
*p = ' ';
|
||||
return 1;
|
||||
}
|
||||
|
||||
while (isspace ((unsigned char) *value))
|
||||
value++;
|
||||
|
||||
ws_set_header_key_value (headers, line, value);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -1619,6 +1661,13 @@ ws_get_handshake (WSClient *client, WSServer *server) {
|
||||
return ws_set_status (client, WS_CLOSE, bytes);
|
||||
}
|
||||
|
||||
/* Ensure we can authenticate the connection */
|
||||
if (wsconfig.auth_secret && wsconfig.auth (client->headers->jwt, wsconfig.auth_secret) != 1) {
|
||||
LOG (("Unable to authenticate connection %d [%s]...\n", client->listener, client->remote_ip));
|
||||
http_error (client, WS_UNAUTHORIZED_STR);
|
||||
return ws_set_status (client, WS_CLOSE, bytes);
|
||||
}
|
||||
|
||||
ws_set_handshake_headers (client->headers);
|
||||
|
||||
/* handshake response */
|
||||
@ -2961,6 +3010,17 @@ ws_set_config_sslkey (const char *sslkey) {
|
||||
wsconfig.sslkey = sslkey;
|
||||
}
|
||||
|
||||
/* Set auth secret for auth. */
|
||||
void
|
||||
ws_set_config_auth_secret (const char *auth_secret) {
|
||||
wsconfig.auth_secret = auth_secret;
|
||||
}
|
||||
|
||||
void
|
||||
ws_set_config_auth_cb (int (*auth_cb) (const char *jwt, const char *secret)) {
|
||||
wsconfig.auth = auth_cb;
|
||||
}
|
||||
|
||||
/* Create a new websocket server context. */
|
||||
WSServer *
|
||||
ws_init (const char *host, const char *port, void (*initopts) (void)) {
|
||||
|
@ -102,6 +102,7 @@
|
||||
#include "gslist.h"
|
||||
|
||||
#define WS_BAD_REQUEST_STR "HTTP/1.1 400 Invalid Request\r\n\r\n"
|
||||
#define WS_UNAUTHORIZED_STR "HTTP/1.1 401 Unauthorized\r\n\r\n"
|
||||
#define WS_SWITCH_PROTO_STR "HTTP/1.1 101 Switching Protocols"
|
||||
#define WS_TOO_BUSY_STR "HTTP/1.1 503 Service Unavailable\r\n\r\n"
|
||||
|
||||
@ -178,6 +179,7 @@ typedef struct WSHeaders_ {
|
||||
|
||||
char *agent;
|
||||
char *path;
|
||||
char *jwt;
|
||||
char *method;
|
||||
char *protocol;
|
||||
char *host;
|
||||
@ -272,6 +274,11 @@ typedef struct WSConfig_ {
|
||||
const char *sslcert;
|
||||
const char *sslkey;
|
||||
const char *unix_socket;
|
||||
const char *auth_secret;
|
||||
|
||||
/* Function pointer for JWT verification */
|
||||
int (*auth) (const char *jwt, const char *secret);
|
||||
|
||||
int echomode;
|
||||
int strict;
|
||||
int max_frm_size;
|
||||
@ -322,6 +329,8 @@ void ws_set_config_port (const char *port);
|
||||
void ws_set_config_sslcert (const char *sslcert);
|
||||
void ws_set_config_sslkey (const char *sslkey);
|
||||
void ws_set_config_strict (int strict);
|
||||
void ws_set_config_auth_secret (const char *auth_secret);
|
||||
void ws_set_config_auth_cb (int (*auth_cb) (const char *jwt, const char *secret));
|
||||
void ws_start (WSServer * server);
|
||||
void ws_stop (WSServer * server);
|
||||
WSServer *ws_init (const char *host, const char *port, void (*initopts) (void));
|
||||
|
421
src/wsauth.c
Normal file
421
src/wsauth.c
Normal file
@ -0,0 +1,421 @@
|
||||
/**
|
||||
* wsauth.c - web socket authentication
|
||||
* ______ ___
|
||||
* / ____/___ / | _____________ __________
|
||||
* / / __/ __ \/ /| |/ ___/ ___/ _ \/ ___/ ___/
|
||||
* / /_/ / /_/ / ___ / /__/ /__/ __(__ |__ )
|
||||
* \____/\____/_/ |_\___/\___/\___/____/____/
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
* Copyright (c) 2009-2024 Gerardo Orellana <hello @ goaccess.io>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <ctype.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <inttypes.h>
|
||||
#include <unistd.h>
|
||||
#include <limits.h>
|
||||
|
||||
#include <openssl/hmac.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/evp.h>
|
||||
|
||||
#include "wsauth.h"
|
||||
|
||||
#include "base64.h"
|
||||
#include "util.h"
|
||||
#include "pdjson.h"
|
||||
#include "settings.h"
|
||||
#include "xmalloc.h"
|
||||
|
||||
char *
|
||||
read_secret_from_file (const char *path) {
|
||||
FILE *file = fopen (path, "r");
|
||||
char *secret = xcalloc (1, MAX_SECRET_SIZE);
|
||||
|
||||
if (!file) {
|
||||
perror ("Error opening secret file");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!secret) {
|
||||
perror ("Error allocating memory");
|
||||
fclose (file);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!fgets (secret, MAX_SECRET_SIZE, file)) {
|
||||
perror ("Error reading secret file");
|
||||
free (secret);
|
||||
fclose (file);
|
||||
return NULL;
|
||||
}
|
||||
fclose (file);
|
||||
|
||||
// Remove trailing newline, if present.
|
||||
secret[strcspn (secret, "\n")] = '\0';
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
/* Generate a new secret (HS256-compatible) and return it as a hex string. */
|
||||
char *
|
||||
generate_ws_auth_secret (void) {
|
||||
char *secret_hex = NULL;
|
||||
int secret_len = 32; // 256 bits
|
||||
unsigned char secret_bytes[32];
|
||||
|
||||
if (RAND_bytes (secret_bytes, secret_len) != 1) {
|
||||
fprintf (stderr, "Error generating random bytes\n");
|
||||
return NULL;
|
||||
}
|
||||
secret_hex = xmalloc (secret_len * 2 + 1);
|
||||
if (!secret_hex)
|
||||
return NULL;
|
||||
for (int i = 0; i < secret_len; i++) {
|
||||
sprintf (&secret_hex[i * 2], "%02x", secret_bytes[i]);
|
||||
}
|
||||
secret_hex[secret_len * 2] = '\0';
|
||||
return secret_hex;
|
||||
}
|
||||
|
||||
static char *
|
||||
create_jwt_payload (const char *sub, long iat, long exp) {
|
||||
const char *aud = "goaccess_ws";
|
||||
const char *scope = "report_access";
|
||||
char *payload = NULL;
|
||||
char hostname[HOST_NAME_MAX];
|
||||
|
||||
if (gethostname (hostname, sizeof (hostname)) != 0) {
|
||||
perror ("gethostname");
|
||||
// Fallback to a default issuer value if hostname retrieval fails.
|
||||
strcpy (hostname, "goaccess");
|
||||
}
|
||||
// Allocate a buffer for the payload JSON.
|
||||
// Adjust the size if you plan on including more data.
|
||||
payload = xcalloc (1, MAX_JWT_PAYLOAD);
|
||||
if (!payload)
|
||||
return NULL;
|
||||
|
||||
// Build the JSON payload.
|
||||
snprintf (payload, MAX_JWT_PAYLOAD,
|
||||
"{\"iss\":\"%s\",\"sub\":\"%s\",\"iat\":%ld,\"exp\":%ld,\"aud\":\"%s\",\"scope\":\"%s\"}",
|
||||
hostname, sub, iat, exp, aud, scope);
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
char *
|
||||
create_jwt_token (void) {
|
||||
char *jwt = NULL, *payload = NULL, report_id[50];
|
||||
time_t now = time (NULL);
|
||||
struct tm *jwt_now_tm = localtime (&now);
|
||||
// Configure token lifetime in seconds
|
||||
long token_lifetime = conf.ws_auth_expire > 0 ? conf.ws_auth_expire : DEFAULT_EXPIRE_TIME;
|
||||
long iat = now;
|
||||
long exp = now + token_lifetime;
|
||||
|
||||
// Format the date as "YYYYMMDD" for the report ID
|
||||
snprintf (report_id, sizeof (report_id), "goaccess_report_%04d%02d%02d",
|
||||
jwt_now_tm->tm_year + 1900, jwt_now_tm->tm_mon + 1, jwt_now_tm->tm_mday);
|
||||
|
||||
if (!conf.ws_auth_secret)
|
||||
return NULL;
|
||||
|
||||
payload = create_jwt_payload (report_id, iat, exp);
|
||||
if (!payload) {
|
||||
fprintf (stderr, "Failed to create JWT payload\n");
|
||||
return NULL;
|
||||
}
|
||||
jwt = generate_jwt (conf.ws_auth_secret, payload);
|
||||
free (payload);
|
||||
return jwt;
|
||||
}
|
||||
|
||||
static int
|
||||
verify_jwt_signature (const char *jwt, const char *secret) {
|
||||
char *token_dup = NULL, *header_part = NULL, *payload_part = NULL, *signature_part = NULL,
|
||||
*signing_input = NULL, *computed_signature = NULL;
|
||||
unsigned char *hmac_result = NULL;
|
||||
unsigned int hmac_len = 0;
|
||||
int valid = 0;
|
||||
size_t signing_input_len = 0;
|
||||
|
||||
if (!jwt || !secret) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
token_dup = strdup (jwt);
|
||||
if (!token_dup) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
header_part = strtok (token_dup, ".");
|
||||
payload_part = strtok (NULL, ".");
|
||||
signature_part = strtok (NULL, ".");
|
||||
if (!header_part || !payload_part || !signature_part) {
|
||||
free (token_dup);
|
||||
return 0;
|
||||
}
|
||||
|
||||
signing_input_len = strlen (header_part) + 1 + strlen (payload_part) + 1;
|
||||
signing_input = malloc (signing_input_len);
|
||||
if (!signing_input) {
|
||||
free (token_dup);
|
||||
return 0;
|
||||
}
|
||||
snprintf (signing_input, signing_input_len, "%s.%s", header_part, payload_part);
|
||||
|
||||
hmac_result = HMAC (EVP_sha256 (), secret, strlen (secret), (unsigned char *) signing_input,
|
||||
strlen (signing_input), NULL, &hmac_len);
|
||||
free (signing_input);
|
||||
if (!hmac_result) {
|
||||
free (token_dup);
|
||||
return 0;
|
||||
}
|
||||
|
||||
computed_signature = base64_encode (hmac_result, hmac_len);
|
||||
if (!computed_signature) {
|
||||
free (token_dup);
|
||||
if (hmac_result)
|
||||
OPENSSL_free (hmac_result);
|
||||
return 0;
|
||||
}
|
||||
|
||||
valid = (strcmp (computed_signature, signature_part) == 0);
|
||||
|
||||
free (computed_signature);
|
||||
free (token_dup);
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
static int
|
||||
validate_jwt_claims (const char *payload_json) {
|
||||
json_stream json;
|
||||
enum json_type t = JSON_ERROR;
|
||||
size_t len = 0, level = 0;
|
||||
enum json_type ctx = JSON_ERROR;
|
||||
char hostname[HOST_NAME_MAX] = { 0 };
|
||||
char *curr_key = NULL;
|
||||
|
||||
/* Validation flags/values. */
|
||||
int valid_iss = 0, valid_sub = 0, valid_aud = 0, valid_scope = 0;
|
||||
long iat = 0, exp = 0;
|
||||
time_t now = time (NULL);
|
||||
|
||||
/* Get hostname for the issuer check. */
|
||||
if (gethostname (hostname, sizeof (hostname)) != 0) {
|
||||
perror ("gethostname");
|
||||
strcpy (hostname, "goaccess");
|
||||
}
|
||||
|
||||
/* Open JSON payload as a stream and disable streaming mode. */
|
||||
json_open_string (&json, payload_json);
|
||||
json_set_streaming (&json, false);
|
||||
|
||||
/* The payload should be a JSON object. */
|
||||
t = json_next (&json);
|
||||
if (t != JSON_OBJECT) {
|
||||
json_close (&json);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Iterate over each token (key or value) in the JSON object. */
|
||||
while ((t = json_next (&json)) != JSON_DONE && t != JSON_ERROR) {
|
||||
ctx = json_get_context (&json, &level);
|
||||
/* keys typically appear when (level % 2) != 0 and not inside an array.
|
||||
* otherwise, the token is a value. */
|
||||
if ((level % 2) != 0 && ctx != JSON_ARRAY) {
|
||||
/* This token is a key. Duplicate it to use for matching. */
|
||||
if (curr_key)
|
||||
free (curr_key);
|
||||
curr_key = xstrdup (json_get_string (&json, &len));
|
||||
} else {
|
||||
/* Assume this token is the value for the last encountered key. */
|
||||
if (curr_key) {
|
||||
char *val = xstrdup (json_get_string (&json, &len));
|
||||
if (strcmp (curr_key, "iss") == 0) {
|
||||
/* "iss" must equal the hostname. */
|
||||
valid_iss = (strcmp (val, hostname) == 0);
|
||||
} else if (strcmp (curr_key, "sub") == 0) {
|
||||
/* "sub" must be non-empty. */
|
||||
valid_sub = (val[0] != '\0');
|
||||
} else if (strcmp (curr_key, "aud") == 0) {
|
||||
valid_aud = (strcmp (val, "goaccess_ws") == 0);
|
||||
} else if (strcmp (curr_key, "scope") == 0) {
|
||||
valid_scope = (strcmp (val, "report_access") == 0);
|
||||
} else if (strcmp (curr_key, "iat") == 0) {
|
||||
iat = strtol (val, NULL, 10);
|
||||
} else if (strcmp (curr_key, "exp") == 0) {
|
||||
exp = strtol (val, NULL, 10);
|
||||
}
|
||||
free (val);
|
||||
free (curr_key);
|
||||
curr_key = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (curr_key)
|
||||
free (curr_key);
|
||||
json_close (&json);
|
||||
|
||||
/* Final validation: All required properties must be valid and the token time window correct.
|
||||
* iat must be > 0, exp must be after iat, and the current time must be between iat and exp.
|
||||
*/
|
||||
if (valid_iss && valid_sub && valid_aud && valid_scope &&
|
||||
iat > 0 && exp > iat && now >= iat && now <= exp) {
|
||||
return 1; // Valid JWT claims.
|
||||
}
|
||||
return 0; // One or more claim validations failed.
|
||||
}
|
||||
|
||||
|
||||
/* verifies the JWT signature.
|
||||
* Returns 1 if valid, 0 if not.
|
||||
*/
|
||||
int
|
||||
verify_jwt_token (const char *jwt, const char *secret) {
|
||||
char *payload_part = NULL, *payload_json = NULL, *token_dup = NULL;
|
||||
size_t payload_len = 0;
|
||||
int valid_signature = 0, valid_claims = 0;
|
||||
|
||||
/* Step 1: Verify the signature */
|
||||
valid_signature = verify_jwt_signature (jwt, secret);
|
||||
if (!valid_signature) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Step 2: Extract the payload part from the JWT */
|
||||
token_dup = strdup (jwt);
|
||||
if (!token_dup) {
|
||||
return 0;
|
||||
}
|
||||
strtok (token_dup, "."); // Skip header
|
||||
payload_part = strtok (NULL, "."); // Get payload
|
||||
if (!payload_part) {
|
||||
free (token_dup);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Step 3: Decode the base64url-encoded payload */
|
||||
payload_json = base64_decode (payload_part, &payload_len);
|
||||
free (token_dup);
|
||||
if (!payload_json) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Null-terminate the payload JSON string for parsing */
|
||||
payload_json = realloc (payload_json, payload_len + 1);
|
||||
if (!payload_json) {
|
||||
return 0;
|
||||
}
|
||||
payload_json[payload_len] = '\0';
|
||||
|
||||
/* Step 4: Validate the claims */
|
||||
valid_claims = validate_jwt_claims (payload_json);
|
||||
|
||||
/* Clean up */
|
||||
free (payload_json);
|
||||
|
||||
return valid_claims;
|
||||
|
||||
}
|
||||
|
||||
/* Generate a JWT signed with HMAC-SHA256.
|
||||
* - secret: the secret key as a string (from conf.ws_auth_secret)
|
||||
* - payload: a JSON string to be used as the JWT payload.
|
||||
*
|
||||
* The JWT header is fixed to {"alg":"HS256","typ":"JWT"}.
|
||||
*
|
||||
* The returned JWT is dynamically allocated and must be freed by the caller.
|
||||
*/
|
||||
char *
|
||||
generate_jwt (const char *secret, const char *payload) {
|
||||
char *encoded_payload = NULL, *encoded_header = NULL, *encoded_signature = NULL;
|
||||
char *signing_input = NULL;
|
||||
const char *header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
|
||||
unsigned char *hmac_result = NULL;
|
||||
unsigned int hmac_len = 0;
|
||||
size_t jwt_len = 0, signing_input_len = 0;
|
||||
char *jwt = NULL;
|
||||
|
||||
encoded_header = base64_encode ((const unsigned char *) header, strlen (header));
|
||||
if (!encoded_header)
|
||||
return NULL;
|
||||
encoded_payload = base64_encode ((const unsigned char *) payload, strlen (payload));
|
||||
if (!encoded_payload) {
|
||||
free (encoded_header);
|
||||
return NULL;
|
||||
}
|
||||
// Create the signing input: "<encoded_header>.<encoded_payload>"
|
||||
signing_input_len = strlen (encoded_header) + 1 + strlen (encoded_payload) + 1;
|
||||
signing_input = malloc (signing_input_len);
|
||||
if (!signing_input) {
|
||||
free (encoded_header);
|
||||
free (encoded_payload);
|
||||
return NULL;
|
||||
}
|
||||
snprintf (signing_input, signing_input_len, "%s.%s", encoded_header, encoded_payload);
|
||||
|
||||
// Compute HMAC-SHA256 signature
|
||||
hmac_result =
|
||||
HMAC (EVP_sha256 (), secret, strlen (secret), (unsigned char *) signing_input,
|
||||
strlen (signing_input), NULL, &hmac_len);
|
||||
if (!hmac_result) {
|
||||
free (encoded_header);
|
||||
free (encoded_payload);
|
||||
free (signing_input);
|
||||
return NULL;
|
||||
}
|
||||
// Base64url-encode the signature
|
||||
encoded_signature = base64_encode (hmac_result, hmac_len);
|
||||
if (!encoded_signature) {
|
||||
free (encoded_header);
|
||||
free (encoded_payload);
|
||||
free (signing_input);
|
||||
return NULL;
|
||||
}
|
||||
// Build the final JWT: "<encoded_header>.<encoded_payload>.<encoded_signature>"
|
||||
jwt_len =
|
||||
strlen (encoded_header) + 1 + strlen (encoded_payload) + 1 + strlen (encoded_signature) + 1;
|
||||
jwt = malloc (jwt_len);
|
||||
if (!jwt) {
|
||||
free (encoded_header);
|
||||
free (encoded_payload);
|
||||
free (signing_input);
|
||||
free (encoded_signature);
|
||||
return NULL;
|
||||
}
|
||||
snprintf (jwt, jwt_len, "%s.%s.%s", encoded_header, encoded_payload, encoded_signature);
|
||||
|
||||
free (encoded_header);
|
||||
free (encoded_payload);
|
||||
free (signing_input);
|
||||
free (encoded_signature);
|
||||
|
||||
return jwt;
|
||||
}
|
43
src/wsauth.h
Normal file
43
src/wsauth.h
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* ______ ___
|
||||
* / ____/___ / | _____________ __________
|
||||
* / / __/ __ \/ /| |/ ___/ ___/ _ \/ ___/ ___/
|
||||
* / /_/ / /_/ / ___ / /__/ /__/ __(__ |__ )
|
||||
* \____/\____/_/ |_\___/\___/\___/____/____/
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
* Copyright (c) 2009-2024 Gerardo Orellana <hello @ goaccess.io>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef WSAUTH_H_INCLUDED
|
||||
#define WSAUTH_H_INCLUDED
|
||||
|
||||
#define MAX_SECRET_SIZE 256
|
||||
#define MAX_JWT_PAYLOAD 512
|
||||
#define DEFAULT_EXPIRE_TIME 28800 // seconds
|
||||
|
||||
char *create_jwt_token (void);
|
||||
char *generate_jwt (const char *secret, const char *payload);
|
||||
char *generate_ws_auth_secret (void);
|
||||
char *read_secret_from_file (const char *path);
|
||||
int verify_jwt_token (const char *jwt, const char *secret);
|
||||
|
||||
#endif // for #ifndef WSAUTH_H
|
Loading…
Reference in New Issue
Block a user