Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17b2aa8111 | ||
|
|
78d484cfe0 | ||
|
|
182e12817d | ||
|
|
9179d57cc9 | ||
|
|
9cb9b746d8 | ||
|
|
57a0089664 | ||
|
|
53f7608872 | ||
|
|
838ce5b45a | ||
|
|
e878b9557e | ||
|
|
6638d37d6f | ||
|
|
4c29e15329 |
@@ -5,6 +5,9 @@ ARG DEBUG_BUILD=false
|
|||||||
|
|
||||||
FROM alpine:3.19 AS builder
|
FROM alpine:3.19 AS builder
|
||||||
|
|
||||||
|
# Re-declare build argument in this stage
|
||||||
|
ARG DEBUG_BUILD=false
|
||||||
|
|
||||||
# Install build dependencies
|
# Install build dependencies
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
build-base \
|
build-base \
|
||||||
|
|||||||
122
api/index.css
122
api/index.css
@@ -305,6 +305,8 @@ h2 {
|
|||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right:5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group {
|
.input-group {
|
||||||
@@ -1107,3 +1109,123 @@ body.dark-mode .sql-results-table tbody tr:nth-child(even) {
|
|||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
SIDE NAVIGATION MENU
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
.side-nav {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: -300px;
|
||||||
|
width: 280px;
|
||||||
|
height: 100vh;
|
||||||
|
background: var(--secondary-color);
|
||||||
|
border-right: var(--border-width) solid var(--border-color);
|
||||||
|
z-index: 1000;
|
||||||
|
transition: left 0.3s ease;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-top: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav.open {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 999;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nav-overlay.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu li {
|
||||||
|
border-bottom: var(--border-width) solid var(--muted-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: block;
|
||||||
|
padding: 15px 20px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid var(--secondary-color);
|
||||||
|
background: none;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
border: 2px solid var(--secondary-color);
|
||||||
|
background:var(--muted-color);
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
text-decoration: underline;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-footer-btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 20px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-footer-btn:hover {
|
||||||
|
background:var(--muted-color);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-footer-btn:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title.clickable:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,11 +9,30 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<!-- Side Navigation Menu -->
|
||||||
|
<nav class="side-nav" id="side-nav">
|
||||||
|
<ul class="nav-menu">
|
||||||
|
<li><button class="nav-item" data-page="statistics">Statistics</button></li>
|
||||||
|
<li><button class="nav-item" data-page="subscriptions">Subscriptions</button></li>
|
||||||
|
<li><button class="nav-item" data-page="configuration">Configuration</button></li>
|
||||||
|
<li><button class="nav-item" data-page="authorization">Authorization</button></li>
|
||||||
|
<li><button class="nav-item" data-page="dm">DM</button></li>
|
||||||
|
<li><button class="nav-item" data-page="database">Database Query</button></li>
|
||||||
|
</ul>
|
||||||
|
<div class="nav-footer">
|
||||||
|
<button class="nav-footer-btn" id="nav-dark-mode-btn">DARK MODE</button>
|
||||||
|
<button class="nav-footer-btn" id="nav-logout-btn">LOGOUT</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Side Navigation Overlay -->
|
||||||
|
<div class="side-nav-overlay" id="side-nav-overlay"></div>
|
||||||
|
|
||||||
<!-- Header with title and profile display -->
|
<!-- Header with title and profile display -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
|
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<div class="header-title">
|
<div class="header-title clickable" id="header-title">
|
||||||
<span class="relay-letter" data-letter="R">R</span>
|
<span class="relay-letter" data-letter="R">R</span>
|
||||||
<span class="relay-letter" data-letter="E">E</span>
|
<span class="relay-letter" data-letter="E">E</span>
|
||||||
<span class="relay-letter" data-letter="L">L</span>
|
<span class="relay-letter" data-letter="L">L</span>
|
||||||
@@ -34,10 +53,7 @@
|
|||||||
<span id="header-user-name" class="header-user-name">Loading...</span>
|
<span id="header-user-name" class="header-user-name">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Logout dropdown -->
|
<!-- Logout dropdown -->
|
||||||
<div class="logout-dropdown" id="logout-dropdown" style="display: none;">
|
<!-- Dropdown menu removed - buttons moved to sidebar -->
|
||||||
<button type="button" id="dark-mode-btn" class="logout-btn">🌙 DARK MODE</button>
|
|
||||||
<button type="button" id="logout-btn" class="logout-btn">LOGOUT</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -54,9 +70,8 @@
|
|||||||
<div class="section flex-section" id="databaseStatisticsSection" style="display: none;">
|
<div class="section flex-section" id="databaseStatisticsSection" style="display: none;">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>DATABASE STATISTICS</h2>
|
<h2>DATABASE STATISTICS</h2>
|
||||||
<!-- Monitoring toggle button will be inserted here by JavaScript -->
|
<!-- Monitoring is now subscription-based - no toggle button needed -->
|
||||||
<!-- Temporarily disable auto-refresh button for real-time monitoring -->
|
<!-- Subscribe to kind 24567 events to receive real-time monitoring data -->
|
||||||
<!-- <button type="button" id="refresh-stats-btn" class="countdown-btn"></button> -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Event Rate Graph Container -->
|
<!-- Event Rate Graph Container -->
|
||||||
@@ -81,10 +96,26 @@
|
|||||||
<td>Total Events</td>
|
<td>Total Events</td>
|
||||||
<td id="total-events">-</td>
|
<td id="total-events">-</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Process ID</td>
|
||||||
|
<td id="process-id">-</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Active Subscriptions</td>
|
<td>Active Subscriptions</td>
|
||||||
<td id="active-subscriptions">-</td>
|
<td id="active-subscriptions">-</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Memory Usage</td>
|
||||||
|
<td id="memory-usage">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>CPU Core</td>
|
||||||
|
<td id="cpu-core">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>CPU Usage</td>
|
||||||
|
<td id="cpu-usage">-</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Oldest Event</td>
|
<td>Oldest Event</td>
|
||||||
<td id="oldest-event">-</td>
|
<td id="oldest-event">-</td>
|
||||||
@@ -185,15 +216,14 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Subscription ID</th>
|
<th>Subscription ID</th>
|
||||||
<th>Client IP</th>
|
<th>Client IP</th>
|
||||||
|
<th>WSI Pointer</th>
|
||||||
<th>Duration</th>
|
<th>Duration</th>
|
||||||
<th>Events Sent</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Filters</th>
|
<th>Filters</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="subscription-details-table-body">
|
<tbody id="subscription-details-table-body">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" style="text-align: center; font-style: italic;">No subscriptions active</td>
|
<td colspan="5" style="text-align: center; font-style: italic;">No subscriptions active</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
816
api/index.js
816
api/index.js
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ class ASCIIBarChart {
|
|||||||
* @param {boolean} [options.useBinMode=false] - Enable time bin mode for data aggregation
|
* @param {boolean} [options.useBinMode=false] - Enable time bin mode for data aggregation
|
||||||
* @param {number} [options.binDuration=10000] - Duration of each time bin in milliseconds (10 seconds default)
|
* @param {number} [options.binDuration=10000] - Duration of each time bin in milliseconds (10 seconds default)
|
||||||
* @param {string} [options.xAxisLabelFormat='elapsed'] - X-axis label format: 'elapsed', 'bins', 'timestamps', 'ranges'
|
* @param {string} [options.xAxisLabelFormat='elapsed'] - X-axis label format: 'elapsed', 'bins', 'timestamps', 'ranges'
|
||||||
|
* @param {boolean} [options.debug=false] - Enable debug logging
|
||||||
*/
|
*/
|
||||||
constructor(containerId, options = {}) {
|
constructor(containerId, options = {}) {
|
||||||
this.container = document.getElementById(containerId);
|
this.container = document.getElementById(containerId);
|
||||||
@@ -29,6 +30,7 @@ class ASCIIBarChart {
|
|||||||
this.xAxisLabel = options.xAxisLabel || '';
|
this.xAxisLabel = options.xAxisLabel || '';
|
||||||
this.yAxisLabel = options.yAxisLabel || '';
|
this.yAxisLabel = options.yAxisLabel || '';
|
||||||
this.autoFitWidth = options.autoFitWidth !== false; // Default to true
|
this.autoFitWidth = options.autoFitWidth !== false; // Default to true
|
||||||
|
this.debug = options.debug || false; // Debug logging option
|
||||||
|
|
||||||
// Time bin configuration
|
// Time bin configuration
|
||||||
this.useBinMode = options.useBinMode !== false; // Default to true
|
this.useBinMode = options.useBinMode !== false; // Default to true
|
||||||
@@ -55,32 +57,21 @@ class ASCIIBarChart {
|
|||||||
this.initializeBins();
|
this.initializeBins();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new data point to the chart
|
* Add a new data point to the chart
|
||||||
* @param {number} value - The numeric value to add
|
* @param {number} value - The numeric value to add
|
||||||
*/
|
*/
|
||||||
addValue(value) {
|
addValue(value) {
|
||||||
if (this.useBinMode) {
|
// Time bin mode: add value to current active bin count
|
||||||
// Time bin mode: increment count in current active bin
|
this.checkBinRotation(); // Ensure we have an active bin
|
||||||
this.checkBinRotation(); // Ensure we have an active bin
|
this.bins[this.currentBinIndex].count += value; // Changed from ++ to += value
|
||||||
this.bins[this.currentBinIndex].count++;
|
this.totalDataPoints++;
|
||||||
this.totalDataPoints++;
|
|
||||||
} else {
|
|
||||||
// Legacy mode: add individual values
|
|
||||||
this.data.push(value);
|
|
||||||
this.totalDataPoints++;
|
|
||||||
|
|
||||||
// Keep only the most recent data points
|
|
||||||
if (this.data.length > this.maxDataPoints) {
|
|
||||||
this.data.shift();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
this.updateInfo();
|
this.updateInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all data from the chart
|
* Clear all data from the chart
|
||||||
*/
|
*/
|
||||||
@@ -98,7 +89,7 @@ class ASCIIBarChart {
|
|||||||
this.render();
|
this.render();
|
||||||
this.updateInfo();
|
this.updateInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the width of the chart in characters
|
* Calculate the width of the chart in characters
|
||||||
* @returns {number} The chart width in characters
|
* @returns {number} The chart width in characters
|
||||||
@@ -119,14 +110,14 @@ class ASCIIBarChart {
|
|||||||
const totalWidth = yAxisPadding + yAxisNumbers + separator + dataWidth + padding;
|
const totalWidth = yAxisPadding + yAxisNumbers + separator + dataWidth + padding;
|
||||||
|
|
||||||
// Only log when width changes
|
// Only log when width changes
|
||||||
if (this.lastChartWidth !== totalWidth) {
|
if (this.debug && this.lastChartWidth !== totalWidth) {
|
||||||
console.log('getChartWidth changed:', { dataLength, totalWidth, previous: this.lastChartWidth });
|
console.log('getChartWidth changed:', { dataLength, totalWidth, previous: this.lastChartWidth });
|
||||||
this.lastChartWidth = totalWidth;
|
this.lastChartWidth = totalWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
return totalWidth;
|
return totalWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adjust font size to fit container width
|
* Adjust font size to fit container width
|
||||||
* @private
|
* @private
|
||||||
@@ -142,7 +133,7 @@ class ASCIIBarChart {
|
|||||||
// Calculate optimal font size
|
// Calculate optimal font size
|
||||||
// For monospace fonts, character width is approximately 0.6 * font size
|
// For monospace fonts, character width is approximately 0.6 * font size
|
||||||
// Use a slightly smaller ratio to fit more content
|
// Use a slightly smaller ratio to fit more content
|
||||||
const charWidthRatio = 0.6;
|
const charWidthRatio = 0.7;
|
||||||
const padding = 30; // Reduce padding to fit more content
|
const padding = 30; // Reduce padding to fit more content
|
||||||
const availableWidth = containerWidth - padding;
|
const availableWidth = containerWidth - padding;
|
||||||
const optimalFontSize = Math.floor((availableWidth / chartWidth) / charWidthRatio);
|
const optimalFontSize = Math.floor((availableWidth / chartWidth) / charWidthRatio);
|
||||||
@@ -151,7 +142,7 @@ class ASCIIBarChart {
|
|||||||
const fontSize = Math.max(4, Math.min(20, optimalFontSize));
|
const fontSize = Math.max(4, Math.min(20, optimalFontSize));
|
||||||
|
|
||||||
// Only log when font size changes
|
// Only log when font size changes
|
||||||
if (this.lastFontSize !== fontSize) {
|
if (this.debug && this.lastFontSize !== fontSize) {
|
||||||
console.log('fontSize changed:', { containerWidth, chartWidth, fontSize, previous: this.lastFontSize });
|
console.log('fontSize changed:', { containerWidth, chartWidth, fontSize, previous: this.lastFontSize });
|
||||||
this.lastFontSize = fontSize;
|
this.lastFontSize = fontSize;
|
||||||
}
|
}
|
||||||
@@ -159,7 +150,7 @@ class ASCIIBarChart {
|
|||||||
this.container.style.fontSize = fontSize + 'px';
|
this.container.style.fontSize = fontSize + 'px';
|
||||||
this.container.style.lineHeight = '1.0';
|
this.container.style.lineHeight = '1.0';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the chart to the container
|
* Render the chart to the container
|
||||||
* @private
|
* @private
|
||||||
@@ -190,7 +181,9 @@ class ASCIIBarChart {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('render() dataToRender:', dataToRender, 'bins length:', this.bins.length);
|
if (this.debug) {
|
||||||
|
console.log('render() dataToRender:', dataToRender, 'bins length:', this.bins.length);
|
||||||
|
}
|
||||||
maxValue = Math.max(...dataToRender);
|
maxValue = Math.max(...dataToRender);
|
||||||
minValue = Math.min(...dataToRender);
|
minValue = Math.min(...dataToRender);
|
||||||
valueRange = maxValue - minValue;
|
valueRange = maxValue - minValue;
|
||||||
@@ -219,12 +212,12 @@ class ASCIIBarChart {
|
|||||||
const yAxisPadding = this.yAxisLabel ? ' ' : '';
|
const yAxisPadding = this.yAxisLabel ? ' ' : '';
|
||||||
|
|
||||||
// Add title if provided (centered)
|
// Add title if provided (centered)
|
||||||
if (this.title) {
|
if (this.title) {
|
||||||
// const chartWidth = 4 + this.maxDataPoints * 2; // Y-axis numbers + data columns // TEMP: commented for no-space test
|
// const chartWidth = 4 + this.maxDataPoints * 2; // Y-axis numbers + data columns // TEMP: commented for no-space test
|
||||||
const chartWidth = 4 + this.maxDataPoints; // Y-axis numbers + data columns // TEMP: adjusted for no-space columns
|
const chartWidth = 4 + this.maxDataPoints; // Y-axis numbers + data columns // TEMP: adjusted for no-space columns
|
||||||
const titlePadding = Math.floor((chartWidth - this.title.length) / 2);
|
const titlePadding = Math.floor((chartWidth - this.title.length) / 2);
|
||||||
output += yAxisPadding + ' '.repeat(Math.max(0, titlePadding)) + this.title + '\n\n';
|
output += yAxisPadding + ' '.repeat(Math.max(0, titlePadding)) + this.title + '\n\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw from top to bottom
|
// Draw from top to bottom
|
||||||
for (let row = scale; row > 0; row--) {
|
for (let row = scale; row > 0; row--) {
|
||||||
@@ -243,8 +236,8 @@ class ASCIIBarChart {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the actual count value this row represents (0 at bottom, increasing upward)
|
// Calculate the actual count value this row represents (1 at bottom, increasing upward)
|
||||||
const rowCount = (row - 1) * scaleFactor;
|
const rowCount = (row - 1) * scaleFactor + 1;
|
||||||
|
|
||||||
// Add Y-axis label (show actual count values)
|
// Add Y-axis label (show actual count values)
|
||||||
line += String(rowCount).padStart(3, ' ') + ' |';
|
line += String(rowCount).padStart(3, ' ') + ' |';
|
||||||
@@ -267,75 +260,75 @@ class ASCIIBarChart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw X-axis
|
// Draw X-axis
|
||||||
// output += yAxisPadding + ' +' + '-'.repeat(this.maxDataPoints * 2) + '\n'; // TEMP: commented out for no-space test
|
// output += yAxisPadding + ' +' + '-'.repeat(this.maxDataPoints * 2) + '\n'; // TEMP: commented out for no-space test
|
||||||
output += yAxisPadding + ' +' + '-'.repeat(this.maxDataPoints) + '\n'; // TEMP: back to original length
|
output += yAxisPadding + ' +' + '-'.repeat(this.maxDataPoints) + '\n'; // TEMP: back to original length
|
||||||
|
|
||||||
// Draw X-axis labels based on mode and format
|
// Draw X-axis labels based on mode and format
|
||||||
let xAxisLabels = yAxisPadding + ' '; // Initial padding to align with X-axis
|
let xAxisLabels = yAxisPadding + ' '; // Initial padding to align with X-axis
|
||||||
|
|
||||||
// Determine label interval (every 5 columns)
|
// Determine label interval (every 5 columns)
|
||||||
const labelInterval = 5;
|
const labelInterval = 5;
|
||||||
|
|
||||||
// Generate all labels first and store in array
|
// Generate all labels first and store in array
|
||||||
let labels = [];
|
let labels = [];
|
||||||
for (let i = 0; i < this.maxDataPoints; i++) {
|
for (let i = 0; i < this.maxDataPoints; i++) {
|
||||||
if (i % labelInterval === 0) {
|
if (i % labelInterval === 0) {
|
||||||
let label = '';
|
let label = '';
|
||||||
if (this.useBinMode) {
|
if (this.useBinMode) {
|
||||||
// For bin mode, show labels for all possible positions
|
// For bin mode, show labels for all possible positions
|
||||||
// i=0 is leftmost (most recent), i=maxDataPoints-1 is rightmost (oldest)
|
// i=0 is leftmost (most recent), i=maxDataPoints-1 is rightmost (oldest)
|
||||||
const elapsedSec = (i * this.binDuration) / 1000;
|
const elapsedSec = (i * this.binDuration) / 1000;
|
||||||
// Format with appropriate precision for sub-second bins
|
// Format with appropriate precision for sub-second bins
|
||||||
if (this.binDuration < 1000) {
|
if (this.binDuration < 1000) {
|
||||||
// Show decimal seconds for sub-second bins
|
// Show decimal seconds for sub-second bins
|
||||||
label = elapsedSec.toFixed(1) + 's';
|
label = elapsedSec.toFixed(1) + 's';
|
||||||
} else {
|
} else {
|
||||||
// Show whole seconds for 1+ second bins
|
// Show whole seconds for 1+ second bins
|
||||||
label = String(Math.round(elapsedSec)) + 's';
|
label = String(Math.round(elapsedSec)) + 's';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For legacy mode, show data point numbers
|
// For legacy mode, show data point numbers
|
||||||
const startIndex = Math.max(1, this.totalDataPoints - this.maxDataPoints + 1);
|
const startIndex = Math.max(1, this.totalDataPoints - this.maxDataPoints + 1);
|
||||||
label = String(startIndex + i);
|
label = String(startIndex + i);
|
||||||
}
|
}
|
||||||
labels.push(label);
|
labels.push(label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the label string with calculated spacing
|
// Build the label string with calculated spacing
|
||||||
for (let i = 0; i < labels.length; i++) {
|
for (let i = 0; i < labels.length; i++) {
|
||||||
const label = labels[i];
|
const label = labels[i];
|
||||||
xAxisLabels += label;
|
xAxisLabels += label;
|
||||||
|
|
||||||
// Add spacing: labelInterval - label.length (except for last label)
|
// Add spacing: labelInterval - label.length (except for last label)
|
||||||
if (i < labels.length - 1) {
|
if (i < labels.length - 1) {
|
||||||
const spacing = labelInterval - label.length;
|
const spacing = labelInterval - label.length;
|
||||||
xAxisLabels += ' '.repeat(spacing);
|
xAxisLabels += ' '.repeat(spacing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the label line extends to match the X-axis dash line length
|
// Ensure the label line extends to match the X-axis dash line length
|
||||||
// The dash line is this.maxDataPoints characters long, starting after " +"
|
// The dash line is this.maxDataPoints characters long, starting after " +"
|
||||||
const dashLineLength = this.maxDataPoints;
|
const dashLineLength = this.maxDataPoints;
|
||||||
const minLabelLineLength = yAxisPadding.length + 4 + dashLineLength; // 4 for " "
|
const minLabelLineLength = yAxisPadding.length + 4 + dashLineLength; // 4 for " "
|
||||||
if (xAxisLabels.length < minLabelLineLength) {
|
if (xAxisLabels.length < minLabelLineLength) {
|
||||||
xAxisLabels += ' '.repeat(minLabelLineLength - xAxisLabels.length);
|
xAxisLabels += ' '.repeat(minLabelLineLength - xAxisLabels.length);
|
||||||
}
|
}
|
||||||
output += xAxisLabels + '\n';
|
output += xAxisLabels + '\n';
|
||||||
|
|
||||||
// Add X-axis label if provided
|
// Add X-axis label if provided
|
||||||
if (this.xAxisLabel) {
|
if (this.xAxisLabel) {
|
||||||
// const labelPadding = Math.floor((this.maxDataPoints * 2 - this.xAxisLabel.length) / 2); // TEMP: commented for no-space test
|
// const labelPadding = Math.floor((this.maxDataPoints * 2 - this.xAxisLabel.length) / 2); // TEMP: commented for no-space test
|
||||||
const labelPadding = Math.floor((this.maxDataPoints - this.xAxisLabel.length) / 2); // TEMP: adjusted for no-space columns
|
const labelPadding = Math.floor((this.maxDataPoints - this.xAxisLabel.length) / 2); // TEMP: adjusted for no-space columns
|
||||||
output += '\n' + yAxisPadding + ' ' + ' '.repeat(Math.max(0, labelPadding)) + this.xAxisLabel + '\n';
|
output += '\n' + yAxisPadding + ' ' + ' '.repeat(Math.max(0, labelPadding)) + this.xAxisLabel + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.container.textContent = output;
|
this.container.textContent = output;
|
||||||
|
|
||||||
// Adjust font size to fit width (only once at initialization)
|
// Adjust font size to fit width (only once at initialization)
|
||||||
if (this.autoFitWidth) {
|
if (this.autoFitWidth) {
|
||||||
this.adjustFontSize();
|
this.adjustFontSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the external info display
|
// Update the external info display
|
||||||
if (this.useBinMode) {
|
if (this.useBinMode) {
|
||||||
@@ -350,7 +343,7 @@ class ASCIIBarChart {
|
|||||||
document.getElementById('scale').textContent = `Min: ${minValue}, Max: ${maxValue}, Height: ${scale}`;
|
document.getElementById('scale').textContent = `Min: ${minValue}, Max: ${maxValue}, Height: ${scale}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the info display
|
* Update the info display
|
||||||
* @private
|
* @private
|
||||||
|
|||||||
298
docs/libwebsockets_proper_pattern.md
Normal file
298
docs/libwebsockets_proper_pattern.md
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
# Libwebsockets Proper Pattern - Message Queue Design
|
||||||
|
|
||||||
|
## Problem Analysis
|
||||||
|
|
||||||
|
### Current Violation
|
||||||
|
We're calling `lws_write()` directly from multiple code paths:
|
||||||
|
1. **Event broadcast** (subscriptions.c:667) - when events arrive
|
||||||
|
2. **OK responses** (websockets.c:855) - when processing EVENT messages
|
||||||
|
3. **EOSE responses** (websockets.c:976) - when processing REQ messages
|
||||||
|
4. **COUNT responses** (websockets.c:1922) - when processing COUNT messages
|
||||||
|
|
||||||
|
This violates libwebsockets' design pattern which requires:
|
||||||
|
- **`lws_write()` ONLY called from `LWS_CALLBACK_SERVER_WRITEABLE`**
|
||||||
|
- Application queues messages and requests writeable callback
|
||||||
|
- Libwebsockets handles write timing and socket buffer management
|
||||||
|
|
||||||
|
### Consequences of Violation
|
||||||
|
1. Partial writes when socket buffer is full
|
||||||
|
2. Multiple concurrent write attempts before callback fires
|
||||||
|
3. "write already pending" errors with single buffer
|
||||||
|
4. Frame corruption from interleaved partial writes
|
||||||
|
5. "Invalid frame header" errors on client side
|
||||||
|
|
||||||
|
## Correct Architecture
|
||||||
|
|
||||||
|
### Message Queue Pattern
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Application Layer │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Event Arrives → Queue Message → Request Writeable Callback │
|
||||||
|
│ REQ Received → Queue EOSE → Request Writeable Callback │
|
||||||
|
│ EVENT Received→ Queue OK → Request Writeable Callback │
|
||||||
|
│ COUNT Received→ Queue COUNT → Request Writeable Callback │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
lws_callback_on_writable(wsi)
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ LWS_CALLBACK_SERVER_WRITEABLE │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 1. Dequeue next message from queue │
|
||||||
|
│ 2. Call lws_write() with message data │
|
||||||
|
│ 3. If queue not empty, request another callback │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
libwebsockets handles:
|
||||||
|
- Socket buffer management
|
||||||
|
- Partial write handling
|
||||||
|
- Frame atomicity
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Structures
|
||||||
|
|
||||||
|
### Message Queue Node
|
||||||
|
```c
|
||||||
|
typedef struct message_queue_node {
|
||||||
|
unsigned char* data; // Message data (with LWS_PRE space)
|
||||||
|
size_t length; // Message length (without LWS_PRE)
|
||||||
|
enum lws_write_protocol type; // LWS_WRITE_TEXT, etc.
|
||||||
|
struct message_queue_node* next;
|
||||||
|
} message_queue_node_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Per-Session Data Updates
|
||||||
|
```c
|
||||||
|
struct per_session_data {
|
||||||
|
// ... existing fields ...
|
||||||
|
|
||||||
|
// Message queue (replaces single buffer)
|
||||||
|
message_queue_node_t* message_queue_head;
|
||||||
|
message_queue_node_t* message_queue_tail;
|
||||||
|
int message_queue_count;
|
||||||
|
int writeable_requested; // Flag to prevent duplicate requests
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Functions
|
||||||
|
|
||||||
|
### 1. Queue Message (Application Layer)
|
||||||
|
```c
|
||||||
|
int queue_message(struct lws* wsi, struct per_session_data* pss,
|
||||||
|
const char* message, size_t length,
|
||||||
|
enum lws_write_protocol type)
|
||||||
|
{
|
||||||
|
// Allocate node
|
||||||
|
message_queue_node_t* node = malloc(sizeof(message_queue_node_t));
|
||||||
|
|
||||||
|
// Allocate buffer with LWS_PRE space
|
||||||
|
node->data = malloc(LWS_PRE + length);
|
||||||
|
memcpy(node->data + LWS_PRE, message, length);
|
||||||
|
node->length = length;
|
||||||
|
node->type = type;
|
||||||
|
node->next = NULL;
|
||||||
|
|
||||||
|
// Add to queue (FIFO)
|
||||||
|
pthread_mutex_lock(&pss->session_lock);
|
||||||
|
if (!pss->message_queue_head) {
|
||||||
|
pss->message_queue_head = node;
|
||||||
|
pss->message_queue_tail = node;
|
||||||
|
} else {
|
||||||
|
pss->message_queue_tail->next = node;
|
||||||
|
pss->message_queue_tail = node;
|
||||||
|
}
|
||||||
|
pss->message_queue_count++;
|
||||||
|
pthread_mutex_unlock(&pss->session_lock);
|
||||||
|
|
||||||
|
// Request writeable callback (only if not already requested)
|
||||||
|
if (!pss->writeable_requested) {
|
||||||
|
pss->writeable_requested = 1;
|
||||||
|
lws_callback_on_writable(wsi);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Process Queue (Writeable Callback)
|
||||||
|
```c
|
||||||
|
int process_message_queue(struct lws* wsi, struct per_session_data* pss)
|
||||||
|
{
|
||||||
|
pthread_mutex_lock(&pss->session_lock);
|
||||||
|
|
||||||
|
// Get next message from queue
|
||||||
|
message_queue_node_t* node = pss->message_queue_head;
|
||||||
|
if (!node) {
|
||||||
|
pss->writeable_requested = 0;
|
||||||
|
pthread_mutex_unlock(&pss->session_lock);
|
||||||
|
return 0; // Queue empty
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from queue
|
||||||
|
pss->message_queue_head = node->next;
|
||||||
|
if (!pss->message_queue_head) {
|
||||||
|
pss->message_queue_tail = NULL;
|
||||||
|
}
|
||||||
|
pss->message_queue_count--;
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&pss->session_lock);
|
||||||
|
|
||||||
|
// Write message (libwebsockets handles partial writes)
|
||||||
|
int result = lws_write(wsi, node->data + LWS_PRE, node->length, node->type);
|
||||||
|
|
||||||
|
// Free node
|
||||||
|
free(node->data);
|
||||||
|
free(node);
|
||||||
|
|
||||||
|
// If queue not empty, request another callback
|
||||||
|
pthread_mutex_lock(&pss->session_lock);
|
||||||
|
if (pss->message_queue_head) {
|
||||||
|
lws_callback_on_writable(wsi);
|
||||||
|
} else {
|
||||||
|
pss->writeable_requested = 0;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&pss->session_lock);
|
||||||
|
|
||||||
|
return (result < 0) ? -1 : 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Refactoring Changes
|
||||||
|
|
||||||
|
### Before (WRONG - Direct Write)
|
||||||
|
```c
|
||||||
|
// websockets.c:855 - OK response
|
||||||
|
int write_result = lws_write(wsi, buf + LWS_PRE, response_len, LWS_WRITE_TEXT);
|
||||||
|
if (write_result < 0) {
|
||||||
|
DEBUG_ERROR("Write failed");
|
||||||
|
} else if ((size_t)write_result != response_len) {
|
||||||
|
// Partial write - queue remaining data
|
||||||
|
queue_websocket_write(wsi, pss, ...);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### After (CORRECT - Queue Message)
|
||||||
|
```c
|
||||||
|
// websockets.c:855 - OK response
|
||||||
|
queue_message(wsi, pss, response_str, response_len, LWS_WRITE_TEXT);
|
||||||
|
// That's it! Writeable callback will handle the actual write
|
||||||
|
```
|
||||||
|
|
||||||
|
### Before (WRONG - Direct Write in Broadcast)
|
||||||
|
```c
|
||||||
|
// subscriptions.c:667 - EVENT broadcast
|
||||||
|
int write_result = lws_write(current_temp->wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
|
||||||
|
if (write_result < 0) {
|
||||||
|
DEBUG_ERROR("Write failed");
|
||||||
|
} else if ((size_t)write_result != msg_len) {
|
||||||
|
queue_websocket_write(...);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### After (CORRECT - Queue Message)
|
||||||
|
```c
|
||||||
|
// subscriptions.c:667 - EVENT broadcast
|
||||||
|
struct per_session_data* pss = lws_wsi_user(current_temp->wsi);
|
||||||
|
queue_message(current_temp->wsi, pss, msg_str, msg_len, LWS_WRITE_TEXT);
|
||||||
|
// Writeable callback will handle the actual write
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits of Correct Pattern
|
||||||
|
|
||||||
|
1. **No Partial Write Handling Needed**
|
||||||
|
- Libwebsockets handles partial writes internally
|
||||||
|
- We just queue complete messages
|
||||||
|
|
||||||
|
2. **No "Write Already Pending" Errors**
|
||||||
|
- Queue can hold unlimited messages
|
||||||
|
- Each processed sequentially from callback
|
||||||
|
|
||||||
|
3. **Thread Safety**
|
||||||
|
- Queue operations protected by session lock
|
||||||
|
- Write only from single callback thread
|
||||||
|
|
||||||
|
4. **Frame Atomicity**
|
||||||
|
- Libwebsockets ensures complete frame transmission
|
||||||
|
- No interleaved partial writes
|
||||||
|
|
||||||
|
5. **Simpler Code**
|
||||||
|
- No complex partial write state machine
|
||||||
|
- Just queue and forget
|
||||||
|
|
||||||
|
6. **Better Performance**
|
||||||
|
- Libwebsockets optimizes write timing
|
||||||
|
- Batches writes when socket ready
|
||||||
|
|
||||||
|
## Migration Steps
|
||||||
|
|
||||||
|
1. ✅ Identify all `lws_write()` call sites
|
||||||
|
2. ✅ Confirm violation of libwebsockets pattern
|
||||||
|
3. ⏳ Design message queue structure
|
||||||
|
4. ⏳ Implement `queue_message()` function
|
||||||
|
5. ⏳ Implement `process_message_queue()` function
|
||||||
|
6. ⏳ Update `per_session_data` structure
|
||||||
|
7. ⏳ Refactor OK response to use queue
|
||||||
|
8. ⏳ Refactor EOSE response to use queue
|
||||||
|
9. ⏳ Refactor COUNT response to use queue
|
||||||
|
10. ⏳ Refactor EVENT broadcast to use queue
|
||||||
|
11. ⏳ Update `LWS_CALLBACK_SERVER_WRITEABLE` handler
|
||||||
|
12. ⏳ Add queue cleanup in `LWS_CALLBACK_CLOSED`
|
||||||
|
13. ⏳ Remove old partial write code
|
||||||
|
14. ⏳ Test with rapid multiple events
|
||||||
|
15. ⏳ Test with large events (>4KB)
|
||||||
|
16. ⏳ Test under load
|
||||||
|
17. ⏳ Verify no frame errors
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Test 1: Multiple Rapid Events
|
||||||
|
```bash
|
||||||
|
# Send 10 events rapidly to same client
|
||||||
|
for i in {1..10}; do
|
||||||
|
echo '["EVENT",{"kind":1,"content":"test'$i'","created_at":'$(date +%s)',...}]' | \
|
||||||
|
websocat ws://localhost:8888 &
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected**: All events queued and sent sequentially, no errors
|
||||||
|
|
||||||
|
### Test 2: Large Events
|
||||||
|
```bash
|
||||||
|
# Send event >4KB (forces multiple socket writes)
|
||||||
|
nak event --content "$(head -c 5000 /dev/urandom | base64)" | \
|
||||||
|
websocat ws://localhost:8888
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected**: Event queued, libwebsockets handles partial writes internally
|
||||||
|
|
||||||
|
### Test 3: Concurrent Connections
|
||||||
|
```bash
|
||||||
|
# 100 concurrent connections, each sending events
|
||||||
|
for i in {1..100}; do
|
||||||
|
(echo '["REQ","sub'$i'",{}]'; sleep 1) | websocat ws://localhost:8888 &
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected**: All subscriptions work, events broadcast correctly
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- ✅ No `lws_write()` calls outside `LWS_CALLBACK_SERVER_WRITEABLE`
|
||||||
|
- ✅ No "write already pending" errors in logs
|
||||||
|
- ✅ No "Invalid frame header" errors on client side
|
||||||
|
- ✅ All messages delivered in correct order
|
||||||
|
- ✅ Large events (>4KB) handled correctly
|
||||||
|
- ✅ Multiple rapid events to same client work
|
||||||
|
- ✅ Concurrent connections stable under load
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [libwebsockets documentation](https://libwebsockets.org/lws-api-doc-main/html/index.html)
|
||||||
|
- [LWS_CALLBACK_SERVER_WRITEABLE](https://libwebsockets.org/lws-api-doc-main/html/group__callback-when-writeable.html)
|
||||||
|
- [lws_callback_on_writable()](https://libwebsockets.org/lws-api-doc-main/html/group__callback-when-writeable.html#ga96f3ad8e1e2c3e0c8e0b0e5e5e5e5e5e)
|
||||||
200
docs/websocket_write_queue_design.md
Normal file
200
docs/websocket_write_queue_design.md
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
# WebSocket Write Queue Design
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
The current partial write handling implementation uses a single buffer per session, which fails when multiple events need to be sent to the same client in rapid succession. This causes:
|
||||||
|
|
||||||
|
1. First event gets partial write → queued successfully
|
||||||
|
2. Second event tries to write → **FAILS** with "write already pending"
|
||||||
|
3. Subsequent events fail similarly, causing data loss
|
||||||
|
|
||||||
|
### Server Log Evidence
|
||||||
|
```
|
||||||
|
[WARN] WS_FRAME_PARTIAL: EVENT partial write, sub=1 sent=3210 expected=5333
|
||||||
|
[TRACE] Queued partial write: len=2123
|
||||||
|
[WARN] WS_FRAME_PARTIAL: EVENT partial write, sub=1 sent=3210 expected=5333
|
||||||
|
[WARN] queue_websocket_write: write already pending, cannot queue new write
|
||||||
|
[ERROR] Failed to queue partial EVENT write for sub=1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
|
||||||
|
WebSocket frames must be sent **atomically** - you cannot interleave multiple frames. The current single-buffer approach correctly enforces this, but it rejects new writes instead of queuing them.
|
||||||
|
|
||||||
|
## Solution: Write Queue Architecture
|
||||||
|
|
||||||
|
### Design Principles
|
||||||
|
|
||||||
|
1. **Frame Atomicity**: Complete one WebSocket frame before starting the next
|
||||||
|
2. **Sequential Processing**: Process queued writes in FIFO order
|
||||||
|
3. **Memory Safety**: Proper cleanup on connection close or errors
|
||||||
|
4. **Thread Safety**: Protect queue operations with existing session lock
|
||||||
|
|
||||||
|
### Data Structures
|
||||||
|
|
||||||
|
#### Write Queue Node
|
||||||
|
```c
|
||||||
|
struct write_queue_node {
|
||||||
|
unsigned char* buffer; // Buffer with LWS_PRE space
|
||||||
|
size_t total_len; // Total length of data to write
|
||||||
|
size_t offset; // How much has been written so far
|
||||||
|
int write_type; // LWS_WRITE_TEXT, etc.
|
||||||
|
struct write_queue_node* next; // Next node in queue
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Per-Session Write Queue
|
||||||
|
```c
|
||||||
|
struct per_session_data {
|
||||||
|
// ... existing fields ...
|
||||||
|
|
||||||
|
// Write queue for handling multiple pending writes
|
||||||
|
struct write_queue_node* write_queue_head; // First item to write
|
||||||
|
struct write_queue_node* write_queue_tail; // Last item in queue
|
||||||
|
int write_queue_length; // Number of items in queue
|
||||||
|
int write_in_progress; // Flag: 1 if currently writing
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Algorithm Flow
|
||||||
|
|
||||||
|
#### 1. Enqueue Write (`queue_websocket_write`)
|
||||||
|
|
||||||
|
```
|
||||||
|
IF write_queue is empty AND no write in progress:
|
||||||
|
- Attempt immediate write with lws_write()
|
||||||
|
- IF complete:
|
||||||
|
- Return success
|
||||||
|
- ELSE (partial write):
|
||||||
|
- Create queue node with remaining data
|
||||||
|
- Add to queue
|
||||||
|
- Set write_in_progress flag
|
||||||
|
- Request LWS_CALLBACK_SERVER_WRITEABLE
|
||||||
|
ELSE:
|
||||||
|
- Create queue node with full data
|
||||||
|
- Append to queue tail
|
||||||
|
- IF no write in progress:
|
||||||
|
- Request LWS_CALLBACK_SERVER_WRITEABLE
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Process Queue (`process_pending_write`)
|
||||||
|
|
||||||
|
```
|
||||||
|
WHILE write_queue is not empty:
|
||||||
|
- Get head node
|
||||||
|
- Calculate remaining data (total_len - offset)
|
||||||
|
- Attempt write with lws_write()
|
||||||
|
|
||||||
|
IF write fails (< 0):
|
||||||
|
- Log error
|
||||||
|
- Remove and free head node
|
||||||
|
- Continue to next node
|
||||||
|
|
||||||
|
ELSE IF partial write (< remaining):
|
||||||
|
- Update offset
|
||||||
|
- Request LWS_CALLBACK_SERVER_WRITEABLE
|
||||||
|
- Break (wait for next callback)
|
||||||
|
|
||||||
|
ELSE (complete write):
|
||||||
|
- Remove and free head node
|
||||||
|
- Continue to next node
|
||||||
|
|
||||||
|
IF queue is empty:
|
||||||
|
- Clear write_in_progress flag
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Cleanup (`LWS_CALLBACK_CLOSED`)
|
||||||
|
|
||||||
|
```
|
||||||
|
WHILE write_queue is not empty:
|
||||||
|
- Get head node
|
||||||
|
- Free buffer
|
||||||
|
- Free node
|
||||||
|
- Move to next
|
||||||
|
Clear queue pointers
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memory Management
|
||||||
|
|
||||||
|
1. **Allocation**: Each queue node allocates buffer with `LWS_PRE + data_len`
|
||||||
|
2. **Ownership**: Queue owns all buffers until write completes or connection closes
|
||||||
|
3. **Deallocation**: Free buffer and node when:
|
||||||
|
- Write completes successfully
|
||||||
|
- Write fails with error
|
||||||
|
- Connection closes
|
||||||
|
|
||||||
|
### Thread Safety
|
||||||
|
|
||||||
|
- Use existing `pss->session_lock` to protect queue operations
|
||||||
|
- Lock during:
|
||||||
|
- Enqueue operations
|
||||||
|
- Dequeue operations
|
||||||
|
- Queue traversal for cleanup
|
||||||
|
|
||||||
|
### Performance Considerations
|
||||||
|
|
||||||
|
1. **Queue Length Limit**: Implement max queue length (e.g., 100 items) to prevent memory exhaustion
|
||||||
|
2. **Memory Pressure**: Monitor total queued bytes per session
|
||||||
|
3. **Backpressure**: If queue exceeds limit, close connection with NOTICE
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
1. **Allocation Failure**: Return error, log, send NOTICE to client
|
||||||
|
2. **Write Failure**: Remove failed frame, continue with next
|
||||||
|
3. **Queue Overflow**: Close connection with appropriate NOTICE
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Data Structure Changes
|
||||||
|
1. Add `write_queue_node` structure to `websockets.h`
|
||||||
|
2. Update `per_session_data` with queue fields
|
||||||
|
3. Remove old single-buffer fields
|
||||||
|
|
||||||
|
### Phase 2: Queue Operations
|
||||||
|
1. Implement `enqueue_write()` helper
|
||||||
|
2. Implement `dequeue_write()` helper
|
||||||
|
3. Update `queue_websocket_write()` to use queue
|
||||||
|
4. Update `process_pending_write()` to process queue
|
||||||
|
|
||||||
|
### Phase 3: Integration
|
||||||
|
1. Update all `lws_write()` call sites
|
||||||
|
2. Update `LWS_CALLBACK_CLOSED` cleanup
|
||||||
|
3. Add queue length monitoring
|
||||||
|
|
||||||
|
### Phase 4: Testing
|
||||||
|
1. Test with rapid multiple events to same client
|
||||||
|
2. Test with large events (>4KB)
|
||||||
|
3. Test under load with concurrent connections
|
||||||
|
4. Verify no "Invalid frame header" errors
|
||||||
|
|
||||||
|
## Expected Outcomes
|
||||||
|
|
||||||
|
1. **No More Rejections**: All writes queued successfully
|
||||||
|
2. **Frame Integrity**: Complete frames sent atomically
|
||||||
|
3. **Memory Safety**: Proper cleanup on all paths
|
||||||
|
4. **Performance**: Minimal overhead for queue management
|
||||||
|
|
||||||
|
## Metrics to Monitor
|
||||||
|
|
||||||
|
1. Average queue length per session
|
||||||
|
2. Maximum queue length observed
|
||||||
|
3. Queue overflow events (if limit implemented)
|
||||||
|
4. Write completion rate
|
||||||
|
5. Partial write frequency
|
||||||
|
|
||||||
|
## Alternative Approaches Considered
|
||||||
|
|
||||||
|
### 1. Larger Single Buffer
|
||||||
|
**Rejected**: Doesn't solve the fundamental problem of multiple concurrent writes
|
||||||
|
|
||||||
|
### 2. Immediate Write Retry
|
||||||
|
**Rejected**: Could cause busy-waiting and CPU waste
|
||||||
|
|
||||||
|
### 3. Drop Frames on Conflict
|
||||||
|
**Rejected**: Violates reliability requirements
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- libwebsockets documentation on `lws_write()` and `LWS_CALLBACK_SERVER_WRITEABLE`
|
||||||
|
- WebSocket RFC 6455 on frame structure
|
||||||
|
- Nostr NIP-01 on relay-to-client communication
|
||||||
@@ -121,10 +121,43 @@ increment_version() {
|
|||||||
print_status "Current version: $LATEST_TAG"
|
print_status "Current version: $LATEST_TAG"
|
||||||
print_status "New version: $NEW_VERSION"
|
print_status "New version: $NEW_VERSION"
|
||||||
|
|
||||||
|
# Update version in src/main.h
|
||||||
|
update_version_in_header "$NEW_VERSION" "$MAJOR" "${NEW_MINOR:-$MINOR}" "${NEW_PATCH:-$PATCH}"
|
||||||
|
|
||||||
# Export for use in other functions
|
# Export for use in other functions
|
||||||
export NEW_VERSION
|
export NEW_VERSION
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Function to update version macros in src/main.h
|
||||||
|
update_version_in_header() {
|
||||||
|
local new_version="$1"
|
||||||
|
local major="$2"
|
||||||
|
local minor="$3"
|
||||||
|
local patch="$4"
|
||||||
|
|
||||||
|
print_status "Updating version in src/main.h..."
|
||||||
|
|
||||||
|
# Check if src/main.h exists
|
||||||
|
if [[ ! -f "src/main.h" ]]; then
|
||||||
|
print_error "src/main.h not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update VERSION macro
|
||||||
|
sed -i "s/#define VERSION \".*\"/#define VERSION \"$new_version\"/" src/main.h
|
||||||
|
|
||||||
|
# Update VERSION_MAJOR macro
|
||||||
|
sed -i "s/#define VERSION_MAJOR [0-9]\+/#define VERSION_MAJOR $major/" src/main.h
|
||||||
|
|
||||||
|
# Update VERSION_MINOR macro
|
||||||
|
sed -i "s/#define VERSION_MINOR .*/#define VERSION_MINOR $minor/" src/main.h
|
||||||
|
|
||||||
|
# Update VERSION_PATCH macro
|
||||||
|
sed -i "s/#define VERSION_PATCH [0-9]\+/#define VERSION_PATCH $patch/" src/main.h
|
||||||
|
|
||||||
|
print_success "Updated version in src/main.h to $new_version"
|
||||||
|
}
|
||||||
|
|
||||||
# Function to commit and push changes
|
# Function to commit and push changes
|
||||||
git_commit_and_push() {
|
git_commit_and_push() {
|
||||||
print_status "Preparing git commit..."
|
print_status "Preparing git commit..."
|
||||||
|
|||||||
11
notes.txt
11
notes.txt
@@ -39,6 +39,11 @@ Even simpler: Use this one-liner
|
|||||||
cd /usr/local/bin/c_relay
|
cd /usr/local/bin/c_relay
|
||||||
sudo -u c-relay ./c_relay --debug-level=5 & sleep 2 && sudo gdb -p $(pgrep c_relay)
|
sudo -u c-relay ./c_relay --debug-level=5 & sleep 2 && sudo gdb -p $(pgrep c_relay)
|
||||||
|
|
||||||
|
Inside gdb, after attaching:
|
||||||
|
|
||||||
|
(gdb) continue
|
||||||
|
Or shorter:
|
||||||
|
(gdb) c
|
||||||
|
|
||||||
|
|
||||||
How to View the Logs
|
How to View the Logs
|
||||||
@@ -75,4 +80,8 @@ sudo systemctl status rsyslog
|
|||||||
|
|
||||||
sudo -u c-relay ./c_relay --debug-level=5 -r 85d0b37e2ae822966dcadd06b2dc9368cde73865f90ea4d44f8b57d47ef0820a -a 1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139
|
sudo -u c-relay ./c_relay --debug-level=5 -r 85d0b37e2ae822966dcadd06b2dc9368cde73865f90ea4d44f8b57d47ef0820a -a 1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139
|
||||||
|
|
||||||
./c_relay_static_x86_64 -p 7889 --debug-level=5 -r 85d0b37e2ae822966dcadd06b2dc9368cde73865f90ea4d44f8b57d47ef0820a -a 1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139
|
./c_relay_static_x86_64 -p 7889 --debug-level=5 -r 85d0b37e2ae822966dcadd06b2dc9368cde73865f90ea4d44f8b57d47ef0820a -a 1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139
|
||||||
|
|
||||||
|
|
||||||
|
sudo ufw allow 8888/tcp
|
||||||
|
sudo ufw delete allow 8888/tcp
|
||||||
|
|||||||
431
src/api.c
431
src/api.c
@@ -40,28 +40,17 @@ const char* get_config_value(const char* key);
|
|||||||
int get_config_bool(const char* key, int default_value);
|
int get_config_bool(const char* key, int default_value);
|
||||||
int update_config_in_table(const char* key, const char* value);
|
int update_config_in_table(const char* key, const char* value);
|
||||||
|
|
||||||
// Monitoring system state
|
// Monitoring system state (throttling now handled per-function)
|
||||||
static time_t last_report_time = 0;
|
|
||||||
|
|
||||||
// Forward declaration for monitoring helper function
|
// Forward declaration for monitoring helper function
|
||||||
int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_func)(void));
|
int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_func)(void));
|
||||||
|
|
||||||
|
// Forward declaration for CPU metrics query function
|
||||||
|
cJSON* query_cpu_metrics(void);
|
||||||
|
|
||||||
// Monitoring system helper functions
|
// Monitoring system helper functions
|
||||||
int is_monitoring_enabled(void) {
|
|
||||||
return get_config_bool("kind_34567_reporting_enabled", 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
int get_monitoring_throttle_seconds(void) {
|
int get_monitoring_throttle_seconds(void) {
|
||||||
return get_config_int("kind_34567_reporting_throttling_sec", 5);
|
return get_config_int("kind_24567_reporting_throttle_sec", 5);
|
||||||
}
|
|
||||||
|
|
||||||
int set_monitoring_enabled(int enabled) {
|
|
||||||
const char* value = enabled ? "1" : "0";
|
|
||||||
if (update_config_in_table("kind_34567_reporting_enabled", value) == 0) {
|
|
||||||
DEBUG_INFO("Monitoring enabled state changed");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query event kind distribution from database
|
// Query event kind distribution from database
|
||||||
@@ -233,51 +222,57 @@ cJSON* query_top_pubkeys(void) {
|
|||||||
return top_pubkeys;
|
return top_pubkeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query active subscriptions from in-memory manager (NO DATABASE QUERY)
|
// Query active subscriptions summary from database
|
||||||
cJSON* query_active_subscriptions(void) {
|
cJSON* query_active_subscriptions(void) {
|
||||||
// Access the global subscription manager
|
extern sqlite3* g_db;
|
||||||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
if (!g_db) {
|
||||||
|
DEBUG_ERROR("Database not available for active subscriptions query");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
int total_subs = g_subscription_manager.total_subscriptions;
|
// Get configuration limits
|
||||||
int max_subs = g_subscription_manager.max_total_subscriptions;
|
int max_subs = g_subscription_manager.max_total_subscriptions;
|
||||||
int max_per_client = g_subscription_manager.max_subscriptions_per_client;
|
int max_per_client = g_subscription_manager.max_subscriptions_per_client;
|
||||||
|
|
||||||
// Calculate per-client statistics by iterating through active subscriptions
|
// Query total active subscriptions from database
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
const char* sql =
|
||||||
|
"SELECT COUNT(*) as total_subs, "
|
||||||
|
"COUNT(DISTINCT client_ip) as client_count "
|
||||||
|
"FROM subscriptions "
|
||||||
|
"WHERE event_type = 'created' AND ended_at IS NULL";
|
||||||
|
|
||||||
|
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||||
|
DEBUG_ERROR("Failed to prepare active subscriptions query");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int total_subs = 0;
|
||||||
int client_count = 0;
|
int client_count = 0;
|
||||||
|
|
||||||
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
|
total_subs = sqlite3_column_int(stmt, 0);
|
||||||
|
client_count = sqlite3_column_int(stmt, 1);
|
||||||
|
}
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
|
// Query max subscriptions per client
|
||||||
int most_subs_per_client = 0;
|
int most_subs_per_client = 0;
|
||||||
|
const char* max_sql =
|
||||||
|
"SELECT MAX(sub_count) FROM ("
|
||||||
|
" SELECT COUNT(*) as sub_count "
|
||||||
|
" FROM subscriptions "
|
||||||
|
" WHERE event_type = 'created' AND ended_at IS NULL "
|
||||||
|
" GROUP BY client_ip"
|
||||||
|
")";
|
||||||
|
|
||||||
// Count subscriptions per WebSocket connection
|
if (sqlite3_prepare_v2(g_db, max_sql, -1, &stmt, NULL) == SQLITE_OK) {
|
||||||
subscription_t* current = g_subscription_manager.active_subscriptions;
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
struct lws* last_wsi = NULL;
|
most_subs_per_client = sqlite3_column_int(stmt, 0);
|
||||||
int current_client_subs = 0;
|
|
||||||
|
|
||||||
while (current) {
|
|
||||||
if (current->wsi != last_wsi) {
|
|
||||||
// New client
|
|
||||||
if (last_wsi != NULL) {
|
|
||||||
client_count++;
|
|
||||||
if (current_client_subs > most_subs_per_client) {
|
|
||||||
most_subs_per_client = current_client_subs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
last_wsi = current->wsi;
|
|
||||||
current_client_subs = 1;
|
|
||||||
} else {
|
|
||||||
current_client_subs++;
|
|
||||||
}
|
}
|
||||||
current = current->next;
|
sqlite3_finalize(stmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle last client
|
|
||||||
if (last_wsi != NULL) {
|
|
||||||
client_count++;
|
|
||||||
if (current_client_subs > most_subs_per_client) {
|
|
||||||
most_subs_per_client = current_client_subs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
|
||||||
|
|
||||||
// Calculate statistics
|
// Calculate statistics
|
||||||
double utilization_percentage = max_subs > 0 ? (total_subs * 100.0 / max_subs) : 0.0;
|
double utilization_percentage = max_subs > 0 ? (total_subs * 100.0 / max_subs) : 0.0;
|
||||||
double avg_subs_per_client = client_count > 0 ? (total_subs * 1.0 / client_count) : 0.0;
|
double avg_subs_per_client = client_count > 0 ? (total_subs * 1.0 / client_count) : 0.0;
|
||||||
@@ -301,10 +296,29 @@ cJSON* query_active_subscriptions(void) {
|
|||||||
return subscriptions;
|
return subscriptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query detailed subscription information from in-memory manager (ADMIN ONLY)
|
// Query detailed subscription information from database log (ADMIN ONLY)
|
||||||
|
// Uses subscriptions table instead of in-memory iteration to avoid mutex contention
|
||||||
cJSON* query_subscription_details(void) {
|
cJSON* query_subscription_details(void) {
|
||||||
// Access the global subscription manager
|
extern sqlite3* g_db;
|
||||||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
if (!g_db) {
|
||||||
|
DEBUG_ERROR("Database not available for subscription details query");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query active subscriptions directly from subscriptions table
|
||||||
|
// Get subscriptions that were created but not yet closed/expired/disconnected
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
const char* sql =
|
||||||
|
"SELECT subscription_id, client_ip, wsi_pointer, filter_json, events_sent, "
|
||||||
|
"created_at, (strftime('%s', 'now') - created_at) as duration_seconds "
|
||||||
|
"FROM subscriptions "
|
||||||
|
"WHERE event_type = 'created' AND ended_at IS NULL "
|
||||||
|
"ORDER BY created_at DESC LIMIT 100";
|
||||||
|
|
||||||
|
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||||
|
DEBUG_ERROR("Failed to prepare subscription details query");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
time_t current_time = time(NULL);
|
time_t current_time = time(NULL);
|
||||||
cJSON* subscriptions_data = cJSON_CreateObject();
|
cJSON* subscriptions_data = cJSON_CreateObject();
|
||||||
@@ -314,70 +328,45 @@ cJSON* query_subscription_details(void) {
|
|||||||
cJSON* data = cJSON_CreateObject();
|
cJSON* data = cJSON_CreateObject();
|
||||||
cJSON* subscriptions_array = cJSON_CreateArray();
|
cJSON* subscriptions_array = cJSON_CreateArray();
|
||||||
|
|
||||||
// Iterate through all active subscriptions
|
// Iterate through query results
|
||||||
subscription_t* current = g_subscription_manager.active_subscriptions;
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
while (current) {
|
|
||||||
cJSON* sub_obj = cJSON_CreateObject();
|
cJSON* sub_obj = cJSON_CreateObject();
|
||||||
|
|
||||||
// Basic subscription info
|
// Extract subscription data from database
|
||||||
cJSON_AddStringToObject(sub_obj, "id", current->id);
|
const char* sub_id = (const char*)sqlite3_column_text(stmt, 0);
|
||||||
cJSON_AddStringToObject(sub_obj, "client_ip", current->client_ip);
|
const char* client_ip = (const char*)sqlite3_column_text(stmt, 1);
|
||||||
cJSON_AddNumberToObject(sub_obj, "created_at", (double)current->created_at);
|
const char* wsi_pointer = (const char*)sqlite3_column_text(stmt, 2);
|
||||||
cJSON_AddNumberToObject(sub_obj, "duration_seconds", (double)(current_time - current->created_at));
|
const char* filter_json = (const char*)sqlite3_column_text(stmt, 3);
|
||||||
cJSON_AddNumberToObject(sub_obj, "events_sent", current->events_sent);
|
long long events_sent = sqlite3_column_int64(stmt, 4);
|
||||||
cJSON_AddBoolToObject(sub_obj, "active", current->active);
|
long long created_at = sqlite3_column_int64(stmt, 5);
|
||||||
|
long long duration_seconds = sqlite3_column_int64(stmt, 6);
|
||||||
|
|
||||||
// Extract filter details
|
// Add basic subscription info
|
||||||
cJSON* filters_array = cJSON_CreateArray();
|
cJSON_AddStringToObject(sub_obj, "id", sub_id ? sub_id : "");
|
||||||
subscription_filter_t* filter = current->filters;
|
cJSON_AddStringToObject(sub_obj, "client_ip", client_ip ? client_ip : "");
|
||||||
|
cJSON_AddStringToObject(sub_obj, "wsi_pointer", wsi_pointer ? wsi_pointer : "");
|
||||||
|
cJSON_AddNumberToObject(sub_obj, "created_at", (double)created_at);
|
||||||
|
cJSON_AddNumberToObject(sub_obj, "duration_seconds", (double)duration_seconds);
|
||||||
|
cJSON_AddNumberToObject(sub_obj, "events_sent", events_sent);
|
||||||
|
cJSON_AddBoolToObject(sub_obj, "active", 1); // All from this view are active
|
||||||
|
|
||||||
while (filter) {
|
// Parse and add filter JSON if available
|
||||||
cJSON* filter_obj = cJSON_CreateObject();
|
if (filter_json) {
|
||||||
|
cJSON* filters = cJSON_Parse(filter_json);
|
||||||
// Add kinds array if present
|
if (filters) {
|
||||||
if (filter->kinds) {
|
cJSON_AddItemToObject(sub_obj, "filters", filters);
|
||||||
cJSON_AddItemToObject(filter_obj, "kinds", cJSON_Duplicate(filter->kinds, 1));
|
} else {
|
||||||
|
// If parsing fails, add empty array
|
||||||
|
cJSON_AddItemToObject(sub_obj, "filters", cJSON_CreateArray());
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// Add authors array if present
|
cJSON_AddItemToObject(sub_obj, "filters", cJSON_CreateArray());
|
||||||
if (filter->authors) {
|
|
||||||
cJSON_AddItemToObject(filter_obj, "authors", cJSON_Duplicate(filter->authors, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add ids array if present
|
|
||||||
if (filter->ids) {
|
|
||||||
cJSON_AddItemToObject(filter_obj, "ids", cJSON_Duplicate(filter->ids, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add since/until timestamps if set
|
|
||||||
if (filter->since > 0) {
|
|
||||||
cJSON_AddNumberToObject(filter_obj, "since", (double)filter->since);
|
|
||||||
}
|
|
||||||
if (filter->until > 0) {
|
|
||||||
cJSON_AddNumberToObject(filter_obj, "until", (double)filter->until);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add limit if set
|
|
||||||
if (filter->limit > 0) {
|
|
||||||
cJSON_AddNumberToObject(filter_obj, "limit", filter->limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add tag filters if present
|
|
||||||
if (filter->tag_filters) {
|
|
||||||
cJSON_AddItemToObject(filter_obj, "tag_filters", cJSON_Duplicate(filter->tag_filters, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
cJSON_AddItemToArray(filters_array, filter_obj);
|
|
||||||
filter = filter->next;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cJSON_AddItemToObject(sub_obj, "filters", filters_array);
|
|
||||||
cJSON_AddItemToArray(subscriptions_array, sub_obj);
|
cJSON_AddItemToArray(subscriptions_array, sub_obj);
|
||||||
|
|
||||||
current = current->next;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
// Add subscriptions array and count to data
|
// Add subscriptions array and count to data
|
||||||
cJSON_AddItemToObject(data, "subscriptions", subscriptions_array);
|
cJSON_AddItemToObject(data, "subscriptions", subscriptions_array);
|
||||||
@@ -388,8 +377,8 @@ cJSON* query_subscription_details(void) {
|
|||||||
return subscriptions_data;
|
return subscriptions_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate and broadcast monitoring event
|
// Generate event-driven monitoring events (triggered by event storage)
|
||||||
int generate_monitoring_event(void) {
|
int generate_event_driven_monitoring(void) {
|
||||||
// Generate event_kinds monitoring event
|
// Generate event_kinds monitoring event
|
||||||
if (generate_monitoring_event_for_type("event_kinds", query_event_kind_distribution) != 0) {
|
if (generate_monitoring_event_for_type("event_kinds", query_event_kind_distribution) != 0) {
|
||||||
DEBUG_ERROR("Failed to generate event_kinds monitoring event");
|
DEBUG_ERROR("Failed to generate event_kinds monitoring event");
|
||||||
@@ -414,16 +403,45 @@ int generate_monitoring_event(void) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate CPU metrics monitoring event (also triggered by event storage)
|
||||||
|
if (generate_monitoring_event_for_type("cpu_metrics", query_cpu_metrics) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to generate cpu_metrics monitoring event");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_INFO("Generated and broadcast event-driven monitoring events");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate subscription-driven monitoring events (triggered by subscription changes)
|
||||||
|
int generate_subscription_driven_monitoring(void) {
|
||||||
|
// Generate active_subscriptions monitoring event (subscription changes affect this)
|
||||||
|
if (generate_monitoring_event_for_type("active_subscriptions", query_active_subscriptions) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to generate active_subscriptions monitoring event");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
// Generate subscription_details monitoring event (admin-only)
|
// Generate subscription_details monitoring event (admin-only)
|
||||||
if (generate_monitoring_event_for_type("subscription_details", query_subscription_details) != 0) {
|
if (generate_monitoring_event_for_type("subscription_details", query_subscription_details) != 0) {
|
||||||
DEBUG_ERROR("Failed to generate subscription_details monitoring event");
|
DEBUG_ERROR("Failed to generate subscription_details monitoring event");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_INFO("Generated and broadcast all monitoring events");
|
// Generate CPU metrics monitoring event (also triggered by subscription changes)
|
||||||
|
if (generate_monitoring_event_for_type("cpu_metrics", query_cpu_metrics) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to generate cpu_metrics monitoring event");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_INFO("Generated and broadcast subscription-driven monitoring events");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate and broadcast monitoring event (legacy function - now calls event-driven version)
|
||||||
|
int generate_monitoring_event(void) {
|
||||||
|
return generate_event_driven_monitoring();
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to generate monitoring event for a specific type
|
// Helper function to generate monitoring event for a specific type
|
||||||
int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_func)(void)) {
|
int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_func)(void)) {
|
||||||
// Query the monitoring data
|
// Query the monitoring data
|
||||||
@@ -461,12 +479,12 @@ int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_f
|
|||||||
}
|
}
|
||||||
free(relay_privkey_hex);
|
free(relay_privkey_hex);
|
||||||
|
|
||||||
// Create monitoring event (kind 34567)
|
// Create monitoring event (kind 24567 - ephemeral)
|
||||||
cJSON* monitoring_event = cJSON_CreateObject();
|
cJSON* monitoring_event = cJSON_CreateObject();
|
||||||
cJSON_AddStringToObject(monitoring_event, "id", ""); // Will be set by signing
|
cJSON_AddStringToObject(monitoring_event, "id", ""); // Will be set by signing
|
||||||
cJSON_AddStringToObject(monitoring_event, "pubkey", relay_pubkey);
|
cJSON_AddStringToObject(monitoring_event, "pubkey", relay_pubkey);
|
||||||
cJSON_AddNumberToObject(monitoring_event, "created_at", (double)time(NULL));
|
cJSON_AddNumberToObject(monitoring_event, "created_at", (double)time(NULL));
|
||||||
cJSON_AddNumberToObject(monitoring_event, "kind", 34567);
|
cJSON_AddNumberToObject(monitoring_event, "kind", 24567);
|
||||||
cJSON_AddStringToObject(monitoring_event, "content", content_json);
|
cJSON_AddStringToObject(monitoring_event, "content", content_json);
|
||||||
|
|
||||||
// Create tags array with d tag for identification
|
// Create tags array with d tag for identification
|
||||||
@@ -482,7 +500,7 @@ int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_f
|
|||||||
|
|
||||||
// Use the library function to create and sign the event
|
// Use the library function to create and sign the event
|
||||||
cJSON* signed_event = nostr_create_and_sign_event(
|
cJSON* signed_event = nostr_create_and_sign_event(
|
||||||
34567, // kind
|
24567, // kind (ephemeral)
|
||||||
cJSON_GetStringValue(cJSON_GetObjectItem(monitoring_event, "content")), // content
|
cJSON_GetStringValue(cJSON_GetObjectItem(monitoring_event, "content")), // content
|
||||||
tags, // tags
|
tags, // tags
|
||||||
relay_privkey, // private key
|
relay_privkey, // private key
|
||||||
@@ -500,55 +518,58 @@ int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_f
|
|||||||
cJSON_Delete(monitoring_event);
|
cJSON_Delete(monitoring_event);
|
||||||
monitoring_event = signed_event;
|
monitoring_event = signed_event;
|
||||||
|
|
||||||
// Broadcast the event to active subscriptions
|
// Broadcast the ephemeral event to active subscriptions (no database storage)
|
||||||
broadcast_event_to_subscriptions(monitoring_event);
|
broadcast_event_to_subscriptions(monitoring_event);
|
||||||
|
|
||||||
// Store in database
|
|
||||||
int store_result = store_event(monitoring_event);
|
|
||||||
|
|
||||||
cJSON_Delete(monitoring_event);
|
cJSON_Delete(monitoring_event);
|
||||||
free(content_json);
|
free(content_json);
|
||||||
|
|
||||||
if (store_result != 0) {
|
DEBUG_LOG("Monitoring event broadcast (ephemeral kind 24567, type: %s)", d_tag_value);
|
||||||
DEBUG_ERROR("Failed to store monitoring event (%s)", d_tag_value);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monitoring hook called when an event is stored
|
// Monitoring hook called when an event is stored
|
||||||
void monitoring_on_event_stored(void) {
|
void monitoring_on_event_stored(void) {
|
||||||
// Check if monitoring is enabled
|
// Check throttling first (cheapest check)
|
||||||
if (!is_monitoring_enabled()) {
|
static time_t last_monitoring_time = 0;
|
||||||
|
time_t current_time = time(NULL);
|
||||||
|
int throttle_seconds = get_monitoring_throttle_seconds();
|
||||||
|
|
||||||
|
if (current_time - last_monitoring_time < throttle_seconds) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check throttling
|
// Check if anyone is subscribed to monitoring events (kind 24567)
|
||||||
time_t now = time(NULL);
|
// This is the ONLY activation check needed - if someone subscribes, they want monitoring
|
||||||
|
if (!has_subscriptions_for_kind(24567)) {
|
||||||
|
return; // No subscribers = no expensive operations
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate event-driven monitoring events only when someone is listening
|
||||||
|
last_monitoring_time = current_time;
|
||||||
|
generate_event_driven_monitoring();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitoring hook called when subscriptions change (create/close)
|
||||||
|
void monitoring_on_subscription_change(void) {
|
||||||
|
// Check throttling first (cheapest check)
|
||||||
|
static time_t last_monitoring_time = 0;
|
||||||
|
time_t current_time = time(NULL);
|
||||||
int throttle_seconds = get_monitoring_throttle_seconds();
|
int throttle_seconds = get_monitoring_throttle_seconds();
|
||||||
|
|
||||||
if (now - last_report_time < throttle_seconds) {
|
if (current_time - last_monitoring_time < throttle_seconds) {
|
||||||
return; // Too soon, skip this report
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate and broadcast monitoring event
|
// Check if anyone is subscribed to monitoring events (kind 24567)
|
||||||
if (generate_monitoring_event() == 0) {
|
// This is the ONLY activation check needed - if someone subscribes, they want monitoring
|
||||||
last_report_time = now;
|
if (!has_subscriptions_for_kind(24567)) {
|
||||||
|
return; // No subscribers = no expensive operations
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize monitoring system
|
// Generate subscription-driven monitoring events only when someone is listening
|
||||||
int init_monitoring_system(void) {
|
last_monitoring_time = current_time;
|
||||||
last_report_time = 0;
|
generate_subscription_driven_monitoring();
|
||||||
DEBUG_INFO("Monitoring system initialized");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup monitoring system
|
|
||||||
void cleanup_monitoring_system(void) {
|
|
||||||
// No cleanup needed for monitoring system
|
|
||||||
DEBUG_INFO("Monitoring system cleaned up");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forward declaration for known_configs (defined in config.c)
|
// Forward declaration for known_configs (defined in config.c)
|
||||||
@@ -1140,6 +1161,68 @@ int handle_embedded_file_writeable(struct lws* wsi) {
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
// Query CPU usage metrics
|
||||||
|
cJSON* query_cpu_metrics(void) {
|
||||||
|
cJSON* cpu_stats = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(cpu_stats, "data_type", "cpu_metrics");
|
||||||
|
cJSON_AddNumberToObject(cpu_stats, "timestamp", (double)time(NULL));
|
||||||
|
|
||||||
|
// Read process CPU times from /proc/self/stat
|
||||||
|
FILE* proc_stat = fopen("/proc/self/stat", "r");
|
||||||
|
if (proc_stat) {
|
||||||
|
unsigned long utime, stime; // user and system CPU time in clock ticks
|
||||||
|
if (fscanf(proc_stat, "%*d %*s %*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %lu %lu", &utime, &stime) == 2) {
|
||||||
|
unsigned long total_proc_time = utime + stime;
|
||||||
|
|
||||||
|
// Get system CPU times from /proc/stat
|
||||||
|
FILE* sys_stat = fopen("/proc/stat", "r");
|
||||||
|
if (sys_stat) {
|
||||||
|
unsigned long user, nice, system, idle, iowait, irq, softirq;
|
||||||
|
if (fscanf(sys_stat, "cpu %lu %lu %lu %lu %lu %lu %lu", &user, &nice, &system, &idle, &iowait, &irq, &softirq) == 7) {
|
||||||
|
unsigned long total_sys_time = user + nice + system + idle + iowait + irq + softirq;
|
||||||
|
|
||||||
|
// Calculate CPU percentages (simplified - would need deltas for accuracy)
|
||||||
|
// For now, just store the raw values - frontend can calculate deltas
|
||||||
|
cJSON_AddNumberToObject(cpu_stats, "process_cpu_time", (double)total_proc_time);
|
||||||
|
cJSON_AddNumberToObject(cpu_stats, "system_cpu_time", (double)total_sys_time);
|
||||||
|
cJSON_AddNumberToObject(cpu_stats, "system_idle_time", (double)idle);
|
||||||
|
}
|
||||||
|
fclose(sys_stat);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current CPU core the process is running on
|
||||||
|
int current_core = sched_getcpu();
|
||||||
|
if (current_core >= 0) {
|
||||||
|
cJSON_AddNumberToObject(cpu_stats, "current_cpu_core", current_core);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(proc_stat);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get process ID
|
||||||
|
pid_t pid = getpid();
|
||||||
|
cJSON_AddNumberToObject(cpu_stats, "process_id", (double)pid);
|
||||||
|
|
||||||
|
// Get memory usage from /proc/self/status
|
||||||
|
FILE* mem_stat = fopen("/proc/self/status", "r");
|
||||||
|
if (mem_stat) {
|
||||||
|
char line[256];
|
||||||
|
while (fgets(line, sizeof(line), mem_stat)) {
|
||||||
|
if (strncmp(line, "VmRSS:", 6) == 0) {
|
||||||
|
unsigned long rss_kb;
|
||||||
|
if (sscanf(line, "VmRSS: %lu kB", &rss_kb) == 1) {
|
||||||
|
double rss_mb = rss_kb / 1024.0;
|
||||||
|
cJSON_AddNumberToObject(cpu_stats, "memory_usage_mb", rss_mb);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(mem_stat);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cpu_stats;
|
||||||
|
}
|
||||||
|
|
||||||
// Generate stats JSON from database queries
|
// Generate stats JSON from database queries
|
||||||
char* generate_stats_json(void) {
|
char* generate_stats_json(void) {
|
||||||
extern sqlite3* g_db;
|
extern sqlite3* g_db;
|
||||||
@@ -2267,24 +2350,8 @@ int handle_monitoring_command(cJSON* event, const char* command, char* error_mes
|
|||||||
if (*p >= 'A' && *p <= 'Z') *p = *p + 32;
|
if (*p >= 'A' && *p <= 'Z') *p = *p + 32;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle commands
|
// Handle set_monitoring_throttle command (only remaining monitoring command)
|
||||||
if (strcmp(cmd, "enable_monitoring") == 0) {
|
if (strcmp(cmd, "set_monitoring_throttle") == 0) {
|
||||||
if (set_monitoring_enabled(1) == 0) {
|
|
||||||
char* response_content = "✅ Monitoring enabled\n\nReal-time monitoring events will now be generated.";
|
|
||||||
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
|
||||||
} else {
|
|
||||||
char* response_content = "❌ Failed to enable monitoring";
|
|
||||||
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
|
||||||
}
|
|
||||||
} else if (strcmp(cmd, "disable_monitoring") == 0) {
|
|
||||||
if (set_monitoring_enabled(0) == 0) {
|
|
||||||
char* response_content = "✅ Monitoring disabled\n\nReal-time monitoring events will no longer be generated.";
|
|
||||||
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
|
||||||
} else {
|
|
||||||
char* response_content = "❌ Failed to disable monitoring";
|
|
||||||
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
|
||||||
}
|
|
||||||
} else if (strcmp(cmd, "set_monitoring_throttle") == 0) {
|
|
||||||
if (arg[0] == '\0') {
|
if (arg[0] == '\0') {
|
||||||
char* response_content = "❌ Missing throttle value\n\nUsage: set_monitoring_throttle <seconds>";
|
char* response_content = "❌ Missing throttle value\n\nUsage: set_monitoring_throttle <seconds>";
|
||||||
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
||||||
@@ -2300,44 +2367,28 @@ int handle_monitoring_command(cJSON* event, const char* command, char* error_mes
|
|||||||
char throttle_str[16];
|
char throttle_str[16];
|
||||||
snprintf(throttle_str, sizeof(throttle_str), "%ld", throttle_seconds);
|
snprintf(throttle_str, sizeof(throttle_str), "%ld", throttle_seconds);
|
||||||
|
|
||||||
if (update_config_in_table("kind_34567_reporting_throttling_sec", throttle_str) == 0) {
|
if (update_config_in_table("kind_24567_reporting_throttle_sec", throttle_str) == 0) {
|
||||||
char response_content[256];
|
char response_content[256];
|
||||||
snprintf(response_content, sizeof(response_content),
|
snprintf(response_content, sizeof(response_content),
|
||||||
"✅ Monitoring throttle updated\n\nMinimum interval between monitoring events: %ld seconds", throttle_seconds);
|
"✅ Monitoring throttle updated\n\n"
|
||||||
|
"Minimum interval between monitoring events: %ld seconds\n\n"
|
||||||
|
"ℹ️ Monitoring activates automatically when you subscribe to kind 24567 events.",
|
||||||
|
throttle_seconds);
|
||||||
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
||||||
} else {
|
} else {
|
||||||
char* response_content = "❌ Failed to update monitoring throttle";
|
char* response_content = "❌ Failed to update monitoring throttle";
|
||||||
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
||||||
}
|
}
|
||||||
} else if (strcmp(cmd, "monitoring_status") == 0) {
|
|
||||||
int enabled = is_monitoring_enabled();
|
|
||||||
int throttle = get_monitoring_throttle_seconds();
|
|
||||||
|
|
||||||
char response_content[512];
|
|
||||||
snprintf(response_content, sizeof(response_content),
|
|
||||||
"📊 Monitoring Status\n"
|
|
||||||
"━━━━━━━━━━━━━━━━━━━━\n"
|
|
||||||
"\n"
|
|
||||||
"Enabled: %s\n"
|
|
||||||
"Throttle: %d seconds\n"
|
|
||||||
"\n"
|
|
||||||
"Commands:\n"
|
|
||||||
"• enable_monitoring\n"
|
|
||||||
"• disable_monitoring\n"
|
|
||||||
"• set_monitoring_throttle <seconds>\n"
|
|
||||||
"• monitoring_status",
|
|
||||||
enabled ? "Yes" : "No", throttle);
|
|
||||||
|
|
||||||
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
|
||||||
} else {
|
} else {
|
||||||
char response_content[256];
|
char response_content[1024];
|
||||||
snprintf(response_content, sizeof(response_content),
|
snprintf(response_content, sizeof(response_content),
|
||||||
"❌ Unknown monitoring command: %s\n\n"
|
"❌ Unknown monitoring command: %s\n\n"
|
||||||
"Available commands:\n"
|
"Available command:\n"
|
||||||
"• enable_monitoring\n"
|
"• set_monitoring_throttle <seconds>\n\n"
|
||||||
"• disable_monitoring\n"
|
"ℹ️ Monitoring is now subscription-based:\n"
|
||||||
"• set_monitoring_throttle <seconds>\n"
|
"Subscribe to kind 24567 events to receive real-time monitoring data.\n"
|
||||||
"• monitoring_status", cmd);
|
"Monitoring automatically activates when subscriptions exist and deactivates when they close.",
|
||||||
|
cmd);
|
||||||
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,11 +60,8 @@ char* execute_sql_query(const char* query, const char* request_id, char* error_m
|
|||||||
int handle_sql_query_unified(cJSON* event, const char* query, char* error_message, size_t error_size, struct lws* wsi);
|
int handle_sql_query_unified(cJSON* event, const char* query, char* error_message, size_t error_size, struct lws* wsi);
|
||||||
|
|
||||||
// Monitoring system functions
|
// Monitoring system functions
|
||||||
int init_monitoring_system(void);
|
|
||||||
void cleanup_monitoring_system(void);
|
|
||||||
void monitoring_on_event_stored(void);
|
void monitoring_on_event_stored(void);
|
||||||
int set_monitoring_enabled(int enabled);
|
void monitoring_on_subscription_change(void);
|
||||||
int is_monitoring_enabled(void);
|
|
||||||
int get_monitoring_throttle_seconds(void);
|
int get_monitoring_throttle_seconds(void);
|
||||||
|
|
||||||
#endif // API_H
|
#endif // API_H
|
||||||
37
src/config.c
37
src/config.c
@@ -3,6 +3,19 @@
|
|||||||
#include "debug.h"
|
#include "debug.h"
|
||||||
#include "default_config_event.h"
|
#include "default_config_event.h"
|
||||||
#include "dm_admin.h"
|
#include "dm_admin.h"
|
||||||
|
|
||||||
|
// Undefine VERSION macros before including nostr_core.h to avoid redefinition warnings
|
||||||
|
// This must come AFTER default_config_event.h so that RELAY_VERSION macro expansion works correctly
|
||||||
|
#ifdef VERSION
|
||||||
|
#undef VERSION
|
||||||
|
#endif
|
||||||
|
#ifdef VERSION_MINOR
|
||||||
|
#undef VERSION_MINOR
|
||||||
|
#endif
|
||||||
|
#ifdef VERSION_PATCH
|
||||||
|
#undef VERSION_PATCH
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -4099,32 +4112,18 @@ int populate_all_config_values_atomic(const char* admin_pubkey, const char* rela
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert monitoring system config entries
|
// Insert monitoring system config entry (ephemeral kind 24567)
|
||||||
|
// Note: Monitoring is automatically activated when clients subscribe to kind 24567
|
||||||
sqlite3_reset(stmt);
|
sqlite3_reset(stmt);
|
||||||
sqlite3_bind_text(stmt, 1, "kind_34567_reporting_enabled", -1, SQLITE_STATIC);
|
sqlite3_bind_text(stmt, 1, "kind_24567_reporting_throttle_sec", -1, SQLITE_STATIC);
|
||||||
sqlite3_bind_text(stmt, 2, "false", -1, SQLITE_STATIC); // boolean, default false
|
|
||||||
sqlite3_bind_text(stmt, 3, "boolean", -1, SQLITE_STATIC);
|
|
||||||
sqlite3_bind_text(stmt, 4, "Enable real-time monitoring event generation", -1, SQLITE_STATIC);
|
|
||||||
sqlite3_bind_text(stmt, 5, "monitoring", -1, SQLITE_STATIC);
|
|
||||||
sqlite3_bind_int(stmt, 6, 0); // does not require restart
|
|
||||||
rc = sqlite3_step(stmt);
|
|
||||||
if (rc != SQLITE_DONE) {
|
|
||||||
DEBUG_ERROR("Failed to insert kind_34567_reporting_enabled: %s", sqlite3_errmsg(g_db));
|
|
||||||
sqlite3_finalize(stmt);
|
|
||||||
sqlite3_exec(g_db, "ROLLBACK;", NULL, NULL, NULL);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlite3_reset(stmt);
|
|
||||||
sqlite3_bind_text(stmt, 1, "kind_34567_reporting_throttling_sec", -1, SQLITE_STATIC);
|
|
||||||
sqlite3_bind_text(stmt, 2, "5", -1, SQLITE_STATIC); // integer, default 5 seconds
|
sqlite3_bind_text(stmt, 2, "5", -1, SQLITE_STATIC); // integer, default 5 seconds
|
||||||
sqlite3_bind_text(stmt, 3, "integer", -1, SQLITE_STATIC);
|
sqlite3_bind_text(stmt, 3, "integer", -1, SQLITE_STATIC);
|
||||||
sqlite3_bind_text(stmt, 4, "Minimum seconds between monitoring event reports", -1, SQLITE_STATIC);
|
sqlite3_bind_text(stmt, 4, "Minimum seconds between monitoring event reports (ephemeral kind 24567)", -1, SQLITE_STATIC);
|
||||||
sqlite3_bind_text(stmt, 5, "monitoring", -1, SQLITE_STATIC);
|
sqlite3_bind_text(stmt, 5, "monitoring", -1, SQLITE_STATIC);
|
||||||
sqlite3_bind_int(stmt, 6, 0); // does not require restart
|
sqlite3_bind_int(stmt, 6, 0); // does not require restart
|
||||||
rc = sqlite3_step(stmt);
|
rc = sqlite3_step(stmt);
|
||||||
if (rc != SQLITE_DONE) {
|
if (rc != SQLITE_DONE) {
|
||||||
DEBUG_ERROR("Failed to insert kind_34567_reporting_throttling_sec: %s", sqlite3_errmsg(g_db));
|
DEBUG_ERROR("Failed to insert kind_24567_reporting_throttle_sec: %s", sqlite3_errmsg(g_db));
|
||||||
sqlite3_finalize(stmt);
|
sqlite3_finalize(stmt);
|
||||||
sqlite3_exec(g_db, "ROLLBACK;", NULL, NULL, NULL);
|
sqlite3_exec(g_db, "ROLLBACK;", NULL, NULL, NULL);
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
@@ -72,7 +72,13 @@ static const struct {
|
|||||||
|
|
||||||
// Performance Settings
|
// Performance Settings
|
||||||
{"default_limit", "500"},
|
{"default_limit", "500"},
|
||||||
{"max_limit", "5000"}
|
{"max_limit", "5000"},
|
||||||
|
|
||||||
|
// Proxy Settings
|
||||||
|
// Trust proxy headers (X-Forwarded-For, X-Real-IP) for accurate client IP detection
|
||||||
|
// Safe for informational/debugging use. Only becomes a security concern if you implement
|
||||||
|
// IP-based rate limiting or access control (which would require firewall protection anyway)
|
||||||
|
{"trust_proxy_headers", "true"}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Number of default configuration values
|
// Number of default configuration values
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
10
src/main.c
10
src/main.c
@@ -149,9 +149,7 @@ int mark_event_as_deleted(const char* event_id, const char* deletion_event_id, c
|
|||||||
// Forward declaration for database functions
|
// Forward declaration for database functions
|
||||||
int store_event(cJSON* event);
|
int store_event(cJSON* event);
|
||||||
|
|
||||||
// Forward declarations for monitoring system
|
// Forward declaration for monitoring system
|
||||||
void init_monitoring_system(void);
|
|
||||||
void cleanup_monitoring_system(void);
|
|
||||||
void monitoring_on_event_stored(void);
|
void monitoring_on_event_stored(void);
|
||||||
|
|
||||||
// Forward declarations for NIP-11 relay information handling
|
// Forward declarations for NIP-11 relay information handling
|
||||||
@@ -1989,9 +1987,6 @@ int main(int argc, char* argv[]) {
|
|||||||
// Initialize NIP-40 expiration configuration
|
// Initialize NIP-40 expiration configuration
|
||||||
init_expiration_config();
|
init_expiration_config();
|
||||||
|
|
||||||
// Initialize monitoring system
|
|
||||||
init_monitoring_system();
|
|
||||||
|
|
||||||
// Update subscription manager configuration
|
// Update subscription manager configuration
|
||||||
update_subscription_manager_config();
|
update_subscription_manager_config();
|
||||||
|
|
||||||
@@ -2023,9 +2018,6 @@ int main(int argc, char* argv[]) {
|
|||||||
ginxsom_request_validator_cleanup();
|
ginxsom_request_validator_cleanup();
|
||||||
cleanup_configuration_system();
|
cleanup_configuration_system();
|
||||||
|
|
||||||
// Cleanup monitoring system
|
|
||||||
cleanup_monitoring_system();
|
|
||||||
|
|
||||||
// Cleanup subscription manager mutexes
|
// Cleanup subscription manager mutexes
|
||||||
pthread_mutex_destroy(&g_subscription_manager.subscriptions_lock);
|
pthread_mutex_destroy(&g_subscription_manager.subscriptions_lock);
|
||||||
pthread_mutex_destroy(&g_subscription_manager.ip_tracking_lock);
|
pthread_mutex_destroy(&g_subscription_manager.ip_tracking_lock);
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
#define MAIN_H
|
#define MAIN_H
|
||||||
|
|
||||||
// Version information (auto-updated by build system)
|
// Version information (auto-updated by build system)
|
||||||
#define VERSION "v0.4.6"
|
#define VERSION "v0.7.37"
|
||||||
#define VERSION_MAJOR 0
|
#define VERSION_MAJOR 0
|
||||||
#define VERSION_MINOR 4
|
#define VERSION_MINOR 7
|
||||||
#define VERSION_PATCH 6
|
#define VERSION_PATCH 37
|
||||||
|
|
||||||
// Relay metadata (authoritative source for NIP-11 information)
|
// Relay metadata (authoritative source for NIP-11 information)
|
||||||
#define RELAY_NAME "C-Relay"
|
#define RELAY_NAME "C-Relay"
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
/* Embedded SQL Schema for C Nostr Relay
|
/* Embedded SQL Schema for C Nostr Relay
|
||||||
* Generated from db/schema.sql - Do not edit manually
|
* Generated from db/schema.sql - Do not edit manually
|
||||||
* Schema Version: 7
|
* Schema Version: 8
|
||||||
*/
|
*/
|
||||||
#ifndef SQL_SCHEMA_H
|
#ifndef SQL_SCHEMA_H
|
||||||
#define SQL_SCHEMA_H
|
#define SQL_SCHEMA_H
|
||||||
|
|
||||||
/* Schema version constant */
|
/* Schema version constant */
|
||||||
#define EMBEDDED_SCHEMA_VERSION "7"
|
#define EMBEDDED_SCHEMA_VERSION "8"
|
||||||
|
|
||||||
/* Embedded SQL schema as C string literal */
|
/* Embedded SQL schema as C string literal */
|
||||||
static const char* const EMBEDDED_SCHEMA_SQL =
|
static const char* const EMBEDDED_SCHEMA_SQL =
|
||||||
@@ -15,7 +15,7 @@ static const char* const EMBEDDED_SCHEMA_SQL =
|
|||||||
-- Configuration system using config table\n\
|
-- Configuration system using config table\n\
|
||||||
\n\
|
\n\
|
||||||
-- Schema version tracking\n\
|
-- Schema version tracking\n\
|
||||||
PRAGMA user_version = 7;\n\
|
PRAGMA user_version = 8;\n\
|
||||||
\n\
|
\n\
|
||||||
-- Enable foreign key support\n\
|
-- Enable foreign key support\n\
|
||||||
PRAGMA foreign_keys = ON;\n\
|
PRAGMA foreign_keys = ON;\n\
|
||||||
@@ -58,8 +58,8 @@ CREATE TABLE schema_info (\n\
|
|||||||
\n\
|
\n\
|
||||||
-- Insert schema metadata\n\
|
-- Insert schema metadata\n\
|
||||||
INSERT INTO schema_info (key, value) VALUES\n\
|
INSERT INTO schema_info (key, value) VALUES\n\
|
||||||
('version', '7'),\n\
|
('version', '8'),\n\
|
||||||
('description', 'Hybrid Nostr relay schema with event-based and table-based configuration'),\n\
|
('description', 'Hybrid Nostr relay schema with subscription deduplication support'),\n\
|
||||||
('created_at', strftime('%s', 'now'));\n\
|
('created_at', strftime('%s', 'now'));\n\
|
||||||
\n\
|
\n\
|
||||||
-- Helper views for common queries\n\
|
-- Helper views for common queries\n\
|
||||||
@@ -181,17 +181,19 @@ END;\n\
|
|||||||
-- Persistent Subscriptions Logging Tables (Phase 2)\n\
|
-- Persistent Subscriptions Logging Tables (Phase 2)\n\
|
||||||
-- Optional database logging for subscription analytics and debugging\n\
|
-- Optional database logging for subscription analytics and debugging\n\
|
||||||
\n\
|
\n\
|
||||||
-- Subscription events log\n\
|
-- Subscriptions log (renamed from subscription_events for clarity)\n\
|
||||||
CREATE TABLE subscription_events (\n\
|
CREATE TABLE subscriptions (\n\
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,\n\
|
id INTEGER PRIMARY KEY AUTOINCREMENT,\n\
|
||||||
subscription_id TEXT NOT NULL, -- Subscription ID from client\n\
|
subscription_id TEXT NOT NULL, -- Subscription ID from client\n\
|
||||||
|
wsi_pointer TEXT NOT NULL, -- WebSocket pointer address (hex string)\n\
|
||||||
client_ip TEXT NOT NULL, -- Client IP address\n\
|
client_ip TEXT NOT NULL, -- Client IP address\n\
|
||||||
event_type TEXT NOT NULL CHECK (event_type IN ('created', 'closed', 'expired', 'disconnected')),\n\
|
event_type TEXT NOT NULL CHECK (event_type IN ('created', 'closed', 'expired', 'disconnected')),\n\
|
||||||
filter_json TEXT, -- JSON representation of filters (for created events)\n\
|
filter_json TEXT, -- JSON representation of filters (for created events)\n\
|
||||||
events_sent INTEGER DEFAULT 0, -- Number of events sent to this subscription\n\
|
events_sent INTEGER DEFAULT 0, -- Number of events sent to this subscription\n\
|
||||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\
|
||||||
ended_at INTEGER, -- When subscription ended (for closed/expired/disconnected)\n\
|
ended_at INTEGER, -- When subscription ended (for closed/expired/disconnected)\n\
|
||||||
duration INTEGER -- Computed: ended_at - created_at\n\
|
duration INTEGER, -- Computed: ended_at - created_at\n\
|
||||||
|
UNIQUE(subscription_id, wsi_pointer) -- Prevent duplicate subscriptions per connection\n\
|
||||||
);\n\
|
);\n\
|
||||||
\n\
|
\n\
|
||||||
-- Subscription metrics summary\n\
|
-- Subscription metrics summary\n\
|
||||||
@@ -218,10 +220,11 @@ CREATE TABLE event_broadcasts (\n\
|
|||||||
);\n\
|
);\n\
|
||||||
\n\
|
\n\
|
||||||
-- Indexes for subscription logging performance\n\
|
-- Indexes for subscription logging performance\n\
|
||||||
CREATE INDEX idx_subscription_events_id ON subscription_events(subscription_id);\n\
|
CREATE INDEX idx_subscriptions_id ON subscriptions(subscription_id);\n\
|
||||||
CREATE INDEX idx_subscription_events_type ON subscription_events(event_type);\n\
|
CREATE INDEX idx_subscriptions_type ON subscriptions(event_type);\n\
|
||||||
CREATE INDEX idx_subscription_events_created ON subscription_events(created_at DESC);\n\
|
CREATE INDEX idx_subscriptions_created ON subscriptions(created_at DESC);\n\
|
||||||
CREATE INDEX idx_subscription_events_client ON subscription_events(client_ip);\n\
|
CREATE INDEX idx_subscriptions_client ON subscriptions(client_ip);\n\
|
||||||
|
CREATE INDEX idx_subscriptions_wsi ON subscriptions(wsi_pointer);\n\
|
||||||
\n\
|
\n\
|
||||||
CREATE INDEX idx_subscription_metrics_date ON subscription_metrics(date DESC);\n\
|
CREATE INDEX idx_subscription_metrics_date ON subscription_metrics(date DESC);\n\
|
||||||
\n\
|
\n\
|
||||||
@@ -231,10 +234,10 @@ CREATE INDEX idx_event_broadcasts_time ON event_broadcasts(broadcast_at DESC);\n
|
|||||||
\n\
|
\n\
|
||||||
-- Trigger to update subscription duration when ended\n\
|
-- Trigger to update subscription duration when ended\n\
|
||||||
CREATE TRIGGER update_subscription_duration\n\
|
CREATE TRIGGER update_subscription_duration\n\
|
||||||
AFTER UPDATE OF ended_at ON subscription_events\n\
|
AFTER UPDATE OF ended_at ON subscriptions\n\
|
||||||
WHEN NEW.ended_at IS NOT NULL AND OLD.ended_at IS NULL\n\
|
WHEN NEW.ended_at IS NOT NULL AND OLD.ended_at IS NULL\n\
|
||||||
BEGIN\n\
|
BEGIN\n\
|
||||||
UPDATE subscription_events\n\
|
UPDATE subscriptions\n\
|
||||||
SET duration = NEW.ended_at - NEW.created_at\n\
|
SET duration = NEW.ended_at - NEW.created_at\n\
|
||||||
WHERE id = NEW.id;\n\
|
WHERE id = NEW.id;\n\
|
||||||
END;\n\
|
END;\n\
|
||||||
@@ -249,7 +252,7 @@ SELECT\n\
|
|||||||
MAX(events_sent) as max_events_sent,\n\
|
MAX(events_sent) as max_events_sent,\n\
|
||||||
AVG(events_sent) as avg_events_sent,\n\
|
AVG(events_sent) as avg_events_sent,\n\
|
||||||
COUNT(DISTINCT client_ip) as unique_clients\n\
|
COUNT(DISTINCT client_ip) as unique_clients\n\
|
||||||
FROM subscription_events\n\
|
FROM subscriptions\n\
|
||||||
GROUP BY date(created_at, 'unixepoch')\n\
|
GROUP BY date(created_at, 'unixepoch')\n\
|
||||||
ORDER BY date DESC;\n\
|
ORDER BY date DESC;\n\
|
||||||
\n\
|
\n\
|
||||||
@@ -262,10 +265,10 @@ SELECT\n\
|
|||||||
events_sent,\n\
|
events_sent,\n\
|
||||||
created_at,\n\
|
created_at,\n\
|
||||||
(strftime('%s', 'now') - created_at) as duration_seconds\n\
|
(strftime('%s', 'now') - created_at) as duration_seconds\n\
|
||||||
FROM subscription_events\n\
|
FROM subscriptions\n\
|
||||||
WHERE event_type = 'created'\n\
|
WHERE event_type = 'created'\n\
|
||||||
AND subscription_id NOT IN (\n\
|
AND subscription_id NOT IN (\n\
|
||||||
SELECT subscription_id FROM subscription_events\n\
|
SELECT subscription_id FROM subscriptions\n\
|
||||||
WHERE event_type IN ('closed', 'expired', 'disconnected')\n\
|
WHERE event_type IN ('closed', 'expired', 'disconnected')\n\
|
||||||
);\n\
|
);\n\
|
||||||
\n\
|
\n\
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ int validate_timestamp_range(long since, long until, char* error_message, size_t
|
|||||||
int validate_numeric_limits(int limit, char* error_message, size_t error_size);
|
int validate_numeric_limits(int limit, char* error_message, size_t error_size);
|
||||||
int validate_search_term(const char* search_term, char* error_message, size_t error_size);
|
int validate_search_term(const char* search_term, char* error_message, size_t error_size);
|
||||||
|
|
||||||
|
// Forward declaration for monitoring function
|
||||||
|
void monitoring_on_subscription_change(void);
|
||||||
|
|
||||||
// Global database variable
|
// Global database variable
|
||||||
extern sqlite3* g_db;
|
extern sqlite3* g_db;
|
||||||
|
|
||||||
@@ -238,27 +241,81 @@ void free_subscription(subscription_t* sub) {
|
|||||||
// Add subscription to global manager (thread-safe)
|
// Add subscription to global manager (thread-safe)
|
||||||
int add_subscription_to_manager(subscription_t* sub) {
|
int add_subscription_to_manager(subscription_t* sub) {
|
||||||
if (!sub) return -1;
|
if (!sub) return -1;
|
||||||
|
|
||||||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
|
||||||
// Check global limits
|
// Check for existing subscription with same ID and WebSocket connection
|
||||||
if (g_subscription_manager.total_subscriptions >= g_subscription_manager.max_total_subscriptions) {
|
// Remove it first to prevent duplicates (implements subscription replacement per NIP-01)
|
||||||
|
subscription_t** current = &g_subscription_manager.active_subscriptions;
|
||||||
|
int found_duplicate = 0;
|
||||||
|
subscription_t* duplicate_old = NULL;
|
||||||
|
|
||||||
|
while (*current) {
|
||||||
|
subscription_t* existing = *current;
|
||||||
|
|
||||||
|
// Match by subscription ID and WebSocket pointer
|
||||||
|
if (strcmp(existing->id, sub->id) == 0 && existing->wsi == sub->wsi) {
|
||||||
|
// Found duplicate: mark inactive and unlink from global list under lock
|
||||||
|
existing->active = 0;
|
||||||
|
*current = existing->next;
|
||||||
|
g_subscription_manager.total_subscriptions--;
|
||||||
|
found_duplicate = 1;
|
||||||
|
duplicate_old = existing; // defer free until after per-session unlink
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = &(existing->next);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check global limits (only if not replacing an existing subscription)
|
||||||
|
if (!found_duplicate && g_subscription_manager.total_subscriptions >= g_subscription_manager.max_total_subscriptions) {
|
||||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||||
DEBUG_ERROR("Maximum total subscriptions reached");
|
DEBUG_ERROR("Maximum total subscriptions reached");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to global list
|
// Add to global list
|
||||||
sub->next = g_subscription_manager.active_subscriptions;
|
sub->next = g_subscription_manager.active_subscriptions;
|
||||||
g_subscription_manager.active_subscriptions = sub;
|
g_subscription_manager.active_subscriptions = sub;
|
||||||
g_subscription_manager.total_subscriptions++;
|
g_subscription_manager.total_subscriptions++;
|
||||||
g_subscription_manager.total_created++;
|
|
||||||
|
// Only increment total_created if this is a new subscription (not a replacement)
|
||||||
|
if (!found_duplicate) {
|
||||||
|
g_subscription_manager.total_created++;
|
||||||
|
}
|
||||||
|
|
||||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
|
||||||
// Log subscription creation to database
|
// If we replaced an existing subscription, unlink it from the per-session list before freeing
|
||||||
|
if (duplicate_old) {
|
||||||
|
// Obtain per-session data for this wsi
|
||||||
|
struct per_session_data* pss = (struct per_session_data*) lws_wsi_user(duplicate_old->wsi);
|
||||||
|
if (pss) {
|
||||||
|
pthread_mutex_lock(&pss->session_lock);
|
||||||
|
struct subscription** scur = &pss->subscriptions;
|
||||||
|
while (*scur) {
|
||||||
|
if (*scur == duplicate_old) {
|
||||||
|
// Unlink by pointer identity to avoid removing the newly-added one
|
||||||
|
*scur = duplicate_old->session_next;
|
||||||
|
if (pss->subscription_count > 0) {
|
||||||
|
pss->subscription_count--;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
scur = &((*scur)->session_next);
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&pss->session_lock);
|
||||||
|
}
|
||||||
|
// Now safe to free the old subscription
|
||||||
|
free_subscription(duplicate_old);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log subscription creation to database (INSERT OR REPLACE handles duplicates)
|
||||||
log_subscription_created(sub);
|
log_subscription_created(sub);
|
||||||
|
|
||||||
|
// Trigger monitoring update for subscription changes
|
||||||
|
monitoring_on_subscription_change();
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,6 +363,9 @@ int remove_subscription_from_manager(const char* sub_id, struct lws* wsi) {
|
|||||||
// Update events sent counter before freeing
|
// Update events sent counter before freeing
|
||||||
update_subscription_events_sent(sub_id_copy, events_sent_copy);
|
update_subscription_events_sent(sub_id_copy, events_sent_copy);
|
||||||
|
|
||||||
|
// Trigger monitoring update for subscription changes
|
||||||
|
monitoring_on_subscription_change();
|
||||||
|
|
||||||
free_subscription(sub);
|
free_subscription(sub);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -324,10 +384,7 @@ int remove_subscription_from_manager(const char* sub_id, struct lws* wsi) {
|
|||||||
|
|
||||||
// Check if an event matches a subscription filter
|
// Check if an event matches a subscription filter
|
||||||
int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
|
int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
|
||||||
DEBUG_TRACE("Checking event against subscription filter");
|
|
||||||
|
|
||||||
if (!event || !filter) {
|
if (!event || !filter) {
|
||||||
DEBUG_TRACE("Exiting event_matches_filter - null parameters");
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,7 +560,6 @@ int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_TRACE("Exiting event_matches_filter - match found");
|
|
||||||
return 1; // All filters passed
|
return 1; // All filters passed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,10 +582,7 @@ int event_matches_subscription(cJSON* event, subscription_t* subscription) {
|
|||||||
|
|
||||||
// Broadcast event to all matching subscriptions (thread-safe)
|
// Broadcast event to all matching subscriptions (thread-safe)
|
||||||
int broadcast_event_to_subscriptions(cJSON* event) {
|
int broadcast_event_to_subscriptions(cJSON* event) {
|
||||||
DEBUG_TRACE("Broadcasting event to subscriptions");
|
|
||||||
|
|
||||||
if (!event) {
|
if (!event) {
|
||||||
DEBUG_TRACE("Exiting broadcast_event_to_subscriptions - null event");
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,12 +664,19 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
|||||||
if (buf) {
|
if (buf) {
|
||||||
memcpy(buf + LWS_PRE, msg_str, msg_len);
|
memcpy(buf + LWS_PRE, msg_str, msg_len);
|
||||||
|
|
||||||
// Send to WebSocket connection with error checking
|
// DEBUG: Log WebSocket frame details before sending
|
||||||
// Note: lws_write can fail if connection is closed, but won't crash
|
DEBUG_TRACE("WS_FRAME_SEND: type=EVENT sub=%s len=%zu data=%.100s%s",
|
||||||
int write_result = lws_write(current_temp->wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
|
current_temp->id,
|
||||||
if (write_result >= 0) {
|
msg_len,
|
||||||
|
msg_str,
|
||||||
|
msg_len > 100 ? "..." : "");
|
||||||
|
|
||||||
|
// Queue message for proper libwebsockets pattern
|
||||||
|
struct per_session_data* pss = (struct per_session_data*)lws_wsi_user(current_temp->wsi);
|
||||||
|
if (queue_message(current_temp->wsi, pss, msg_str, msg_len, LWS_WRITE_TEXT) == 0) {
|
||||||
|
// Message queued successfully
|
||||||
broadcasts++;
|
broadcasts++;
|
||||||
|
|
||||||
// Update events sent counter for this subscription
|
// Update events sent counter for this subscription
|
||||||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||||
subscription_t* update_sub = g_subscription_manager.active_subscriptions;
|
subscription_t* update_sub = g_subscription_manager.active_subscriptions;
|
||||||
@@ -630,12 +690,14 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
|||||||
update_sub = update_sub->next;
|
update_sub = update_sub->next;
|
||||||
}
|
}
|
||||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
|
||||||
// Log event broadcast to database (optional - can be disabled for performance)
|
// Log event broadcast to database (optional - can be disabled for performance)
|
||||||
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
|
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
|
||||||
if (event_id_obj && cJSON_IsString(event_id_obj)) {
|
if (event_id_obj && cJSON_IsString(event_id_obj)) {
|
||||||
log_event_broadcast(cJSON_GetStringValue(event_id_obj), current_temp->id, current_temp->client_ip);
|
log_event_broadcast(cJSON_GetStringValue(event_id_obj), current_temp->id, current_temp->client_ip);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
DEBUG_ERROR("Failed to queue EVENT message for sub=%s", current_temp->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
free(buf);
|
free(buf);
|
||||||
@@ -660,10 +722,41 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
|||||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
|
||||||
DEBUG_LOG("Event broadcast complete: %d subscriptions matched", broadcasts);
|
DEBUG_LOG("Event broadcast complete: %d subscriptions matched", broadcasts);
|
||||||
DEBUG_TRACE("Exiting broadcast_event_to_subscriptions");
|
|
||||||
return broadcasts;
|
return broadcasts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if any active subscription exists for a specific event kind (thread-safe)
|
||||||
|
int has_subscriptions_for_kind(int event_kind) {
|
||||||
|
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
|
||||||
|
subscription_t* sub = g_subscription_manager.active_subscriptions;
|
||||||
|
while (sub) {
|
||||||
|
if (sub->active && sub->filters) {
|
||||||
|
subscription_filter_t* filter = sub->filters;
|
||||||
|
while (filter) {
|
||||||
|
// Check if this filter includes our event kind
|
||||||
|
if (filter->kinds && cJSON_IsArray(filter->kinds)) {
|
||||||
|
cJSON* kind_item = NULL;
|
||||||
|
cJSON_ArrayForEach(kind_item, filter->kinds) {
|
||||||
|
if (cJSON_IsNumber(kind_item)) {
|
||||||
|
int filter_kind = (int)cJSON_GetNumberValue(kind_item);
|
||||||
|
if (filter_kind == event_kind) {
|
||||||
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
return 1; // Found matching subscription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filter = filter->next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sub = sub->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
return 0; // No matching subscriptions
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -675,6 +768,10 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
|||||||
void log_subscription_created(const subscription_t* sub) {
|
void log_subscription_created(const subscription_t* sub) {
|
||||||
if (!g_db || !sub) return;
|
if (!g_db || !sub) return;
|
||||||
|
|
||||||
|
// Convert wsi pointer to string
|
||||||
|
char wsi_str[32];
|
||||||
|
snprintf(wsi_str, sizeof(wsi_str), "%p", (void*)sub->wsi);
|
||||||
|
|
||||||
// Create filter JSON for logging
|
// Create filter JSON for logging
|
||||||
char* filter_json = NULL;
|
char* filter_json = NULL;
|
||||||
if (sub->filters) {
|
if (sub->filters) {
|
||||||
@@ -721,16 +818,18 @@ void log_subscription_created(const subscription_t* sub) {
|
|||||||
cJSON_Delete(filters_array);
|
cJSON_Delete(filters_array);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use INSERT OR REPLACE to handle duplicates automatically
|
||||||
const char* sql =
|
const char* sql =
|
||||||
"INSERT INTO subscription_events (subscription_id, client_ip, event_type, filter_json) "
|
"INSERT OR REPLACE INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type, filter_json) "
|
||||||
"VALUES (?, ?, 'created', ?)";
|
"VALUES (?, ?, ?, 'created', ?)";
|
||||||
|
|
||||||
sqlite3_stmt* stmt;
|
sqlite3_stmt* stmt;
|
||||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||||
if (rc == SQLITE_OK) {
|
if (rc == SQLITE_OK) {
|
||||||
sqlite3_bind_text(stmt, 1, sub->id, -1, SQLITE_STATIC);
|
sqlite3_bind_text(stmt, 1, sub->id, -1, SQLITE_STATIC);
|
||||||
sqlite3_bind_text(stmt, 2, sub->client_ip, -1, SQLITE_STATIC);
|
sqlite3_bind_text(stmt, 2, wsi_str, -1, SQLITE_TRANSIENT);
|
||||||
sqlite3_bind_text(stmt, 3, filter_json ? filter_json : "[]", -1, SQLITE_TRANSIENT);
|
sqlite3_bind_text(stmt, 3, sub->client_ip, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 4, filter_json ? filter_json : "[]", -1, SQLITE_TRANSIENT);
|
||||||
|
|
||||||
sqlite3_step(stmt);
|
sqlite3_step(stmt);
|
||||||
sqlite3_finalize(stmt);
|
sqlite3_finalize(stmt);
|
||||||
@@ -745,8 +844,8 @@ void log_subscription_closed(const char* sub_id, const char* client_ip, const ch
|
|||||||
if (!g_db || !sub_id) return;
|
if (!g_db || !sub_id) return;
|
||||||
|
|
||||||
const char* sql =
|
const char* sql =
|
||||||
"INSERT INTO subscription_events (subscription_id, client_ip, event_type) "
|
"INSERT INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type) "
|
||||||
"VALUES (?, ?, 'closed')";
|
"VALUES (?, '', ?, 'closed')";
|
||||||
|
|
||||||
sqlite3_stmt* stmt;
|
sqlite3_stmt* stmt;
|
||||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||||
@@ -760,7 +859,7 @@ void log_subscription_closed(const char* sub_id, const char* client_ip, const ch
|
|||||||
|
|
||||||
// Update the corresponding 'created' entry with end time and events sent
|
// Update the corresponding 'created' entry with end time and events sent
|
||||||
const char* update_sql =
|
const char* update_sql =
|
||||||
"UPDATE subscription_events "
|
"UPDATE subscriptions "
|
||||||
"SET ended_at = strftime('%s', 'now') "
|
"SET ended_at = strftime('%s', 'now') "
|
||||||
"WHERE subscription_id = ? AND event_type = 'created' AND ended_at IS NULL";
|
"WHERE subscription_id = ? AND event_type = 'created' AND ended_at IS NULL";
|
||||||
|
|
||||||
@@ -778,7 +877,7 @@ void log_subscription_disconnected(const char* client_ip) {
|
|||||||
|
|
||||||
// Mark all active subscriptions for this client as disconnected
|
// Mark all active subscriptions for this client as disconnected
|
||||||
const char* sql =
|
const char* sql =
|
||||||
"UPDATE subscription_events "
|
"UPDATE subscriptions "
|
||||||
"SET ended_at = strftime('%s', 'now') "
|
"SET ended_at = strftime('%s', 'now') "
|
||||||
"WHERE client_ip = ? AND event_type = 'created' AND ended_at IS NULL";
|
"WHERE client_ip = ? AND event_type = 'created' AND ended_at IS NULL";
|
||||||
|
|
||||||
@@ -793,8 +892,8 @@ void log_subscription_disconnected(const char* client_ip) {
|
|||||||
if (changes > 0) {
|
if (changes > 0) {
|
||||||
// Log a disconnection event
|
// Log a disconnection event
|
||||||
const char* insert_sql =
|
const char* insert_sql =
|
||||||
"INSERT INTO subscription_events (subscription_id, client_ip, event_type) "
|
"INSERT INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type) "
|
||||||
"VALUES ('disconnect', ?, 'disconnected')";
|
"VALUES ('disconnect', '', ?, 'disconnected')";
|
||||||
|
|
||||||
rc = sqlite3_prepare_v2(g_db, insert_sql, -1, &stmt, NULL);
|
rc = sqlite3_prepare_v2(g_db, insert_sql, -1, &stmt, NULL);
|
||||||
if (rc == SQLITE_OK) {
|
if (rc == SQLITE_OK) {
|
||||||
@@ -831,7 +930,7 @@ void update_subscription_events_sent(const char* sub_id, int events_sent) {
|
|||||||
if (!g_db || !sub_id) return;
|
if (!g_db || !sub_id) return;
|
||||||
|
|
||||||
const char* sql =
|
const char* sql =
|
||||||
"UPDATE subscription_events "
|
"UPDATE subscriptions "
|
||||||
"SET events_sent = ? "
|
"SET events_sent = ? "
|
||||||
"WHERE subscription_id = ? AND event_type = 'created'";
|
"WHERE subscription_id = ? AND event_type = 'created'";
|
||||||
|
|
||||||
|
|||||||
@@ -118,4 +118,7 @@ void log_subscription_disconnected(const char* client_ip);
|
|||||||
void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip);
|
void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip);
|
||||||
void update_subscription_events_sent(const char* sub_id, int events_sent);
|
void update_subscription_events_sent(const char* sub_id, int events_sent);
|
||||||
|
|
||||||
|
// Subscription query functions
|
||||||
|
int has_subscriptions_for_kind(int event_kind);
|
||||||
|
|
||||||
#endif // SUBSCRIPTIONS_H
|
#endif // SUBSCRIPTIONS_H
|
||||||
341
src/websockets.c
341
src/websockets.c
@@ -108,6 +108,136 @@ struct subscription_manager g_subscription_manager;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Message queue functions for proper libwebsockets pattern
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue a message for WebSocket writing following libwebsockets' proper pattern.
|
||||||
|
* This function adds messages to a per-session queue and requests writeable callback.
|
||||||
|
*
|
||||||
|
* @param wsi WebSocket instance
|
||||||
|
* @param pss Per-session data containing message queue
|
||||||
|
* @param message Message string to write
|
||||||
|
* @param length Length of message string
|
||||||
|
* @param type LWS_WRITE_* type (LWS_WRITE_TEXT, etc.)
|
||||||
|
* @return 0 on success, -1 on error
|
||||||
|
*/
|
||||||
|
int queue_message(struct lws* wsi, struct per_session_data* pss, const char* message, size_t length, enum lws_write_protocol type) {
|
||||||
|
if (!wsi || !pss || !message || length == 0) {
|
||||||
|
DEBUG_ERROR("queue_message: invalid parameters");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate message queue node
|
||||||
|
struct message_queue_node* node = malloc(sizeof(struct message_queue_node));
|
||||||
|
if (!node) {
|
||||||
|
DEBUG_ERROR("queue_message: failed to allocate queue node");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate buffer with LWS_PRE space
|
||||||
|
size_t buffer_size = LWS_PRE + length;
|
||||||
|
unsigned char* buffer = malloc(buffer_size);
|
||||||
|
if (!buffer) {
|
||||||
|
DEBUG_ERROR("queue_message: failed to allocate message buffer");
|
||||||
|
free(node);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy message to buffer with LWS_PRE offset
|
||||||
|
memcpy(buffer + LWS_PRE, message, length);
|
||||||
|
|
||||||
|
// Initialize node
|
||||||
|
node->data = buffer;
|
||||||
|
node->length = length;
|
||||||
|
node->type = type;
|
||||||
|
node->next = NULL;
|
||||||
|
|
||||||
|
// Add to queue (thread-safe)
|
||||||
|
pthread_mutex_lock(&pss->session_lock);
|
||||||
|
|
||||||
|
if (!pss->message_queue_head) {
|
||||||
|
// Queue was empty
|
||||||
|
pss->message_queue_head = node;
|
||||||
|
pss->message_queue_tail = node;
|
||||||
|
} else {
|
||||||
|
// Add to end of queue
|
||||||
|
pss->message_queue_tail->next = node;
|
||||||
|
pss->message_queue_tail = node;
|
||||||
|
}
|
||||||
|
pss->message_queue_count++;
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&pss->session_lock);
|
||||||
|
|
||||||
|
// Request writeable callback (only if not already requested)
|
||||||
|
if (!pss->writeable_requested) {
|
||||||
|
pss->writeable_requested = 1;
|
||||||
|
lws_callback_on_writable(wsi);
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_TRACE("Queued message: len=%zu, queue_count=%d", length, pss->message_queue_count);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process message queue when the socket becomes writeable.
|
||||||
|
* This function is called from LWS_CALLBACK_SERVER_WRITEABLE.
|
||||||
|
*
|
||||||
|
* @param wsi WebSocket instance
|
||||||
|
* @param pss Per-session data containing message queue
|
||||||
|
* @return 0 on success, -1 on error
|
||||||
|
*/
|
||||||
|
int process_message_queue(struct lws* wsi, struct per_session_data* pss) {
|
||||||
|
if (!wsi || !pss) {
|
||||||
|
DEBUG_ERROR("process_message_queue: invalid parameters");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get next message from queue (thread-safe)
|
||||||
|
pthread_mutex_lock(&pss->session_lock);
|
||||||
|
|
||||||
|
struct message_queue_node* node = pss->message_queue_head;
|
||||||
|
if (!node) {
|
||||||
|
// Queue is empty
|
||||||
|
pss->writeable_requested = 0;
|
||||||
|
pthread_mutex_unlock(&pss->session_lock);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from queue
|
||||||
|
pss->message_queue_head = node->next;
|
||||||
|
if (!pss->message_queue_head) {
|
||||||
|
pss->message_queue_tail = NULL;
|
||||||
|
}
|
||||||
|
pss->message_queue_count--;
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&pss->session_lock);
|
||||||
|
|
||||||
|
// Write message (libwebsockets handles partial writes internally)
|
||||||
|
int write_result = lws_write(wsi, node->data + LWS_PRE, node->length, node->type);
|
||||||
|
|
||||||
|
// Free node resources
|
||||||
|
free(node->data);
|
||||||
|
free(node);
|
||||||
|
|
||||||
|
if (write_result < 0) {
|
||||||
|
DEBUG_ERROR("process_message_queue: write failed, result=%d", write_result);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_TRACE("Processed message: wrote %d bytes, remaining in queue: %d", write_result, pss->message_queue_count);
|
||||||
|
|
||||||
|
// If queue not empty, request another callback
|
||||||
|
pthread_mutex_lock(&pss->session_lock);
|
||||||
|
if (pss->message_queue_head) {
|
||||||
|
lws_callback_on_writable(wsi);
|
||||||
|
} else {
|
||||||
|
pss->writeable_requested = 0;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&pss->session_lock);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// WEBSOCKET PROTOCOL
|
// WEBSOCKET PROTOCOL
|
||||||
@@ -247,7 +377,57 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
|
|
||||||
// Get real client IP address
|
// Get real client IP address
|
||||||
char client_ip[CLIENT_IP_MAX_LENGTH];
|
char client_ip[CLIENT_IP_MAX_LENGTH];
|
||||||
lws_get_peer_simple(wsi, client_ip, sizeof(client_ip));
|
memset(client_ip, 0, sizeof(client_ip));
|
||||||
|
|
||||||
|
// Check if we should trust proxy headers
|
||||||
|
int trust_proxy = get_config_bool("trust_proxy_headers", 0);
|
||||||
|
|
||||||
|
if (trust_proxy) {
|
||||||
|
// Try to get IP from X-Forwarded-For header first
|
||||||
|
char x_forwarded_for[CLIENT_IP_MAX_LENGTH];
|
||||||
|
int header_len = lws_hdr_copy(wsi, x_forwarded_for, sizeof(x_forwarded_for) - 1, WSI_TOKEN_X_FORWARDED_FOR);
|
||||||
|
|
||||||
|
if (header_len > 0) {
|
||||||
|
x_forwarded_for[header_len] = '\0';
|
||||||
|
// X-Forwarded-For can contain multiple IPs (client, proxy1, proxy2, ...)
|
||||||
|
// We want the first (leftmost) IP which is the original client
|
||||||
|
char* comma = strchr(x_forwarded_for, ',');
|
||||||
|
if (comma) {
|
||||||
|
*comma = '\0'; // Truncate at first comma
|
||||||
|
}
|
||||||
|
// Trim leading/trailing whitespace
|
||||||
|
char* ip_start = x_forwarded_for;
|
||||||
|
while (*ip_start == ' ' || *ip_start == '\t') ip_start++;
|
||||||
|
size_t ip_len = strlen(ip_start);
|
||||||
|
while (ip_len > 0 && (ip_start[ip_len-1] == ' ' || ip_start[ip_len-1] == '\t')) {
|
||||||
|
ip_start[--ip_len] = '\0';
|
||||||
|
}
|
||||||
|
if (ip_len > 0 && ip_len < CLIENT_IP_MAX_LENGTH) {
|
||||||
|
strncpy(client_ip, ip_start, CLIENT_IP_MAX_LENGTH - 1);
|
||||||
|
client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0';
|
||||||
|
DEBUG_TRACE("Using X-Forwarded-For IP: %s", client_ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If X-Forwarded-For didn't work, try X-Real-IP
|
||||||
|
if (client_ip[0] == '\0') {
|
||||||
|
char x_real_ip[CLIENT_IP_MAX_LENGTH];
|
||||||
|
header_len = lws_hdr_copy(wsi, x_real_ip, sizeof(x_real_ip) - 1, WSI_TOKEN_HTTP_X_REAL_IP);
|
||||||
|
|
||||||
|
if (header_len > 0) {
|
||||||
|
x_real_ip[header_len] = '\0';
|
||||||
|
strncpy(client_ip, x_real_ip, CLIENT_IP_MAX_LENGTH - 1);
|
||||||
|
client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0';
|
||||||
|
DEBUG_TRACE("Using X-Real-IP: %s", client_ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to direct connection IP if proxy headers not available or not trusted
|
||||||
|
if (client_ip[0] == '\0') {
|
||||||
|
lws_get_peer_simple(wsi, client_ip, sizeof(client_ip));
|
||||||
|
DEBUG_TRACE("Using direct connection IP: %s", client_ip);
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure client_ip is null-terminated and copy safely
|
// Ensure client_ip is null-terminated and copy safely
|
||||||
client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0';
|
client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0';
|
||||||
@@ -628,16 +808,24 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
DEBUG_TRACE("Storing regular event in database");
|
// Check if this is an ephemeral event (kinds 20000-29999)
|
||||||
// Regular event - store in database and broadcast
|
// Per NIP-01: ephemeral events are broadcast but never stored
|
||||||
if (store_event(event) != 0) {
|
if (event_kind >= 20000 && event_kind < 30000) {
|
||||||
DEBUG_ERROR("Failed to store event in database");
|
DEBUG_TRACE("Ephemeral event (kind %d) - broadcasting without storage", event_kind);
|
||||||
result = -1;
|
// Broadcast directly to subscriptions without database storage
|
||||||
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
|
|
||||||
} else {
|
|
||||||
DEBUG_LOG("Event stored and broadcast (kind %d)", event_kind);
|
|
||||||
// Broadcast event to matching persistent subscriptions
|
|
||||||
broadcast_event_to_subscriptions(event);
|
broadcast_event_to_subscriptions(event);
|
||||||
|
} else {
|
||||||
|
DEBUG_TRACE("Storing regular event in database");
|
||||||
|
// Regular event - store in database and broadcast
|
||||||
|
if (store_event(event) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to store event in database");
|
||||||
|
result = -1;
|
||||||
|
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
|
||||||
|
} else {
|
||||||
|
DEBUG_LOG("Event stored and broadcast (kind %d)", event_kind);
|
||||||
|
// Broadcast event to matching persistent subscriptions
|
||||||
|
broadcast_event_to_subscriptions(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -661,16 +849,22 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
cJSON_AddItemToArray(response, cJSON_CreateString(cJSON_GetStringValue(event_id)));
|
cJSON_AddItemToArray(response, cJSON_CreateString(cJSON_GetStringValue(event_id)));
|
||||||
cJSON_AddItemToArray(response, cJSON_CreateBool(result == 0));
|
cJSON_AddItemToArray(response, cJSON_CreateBool(result == 0));
|
||||||
cJSON_AddItemToArray(response, cJSON_CreateString(strlen(error_message) > 0 ? error_message : ""));
|
cJSON_AddItemToArray(response, cJSON_CreateString(strlen(error_message) > 0 ? error_message : ""));
|
||||||
|
|
||||||
char *response_str = cJSON_Print(response);
|
char *response_str = cJSON_Print(response);
|
||||||
if (response_str) {
|
if (response_str) {
|
||||||
size_t response_len = strlen(response_str);
|
size_t response_len = strlen(response_str);
|
||||||
unsigned char *buf = malloc(LWS_PRE + response_len);
|
|
||||||
if (buf) {
|
// DEBUG: Log WebSocket frame details before sending
|
||||||
memcpy(buf + LWS_PRE, response_str, response_len);
|
DEBUG_TRACE("WS_FRAME_SEND: type=OK len=%zu data=%.100s%s",
|
||||||
lws_write(wsi, buf + LWS_PRE, response_len, LWS_WRITE_TEXT);
|
response_len,
|
||||||
free(buf);
|
response_str,
|
||||||
|
response_len > 100 ? "..." : "");
|
||||||
|
|
||||||
|
// Queue message for proper libwebsockets pattern
|
||||||
|
if (queue_message(wsi, pss, response_str, response_len, LWS_WRITE_TEXT) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to queue OK response message");
|
||||||
}
|
}
|
||||||
|
|
||||||
free(response_str);
|
free(response_str);
|
||||||
}
|
}
|
||||||
cJSON_Delete(response);
|
cJSON_Delete(response);
|
||||||
@@ -765,12 +959,18 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
char *eose_str = cJSON_Print(eose_response);
|
char *eose_str = cJSON_Print(eose_response);
|
||||||
if (eose_str) {
|
if (eose_str) {
|
||||||
size_t eose_len = strlen(eose_str);
|
size_t eose_len = strlen(eose_str);
|
||||||
unsigned char *buf = malloc(LWS_PRE + eose_len);
|
|
||||||
if (buf) {
|
// DEBUG: Log WebSocket frame details before sending
|
||||||
memcpy(buf + LWS_PRE, eose_str, eose_len);
|
DEBUG_TRACE("WS_FRAME_SEND: type=EOSE len=%zu data=%.100s%s",
|
||||||
lws_write(wsi, buf + LWS_PRE, eose_len, LWS_WRITE_TEXT);
|
eose_len,
|
||||||
free(buf);
|
eose_str,
|
||||||
|
eose_len > 100 ? "..." : "");
|
||||||
|
|
||||||
|
// Queue message for proper libwebsockets pattern
|
||||||
|
if (queue_message(wsi, pss, eose_str, eose_len, LWS_WRITE_TEXT) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to queue EOSE message");
|
||||||
}
|
}
|
||||||
|
|
||||||
free(eose_str);
|
free(eose_str);
|
||||||
}
|
}
|
||||||
cJSON_Delete(eose_response);
|
cJSON_Delete(eose_response);
|
||||||
@@ -850,9 +1050,22 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRITICAL FIX: Remove from session list FIRST (while holding lock)
|
// CRITICAL FIX: Mark subscription as inactive in global manager FIRST
|
||||||
// to prevent race condition where global manager frees the subscription
|
// This prevents other threads from accessing it during removal
|
||||||
// while we're still iterating through the session list
|
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
|
||||||
|
subscription_t* target_sub = g_subscription_manager.active_subscriptions;
|
||||||
|
while (target_sub) {
|
||||||
|
if (strcmp(target_sub->id, subscription_id) == 0 && target_sub->wsi == wsi) {
|
||||||
|
target_sub->active = 0; // Mark as inactive immediately
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
target_sub = target_sub->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
|
||||||
|
// Now safe to remove from session list
|
||||||
if (pss) {
|
if (pss) {
|
||||||
pthread_mutex_lock(&pss->session_lock);
|
pthread_mutex_lock(&pss->session_lock);
|
||||||
|
|
||||||
@@ -870,8 +1083,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
pthread_mutex_unlock(&pss->session_lock);
|
pthread_mutex_unlock(&pss->session_lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from global manager AFTER removing from session list
|
// Finally remove from global manager (which will free it)
|
||||||
// This prevents use-after-free when iterating session subscriptions
|
|
||||||
remove_subscription_from_manager(subscription_id, wsi);
|
remove_subscription_from_manager(subscription_id, wsi);
|
||||||
|
|
||||||
// Subscription closed
|
// Subscription closed
|
||||||
@@ -914,6 +1126,13 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case LWS_CALLBACK_SERVER_WRITEABLE:
|
||||||
|
// Handle message queue when socket becomes writeable
|
||||||
|
if (pss) {
|
||||||
|
process_message_queue(wsi, pss);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case LWS_CALLBACK_CLOSED:
|
case LWS_CALLBACK_CLOSED:
|
||||||
DEBUG_TRACE("WebSocket connection closed");
|
DEBUG_TRACE("WebSocket connection closed");
|
||||||
|
|
||||||
@@ -947,20 +1166,66 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
auth_status,
|
auth_status,
|
||||||
reason);
|
reason);
|
||||||
|
|
||||||
// Clean up session subscriptions
|
// Clean up message queue to prevent memory leaks
|
||||||
|
while (pss->message_queue_head) {
|
||||||
|
struct message_queue_node* node = pss->message_queue_head;
|
||||||
|
pss->message_queue_head = node->next;
|
||||||
|
free(node->data);
|
||||||
|
free(node);
|
||||||
|
}
|
||||||
|
pss->message_queue_tail = NULL;
|
||||||
|
pss->message_queue_count = 0;
|
||||||
|
pss->writeable_requested = 0;
|
||||||
|
|
||||||
|
// Clean up session subscriptions - copy IDs first to avoid use-after-free
|
||||||
pthread_mutex_lock(&pss->session_lock);
|
pthread_mutex_lock(&pss->session_lock);
|
||||||
|
|
||||||
|
// First pass: collect subscription IDs safely
|
||||||
|
typedef struct temp_sub_id {
|
||||||
|
char id[SUBSCRIPTION_ID_MAX_LENGTH];
|
||||||
|
struct temp_sub_id* next;
|
||||||
|
} temp_sub_id_t;
|
||||||
|
|
||||||
|
temp_sub_id_t* temp_ids = NULL;
|
||||||
|
temp_sub_id_t* temp_tail = NULL;
|
||||||
|
int temp_count = 0;
|
||||||
|
|
||||||
struct subscription* sub = pss->subscriptions;
|
struct subscription* sub = pss->subscriptions;
|
||||||
while (sub) {
|
while (sub) {
|
||||||
struct subscription* next = sub->session_next;
|
if (sub->active) { // Only process active subscriptions
|
||||||
remove_subscription_from_manager(sub->id, wsi);
|
temp_sub_id_t* temp = malloc(sizeof(temp_sub_id_t));
|
||||||
sub = next;
|
if (temp) {
|
||||||
|
memcpy(temp->id, sub->id, SUBSCRIPTION_ID_MAX_LENGTH);
|
||||||
|
temp->id[SUBSCRIPTION_ID_MAX_LENGTH - 1] = '\0';
|
||||||
|
temp->next = NULL;
|
||||||
|
|
||||||
|
if (!temp_ids) {
|
||||||
|
temp_ids = temp;
|
||||||
|
temp_tail = temp;
|
||||||
|
} else {
|
||||||
|
temp_tail->next = temp;
|
||||||
|
temp_tail = temp;
|
||||||
|
}
|
||||||
|
temp_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sub = sub->session_next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear session list immediately
|
||||||
pss->subscriptions = NULL;
|
pss->subscriptions = NULL;
|
||||||
pss->subscription_count = 0;
|
pss->subscription_count = 0;
|
||||||
|
|
||||||
pthread_mutex_unlock(&pss->session_lock);
|
pthread_mutex_unlock(&pss->session_lock);
|
||||||
|
|
||||||
|
// Second pass: remove from global manager using copied IDs
|
||||||
|
temp_sub_id_t* current_temp = temp_ids;
|
||||||
|
while (current_temp) {
|
||||||
|
temp_sub_id_t* next_temp = current_temp->next;
|
||||||
|
remove_subscription_from_manager(current_temp->id, wsi);
|
||||||
|
free(current_temp);
|
||||||
|
current_temp = next_temp;
|
||||||
|
}
|
||||||
pthread_mutex_destroy(&pss->session_lock);
|
pthread_mutex_destroy(&pss->session_lock);
|
||||||
} else {
|
} else {
|
||||||
DEBUG_LOG("WebSocket CLOSED: ip=unknown duration=0s subscriptions=0 authenticated=no reason=unknown");
|
DEBUG_LOG("WebSocket CLOSED: ip=unknown duration=0s subscriptions=0 authenticated=no reason=unknown");
|
||||||
@@ -1627,12 +1892,18 @@ int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, st
|
|||||||
char *count_str = cJSON_Print(count_response);
|
char *count_str = cJSON_Print(count_response);
|
||||||
if (count_str) {
|
if (count_str) {
|
||||||
size_t count_len = strlen(count_str);
|
size_t count_len = strlen(count_str);
|
||||||
unsigned char *buf = malloc(LWS_PRE + count_len);
|
|
||||||
if (buf) {
|
// DEBUG: Log WebSocket frame details before sending
|
||||||
memcpy(buf + LWS_PRE, count_str, count_len);
|
DEBUG_TRACE("WS_FRAME_SEND: type=COUNT len=%zu data=%.100s%s",
|
||||||
lws_write(wsi, buf + LWS_PRE, count_len, LWS_WRITE_TEXT);
|
count_len,
|
||||||
free(buf);
|
count_str,
|
||||||
|
count_len > 100 ? "..." : "");
|
||||||
|
|
||||||
|
// Queue message for proper libwebsockets pattern
|
||||||
|
if (queue_message(wsi, pss, count_str, count_len, LWS_WRITE_TEXT) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to queue COUNT message");
|
||||||
}
|
}
|
||||||
|
|
||||||
free(count_str);
|
free(count_str);
|
||||||
}
|
}
|
||||||
cJSON_Delete(count_response);
|
cJSON_Delete(count_response);
|
||||||
|
|||||||
@@ -31,6 +31,14 @@
|
|||||||
#define MAX_SEARCH_LENGTH 256
|
#define MAX_SEARCH_LENGTH 256
|
||||||
#define MAX_TAG_VALUE_LENGTH 1024
|
#define MAX_TAG_VALUE_LENGTH 1024
|
||||||
|
|
||||||
|
// Message queue node for proper libwebsockets pattern
|
||||||
|
struct message_queue_node {
|
||||||
|
unsigned char* data; // Message data (with LWS_PRE space)
|
||||||
|
size_t length; // Message length (without LWS_PRE)
|
||||||
|
enum lws_write_protocol type; // LWS_WRITE_TEXT, etc.
|
||||||
|
struct message_queue_node* next; // Next node in queue
|
||||||
|
};
|
||||||
|
|
||||||
// Enhanced per-session data with subscription management, NIP-42 authentication, and rate limiting
|
// Enhanced per-session data with subscription management, NIP-42 authentication, and rate limiting
|
||||||
struct per_session_data {
|
struct per_session_data {
|
||||||
int authenticated;
|
int authenticated;
|
||||||
@@ -59,6 +67,12 @@ struct per_session_data {
|
|||||||
int malformed_request_count; // Count of malformed requests in current hour
|
int malformed_request_count; // Count of malformed requests in current hour
|
||||||
time_t malformed_request_window_start; // Start of current hour window
|
time_t malformed_request_window_start; // Start of current hour window
|
||||||
time_t malformed_request_blocked_until; // Time until blocked for malformed requests
|
time_t malformed_request_blocked_until; // Time until blocked for malformed requests
|
||||||
|
|
||||||
|
// Message queue for proper libwebsockets pattern (replaces single buffer)
|
||||||
|
struct message_queue_node* message_queue_head; // Head of message queue
|
||||||
|
struct message_queue_node* message_queue_tail; // Tail of message queue
|
||||||
|
int message_queue_count; // Number of messages in queue
|
||||||
|
int writeable_requested; // Flag: 1 if writeable callback requested
|
||||||
};
|
};
|
||||||
|
|
||||||
// NIP-11 HTTP session data structure for managing buffer lifetime
|
// NIP-11 HTTP session data structure for managing buffer lifetime
|
||||||
@@ -73,6 +87,10 @@ struct nip11_session_data {
|
|||||||
// Function declarations
|
// Function declarations
|
||||||
int start_websocket_relay(int port_override, int strict_port);
|
int start_websocket_relay(int port_override, int strict_port);
|
||||||
|
|
||||||
|
// Message queue functions for proper libwebsockets pattern
|
||||||
|
int queue_message(struct lws* wsi, struct per_session_data* pss, const char* message, size_t length, enum lws_write_protocol type);
|
||||||
|
int process_message_queue(struct lws* wsi, struct per_session_data* pss);
|
||||||
|
|
||||||
// Auth rules checking function from request_validator.c
|
// Auth rules checking function from request_validator.c
|
||||||
int check_database_auth_rules(const char *pubkey, const char *operation, const char *resource_hash);
|
int check_database_auth_rules(const char *pubkey, const char *operation, const char *resource_hash);
|
||||||
|
|
||||||
|
|||||||
35
tests/ephemeral_test.sh
Executable file
35
tests/ephemeral_test.sh
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Simplified Ephemeral Event Test
|
||||||
|
# Tests that ephemeral events are broadcast to active subscriptions
|
||||||
|
|
||||||
|
echo "=== Generating Ephemeral Event (kind 20000) ==="
|
||||||
|
event=$(nak event --kind 20000 --content "test ephemeral event")
|
||||||
|
echo "$event"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "=== Testing Ephemeral Event Broadcast ==="
|
||||||
|
subscription='["REQ","test_sub",{"kinds":[20000],"limit":10}]'
|
||||||
|
echo "Subscription Filter:"
|
||||||
|
echo "$subscription"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
event_msg='["EVENT",'"$event"']'
|
||||||
|
echo "Event Message:"
|
||||||
|
echo "$event_msg"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "=== Relay Responses ==="
|
||||||
|
(
|
||||||
|
# Send subscription
|
||||||
|
printf "%s\n" "$subscription"
|
||||||
|
# Wait for subscription to establish
|
||||||
|
sleep 1
|
||||||
|
# Send ephemeral event on same connection
|
||||||
|
printf "%s\n" "$event_msg"
|
||||||
|
# Wait for responses
|
||||||
|
sleep 2
|
||||||
|
) | timeout 5 websocat ws://127.0.0.1:8888
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Test complete!"
|
||||||
63
tests/large_event_test.sh
Executable file
63
tests/large_event_test.sh
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Test script for posting large events (>4KB) to test partial write handling
|
||||||
|
# Uses nak to properly sign events with large content
|
||||||
|
|
||||||
|
RELAY_URL="ws://localhost:8888"
|
||||||
|
|
||||||
|
# Check if nak is installed
|
||||||
|
if ! command -v nak &> /dev/null; then
|
||||||
|
echo "Error: nak is not installed. Install with: go install github.com/fiatjaf/nak@latest"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate a test private key if not set
|
||||||
|
if [ -z "$NOSTR_PRIVATE_KEY" ]; then
|
||||||
|
echo "Generating temporary test key..."
|
||||||
|
export NOSTR_PRIVATE_KEY=$(nak key generate)
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Large Event Test ==="
|
||||||
|
echo "Testing partial write handling with events >4KB"
|
||||||
|
echo "Relay: $RELAY_URL"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 1: 5KB event
|
||||||
|
echo "Test 1: Posting 5KB event..."
|
||||||
|
CONTENT_5KB=$(python3 -c "print('A' * 5000)")
|
||||||
|
echo "$CONTENT_5KB" | nak event -k 1 --content - $RELAY_URL
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Test 2: 10KB event
|
||||||
|
echo ""
|
||||||
|
echo "Test 2: Posting 10KB event..."
|
||||||
|
CONTENT_10KB=$(python3 -c "print('B' * 10000)")
|
||||||
|
echo "$CONTENT_10KB" | nak event -k 1 --content - $RELAY_URL
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Test 3: 20KB event
|
||||||
|
echo ""
|
||||||
|
echo "Test 3: Posting 20KB event..."
|
||||||
|
CONTENT_20KB=$(python3 -c "print('C' * 20000)")
|
||||||
|
echo "$CONTENT_20KB" | nak event -k 1 --content - $RELAY_URL
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Test 4: 50KB event (very large)
|
||||||
|
echo ""
|
||||||
|
echo "Test 4: Posting 50KB event..."
|
||||||
|
CONTENT_50KB=$(python3 -c "print('D' * 50000)")
|
||||||
|
echo "$CONTENT_50KB" | nak event -k 1 --content - $RELAY_URL
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Test Complete ==="
|
||||||
|
echo ""
|
||||||
|
echo "Check relay.log for:"
|
||||||
|
echo " - 'Queued partial write' messages (indicates buffering is working)"
|
||||||
|
echo " - 'write completed' messages (indicates retry succeeded)"
|
||||||
|
echo " - No 'Invalid frame header' errors"
|
||||||
|
echo ""
|
||||||
|
echo "To view logs in real-time:"
|
||||||
|
echo " tail -f relay.log | grep -E '(partial|write completed|Invalid frame)'"
|
||||||
|
echo ""
|
||||||
|
echo "To check if events were stored:"
|
||||||
|
echo " sqlite3 build/*.db 'SELECT id, length(content) as content_size FROM events ORDER BY created_at DESC LIMIT 4;'"
|
||||||
@@ -3,6 +3,19 @@
|
|||||||
# Test script to post kind 1 events to the relay every second
|
# Test script to post kind 1 events to the relay every second
|
||||||
# Cycles through three different secret keys
|
# Cycles through three different secret keys
|
||||||
# Content includes current timestamp
|
# Content includes current timestamp
|
||||||
|
#
|
||||||
|
# Usage: ./post_events.sh <relay_url>
|
||||||
|
# Example: ./post_events.sh ws://localhost:8888
|
||||||
|
# Example: ./post_events.sh wss://relay.laantungir.net
|
||||||
|
|
||||||
|
# Check if relay URL is provided
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Error: Relay URL is required"
|
||||||
|
echo "Usage: $0 <relay_url>"
|
||||||
|
echo "Example: $0 ws://localhost:8888"
|
||||||
|
echo "Example: $0 wss://relay.laantungir.net"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Array of secret keys to cycle through
|
# Array of secret keys to cycle through
|
||||||
SECRET_KEYS=(
|
SECRET_KEYS=(
|
||||||
@@ -11,7 +24,7 @@ SECRET_KEYS=(
|
|||||||
"1618aaa21f5bd45c5ffede0d9a60556db67d4a046900e5f66b0bae5c01c801fb"
|
"1618aaa21f5bd45c5ffede0d9a60556db67d4a046900e5f66b0bae5c01c801fb"
|
||||||
)
|
)
|
||||||
|
|
||||||
RELAY_URL="ws://localhost:8888"
|
RELAY_URL="$1"
|
||||||
KEY_INDEX=0
|
KEY_INDEX=0
|
||||||
|
|
||||||
echo "Starting event posting test to $RELAY_URL"
|
echo "Starting event posting test to $RELAY_URL"
|
||||||
@@ -36,5 +49,5 @@ while true; do
|
|||||||
KEY_INDEX=$(( (KEY_INDEX + 1) % ${#SECRET_KEYS[@]} ))
|
KEY_INDEX=$(( (KEY_INDEX + 1) % ${#SECRET_KEYS[@]} ))
|
||||||
|
|
||||||
# Wait 1 second
|
# Wait 1 second
|
||||||
sleep 1
|
sleep .2
|
||||||
done
|
done
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Rate Limiting Test Suite for C-Relay
|
|
||||||
# Tests rate limiting and abuse prevention mechanisms
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
RELAY_HOST="127.0.0.1"
|
|
||||||
RELAY_PORT="8888"
|
|
||||||
TEST_TIMEOUT=15
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Test counters
|
|
||||||
TOTAL_TESTS=0
|
|
||||||
PASSED_TESTS=0
|
|
||||||
FAILED_TESTS=0
|
|
||||||
|
|
||||||
# Function to test rate limiting
|
|
||||||
test_rate_limiting() {
|
|
||||||
local description="$1"
|
|
||||||
local message="$2"
|
|
||||||
local burst_count="${3:-10}"
|
|
||||||
local expected_limited="${4:-false}"
|
|
||||||
|
|
||||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
|
||||||
|
|
||||||
echo -n "Testing $description... "
|
|
||||||
|
|
||||||
local rate_limited=false
|
|
||||||
local success_count=0
|
|
||||||
local error_count=0
|
|
||||||
|
|
||||||
# Send burst of messages
|
|
||||||
for i in $(seq 1 "$burst_count"); do
|
|
||||||
local response
|
|
||||||
response=$(echo "$message" | timeout 2 websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -1 || echo 'TIMEOUT')
|
|
||||||
|
|
||||||
if [[ "$response" == *"rate limit"* ]] || [[ "$response" == *"too many"* ]] || [[ "$response" == *"TOO_MANY"* ]]; then
|
|
||||||
rate_limited=true
|
|
||||||
elif [[ "$response" == *"EOSE"* ]] || [[ "$response" == *"EVENT"* ]] || [[ "$response" == *"OK"* ]]; then
|
|
||||||
((success_count++))
|
|
||||||
else
|
|
||||||
((error_count++))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Small delay between requests
|
|
||||||
sleep 0.05
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$expected_limited" == "true" ]]; then
|
|
||||||
if [[ "$rate_limited" == "true" ]]; then
|
|
||||||
echo -e "${GREEN}PASSED${NC} - Rate limiting triggered as expected"
|
|
||||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo -e "${RED}FAILED${NC} - Rate limiting not triggered (expected)"
|
|
||||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if [[ "$rate_limited" == "false" ]]; then
|
|
||||||
echo -e "${GREEN}PASSED${NC} - No rate limiting for normal traffic"
|
|
||||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo -e "${YELLOW}UNCERTAIN${NC} - Unexpected rate limiting"
|
|
||||||
PASSED_TESTS=$((PASSED_TESTS + 1)) # Count as passed since it's conservative
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to test sustained load
|
|
||||||
test_sustained_load() {
|
|
||||||
local description="$1"
|
|
||||||
local message="$2"
|
|
||||||
local duration="${3:-10}"
|
|
||||||
|
|
||||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
|
||||||
|
|
||||||
echo -n "Testing $description... "
|
|
||||||
|
|
||||||
local start_time
|
|
||||||
start_time=$(date +%s)
|
|
||||||
local rate_limited=false
|
|
||||||
local total_requests=0
|
|
||||||
local successful_requests=0
|
|
||||||
|
|
||||||
while [[ $(($(date +%s) - start_time)) -lt duration ]]; do
|
|
||||||
((total_requests++))
|
|
||||||
local response
|
|
||||||
response=$(echo "$message" | timeout 1 websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -1 || echo 'TIMEOUT')
|
|
||||||
|
|
||||||
if [[ "$response" == *"rate limit"* ]] || [[ "$response" == *"too many"* ]] || [[ "$response" == *"TOO_MANY"* ]]; then
|
|
||||||
rate_limited=true
|
|
||||||
elif [[ "$response" == *"EOSE"* ]] || [[ "$response" == *"EVENT"* ]] || [[ "$response" == *"OK"* ]]; then
|
|
||||||
((successful_requests++))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Small delay to avoid overwhelming
|
|
||||||
sleep 0.1
|
|
||||||
done
|
|
||||||
|
|
||||||
local success_rate=0
|
|
||||||
if [[ $total_requests -gt 0 ]]; then
|
|
||||||
success_rate=$((successful_requests * 100 / total_requests))
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$rate_limited" == "true" ]]; then
|
|
||||||
echo -e "${GREEN}PASSED${NC} - Rate limiting activated under sustained load (${success_rate}% success rate)"
|
|
||||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo -e "${YELLOW}UNCERTAIN${NC} - No rate limiting detected (${success_rate}% success rate)"
|
|
||||||
# This might be acceptable if rate limiting is very permissive
|
|
||||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "=========================================="
|
|
||||||
echo "C-Relay Rate Limiting Test Suite"
|
|
||||||
echo "=========================================="
|
|
||||||
echo "Testing rate limiting against relay at ws://$RELAY_HOST:$RELAY_PORT"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Test basic connectivity first
|
|
||||||
echo "=== Basic Connectivity Test ==="
|
|
||||||
test_rate_limiting "Basic connectivity" '["REQ","rate_test",{}]' 1 false
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "=== Burst Request Testing ==="
|
|
||||||
# Test rapid succession of requests
|
|
||||||
test_rate_limiting "Rapid REQ messages" '["REQ","burst_req_'$(date +%s%N)'",{}]' 20 true
|
|
||||||
test_rate_limiting "Rapid COUNT messages" '["COUNT","burst_count_'$(date +%s%N)'",{}]' 20 true
|
|
||||||
test_rate_limiting "Rapid CLOSE messages" '["CLOSE","burst_close"]' 20 true
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "=== Malformed Message Rate Limiting ==="
|
|
||||||
# Test if malformed messages trigger rate limiting faster
|
|
||||||
test_rate_limiting "Malformed JSON burst" '["REQ","malformed"' 15 true
|
|
||||||
test_rate_limiting "Invalid message type burst" '["INVALID","test",{}]' 15 true
|
|
||||||
test_rate_limiting "Empty message burst" '[]' 15 true
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "=== Sustained Load Testing ==="
|
|
||||||
# Test sustained moderate load
|
|
||||||
test_sustained_load "Sustained REQ load" '["REQ","sustained_'$(date +%s%N)'",{}]' 10
|
|
||||||
test_sustained_load "Sustained COUNT load" '["COUNT","sustained_count_'$(date +%s%N)'",{}]' 10
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "=== Filter Complexity Testing ==="
|
|
||||||
# Test if complex filters trigger rate limiting
|
|
||||||
test_rate_limiting "Complex filter burst" '["REQ","complex_'$(date +%s%N)'",{"authors":["a","b","c"],"kinds":[1,2,3],"#e":["x","y","z"],"#p":["m","n","o"],"since":1000000000,"until":2000000000,"limit":100}]' 10 true
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "=== Subscription Management Testing ==="
|
|
||||||
# Test subscription creation/deletion rate limiting
|
|
||||||
echo -n "Testing subscription churn... "
|
|
||||||
local churn_test_passed=true
|
|
||||||
for i in $(seq 1 25); do
|
|
||||||
# Create subscription
|
|
||||||
echo "[\"REQ\",\"churn_${i}_$(date +%s%N)\",{}]" | timeout 1 websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# Close subscription
|
|
||||||
echo "[\"CLOSE\",\"churn_${i}_*\"]" | timeout 1 websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
sleep 0.05
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check if relay is still responsive
|
|
||||||
if echo 'ping' | timeout 2 websocat -n1 ws://$RELAY_HOST:$RELAY_PORT >/dev/null 2>&1; then
|
|
||||||
echo -e "${GREEN}PASSED${NC} - Subscription churn handled"
|
|
||||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
|
||||||
PASSED_TESTS=$((PASSED_TESTS + 1))
|
|
||||||
else
|
|
||||||
echo -e "${RED}FAILED${NC} - Relay unresponsive after subscription churn"
|
|
||||||
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
|
||||||
FAILED_TESTS=$((FAILED_TESTS + 1))
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "=== Test Results ==="
|
|
||||||
echo "Total tests: $TOTAL_TESTS"
|
|
||||||
echo -e "Passed: ${GREEN}$PASSED_TESTS${NC}"
|
|
||||||
echo -e "Failed: ${RED}$FAILED_TESTS${NC}"
|
|
||||||
|
|
||||||
if [[ $FAILED_TESTS -eq 0 ]]; then
|
|
||||||
echo -e "${GREEN}✓ All rate limiting tests passed!${NC}"
|
|
||||||
echo "Rate limiting appears to be working correctly."
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
echo -e "${RED}✗ Some rate limiting tests failed!${NC}"
|
|
||||||
echo "Rate limiting may not be properly configured."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
Submodule text_graph updated: 0762bfbd1e...bf1785f372
Reference in New Issue
Block a user