Compare commits

...
6 Commits
35 changed files with 734813 additions and 59868 deletions
+2 -1
View File
@@ -5,4 +5,5 @@ blobs/
c-relay/
text_graph/
.test_keys
ginxsom-local.service
ginxsom-local.service
*.tar.gz
+23 -20
View File
@@ -31,6 +31,7 @@ RUN apk add --no-cache \
linux-headers \
wget \
bash \
openssh-client \
nghttp2-dev \
nghttp2-static \
c-ares-dev \
@@ -58,41 +59,43 @@ RUN cd /tmp && \
make install && \
rm -rf /tmp/secp256k1
# Copy only submodule configuration and git directory
COPY .gitmodules /build/.gitmodules
COPY .git /build/.git
# Copy nostr_core_lib from local submodule (avoids remote git dependency)
COPY nostr_core_lib /build/nostr_core_lib
# Initialize submodules (cached unless .gitmodules changes)
RUN git submodule update --init --recursive
# Copy nostr_core_lib source files (cached unless nostr_core_lib changes)
COPY nostr_core_lib /build/nostr_core_lib/
# Build nostr_core_lib with required NIPs (cached unless nostr_core_lib changes)
# Build nostr_core_lib with required NIPs (cached unless submodule changes)
# Disable fortification in build.sh to prevent __*_chk symbol issues
# NIPs: 001(Basic), 006(Keys), 013(PoW), 017(DMs), 019(Bech32), 042(Auth), 044(Encryption), 059(Gift Wrap)
RUN cd nostr_core_lib && \
chmod +x build.sh && \
sed -i 's/CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -fPIC -O2"/' build.sh && \
if [ "$(uname -m)" = "aarch64" ] && ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
ln -sf /usr/bin/gcc /usr/local/bin/aarch64-linux-gnu-gcc; \
fi && \
rm -f *.o *.a 2>/dev/null || true && \
./build.sh --nips=1,6,13,17,19,42,44,59
./build.sh --nips=1,6,13,17,19,42,44,59 && \
if [ -f libnostr_core_arm64.a ]; then \
cp libnostr_core_arm64.a libnostr_core.a; \
elif [ -f libnostr_core_x64.a ]; then \
cp libnostr_core_x64.a libnostr_core.a; \
else \
echo "ERROR: No supported nostr_core static library produced"; \
ls -la *.a 2>/dev/null || true; \
exit 1; \
fi
# Copy web interface files for embedding
# Note: Changes to api/ files will trigger rebuild from this point
COPY api/ /build/api/
COPY scripts/embed_web_files.sh /build/scripts/
# Create src directory and embed web files into C headers
RUN mkdir -p src && \
chmod +x scripts/embed_web_files.sh && \
./scripts/embed_web_files.sh
# Copy Ginxsom source files LAST (only this layer rebuilds on source changes)
# Note: The embedded header from previous step will be overwritten by this COPY
# So we need to ensure src/admin_interface_embedded.h is NOT in src/ directory
# Copy Ginxsom source files
COPY src/ /build/src/
COPY include/ /build/include/
# Embed web files AFTER source copy so src/admin_interface_embedded.h is always fresh
RUN chmod +x scripts/embed_web_files.sh && \
./scripts/embed_web_files.sh
# Build Ginxsom with full static linking (only rebuilds when src/ changes)
# Disable fortification to avoid __*_chk symbols that don't exist in MUSL
# Use conditional compilation flags based on DEBUG_BUILD argument
@@ -115,7 +118,7 @@ RUN if [ "$DEBUG_BUILD" = "true" ]; then \
src/request_validator.c src/relay_client.c \
nostr_core_lib/nostr_core/core_relay_pool.c \
-o /build/ginxsom-fcgi_static \
nostr_core_lib/libnostr_core_x64.a \
nostr_core_lib/libnostr_core.a \
-lfcgi -lsqlite3 -lsecp256k1 -lssl -lcrypto -lcurl \
-lnghttp2 -lcares -lidn2 -lunistring -lpsl -lbrotlidec -lbrotlicommon \
-lz -lpthread -lm -ldl && \
View File
+155
View File
@@ -1308,3 +1308,158 @@ body.dark-mode .sql-results-table tbody tr:nth-child(even) {
font-size: 12px;
}
/* ================================
BLOB BROWSER
================================ */
.blob-browser-controls {
gap: 8px;
flex-wrap: wrap;
}
.blob-browser-controls button {
min-width: 160px;
}
.blob-pagination-controls {
gap: 8px;
}
.blob-pagination-controls button {
min-width: 120px;
}
.blob-grid-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 12px;
margin: 12px 0;
}
.blob-card {
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: 10px;
background: var(--secondary-color);
}
.blob-thumb-link {
display: block;
width: 100%;
height: 140px;
border: 1px solid var(--muted-color);
border-radius: 4px;
overflow: hidden;
text-decoration: none;
margin-bottom: 8px;
}
.blob-thumb {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.blob-thumb-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: var(--muted-color);
color: var(--primary-color);
font-size: 12px;
text-align: center;
padding: 8px;
}
.blob-card-meta {
font-size: 12px;
line-height: 1.35;
word-break: break-word;
}
.blob-hash-link {
color: var(--primary-color);
text-decoration: none;
}
.blob-hash-link:hover {
color: var(--accent-color);
}
.blob-list-preview {
width: 58px;
height: 44px;
object-fit: cover;
border: 1px solid var(--muted-color);
border-radius: 4px;
display: block;
}
.blob-list-placeholder {
width: 58px;
height: 44px;
border: 1px solid var(--muted-color);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
background: var(--muted-color);
}
.blob-list-view .config-table td {
vertical-align: middle;
}
.blob-list-view th.blob-sortable-th {
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.blob-list-view th.blob-sortable-th:hover {
color: var(--accent-color);
}
.blob-sort-indicator {
display: inline-block;
width: 14px;
text-align: center;
margin-left: 4px;
font-size: 10px;
}
.blob-delete-btn {
min-width: 86px;
font-size: 10px;
padding: 6px 8px;
border: 1px solid var(--accent-color);
background: transparent;
color: var(--accent-color);
cursor: pointer;
border-radius: 4px;
font-family: var(--font-family);
}
.blob-delete-btn:hover {
background: var(--accent-color);
color: var(--secondary-color);
}
.blob-delete-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.blob-empty {
border: 1px dashed var(--border-color);
border-radius: var(--border-radius);
padding: 24px;
text-align: center;
font-style: italic;
color: var(--primary-color);
}
+52
View File
@@ -13,6 +13,7 @@
<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="blob-browser">Blob Browser</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="relay-events">Blossom Events</button></li>
@@ -372,6 +373,57 @@ AUTH RULES MANAGEMENT
</div>
</div>
<!-- BLOB BROWSER Section -->
<div class="section" id="blobBrowserSection" style="display: none;">
<div class="section-header">
<h2>BLOB BROWSER</h2>
</div>
<div class="input-group">
<label for="blob-filter-pubkey">Uploader Pubkey (hex or npub):</label>
<input type="text" id="blob-filter-pubkey" placeholder="Optional: npub1... or 64-char hex pubkey">
</div>
<div class="inline-buttons blob-browser-controls">
<button type="button" id="blob-filter-apply-btn">APPLY FILTER</button>
<button type="button" id="blob-filter-clear-btn">CLEAR FILTER</button>
<button type="button" id="blob-view-toggle-btn">SWITCH TO LIST VIEW</button>
<button type="button" id="blob-refresh-btn">REFRESH</button>
</div>
<div id="blob-browser-summary" class="info-box">No data loaded.</div>
<div id="blob-grid-view" class="blob-grid-view"></div>
<div id="blob-list-view" class="blob-list-view" style="display: none;">
<div class="config-table-container">
<table class="config-table" id="blob-list-table">
<thead>
<tr>
<th>Delete</th>
<th>Preview</th>
<th class="blob-sortable-th" data-sort-key="hash">Hash <span class="blob-sort-indicator"></span></th>
<th class="blob-sortable-th" data-sort-key="uploader">Uploader <span class="blob-sort-indicator"></span></th>
<th class="blob-sortable-th" data-sort-key="type">Type <span class="blob-sort-indicator"></span></th>
<th class="blob-sortable-th" data-sort-key="size">Size <span class="blob-sort-indicator"></span></th>
<th class="blob-sortable-th" data-sort-key="uploaded">Uploaded <span class="blob-sort-indicator"></span></th>
</tr>
</thead>
<tbody id="blob-list-table-body">
<tr>
<td colspan="7" style="text-align: center; font-style: italic;">No blobs loaded</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="inline-buttons blob-pagination-controls">
<button type="button" id="blob-prev-page-btn">PREV</button>
<button type="button" id="blob-next-page-btn">NEXT</button>
</div>
</div>
<!-- SQL QUERY Section -->
<div class="section" id="sqlQuerySection" style="display: none;">
<div class="section-header">
+473
View File
@@ -39,6 +39,20 @@ let pendingSqlQueries = new Map();
let eventRateChart = null;
let previousTotalBlobs = 0; // Track previous total for rate calculation
// Blob Browser state
const blobBrowserState = {
limit: 24,
offset: 0,
total: 0,
files: [],
pubkeyFilter: '',
viewMode: 'grid',
sortKey: 'uploaded',
sortDirection: 'desc',
deletingHashes: new Set(),
loading: false
};
// Relay Events state - now handled by main subscription
// DOM elements
@@ -3405,6 +3419,457 @@ function formatTimestamp(timestamp) {
return date.toLocaleString();
}
// ================================
// BLOB BROWSER FUNCTIONS
// ================================
function blobEscapeHtml(value) {
if (value === null || value === undefined) return '';
return String(value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, "'");
}
function blobIsImageType(mimeType) {
return typeof mimeType === 'string' && mimeType.toLowerCase().startsWith('image/');
}
function blobShortHash(hash) {
if (!hash || hash.length < 16) return hash || '-';
return `${hash.substring(0, 10)}...${hash.substring(hash.length - 8)}`;
}
function blobPubkeyToDisplay(pubkeyHex) {
if (!pubkeyHex) return '-';
try {
if (window.NostrTools && window.NostrTools.nip19 && window.NostrTools.nip19.npubEncode && /^[0-9a-fA-F]{64}$/.test(pubkeyHex)) {
return window.NostrTools.nip19.npubEncode(pubkeyHex);
}
} catch (error) {
console.log('Failed to encode uploader pubkey to npub:', error.message);
}
return pubkeyHex;
}
function blobNormalizePubkeyFilter(inputValue) {
const trimmed = (inputValue || '').trim();
if (!trimmed) {
return '';
}
// Use existing decoder utility - supports npub and 64-char hex
const decoded = nsecToHex(trimmed);
if (!decoded || !/^[0-9a-fA-F]{64}$/.test(decoded)) {
throw new Error('Invalid pubkey format. Use npub1... or 64-character hex pubkey.');
}
return decoded;
}
function blobBuildApiUrl() {
const params = new URLSearchParams();
params.set('limit', String(blobBrowserState.limit));
params.set('offset', String(blobBrowserState.offset));
if (blobBrowserState.pubkeyFilter) {
params.set('pubkey', blobBrowserState.pubkeyFilter);
}
return `/api/files?${params.toString()}`;
}
function blobSetViewMode(mode) {
blobBrowserState.viewMode = mode === 'list' ? 'list' : 'grid';
const gridView = document.getElementById('blob-grid-view');
const listView = document.getElementById('blob-list-view');
const toggleBtn = document.getElementById('blob-view-toggle-btn');
if (gridView) {
gridView.style.display = blobBrowserState.viewMode === 'grid' ? 'grid' : 'none';
}
if (listView) {
listView.style.display = blobBrowserState.viewMode === 'list' ? 'block' : 'none';
}
if (toggleBtn) {
toggleBtn.textContent = blobBrowserState.viewMode === 'grid' ? 'SWITCH TO LIST VIEW' : 'SWITCH TO GRID VIEW';
}
}
function blobRenderGrid(files) {
const grid = document.getElementById('blob-grid-view');
if (!grid) return;
if (!files || files.length === 0) {
grid.innerHTML = '<div class="blob-empty">No blobs found for this filter.</div>';
return;
}
const cards = files.map(file => {
const url = blobEscapeHtml(file.url || '#');
const hash = blobEscapeHtml(file.sha256 || '');
const type = blobEscapeHtml(file.type || 'unknown');
const uploaderHex = file.uploader_pubkey || '';
const uploaderDisplay = blobEscapeHtml(blobPubkeyToDisplay(uploaderHex));
const uploadedAt = blobEscapeHtml(formatTimestamp(file.uploaded_at));
const size = blobEscapeHtml(formatFileSize(file.size));
const previewHtml = blobIsImageType(file.type) && file.url
? `<a class="blob-thumb-link" href="${url}" target="_blank" rel="noopener noreferrer"><img class="blob-thumb" src="${url}" alt="${hash}" loading="lazy"></a>`
: `<a class="blob-thumb-link" href="${url}" target="_blank" rel="noopener noreferrer"><div class="blob-thumb-placeholder">${type}</div></a>`;
return `
<article class="blob-card">
${previewHtml}
<div class="blob-card-meta">
<div><strong>Hash:</strong> <a class="blob-hash-link" href="${url}" target="_blank" rel="noopener noreferrer">${blobEscapeHtml(blobShortHash(file.sha256 || ''))}</a></div>
<div><strong>Uploader:</strong> ${uploaderDisplay}</div>
<div><strong>Type:</strong> ${type}</div>
<div><strong>Size:</strong> ${size}</div>
<div><strong>Uploaded:</strong> ${uploadedAt}</div>
</div>
</article>
`;
});
grid.innerHTML = cards.join('');
}
function blobRenderList(files) {
const tbody = document.getElementById('blob-list-table-body');
if (!tbody) return;
if (!files || files.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; font-style: italic;">No blobs found for this filter</td></tr>';
return;
}
const rows = files.map(file => {
const url = blobEscapeHtml(file.url || '#');
const hash = blobEscapeHtml(file.sha256 || '');
const type = blobEscapeHtml(file.type || 'unknown');
const uploaderDisplay = blobEscapeHtml(blobPubkeyToDisplay(file.uploader_pubkey || ''));
const size = blobEscapeHtml(formatFileSize(file.size));
const uploadedAt = blobEscapeHtml(formatTimestamp(file.uploaded_at));
const previewHtml = blobIsImageType(file.type) && file.url
? `<a href="${url}" target="_blank" rel="noopener noreferrer"><img class="blob-list-preview" src="${url}" alt="${hash}" loading="lazy"></a>`
: `<a href="${url}" target="_blank" rel="noopener noreferrer"><div class="blob-list-placeholder">FILE</div></a>`;
const sha256Raw = file.sha256 || '';
const shaAttr = blobEscapeHtml(sha256Raw);
const isDeleting = blobBrowserState.deletingHashes.has(sha256Raw);
return `
<tr>
<td><button type="button" class="blob-delete-btn" data-sha256="${shaAttr}" ${isDeleting ? 'disabled' : ''}>${isDeleting ? 'DELETING...' : 'DELETE'}</button></td>
<td>${previewHtml}</td>
<td><a class="blob-hash-link" href="${url}" target="_blank" rel="noopener noreferrer">${blobEscapeHtml(blobShortHash(file.sha256 || ''))}</a></td>
<td>${uploaderDisplay}</td>
<td>${type}</td>
<td>${size}</td>
<td>${uploadedAt}</td>
</tr>
`;
});
tbody.innerHTML = rows.join('');
}
function blobGetSortableValue(file, sortKey) {
switch (sortKey) {
case 'hash':
return (file.sha256 || '').toLowerCase();
case 'uploader':
return blobPubkeyToDisplay(file.uploader_pubkey || '').toLowerCase();
case 'type':
return (file.type || '').toLowerCase();
case 'size':
return Number(file.size) || 0;
case 'uploaded':
return Number(file.uploaded_at) || 0;
default:
return '';
}
}
function blobGetSortedFiles(files) {
if (!Array.isArray(files) || files.length <= 1) {
return Array.isArray(files) ? files : [];
}
const direction = blobBrowserState.sortDirection === 'asc' ? 1 : -1;
const sortKey = blobBrowserState.sortKey;
return [...files].sort((a, b) => {
const left = blobGetSortableValue(a, sortKey);
const right = blobGetSortableValue(b, sortKey);
if (typeof left === 'number' && typeof right === 'number') {
return (left - right) * direction;
}
return String(left).localeCompare(String(right)) * direction;
});
}
function blobUpdateSortIndicators() {
const headers = document.querySelectorAll('#blob-list-table th.blob-sortable-th');
headers.forEach((header) => {
const key = header.dataset.sortKey;
const indicator = header.querySelector('.blob-sort-indicator');
if (!indicator) return;
if (key === blobBrowserState.sortKey) {
indicator.textContent = blobBrowserState.sortDirection === 'asc' ? '▲' : '▼';
} else {
indicator.textContent = '';
}
});
}
function blobRenderSummary() {
const summary = document.getElementById('blob-browser-summary');
const prevBtn = document.getElementById('blob-prev-page-btn');
const nextBtn = document.getElementById('blob-next-page-btn');
if (summary) {
const from = blobBrowserState.total === 0 ? 0 : blobBrowserState.offset + 1;
const to = Math.min(blobBrowserState.offset + blobBrowserState.files.length, blobBrowserState.total);
const filterText = blobBrowserState.pubkeyFilter ? ` | filter: ${blobBrowserState.pubkeyFilter}` : '';
summary.textContent = `Showing ${from}-${to} of ${blobBrowserState.total}${filterText}`;
}
if (prevBtn) {
prevBtn.disabled = blobBrowserState.offset <= 0 || blobBrowserState.loading;
}
if (nextBtn) {
nextBtn.disabled = blobBrowserState.loading || (blobBrowserState.offset + blobBrowserState.limit >= blobBrowserState.total);
}
}
function blobRenderAll() {
const sortedFiles = blobGetSortedFiles(blobBrowserState.files);
blobRenderGrid(sortedFiles);
blobRenderList(sortedFiles);
blobRenderSummary();
blobSetViewMode(blobBrowserState.viewMode);
blobUpdateSortIndicators();
}
async function blobDeleteFile(sha256) {
if (!sha256 || !/^[0-9a-fA-F]{64}$/.test(sha256)) {
alert('Invalid blob hash');
return;
}
if (blobBrowserState.deletingHashes.has(sha256)) {
return;
}
const confirmed = window.confirm(`Delete blob ${blobShortHash(sha256)}? This removes both file and database reference.`);
if (!confirmed) {
return;
}
blobBrowserState.deletingHashes.add(sha256);
blobRenderAll();
try {
const responseData = await sendAdminCommandHTTP(['blob_delete', sha256]);
if (!responseData || responseData.status !== 'success') {
throw new Error((responseData && responseData.error) ? responseData.error : 'Delete failed');
}
log(`Deleted blob ${blobShortHash(sha256)}`, 'INFO');
const shouldResetOffset = blobBrowserState.files.length === 1 && blobBrowserState.offset > 0;
await fetchBlobBrowserData(shouldResetOffset);
} catch (error) {
console.error('Blob delete failed:', error);
alert(`Delete failed: ${error.message}`);
} finally {
blobBrowserState.deletingHashes.delete(sha256);
blobRenderAll();
}
}
async function fetchBlobBrowserData(resetOffset = false) {
if (resetOffset) {
blobBrowserState.offset = 0;
}
blobBrowserState.loading = true;
blobRenderSummary();
try {
const response = await fetch(blobBuildApiUrl(), {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const payload = await response.json();
const data = payload && payload.data ? payload.data : {};
blobBrowserState.files = Array.isArray(data.files) ? data.files : [];
blobBrowserState.total = Number.isFinite(data.total) ? data.total : 0;
if (typeof data.pubkey_filter === 'string' && data.pubkey_filter.length > 0) {
blobBrowserState.pubkeyFilter = data.pubkey_filter;
}
blobRenderAll();
} catch (error) {
console.error('Failed to fetch blob browser data:', error);
const summary = document.getElementById('blob-browser-summary');
if (summary) {
summary.textContent = `Failed to load blobs: ${error.message}`;
}
blobBrowserState.files = [];
blobBrowserState.total = 0;
blobRenderAll();
} finally {
blobBrowserState.loading = false;
blobRenderSummary();
}
}
function initializeBlobBrowserControls() {
const applyBtn = document.getElementById('blob-filter-apply-btn');
const clearBtn = document.getElementById('blob-filter-clear-btn');
const toggleBtn = document.getElementById('blob-view-toggle-btn');
const refreshBtn = document.getElementById('blob-refresh-btn');
const prevBtn = document.getElementById('blob-prev-page-btn');
const nextBtn = document.getElementById('blob-next-page-btn');
const filterInput = document.getElementById('blob-filter-pubkey');
if (applyBtn && !applyBtn.dataset.bound) {
applyBtn.addEventListener('click', () => {
try {
blobBrowserState.pubkeyFilter = blobNormalizePubkeyFilter(filterInput ? filterInput.value : '');
if (filterInput && blobBrowserState.pubkeyFilter) {
filterInput.value = blobBrowserState.pubkeyFilter;
}
fetchBlobBrowserData(true);
} catch (error) {
log(error.message, 'ERROR');
}
});
applyBtn.dataset.bound = 'true';
}
if (clearBtn && !clearBtn.dataset.bound) {
clearBtn.addEventListener('click', () => {
blobBrowserState.pubkeyFilter = '';
if (filterInput) {
filterInput.value = '';
}
fetchBlobBrowserData(true);
});
clearBtn.dataset.bound = 'true';
}
if (toggleBtn && !toggleBtn.dataset.bound) {
toggleBtn.addEventListener('click', () => {
blobSetViewMode(blobBrowserState.viewMode === 'grid' ? 'list' : 'grid');
});
toggleBtn.dataset.bound = 'true';
}
if (refreshBtn && !refreshBtn.dataset.bound) {
refreshBtn.addEventListener('click', () => fetchBlobBrowserData(false));
refreshBtn.dataset.bound = 'true';
}
if (prevBtn && !prevBtn.dataset.bound) {
prevBtn.addEventListener('click', () => {
if (blobBrowserState.offset >= blobBrowserState.limit) {
blobBrowserState.offset -= blobBrowserState.limit;
fetchBlobBrowserData(false);
}
});
prevBtn.dataset.bound = 'true';
}
if (nextBtn && !nextBtn.dataset.bound) {
nextBtn.addEventListener('click', () => {
if (blobBrowserState.offset + blobBrowserState.limit < blobBrowserState.total) {
blobBrowserState.offset += blobBrowserState.limit;
fetchBlobBrowserData(false);
}
});
nextBtn.dataset.bound = 'true';
}
if (filterInput && !filterInput.dataset.bound) {
filterInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
if (applyBtn) {
applyBtn.click();
}
}
});
filterInput.dataset.bound = 'true';
}
const listBody = document.getElementById('blob-list-table-body');
if (listBody && !listBody.dataset.deleteBound) {
listBody.addEventListener('click', (event) => {
const deleteBtn = event.target.closest('.blob-delete-btn');
if (!deleteBtn) {
return;
}
const sha256 = deleteBtn.dataset.sha256 || '';
blobDeleteFile(sha256);
});
listBody.dataset.deleteBound = 'true';
}
const sortableHeaders = document.querySelectorAll('#blob-list-table th.blob-sortable-th');
sortableHeaders.forEach((header) => {
if (header.dataset.bound) {
return;
}
header.addEventListener('click', () => {
const clickedSortKey = header.dataset.sortKey;
if (!clickedSortKey) {
return;
}
if (blobBrowserState.sortKey === clickedSortKey) {
blobBrowserState.sortDirection = blobBrowserState.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
blobBrowserState.sortKey = clickedSortKey;
blobBrowserState.sortDirection = clickedSortKey === 'uploaded' ? 'desc' : 'asc';
}
blobRenderAll();
});
header.dataset.bound = 'true';
});
blobSetViewMode(blobBrowserState.viewMode);
blobUpdateSortIndicators();
}
// Update statistics cell with flash animation if value changed
function updateStatsCell(cellId, newValue) {
const cell = document.getElementById(cellId);
@@ -3698,6 +4163,7 @@ function switchPage(pageName) {
// Hide all sections
const sections = [
'databaseStatisticsSection',
'blobBrowserSection',
'subscriptionDetailsSection',
'div_config',
'authRulesSection',
@@ -3716,6 +4182,7 @@ function switchPage(pageName) {
// Show selected section
const pageMap = {
'statistics': 'databaseStatisticsSection',
'blob-browser': 'blobBrowserSection',
'subscriptions': 'subscriptionDetailsSection',
'configuration': 'div_config',
'authorization': 'authRulesSection',
@@ -3744,6 +4211,12 @@ function switchPage(pageName) {
});
}
// Blob browser page setup
if (pageName === 'blob-browser') {
initializeBlobBrowserControls();
fetchBlobBrowserData(false);
}
// Close side navigation
closeSideNav();
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+106 -4
View File
@@ -11,8 +11,40 @@ DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl"
# Parse command line arguments
DEBUG_BUILD=false
if [[ "$1" == "--debug" ]]; then
DEBUG_BUILD=true
TARGET_ARCH=""
while [[ $# -gt 0 ]]; do
case "$1" in
--debug)
DEBUG_BUILD=true
shift
;;
--arch)
if [[ -z "$2" ]]; then
echo "ERROR: --arch requires a value"
echo "Usage: $0 [--debug] [--arch <arm64|armv7|x86_64>]"
exit 1
fi
case "$2" in
arm64|armv7|x86_64)
TARGET_ARCH="$2"
;;
*)
echo "ERROR: Unsupported architecture '$2'"
echo "Supported values: arm64, armv7, x86_64"
exit 1
esac
shift 2
;;
*)
echo "ERROR: Unknown argument '$1'"
echo "Usage: $0 [--debug] [--arch <arm64|armv7|x86_64>]"
exit 1
;;
esac
done
if [ "$DEBUG_BUILD" = true ]; then
echo "=========================================="
echo "Ginxsom MUSL Static Binary Builder (DEBUG MODE)"
echo "=========================================="
@@ -24,6 +56,9 @@ fi
echo "Project directory: $SCRIPT_DIR"
echo "Build directory: $BUILD_DIR"
echo "Debug build: $DEBUG_BUILD"
if [[ -n "$TARGET_ARCH" ]]; then
echo "Requested target architecture: $TARGET_ARCH"
fi
echo ""
# Create build directory
@@ -58,17 +93,43 @@ DOCKER_CMD="docker"
echo "✓ Docker is available and running"
echo ""
# Detect architecture
# Detect host architecture
ARCH=$(uname -m)
case "$ARCH" in
x86_64)
HOST_ARCH="x86_64"
;;
aarch64|arm64)
HOST_ARCH="arm64"
;;
armv7l|armv7)
HOST_ARCH="armv7"
;;
*)
HOST_ARCH="unknown"
;;
esac
# Resolve target architecture
if [[ -n "$TARGET_ARCH" ]]; then
ARCH="$TARGET_ARCH"
fi
case "$ARCH" in
x86_64)
PLATFORM="linux/amd64"
OUTPUT_NAME="ginxsom-fcgi_static_x86_64"
;;
aarch64|arm64)
ARCH="arm64"
PLATFORM="linux/arm64"
OUTPUT_NAME="ginxsom-fcgi_static_arm64"
;;
armv7l|armv7)
ARCH="armv7"
PLATFORM="linux/arm/v7"
OUTPUT_NAME="ginxsom-fcgi_static_armv7"
;;
*)
echo "WARNING: Unknown architecture: $ARCH"
echo "Defaulting to linux/amd64"
@@ -77,10 +138,51 @@ case "$ARCH" in
;;
esac
# When cross-building, ensure buildx exists and warn about QEMU/binfmt setup
if [[ "$HOST_ARCH" != "unknown" && "$ARCH" != "$HOST_ARCH" ]]; then
echo "NOTE: Cross-building from host '$HOST_ARCH' to target '$ARCH'."
echo " Docker QEMU/binfmt support is required for this to work."
if ! docker buildx version >/dev/null 2>&1; then
echo "ERROR: docker buildx is required for cross-architecture builds but is not available."
echo "Install/enable buildx and binfmt support, then retry."
exit 1
fi
echo "✓ docker buildx is available for cross-architecture build"
echo ""
fi
# Append _debug suffix to output name for debug builds so production binary is never overwritten
if [ "$DEBUG_BUILD" = true ]; then
OUTPUT_NAME="${OUTPUT_NAME}_debug"
fi
echo "Building for platform: $PLATFORM"
echo "Output binary: $OUTPUT_NAME"
echo ""
# Check if Alpine base image is cached
echo "Checking for cached Alpine Docker image..."
if ! docker images alpine:3.19 --format "{{.Repository}}:{{.Tag}}" | grep -q "alpine:3.19"; then
echo "⚠ Alpine 3.19 image not found in cache"
echo "Attempting to pull Alpine 3.19 image..."
if ! docker pull alpine:3.19; then
echo ""
echo "ERROR: Failed to pull Alpine 3.19 image"
echo "This is required for the static build."
echo ""
echo "Possible solutions:"
echo " 1. Check your internet connection"
echo " 2. Try again later (Docker Hub may be temporarily unavailable)"
echo " 3. If you have IPv6 issues, disable IPv6 for Docker"
echo ""
exit 1
fi
echo "✓ Alpine 3.19 image pulled successfully"
else
echo "✓ Alpine 3.19 image found in cache"
fi
echo ""
# Build the Docker image
echo "=========================================="
echo "Step 1: Building Alpine Docker image"
@@ -220,4 +322,4 @@ fi
echo ""
echo "Deployment:"
echo " scp $BUILD_DIR/$OUTPUT_NAME user@server:/path/to/ginxsom/"
echo ""
echo ""
+63 -40
View File
@@ -199,56 +199,43 @@ else
fi
echo ""
# Step 8: Start ginxsom FastCGI process
# Step 8: Start ginxsom through systemd (keeps process supervised and auto-restarted)
print_status "Step 8: Starting ginxsom service..."
ssh $REMOTE_USER@$REMOTE_HOST << EOF
ssh $REMOTE_USER@$REMOTE_HOST << 'EOF'
set -e
echo "Starting ginxsom FastCGI with configuration:"
echo " Binary: $REMOTE_BINARY_PATH"
echo " Database: $REMOTE_DB_PATH"
echo " Storage: $REMOTE_BLOB_DIR"
echo " Socket: $REMOTE_SOCKET"
echo ""
sudo spawn-fcgi \
-M 666 \
-u www-data \
-g www-data \
-s $REMOTE_SOCKET \
-U www-data \
-G www-data \
-d $REMOTE_BINARY_DIR \
-- $REMOTE_BINARY_PATH \
--admin-pubkey $ADMIN_PUBKEY \
--server-privkey $SERVER_PRIVKEY \
--db-path $REMOTE_DB_PATH \
--storage-dir $REMOTE_BLOB_DIR
# Give it a moment to start
echo "Restarting systemd service..."
sudo systemctl restart ginxsom.service
sleep 2
# Verify process is running
if [ -S $REMOTE_SOCKET ]; then
echo "FastCGI socket created successfully"
ls -la $REMOTE_SOCKET
if sudo systemctl is-active --quiet ginxsom.service; then
echo "Service is active"
else
echo "ERROR: Socket not created"
echo "ERROR: ginxsom.service is not active"
sudo systemctl status --no-pager ginxsom.service || true
sudo journalctl -u ginxsom.service -n 80 --no-pager || true
exit 1
fi
# Check if process is running
if pgrep -f ginxsom-fcgi > /dev/null; then
echo "Process is running (PID: \$(pgrep -f ginxsom-fcgi))"
# Verify socket is listening
if [ -S /tmp/ginxsom-fcgi.sock ]; then
echo "FastCGI socket created successfully"
ls -la /tmp/ginxsom-fcgi.sock
else
echo "WARNING: Process not found by pgrep (may be normal for FastCGI)"
echo "ERROR: Socket not created"
sudo systemctl status --no-pager ginxsom.service || true
sudo journalctl -u ginxsom.service -n 80 --no-pager || true
exit 1
fi
# Show process list for diagnostics
pgrep -af ginxsom-fcgi || true
EOF
if [ $? -eq 0 ]; then
print_success "FastCGI process started"
print_success "FastCGI service started"
else
print_error "Failed to start FastCGI process"
print_error "Failed to start FastCGI service"
exit 1
fi
echo ""
@@ -282,13 +269,42 @@ echo ""
# Wait a moment for service to fully start
sleep 2
VERIFY_FAILED=false
# Test health endpoint
echo "Testing health endpoint..."
if curl -k -s --max-time 10 "https://blossom.laantungir.net/health" | grep -q "OK"; then
print_success "✓ Health check passed"
else
print_warning "✗ Health check failed - checking response..."
curl -k -v --max-time 10 "https://blossom.laantungir.net/health" 2>&1 | head -10
print_error "✗ Health check failed"
curl -k -v --max-time 10 "https://blossom.laantungir.net/health" 2>&1 | head -10 || true
VERIFY_FAILED=true
fi
# Test API health endpoint
echo ""
echo "Testing API health endpoint..."
API_HEALTH_STATUS=$(curl -k -s -o /dev/null -w "%{http_code}" --max-time 10 "https://blossom.laantungir.net/api/health" || echo "000")
if [[ "$API_HEALTH_STATUS" == "200" ]]; then
print_success "✓ API health endpoint responding (200)"
else
print_error "✗ API health endpoint failed (HTTP $API_HEALTH_STATUS)"
curl -k -v --max-time 10 "https://blossom.laantungir.net/api/health" 2>&1 | head -20 || true
VERIFY_FAILED=true
fi
# Test files API endpoint
echo ""
echo "Testing files API endpoint..."
API_FILES_STATUS=$(curl -k -s -o /dev/null -w "%{http_code}" --max-time 10 "https://blossom.laantungir.net/api/files?limit=1" || echo "000")
if [[ "$API_FILES_STATUS" == "200" ]]; then
print_success "✓ Files API endpoint responding (200)"
elif [[ "$API_FILES_STATUS" == "401" || "$API_FILES_STATUS" == "403" ]]; then
print_success "✓ Files API endpoint reachable (HTTP $API_FILES_STATUS indicates auth is enforced)"
else
print_error "✗ Files API endpoint failed (HTTP $API_FILES_STATUS)"
curl -k -v --max-time 10 "https://blossom.laantungir.net/api/files?limit=1" 2>&1 | head -20 || true
VERIFY_FAILED=true
fi
# Test root endpoint
@@ -300,6 +316,11 @@ else
print_warning "✗ Root endpoint not responding as expected"
fi
if [ "$VERIFY_FAILED" = true ]; then
print_error "Deployment verification failed - one or more critical endpoints are unhealthy"
exit 1
fi
echo ""
print_status "=========================================="
print_success "Deployment completed!"
@@ -314,6 +335,8 @@ echo " Socket: $REMOTE_SOCKET"
echo ""
print_status "Test Commands:"
echo " Health: curl -k https://blossom.laantungir.net/health"
echo " API Health: curl -k https://blossom.laantungir.net/api/health"
echo " Files API: curl -k 'https://blossom.laantungir.net/api/files?limit=5'"
echo " Info: curl -k https://blossom.laantungir.net/"
echo " Upload: ./tests/file_put_bud02.sh"
echo ""
+354 -230
View File
@@ -8,17 +8,65 @@ YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_status() { echo -e "${BLUE}[INFO]${NC} $1"; }
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
print_status() { echo -e "${BLUE}[INFO]${NC} $1" >&2; }
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" >&2; }
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" >&2; }
print_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
# Global variables
COMMIT_MESSAGE=""
RELEASE_MODE=false
VERSION_INCREMENT_TYPE="patch" # "patch", "minor", or "major"
VERSION_INCREMENT_EXPLICIT=false
# TODO: Update this URL to match your actual Gitea repository
GITEA_REPO_URL="https://git.example.com/api/v1/repos/username/ginxsom"
GITEA_REPO_URL="https://git.laantungir.net/api/v1/repos/laantungir/ginxsom"
show_usage() {
echo "Ginxsom Increment and Push Script"
echo ""
echo "USAGE:"
echo " $0 [OPTIONS] \"commit message\""
echo ""
echo "COMMANDS:"
echo " $0 \"commit message\" Default: increment patch, commit & push"
echo " $0 -p \"commit message\" Increment patch version"
echo " $0 -m \"commit message\" Increment minor version"
echo " $0 -M \"commit message\" Increment major version"
echo " $0 -r \"commit message\" Create release with assets (no version increment)"
echo " $0 -r -p \"commit message\" Create release with patch version increment"
echo " $0 -r -m \"commit message\" Create release with minor version increment"
echo " $0 -h Show this help message"
echo ""
echo "OPTIONS:"
echo " -p, --patch Increment patch version (default)"
echo " -m, --minor Increment minor version"
echo " -M, --major Increment major version"
echo " -r, --release Create release with assets"
echo " -h, --help Show this help message"
echo ""
echo "EXAMPLES:"
echo " $0 \"Fixed authentication bug\""
echo " $0 -m \"Added new features\""
echo " $0 -M \"Breaking API changes\""
echo " $0 -r \"Release current version\""
echo " $0 -r -p \"Release with patch increment\""
echo " $0 -r -m \"Release with minor increment\""
echo ""
echo "VERSION INCREMENT MODES:"
echo " -p, --patch (default): Increment patch version (v1.2.3 → v1.2.4)"
echo " -m, --minor: Increment minor version, zero patch (v1.2.3 → v1.3.0)"
echo " -M, --major: Increment major version, zero minor+patch (v1.2.3 → v2.0.0)"
echo ""
echo "RELEASE MODE (-r flag):"
echo " - Build static binaries for x86_64 and ARM64 using build_static.sh"
echo " - Create source tarball"
echo " - Git add, commit, push, and create Gitea release with assets"
echo " - Can be combined with version increment flags"
echo ""
echo "REQUIREMENTS FOR RELEASE MODE:"
echo " - Gitea token in ~/.gitea_token for release uploads"
echo " - Docker installed for static binary builds"
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
@@ -27,6 +75,21 @@ while [[ $# -gt 0 ]]; do
RELEASE_MODE=true
shift
;;
-p|--patch)
VERSION_INCREMENT_TYPE="patch"
VERSION_INCREMENT_EXPLICIT=true
shift
;;
-m|--minor)
VERSION_INCREMENT_TYPE="minor"
VERSION_INCREMENT_EXPLICIT=true
shift
;;
-M|--major)
VERSION_INCREMENT_TYPE="major"
VERSION_INCREMENT_EXPLICIT=true
shift
;;
-h|--help)
show_usage
exit 0
@@ -41,32 +104,6 @@ while [[ $# -gt 0 ]]; do
esac
done
show_usage() {
echo "Ginxsom Build and Push Script"
echo ""
echo "Usage:"
echo " $0 \"commit message\" - Default: compile, increment patch, commit & push"
echo " $0 -r \"commit message\" - Release: compile, increment minor, create release"
echo ""
echo "Examples:"
echo " $0 \"Fixed authentication bug\""
echo " $0 --release \"Major release with admin API\""
echo ""
echo "Default Mode (patch increment):"
echo " - Compile Ginxsom FastCGI server"
echo " - Increment patch version (v1.2.3 → v1.2.4)"
echo " - Git add, commit with message, and push"
echo ""
echo "Release Mode (-r flag):"
echo " - Compile Ginxsom FastCGI server"
echo " - Increment minor version, zero patch (v1.2.3 → v1.3.0)"
echo " - Git add, commit, push, and create Gitea release"
echo ""
echo "Requirements for Release Mode:"
echo " - Gitea token in ~/.gitea_token for release uploads"
echo " - Update GITEA_REPO_URL in script for your repository"
}
# Validate inputs
if [[ -z "$COMMIT_MESSAGE" ]]; then
print_error "Commit message is required"
@@ -85,20 +122,20 @@ check_git_repo() {
# Function to get current version and increment appropriately
increment_version() {
local increment_type="$1" # "patch" or "minor"
local increment_type="$1" # "patch", "minor", or "major"
print_status "Getting current version..."
# Get the highest version tag (not chronologically latest)
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "")
if [[ -z "$LATEST_TAG" ]]; then
LATEST_TAG="v0.0.0"
print_warning "No version tags found, starting from $LATEST_TAG"
fi
# Extract version components (remove 'v' prefix)
VERSION=${LATEST_TAG#v}
# Parse major.minor.patch using regex
if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
MAJOR=${BASH_REMATCH[1]}
@@ -109,121 +146,74 @@ increment_version() {
print_error "Expected format: v0.1.0"
exit 1
fi
# Increment version based on type
if [[ "$increment_type" == "minor" ]]; then
# Minor release: increment minor, zero patch
if [[ "$increment_type" == "major" ]]; then
NEW_MAJOR=$((MAJOR + 1))
NEW_MINOR=0
NEW_PATCH=0
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
print_status "Major version increment: incrementing major version"
elif [[ "$increment_type" == "minor" ]]; then
NEW_MAJOR=$MAJOR
NEW_MINOR=$((MINOR + 1))
NEW_PATCH=0
NEW_VERSION="v${MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
print_status "Release mode: incrementing minor version"
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
print_status "Minor version increment: incrementing minor version"
else
# Default: increment patch
NEW_MAJOR=$MAJOR
NEW_MINOR=$MINOR
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="v${MAJOR}.${MINOR}.${NEW_PATCH}"
print_status "Default mode: incrementing patch version"
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
print_status "Patch version increment: incrementing patch version"
fi
print_status "Current version: $LATEST_TAG"
print_status "New version: $NEW_VERSION"
# Update version in src/ginxsom.h
update_version_in_header "$NEW_VERSION" "$NEW_MAJOR" "$NEW_MINOR" "$NEW_PATCH"
# Export for use in other functions
export NEW_VERSION
}
# Function to update version in header file
# Function to update version macros in src/ginxsom.h
update_version_in_header() {
local version="$1"
print_status "Updating version in src/ginxsom.h to $version..."
local new_version="$1"
local major="$2"
local minor="$3"
local patch="$4"
# Extract version components (remove 'v' prefix)
local version_no_v=${version#v}
print_status "Updating version in src/ginxsom.h..."
# Parse major.minor.patch using regex
if [[ $version_no_v =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
local major=${BASH_REMATCH[1]}
local minor=${BASH_REMATCH[2]}
local patch=${BASH_REMATCH[3]}
# Update the header file
sed -i "s/#define VERSION_MAJOR [0-9]\+/#define VERSION_MAJOR $major/" src/ginxsom.h
sed -i "s/#define VERSION_MINOR [0-9]\+/#define VERSION_MINOR $minor/" src/ginxsom.h
sed -i "s/#define VERSION_PATCH [0-9]\+/#define VERSION_PATCH $patch/" src/ginxsom.h
sed -i "s/#define VERSION \"v[0-9]\+\.[0-9]\+\.[0-9]\+\"/#define VERSION \"$version\"/" src/ginxsom.h
print_success "Updated version in header file"
else
print_error "Invalid version format: $version"
if [[ ! -f "src/ginxsom.h" ]]; then
print_error "src/ginxsom.h not found"
exit 1
fi
}
# Function to compile the Ginxsom project
compile_project() {
print_status "Compiling Ginxsom FastCGI server..."
sed -i "s/#define VERSION_MAJOR [0-9]\+/#define VERSION_MAJOR $major/" src/ginxsom.h
sed -i "s/#define VERSION_MINOR [0-9]\+/#define VERSION_MINOR $minor/" src/ginxsom.h
sed -i "s/#define VERSION_PATCH [0-9]\+/#define VERSION_PATCH $patch/" src/ginxsom.h
sed -i "s/#define VERSION \"v[0-9]\+\.[0-9]\+\.[0-9]\+\"/#define VERSION \"$new_version\"/" src/ginxsom.h
# Clean previous build
if make clean > /dev/null 2>&1; then
print_success "Cleaned previous build"
else
print_warning "Clean failed or no Makefile found"
fi
# Compile the project
if make > /dev/null 2>&1; then
print_success "Ginxsom compiled successfully"
# Verify the binary was created
if [[ -f "build/ginxsom-fcgi" ]]; then
print_success "Binary created: build/ginxsom-fcgi"
else
print_error "Binary not found after compilation"
exit 1
fi
else
print_error "Compilation failed"
exit 1
fi
}
# Function to build release binary
build_release_binary() {
print_status "Building release binary..."
# Build the FastCGI server
print_status "Building Ginxsom FastCGI server..."
make clean > /dev/null 2>&1
if make > /dev/null 2>&1; then
if [[ -f "build/ginxsom-fcgi" ]]; then
cp build/ginxsom-fcgi ginxsom-fcgi-linux-x86_64
print_success "Release binary created: ginxsom-fcgi-linux-x86_64"
else
print_error "Binary not found after compilation"
exit 1
fi
else
print_error "Build failed"
exit 1
fi
print_success "Updated version in src/ginxsom.h to $new_version"
}
# Function to commit and push changes
git_commit_and_push() {
print_status "Preparing git commit..."
# Stage all changes
if git add . > /dev/null 2>&1; then
print_success "Staged all changes"
else
print_error "Failed to stage changes"
exit 1
fi
# Check if there are changes to commit
if git diff --staged --quiet; then
print_warning "No changes to commit"
else
# Commit changes
if git commit -m "$NEW_VERSION - $COMMIT_MESSAGE" > /dev/null 2>&1; then
print_success "Committed changes"
else
@@ -231,15 +221,13 @@ git_commit_and_push() {
exit 1
fi
fi
# Create new git tag
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Created tag: $NEW_VERSION"
else
print_warning "Tag $NEW_VERSION already exists"
fi
# Push changes and tags
print_status "Pushing to remote repository..."
if git push > /dev/null 2>&1; then
print_success "Pushed changes"
@@ -247,8 +235,7 @@ git_commit_and_push() {
print_error "Failed to push changes"
exit 1
fi
# Push only the new tag to avoid conflicts with existing tags
if git push origin "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Pushed tag: $NEW_VERSION"
else
@@ -265,20 +252,17 @@ git_commit_and_push() {
# Function to commit and push changes without creating a tag (tag already created)
git_commit_and_push_no_tag() {
print_status "Preparing git commit..."
# Stage all changes
if git add . > /dev/null 2>&1; then
print_success "Staged all changes"
else
print_error "Failed to stage changes"
exit 1
fi
# Check if there are changes to commit
if git diff --staged --quiet; then
print_warning "No changes to commit"
else
# Commit changes
if git commit -m "$NEW_VERSION - $COMMIT_MESSAGE" > /dev/null 2>&1; then
print_success "Committed changes"
else
@@ -286,8 +270,7 @@ git_commit_and_push_no_tag() {
exit 1
fi
fi
# Push changes and tags
print_status "Pushing to remote repository..."
if git push > /dev/null 2>&1; then
print_success "Pushed changes"
@@ -295,8 +278,7 @@ git_commit_and_push_no_tag() {
print_error "Failed to push changes"
exit 1
fi
# Push only the new tag to avoid conflicts with existing tags
if git push origin "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Pushed tag: $NEW_VERSION"
else
@@ -310,42 +292,198 @@ git_commit_and_push_no_tag() {
fi
}
# Function to build release binaries
build_release_binary() {
print_status "Building release binaries..."
if [[ ! -f "build_static.sh" ]]; then
print_error "build_static.sh not found"
return 1
fi
# Build x86_64 first (required for success)
if ./build_static.sh > /dev/null 2>&1; then
print_success "Built x86_64 static binary successfully"
else
print_error "Failed to build x86_64 static binary"
return 1
fi
# Build arm64 second (warning-only on failure)
if ./build_static.sh --arch arm64 > /dev/null 2>&1; then
print_success "Built ARM64 static binary successfully"
else
print_warning "Failed to build ARM64 static binary (continuing with release)"
fi
return 0
}
# Function to create source tarball
create_source_tarball() {
print_status "Creating source tarball..."
local tarball_name="ginxsom-${NEW_VERSION#v}.tar.gz"
# Remove any existing tarball first to avoid self-inclusion
rm -f "$tarball_name"
if tar -czf "$tarball_name" \
--exclude='./build' \
--exclude='./.git' \
--exclude='./.gitmodules' \
--exclude='./db/*.db' \
--exclude='./db/*.db-*' \
--exclude='./*.log' \
--exclude="./$tarball_name" \
--exclude='./Trash' \
--exclude='./*.tar.gz' \
. 2>/dev/null; then
print_success "Created source tarball: $tarball_name"
echo "$tarball_name"
return 0
else
print_error "Failed to create source tarball"
return 1
fi
}
# Function to upload release assets to Gitea
upload_release_assets() {
local release_id="$1"
local binary_path_x86="$2"
local tarball_path="$3"
local binary_path_arm64="$4"
print_status "Uploading release assets..."
if [[ ! -f "$HOME/.gitea_token" ]]; then
print_warning "No ~/.gitea_token found. Skipping asset uploads."
return 0
fi
local token=$(cat "$HOME/.gitea_token" | tr -d '\n\r')
local assets_url="$GITEA_REPO_URL/releases/$release_id/assets"
print_status "Assets URL: $assets_url"
# Upload x86_64 binary
if [[ -f "$binary_path_x86" ]]; then
print_status "Uploading binary: $(basename "$binary_path_x86")"
local max_attempts=3
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
print_status "Upload attempt $attempt/$max_attempts"
local binary_response=$(curl -fS -X POST "$assets_url" \
-H "Authorization: token $token" \
-F "attachment=@$binary_path_x86;filename=$(basename "$binary_path_x86")" \
-F "name=$(basename "$binary_path_x86")")
if echo "$binary_response" | grep -q '"id"'; then
print_success "Uploaded x86_64 binary successfully"
break
else
print_warning "Upload attempt $attempt failed"
if [[ $attempt -lt $max_attempts ]]; then
print_status "Retrying in 2 seconds..."
sleep 2
else
print_error "Failed to upload x86_64 binary after $max_attempts attempts"
print_error "Response: $binary_response"
fi
fi
((attempt++))
done
fi
# Upload ARM64 binary
if [[ -f "$binary_path_arm64" ]]; then
print_status "Uploading binary: $(basename "$binary_path_arm64")"
local max_attempts=3
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
print_status "Upload attempt $attempt/$max_attempts"
local binary_response=$(curl -fS -X POST "$assets_url" \
-H "Authorization: token $token" \
-F "attachment=@$binary_path_arm64;filename=$(basename "$binary_path_arm64")" \
-F "name=$(basename "$binary_path_arm64")")
if echo "$binary_response" | grep -q '"id"'; then
print_success "Uploaded ARM64 binary successfully"
break
else
print_warning "Upload attempt $attempt failed"
if [[ $attempt -lt $max_attempts ]]; then
print_status "Retrying in 2 seconds..."
sleep 2
else
print_error "Failed to upload ARM64 binary after $max_attempts attempts"
print_error "Response: $binary_response"
fi
fi
((attempt++))
done
fi
# Upload source tarball
if [[ -f "$tarball_path" ]]; then
print_status "Uploading source tarball: $(basename "$tarball_path")"
local tarball_response=$(curl -s -X POST "$GITEA_REPO_URL/releases/$release_id/assets" \
-H "Authorization: token $token" \
-F "attachment=@$tarball_path;filename=$(basename "$tarball_path")")
if echo "$tarball_response" | grep -q '"id"'; then
print_success "Uploaded source tarball successfully"
else
print_warning "Failed to upload source tarball: $tarball_response"
fi
fi
}
# Function to create Gitea release
create_gitea_release() {
print_status "Creating Gitea release..."
# Check for Gitea token
if [[ ! -f "$HOME/.gitea_token" ]]; then
print_warning "No ~/.gitea_token found. Skipping release creation."
print_warning "Create ~/.gitea_token with your Gitea access token to enable releases."
return 0
fi
local token=$(cat "$HOME/.gitea_token" | tr -d '\n\r')
# Create release
print_status "Creating release $NEW_VERSION..."
local response=$(curl -s -X POST "$GITEA_REPO_URL/releases" \
-H "Authorization: token $token" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}")
-H "Authorization: token $token" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}")
if echo "$response" | grep -q '"id"'; then
print_success "Created release $NEW_VERSION"
upload_release_binary "$token"
local release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
echo $release_id
elif echo "$response" | grep -q "already exists"; then
print_warning "Release $NEW_VERSION already exists"
upload_release_binary "$token"
local check_response=$(curl -s -H "Authorization: token $token" "$GITEA_REPO_URL/releases/tags/$NEW_VERSION")
if echo "$check_response" | grep -q '"id"'; then
local release_id=$(echo "$check_response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
print_status "Using existing release ID: $release_id"
echo $release_id
else
print_error "Could not find existing release ID"
return 1
fi
else
print_error "Failed to create release $NEW_VERSION"
print_error "Response: $response"
# Try to check if the release exists anyway
print_status "Checking if release exists..."
local check_response=$(curl -s -H "Authorization: token $token" "$GITEA_REPO_URL/releases/tags/$NEW_VERSION")
if echo "$check_response" | grep -q '"id"'; then
print_warning "Release exists but creation response was unexpected"
upload_release_binary "$token"
local release_id=$(echo "$check_response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
echo $release_id
else
print_error "Release does not exist and creation failed"
return 1
@@ -353,62 +491,26 @@ create_gitea_release() {
fi
}
# Function to upload release binary
upload_release_binary() {
local token="$1"
# Get release ID with more robust parsing
print_status "Getting release ID for $NEW_VERSION..."
local response=$(curl -s -H "Authorization: token $token" "$GITEA_REPO_URL/releases/tags/$NEW_VERSION")
local release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2)
if [[ -z "$release_id" ]]; then
print_error "Could not get release ID for $NEW_VERSION"
print_error "API Response: $response"
# Try to list all releases to debug
print_status "Available releases:"
curl -s -H "Authorization: token $token" "$GITEA_REPO_URL/releases" | grep -o '"tag_name":"[^"]*"' | head -5
return 1
fi
print_success "Found release ID: $release_id"
# Upload FastCGI binary
if [[ -f "ginxsom-fcgi-linux-x86_64" ]]; then
print_status "Uploading Ginxsom FastCGI binary..."
if curl -s -X POST "$GITEA_REPO_URL/releases/$release_id/assets" \
-H "Authorization: token $token" \
-F "attachment=@ginxsom-fcgi-linux-x86_64;filename=ginxsom-fcgi-${NEW_VERSION}-linux-x86_64" > /dev/null; then
print_success "Uploaded FastCGI binary"
else
print_warning "Failed to upload FastCGI binary"
fi
fi
}
# Function to clean up release binary
cleanup_release_binary() {
if [[ -f "ginxsom-fcgi-linux-x86_64" ]]; then
rm -f ginxsom-fcgi-linux-x86_64
print_status "Cleaned up release binary"
fi
}
# Main execution
main() {
print_status "Ginxsom Build and Push Script"
print_status "Ginxsom Increment and Push Script"
# Check prerequisites
check_git_repo
if [[ "$RELEASE_MODE" == true ]]; then
print_status "=== RELEASE MODE ==="
# Increment minor version for releases
increment_version "minor"
# Create new git tag BEFORE compilation
# Only increment version if explicitly requested
if [[ "$VERSION_INCREMENT_EXPLICIT" == true ]]; then
increment_version "$VERSION_INCREMENT_TYPE"
else
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "v0.0.0")
NEW_VERSION="$LATEST_TAG"
export NEW_VERSION
fi
# Create new git tag BEFORE compilation so version is picked up
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Created tag: $NEW_VERSION"
else
@@ -416,35 +518,63 @@ main() {
git tag -d "$NEW_VERSION" > /dev/null 2>&1
git tag "$NEW_VERSION" > /dev/null 2>&1
fi
# Update version in header file
update_version_in_header "$NEW_VERSION"
# Compile project
compile_project
# Build release binary
build_release_binary
# Commit and push (but skip tag creation since we already did it)
git_commit_and_push_no_tag
# Create Gitea release with binary
create_gitea_release
# Cleanup
cleanup_release_binary
print_success "Release $NEW_VERSION completed successfully!"
print_status "Binary uploaded to Gitea release"
# Build release binaries
local binary_path_x86=""
local binary_path_arm64=""
if build_release_binary; then
if [[ -f "build/ginxsom-fcgi_static_x86_64" ]]; then
binary_path_x86="build/ginxsom-fcgi_static_x86_64"
fi
if [[ -f "build/ginxsom-fcgi_static_arm64" ]]; then
binary_path_arm64="build/ginxsom-fcgi_static_arm64"
fi
else
print_warning "x86_64 binary build failed, continuing with release creation"
if [[ -f "build/ginxsom-fcgi_static_x86_64" ]]; then
print_status "Using existing x86_64 binary from previous build"
binary_path_x86="build/ginxsom-fcgi_static_x86_64"
fi
if [[ -f "build/ginxsom-fcgi_static_arm64" ]]; then
print_status "Using existing ARM64 binary from previous build"
binary_path_arm64="build/ginxsom-fcgi_static_arm64"
fi
fi
# Create source tarball
local tarball_path=""
if tarball_path=$(create_source_tarball); then
: # tarball_path is set by the function
else
print_warning "Source tarball creation failed, continuing with release creation"
fi
# Create Gitea release
local release_id=""
if release_id=$(create_gitea_release); then
if [[ "$release_id" =~ ^[0-9]+$ ]]; then
if [[ -n "$release_id" && (-n "$binary_path_x86" || -n "$binary_path_arm64" || -n "$tarball_path") ]]; then
upload_release_assets "$release_id" "$binary_path_x86" "$tarball_path" "$binary_path_arm64"
fi
print_success "Release $NEW_VERSION completed successfully!"
else
print_error "Invalid release_id: $release_id"
exit 1
fi
else
print_error "Release creation failed"
fi
else
print_status "=== DEFAULT MODE ==="
# Increment patch version for regular commits
increment_version "patch"
# Create new git tag BEFORE compilation
# Increment version based on type (default to patch)
increment_version "$VERSION_INCREMENT_TYPE"
# Create new git tag BEFORE compilation so version is picked up
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Created tag: $NEW_VERSION"
else
@@ -452,17 +582,11 @@ main() {
git tag -d "$NEW_VERSION" > /dev/null 2>&1
git tag "$NEW_VERSION" > /dev/null 2>&1
fi
# Update version in header file
update_version_in_header "$NEW_VERSION"
# Compile project
compile_project
# Commit and push (but skip tag creation since we already did it)
git_commit_and_push_no_tag
print_success "Build and push completed successfully!"
print_success "Increment and push completed successfully!"
print_status "Version $NEW_VERSION pushed to repository"
fi
}
+9 -3
View File
@@ -63,9 +63,15 @@ for file in "${FILES[@]}"; do
# Convert file to C byte array
echo "static const unsigned char embedded_${varname}[] = {" >> "${OUTPUT_FILE}"
# Use xxd to convert to hex, then format as C array
xxd -i < "${filepath}" >> "${OUTPUT_FILE}"
# Use xxd when available, otherwise fall back to od
if command -v xxd >/dev/null 2>&1; then
xxd -i < "${filepath}" >> "${OUTPUT_FILE}"
else
od -An -tx1 -v "${filepath}" | tr -s ' ' '\n' | sed '/^$/d' | while read -r byte; do
printf "0x%s,\n" "${byte}" >> "${OUTPUT_FILE}"
done
fi
echo "};" >> "${OUTPUT_FILE}"
echo "static const size_t embedded_${varname}_size = sizeof(embedded_${varname});" >> "${OUTPUT_FILE}"
done
+107 -44
View File
@@ -33,6 +33,7 @@ int parse_query_params(const char* query_string, char params[][256], int max_par
static int admin_nip94_get_origin(char* out, size_t out_size);
static void admin_nip94_build_blob_url(const char* origin, const char* sha256, const char* mime_type, char* out, size_t out_size);
static const char* admin_mime_to_extension(const char* mime_type);
static int validate_hex64_format(const char* hash);
// Local utility functions (from main.c but implemented here for admin API)
static int admin_nip94_get_origin(char* out, size_t out_size) {
@@ -45,34 +46,45 @@ static int admin_nip94_get_origin(char* out, size_t out_size) {
int rc;
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc) {
// Default on DB error
strncpy(out, "http://localhost:9001", out_size - 1);
out[out_size - 1] = '\0';
if (rc == SQLITE_OK) {
const char* sql = "SELECT value FROM config WHERE key = 'cdn_origin'";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
const char* value = (const char*)sqlite3_column_text(stmt, 0);
if (value && value[0] != '\0' && strcmp(value, "http://localhost:9001") != 0) {
strncpy(out, value, out_size - 1);
out[out_size - 1] = '\0';
sqlite3_finalize(stmt);
sqlite3_close(db);
return 1;
}
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
}
// Dynamic fallback from request headers (preferred over localhost)
const char* host = getenv("HTTP_HOST");
const char* forwarded_proto = getenv("HTTP_X_FORWARDED_PROTO");
const char* https = getenv("HTTPS");
if (host && host[0] != '\0') {
const char* proto = "http";
if (forwarded_proto && forwarded_proto[0] != '\0') {
proto = forwarded_proto;
} else if (https && (strcmp(https, "on") == 0 || strcmp(https, "1") == 0)) {
proto = "https";
}
snprintf(out, out_size, "%s://%s", proto, host);
return 1;
}
const char* sql = "SELECT value FROM config WHERE key = 'cdn_origin'";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
const char* value = (const char*)sqlite3_column_text(stmt, 0);
if (value) {
strncpy(out, value, out_size - 1);
out[out_size - 1] = '\0';
sqlite3_finalize(stmt);
sqlite3_close(db);
return 1;
}
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
// Default fallback
strncpy(out, "http://localhost:9001", out_size - 1);
// Final fallback
strncpy(out, "https://blossom.laantungir.net", out_size - 1);
out[out_size - 1] = '\0';
return 1;
}
@@ -93,6 +105,22 @@ static const char* admin_mime_to_extension(const char* mime_type) {
return mime_to_extension(mime_type);
}
static int validate_hex64_format(const char* hash) {
if (!hash || strlen(hash) != 64) {
return 0;
}
for (size_t i = 0; i < 64; i++) {
if (!((hash[i] >= '0' && hash[i] <= '9') ||
(hash[i] >= 'a' && hash[i] <= 'f') ||
(hash[i] >= 'A' && hash[i] <= 'F'))) {
return 0;
}
}
return 1;
}
// Main API request handler
void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated) {
const char* path = uri + 4; // Skip "/api"
@@ -107,7 +135,8 @@ void handle_admin_api_request(const char* method, const char* uri, const char* v
// Health endpoint and POST /admin (Kind 23456 events) are exempt from authentication requirement
// Kind 23456 events authenticate themselves via signed event validation
int skip_auth = (strcmp(path, "/health") == 0) ||
(strcmp(method, "POST") == 0 && strcmp(path, "/admin") == 0);
(strcmp(method, "POST") == 0 && strcmp(path, "/admin") == 0) ||
(strcmp(method, "GET") == 0 && strncmp(path, "/files", 6) == 0);
if (!skip_auth) {
if (!is_authenticated || !validated_pubkey) {
@@ -577,14 +606,15 @@ void handle_config_key_put_api(const char* key) {
}
void handle_files_api(void) {
// Parse query parameters for pagination
// Parse query parameters for pagination and optional pubkey filtering
const char* query_string = getenv("QUERY_STRING");
int limit = 50;
int offset = 0;
char pubkey_filter[65] = {0};
if (query_string) {
char params[10][256];
int param_count = parse_query_params(query_string, params, 10);
char params[12][256];
int param_count = parse_query_params(query_string, params, 12);
for (int i = 0; i < param_count; i++) {
char* key = params[i];
@@ -597,6 +627,12 @@ void handle_files_api(void) {
} else if (strcmp(key, "offset") == 0) {
offset = atoi(value);
if (offset < 0) offset = 0;
} else if (strcmp(key, "pubkey") == 0) {
strncpy(pubkey_filter, value, sizeof(pubkey_filter) - 1);
pubkey_filter[sizeof(pubkey_filter) - 1] = '\0';
if (!validate_hex64_format(pubkey_filter)) {
pubkey_filter[0] = '\0';
}
}
}
}
@@ -612,9 +648,12 @@ void handle_files_api(void) {
return;
}
// Query recent files with pagination
const char* sql = "SELECT sha256, size, type, uploaded_at, uploader_pubkey, filename "
"FROM blobs ORDER BY uploaded_at DESC LIMIT ? OFFSET ?";
// Query recent files with pagination and optional uploader filter
const char* sql = (pubkey_filter[0] != '\0')
? "SELECT sha256, size, type, uploaded_at, uploader_pubkey, filename "
"FROM blobs WHERE uploader_pubkey = ? ORDER BY uploaded_at DESC LIMIT ? OFFSET ?"
: "SELECT sha256, size, type, uploaded_at, uploader_pubkey, filename "
"FROM blobs ORDER BY uploaded_at DESC LIMIT ? OFFSET ?";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
@@ -623,23 +662,38 @@ void handle_files_api(void) {
return;
}
sqlite3_bind_int(stmt, 1, limit);
sqlite3_bind_int(stmt, 2, offset);
int bind_index = 1;
if (pubkey_filter[0] != '\0') {
sqlite3_bind_text(stmt, bind_index++, pubkey_filter, -1, SQLITE_STATIC);
}
sqlite3_bind_int(stmt, bind_index++, limit);
sqlite3_bind_int(stmt, bind_index++, offset);
cJSON* response = cJSON_CreateObject();
cJSON* data = cJSON_CreateObject();
cJSON* files_array = cJSON_CreateArray();
if (!response || !data || !files_array) {
if (response) cJSON_Delete(response);
if (data) cJSON_Delete(data);
if (files_array) cJSON_Delete(files_array);
sqlite3_finalize(stmt);
sqlite3_close(db);
send_json_error(500, "memory_error", "Failed to allocate response JSON");
return;
}
cJSON_AddStringToObject(response, "status", "success");
cJSON_AddItemToObject(response, "data", data);
cJSON_AddItemToObject(data, "files", files_array);
cJSON_AddNumberToObject(data, "limit", limit);
cJSON_AddNumberToObject(data, "offset", offset);
int total_count = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
total_count++;
cJSON* file_obj = cJSON_CreateObject();
if (!file_obj) {
continue;
}
cJSON_AddItemToArray(files_array, file_obj);
const char* sha256 = (const char*)sqlite3_column_text(stmt, 0);
@@ -669,12 +723,18 @@ void handle_files_api(void) {
}
}
// Get total count for pagination info
const char* count_sql = "SELECT COUNT(*) FROM blobs";
// Get total count for pagination info (respecting optional pubkey filter)
const char* count_sql = (pubkey_filter[0] != '\0')
? "SELECT COUNT(*) FROM blobs WHERE uploader_pubkey = ?"
: "SELECT COUNT(*) FROM blobs";
sqlite3_stmt* count_stmt;
rc = sqlite3_prepare_v2(db, count_sql, -1, &count_stmt, NULL);
if (rc == SQLITE_OK) {
if (pubkey_filter[0] != '\0') {
sqlite3_bind_text(count_stmt, 1, pubkey_filter, -1, SQLITE_STATIC);
}
rc = sqlite3_step(count_stmt);
if (rc == SQLITE_ROW) {
int total = sqlite3_column_int(count_stmt, 0);
@@ -683,6 +743,10 @@ void handle_files_api(void) {
sqlite3_finalize(count_stmt);
}
if (pubkey_filter[0] != '\0') {
cJSON_AddStringToObject(data, "pubkey_filter", pubkey_filter);
}
sqlite3_finalize(stmt);
sqlite3_close(db);
@@ -785,15 +849,14 @@ int parse_query_params(const char* query_string, char params[][256], int max_par
memcpy(query_copy, query_string, query_len + 1);
int count = 0;
char* token = strtok(query_copy, "&");
char* saveptr = NULL;
char* token = strtok_r(query_copy, "&", &saveptr);
while (token && count < max_params) {
if (strlen(token) >= sizeof(params[0])) {
token[sizeof(params[0]) - 1] = '\0';
}
strcpy(params[count], token);
strncpy(params[count], token, sizeof(params[0]) - 1);
params[count][sizeof(params[0]) - 1] = '\0';
count++;
token = strtok(NULL, "&");
token = strtok_r(NULL, "&", &saveptr);
}
free(query_copy);
+142
View File
@@ -11,6 +11,9 @@
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include "ginxsom.h"
// Global state
static struct {
@@ -128,6 +131,9 @@ cJSON* admin_commands_process(cJSON* command_array, const char* request_event_id
else if (strcmp(command, "blob_list") == 0) {
return admin_cmd_blob_list(command_array);
}
else if (strcmp(command, "blob_delete") == 0) {
return admin_cmd_blob_delete(command_array);
}
else if (strcmp(command, "storage_stats") == 0) {
return admin_cmd_storage_stats(command_array);
}
@@ -585,6 +591,142 @@ cJSON* admin_cmd_blob_list(cJSON* args) {
return response;
}
cJSON* admin_cmd_blob_delete(cJSON* args) {
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "blob_delete");
// Expected format: ["blob_delete", "<sha256>"]
if (cJSON_GetArraySize(args) < 2) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Missing blob hash");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
cJSON* hash_item = cJSON_GetArrayItem(args, 1);
if (!cJSON_IsString(hash_item) || !validate_sha256_format(hash_item->valuestring)) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Invalid SHA-256 hash format");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
const char* sha256 = hash_item->valuestring;
sqlite3* db;
int rc = sqlite3_open_v2(g_admin_state.db_path, &db, SQLITE_OPEN_READWRITE, NULL);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to open database");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
rc = sqlite3_exec(db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to begin transaction");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
sqlite3_stmt* stmt = NULL;
const char* select_sql = "SELECT type FROM blobs WHERE sha256 = ?";
rc = sqlite3_prepare_v2(db, select_sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to prepare metadata query");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
sqlite3_bind_text(stmt, 1, sha256, -1, SQLITE_STATIC);
char blob_type[128] = {0};
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
const char* type = (const char*)sqlite3_column_text(stmt, 0);
if (type) {
strncpy(blob_type, type, sizeof(blob_type) - 1);
blob_type[sizeof(blob_type) - 1] = '\0';
}
} else {
sqlite3_finalize(stmt);
sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Blob not found");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
sqlite3_finalize(stmt);
const char* extension = mime_to_extension(blob_type);
char filepath[4096];
snprintf(filepath, sizeof(filepath), "%s/%s%s", g_storage_dir, sha256, extension ? extension : "");
int file_deleted = 0;
if (unlink(filepath) == 0) {
file_deleted = 1;
} else if (errno == ENOENT) {
// File already absent; still remove DB reference
file_deleted = 0;
} else {
sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
char errbuf[256];
snprintf(errbuf, sizeof(errbuf), "Failed to delete file: %s", strerror(errno));
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", errbuf);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
const char* delete_sql = "DELETE FROM blobs WHERE sha256 = ?";
rc = sqlite3_prepare_v2(db, delete_sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to prepare delete query");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
sqlite3_bind_text(stmt, 1, sha256, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to delete blob metadata");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
sqlite3_close(db);
return response;
}
rc = sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
sqlite3_close(db);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "error", "Failed to commit blob delete transaction");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
cJSON_AddStringToObject(response, "status", "success");
cJSON_AddStringToObject(response, "sha256", sha256);
cJSON_AddBoolToObject(response, "file_deleted", file_deleted ? 1 : 0);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
return response;
}
cJSON* admin_cmd_storage_stats(cJSON* args) {
(void)args;
+1
View File
@@ -33,6 +33,7 @@ cJSON* admin_cmd_config_update(cJSON* args);
cJSON* admin_cmd_stats_query(cJSON* args);
cJSON* admin_cmd_system_status(cJSON* args);
cJSON* admin_cmd_blob_list(cJSON* args);
cJSON* admin_cmd_blob_delete(cJSON* args);
cJSON* admin_cmd_storage_stats(cJSON* args);
cJSON* admin_cmd_sql_query(cJSON* args);
cJSON* admin_cmd_query_view(cJSON* args);
+733097 -59244
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -9,9 +9,9 @@
#define GINXSOM_H
// Version information (auto-updated by build system)
#define VERSION_MAJOR 0
#define VERSION_MINOR 1
#define VERSION_PATCH 33
#define VERSION "v0.1.33"
#define VERSION_MINOR 2
#define VERSION_PATCH 3
#define VERSION "v0.2.3"
#include <stddef.h>
#include <stdint.h>
+165 -277
View File
@@ -314,14 +314,14 @@ int initialize_database(const char *db_path) {
const char *insert_config =
"INSERT OR IGNORE INTO config (key, value, description) VALUES"
" ('max_file_size', '104857600', 'Maximum file size in bytes (100MB)'),"
" ('auth_rules_enabled', 'true', 'Whether authentication rules are enabled for uploads'),"
" ('auth_rules_enabled', 'false', 'Whether authentication rules are enabled for uploads'),"
" ('server_name', 'ginxsom', 'Server name for responses'),"
" ('admin_pubkey', '', 'Admin public key for API access'),"
" ('admin_enabled', 'true', 'Whether admin API is enabled'),"
" ('nip42_require_auth', 'false', 'Enable NIP-42 challenge/response authentication'),"
" ('nip42_challenge_timeout', '600', 'NIP-42 challenge timeout in seconds'),"
" ('nip42_time_tolerance', '300', 'NIP-42 timestamp tolerance in seconds'),"
" ('enable_relay_connect', 'true', 'Enable connection to Nostr relays'),"
" ('enable_relay_connect', 'false', 'Enable connection to Nostr relays'),"
" ('kind_0_content', '{\"name\":\"Ginxsom Blossom Server\",\"about\":\"A Nostr-enabled Blossom media server\",\"picture\":\"\"}', 'JSON content for Kind 0 profile event'),"
" ('kind_10002_tags', '[\"wss://relay.laantungir.net\"]', 'JSON array of relay URLs for Kind 10002');";
@@ -471,6 +471,7 @@ void handle_auth_challenge_request(void);
// Handler function declarations with validation support
void handle_delete_request_with_validation(const char *sha256, nostr_request_result_t *validation_result);
void handle_get_request(const char *sha256);
// Key management function implementations
@@ -1051,7 +1052,7 @@ int get_blob_metadata(const char *sha256, blob_metadata_t *metadata) {
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_CREATE, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc) {
app_log(LOG_ERROR, "Can't open database: %s\n", sqlite3_errmsg(db));
return 0;
@@ -1170,6 +1171,100 @@ void handle_head_request(const char *sha256) {
// HEAD request - no body content
}
// Handle GET request for blob content
void handle_get_request(const char *sha256) {
blob_metadata_t metadata = {0};
// Validate SHA-256 format (64 hex characters)
if (!sha256 || strlen(sha256) != 64) {
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Invalid SHA-256 hash format\n");
return;
}
// Check if blob exists in database. If metadata is missing, fall back to
// filesystem lookup for anonymous/basic deployments where DB continuity may
// be unavailable.
int has_metadata = get_blob_metadata(sha256, &metadata);
const char *extension = NULL;
if (has_metadata) {
extension = mime_to_extension(metadata.type);
} else {
// Fallback to default .bin blob path
strncpy(metadata.sha256, sha256, sizeof(metadata.sha256) - 1);
metadata.sha256[sizeof(metadata.sha256) - 1] = '\0';
strncpy(metadata.type, "application/octet-stream", sizeof(metadata.type) - 1);
metadata.type[sizeof(metadata.type) - 1] = '\0';
metadata.filename[0] = '\0';
extension = ".bin";
}
// Resolve file path from extension
char filepath[MAX_PATH_LEN];
size_t dir_len = strlen(g_storage_dir);
size_t sha_len = strlen(sha256);
size_t ext_len = strlen(extension);
size_t total_len = dir_len + 1 + sha_len + ext_len + 1;
if (total_len > sizeof(filepath)) {
send_error_response(500, "path_too_long", "Blob path too long",
"Internal path construction overflow prevention triggered");
return;
}
memcpy(filepath, g_storage_dir, dir_len);
filepath[dir_len] = '/';
memcpy(filepath + dir_len + 1, sha256, sha_len);
memcpy(filepath + dir_len + 1 + sha_len, extension, ext_len);
filepath[total_len - 1] = '\0';
FILE *infile = fopen(filepath, "rb");
if (!infile) {
if (!has_metadata) {
printf("Status: 404 Not Found\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Blob not found\n");
return;
}
send_error_response(404, "blob_not_found", "Blob file not found",
"Metadata exists but blob file is missing on disk");
return;
}
if (!has_metadata) {
struct stat st;
if (stat(filepath, &st) == 0) {
metadata.size = st.st_size;
}
}
// Return response headers
printf("Status: 200 OK\r\n");
printf("Content-Type: %s\r\n", metadata.type);
printf("Content-Length: %ld\r\n", metadata.size);
printf("Cache-Control: public, max-age=31536000, immutable\r\n");
printf("ETag: \"%s\"\r\n", metadata.sha256);
if (strlen(metadata.filename) > 0) {
printf("X-Original-Filename: %s\r\n", metadata.filename);
}
printf("\r\n");
// Stream file body
unsigned char buffer[8192];
size_t nread;
while ((nread = fread(buffer, 1, sizeof(buffer), infile)) > 0) {
if (fwrite(buffer, 1, nread, stdout) != nread) {
break;
}
}
fclose(infile);
}
// Extract SHA-256 from request URI (Blossom compliant - ignores any extension)
const char *extract_sha256_from_uri(const char *uri) {
static char sha256_buffer[MAX_SHA256_LEN];
@@ -1451,16 +1546,12 @@ void handle_delete_request_with_validation(const char *sha256, nostr_request_res
}
}
// Get authenticated pubkey from centralized validation result
// Authentication is optional for delete when auth bypass is enabled.
// If a valid pubkey is present, we preserve ownership enforcement.
const char *auth_pubkey = NULL;
if (validation_result && validation_result->valid && strlen(validation_result->pubkey) == 64) {
if (validation_result && validation_result->valid &&
strlen(validation_result->pubkey) == 64) {
auth_pubkey = validation_result->pubkey;
} else {
// No valid authentication - this should have been caught by centralized validation
send_error_response(401, "unauthorized", "Authentication required for delete operations",
"Valid authentication is required to delete blobs");
log_request("DELETE", "/delete", "unauthenticated", 401);
return;
}
// Check if blob exists in database
@@ -1519,15 +1610,16 @@ void handle_delete_request_with_validation(const char *sha256, nostr_request_res
sqlite3_finalize(stmt);
// Check ownership - only the uploader can delete
if (!uploader_pubkey_copy[0] ||
strcmp(uploader_pubkey_copy, auth_pubkey) != 0) {
sqlite3_close(db);
send_error_response(403, "access_denied", "Access denied",
"You can only delete blobs that you uploaded");
log_request("DELETE", "/delete", "ownership_denied", 403);
return;
} else {
// Enforce ownership only when an authenticated pubkey is available.
// Anonymous delete is allowed when authentication is bypassed.
if (auth_pubkey && auth_pubkey[0]) {
if (!uploader_pubkey_copy[0] || strcmp(uploader_pubkey_copy, auth_pubkey) != 0) {
sqlite3_close(db);
send_error_response(403, "access_denied", "Access denied",
"You can only delete blobs that you uploaded");
log_request("DELETE", "/delete", "ownership_denied", 403);
return;
}
}
// Delete from database first
@@ -1599,256 +1691,43 @@ void handle_delete_request_with_validation(const char *sha256, nostr_request_res
// Handle PUT /upload requests
void handle_upload_request(void) {
// Log the incoming request
log_request("PUT", "/upload", "pending", 0);
// Get HTTP headers
// Legacy entry point: validate request, then reuse the unified upload handler.
const char *request_uri = getenv("REQUEST_URI");
const char *auth_header = getenv("HTTP_AUTHORIZATION");
const char *content_type = getenv("CONTENT_TYPE");
const char *content_length_str = getenv("CONTENT_LENGTH");
long content_length = content_length_str ? atol(content_length_str) : 0;
// Validate required headers
if (!content_type) {
send_error_response(
400, "missing_header", "Content-Type header required",
"The Content-Type header must be specified for file uploads");
log_request("PUT", "/upload", "none", 400);
nostr_request_result_t result;
memset(&result, 0, sizeof(result));
nostr_unified_request_t request = {
.operation = "upload",
.auth_header = auth_header,
.event = NULL,
.resource_hash = NULL,
.mime_type = content_type,
.file_size = content_length,
.request_url = "ginxsom",
.challenge_id = NULL,
.nip42_enabled = 1,
.client_ip = getenv("REMOTE_ADDR"),
.app_context = NULL};
int validation_result = nostr_validate_unified_request(&request, &result);
if (validation_result != NOSTR_SUCCESS || !result.valid) {
const char *message =
result.reason[0] ? result.reason : "Authentication failed";
send_error_response(401, "authentication_failed", message,
"Authentication validation failed");
log_request("PUT", request_uri ? request_uri : "/upload", "auth_failed",
401);
nostr_request_result_free_file_data(&result);
return;
}
if (!content_length_str) {
send_error_response(
400, "missing_header", "Content-Length header required",
"The Content-Length header must be specified for file uploads");
log_request("PUT", "/upload", "none", 400);
return;
}
long content_length = atol(content_length_str);
if (content_length <= 0 ||
content_length > 100 * 1024 * 1024) { // 100MB limit
send_error_response(413, "payload_too_large",
"File size must be between 1 byte and 100MB",
"Maximum allowed file size is 100MB");
log_request("PUT", "/upload", "none", 413);
return;
}
// Get Authorization header for authentication
const char *auth_header = getenv("HTTP_AUTHORIZATION");
// Store uploader pubkey for metadata (will be extracted during auth if
// provided)
const char *uploader_pubkey = NULL;
if (auth_header) {
log_request("PUT", "/upload", "auth_provided", 0);
} else {
log_request("PUT", "/upload", "anonymous", 0);
}
// Read file data from stdin
unsigned char *file_data = malloc(content_length);
if (!file_data) {
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Memory allocation failed\n");
return;
}
size_t bytes_read = fread(file_data, 1, content_length, stdin);
if (bytes_read != (size_t)content_length) {
free(file_data);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Failed to read complete file data\n");
return;
}
// Calculate SHA-256 hash using nostr_core function
unsigned char hash[32];
if (nostr_sha256(file_data, content_length, hash) != NOSTR_SUCCESS) {
free(file_data);
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Hash calculation failed\n");
return;
}
// Convert hash to hex string
char sha256_hex[65];
nostr_bytes_to_hex(hash, 32, sha256_hex);
fflush(stderr);
// Get dimensions from in-memory buffer before saving file
int width = 0, height = 0;
nip94_get_dimensions(file_data, content_length, content_type, &width,
&height);
// Determine file extension from Content-Type using centralized mapping
const char *extension = mime_to_extension(content_type);
// Save file to storage directory with SHA-256 + extension
char filepath[MAX_PATH_LEN];
// Construct path safely
size_t dir_len = strlen(g_storage_dir);
size_t sha_len = strlen(sha256_hex);
size_t ext_len = strlen(extension);
size_t total_len = dir_len + 1 + sha_len + ext_len + 1; // +1 for /, +1 for null
if (total_len > sizeof(filepath)) {
app_log(LOG_WARN, "WARNING: File path too long for buffer: %s/%s%s\n", g_storage_dir, sha256_hex, extension);
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("File path too long\n");
return;
}
// Build path manually to avoid compiler warnings
memcpy(filepath, g_storage_dir, dir_len);
filepath[dir_len] = '/';
memcpy(filepath + dir_len + 1, sha256_hex, sha_len);
memcpy(filepath + dir_len + 1 + sha_len, extension, ext_len);
filepath[total_len - 1] = '\0';
FILE *outfile = fopen(filepath, "wb");
if (!outfile) {
free(file_data);
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Failed to create file\n");
return;
}
size_t bytes_written = fwrite(file_data, 1, content_length, outfile);
fclose(outfile);
// Set file permissions to 644 (owner read/write, group/others read) -
// standard for web files
if (chmod(filepath, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) != 0) {
app_log(LOG_WARN, "WARNING: Failed to set file permissions for %s\r\n",
filepath);
// Continue anyway - this is not a fatal error
} else {
}
free(file_data);
if (bytes_written != (size_t)content_length) {
// Clean up partial file
unlink(filepath);
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Failed to write complete file\n");
return;
}
// Extract filename from Content-Disposition header if present
const char *filename = NULL;
const char *content_disposition = getenv("HTTP_CONTENT_DISPOSITION");
if (content_disposition) {
// Look for filename= in Content-Disposition header
const char *filename_start = strstr(content_disposition, "filename=");
if (filename_start) {
filename_start += 9; // Skip "filename="
// Handle quoted filenames
if (*filename_start == '"') {
filename_start++; // Skip opening quote
// Find closing quote
const char *filename_end = strchr(filename_start, '"');
if (filename_end) {
// Extract filename between quotes
static char filename_buffer[256];
size_t filename_len = filename_end - filename_start;
if (filename_len < sizeof(filename_buffer)) {
strncpy(filename_buffer, filename_start, filename_len);
filename_buffer[filename_len] = '\0';
filename = filename_buffer;
} else {
}
} else {
}
} else {
// Unquoted filename - extract until space or end
const char *filename_end = filename_start;
while (*filename_end && *filename_end != ' ' && *filename_end != ';') {
filename_end++;
}
static char filename_buffer[256];
size_t filename_len = filename_end - filename_start;
if (filename_len < sizeof(filename_buffer)) {
strncpy(filename_buffer, filename_start, filename_len);
filename_buffer[filename_len] = '\0';
filename = filename_buffer;
} else {
}
}
} else {
}
} else {
}
// Store blob metadata in database
time_t uploaded_time = time(NULL);
if (!insert_blob_metadata(sha256_hex, content_length, content_type,
uploaded_time, uploader_pubkey, filename)) {
// Database insertion failed - clean up the physical file to maintain
// consistency
unlink(filepath);
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Failed to store blob metadata\n");
return;
}
// Get origin from config
char origin[256];
nip94_get_origin(origin, sizeof(origin));
// Build canonical blob URL
char blob_url[512];
nip94_build_blob_url(origin, sha256_hex, content_type, blob_url,
sizeof(blob_url));
// Return success response with blob descriptor
printf("Status: 200 OK\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\n");
printf(" \"sha256\": \"%s\",\n", sha256_hex);
printf(" \"size\": %ld,\n", content_length);
printf(" \"type\": \"%s\",\n", content_type);
printf(" \"uploaded\": %ld,\n", uploaded_time);
printf(" \"url\": \"%s\"", blob_url);
// Add NIP-94 metadata if enabled
if (nip94_is_enabled()) {
printf(",\n");
nip94_emit_field(blob_url, content_type, sha256_hex, content_length, width,
height);
}
printf("\n}\n");
handle_upload_request_with_validation(&result);
nostr_request_result_free_file_data(&result);
}
// Handle PUT /upload requests with pre-validated file data from validator
@@ -1893,7 +1772,7 @@ void handle_upload_request_with_validation(nostr_request_result_t* validation_re
// Extract uploader pubkey from validation result
const char *uploader_pubkey = NULL;
if (validation_result && strlen(validation_result->pubkey) == 64) {
if (validation_result && validation_result->valid && strlen(validation_result->pubkey) == 64) {
uploader_pubkey = validation_result->pubkey;
log_request("PUT", "/upload", "authenticated", 0);
} else if (auth_header) {
@@ -3050,8 +2929,13 @@ if (!config_loaded /* && !initialize_server_config() */) {
"Pubkey must be 64 hex characters");
log_request("GET", request_uri, "none", 400);
}
} else if (strcmp(request_method, "GET") == 0 &&
strcmp(request_uri, "/") == 0) {
} else if (strcmp(request_method, "GET") == 0) {
// Handle GET /<sha256> blob retrieval
const char *sha256 = extract_sha256_from_uri(request_uri);
if (sha256) {
handle_get_request(sha256);
log_request("GET", request_uri, "public", 200);
} else if (strcmp(request_uri, "/") == 0) {
// Handle GET / requests - Server info endpoint (NIP-11)
printf("Status: 200 OK\r\n");
printf("Content-Type: application/nostr+json\r\n\r\n");
@@ -3093,18 +2977,16 @@ if (!config_loaded /* && !initialize_server_config() */) {
printf(" },\n");
printf(" \"authentication\": {\n");
printf(" \"required_for_upload\": false,\n");
printf(" \"required_for_delete\": true,\n");
printf(" \"required_for_delete\": false,\n");
printf(" \"required_for_list\": false,\n");
printf(" \"nip42_enabled\": true\n");
printf(" \"nip42_enabled\": false\n");
printf(" }\n");
printf("}\n");
log_request("GET", "/", "server_info", 200);
} else if (strcmp(request_method, "GET") == 0 &&
strcmp(request_uri, "/auth") == 0) {
// Handle GET /auth requests using the existing handler
handle_auth_challenge_request();
} else if (strcmp(request_method, "GET") == 0 &&
strcmp(request_uri, "/health") == 0) {
log_request("GET", "/", "server_info", 200);
} else if (strcmp(request_uri, "/auth") == 0) {
// Handle GET /auth requests using the existing handler
handle_auth_challenge_request();
} else if (strcmp(request_uri, "/health") == 0) {
// Handle GET /health requests - simple public health response
time_t now = time(NULL);
long uptime_seconds = 0;
@@ -3119,7 +3001,13 @@ if (!config_loaded /* && !initialize_server_config() */) {
printf(" \"version\": \"%s\",\n", VERSION);
printf(" \"uptime\": %ld\n", uptime_seconds);
printf("}\n");
log_request("GET", "/health", "public", 200);
log_request("GET", "/health", "public", 200);
} else {
printf("Status: 404 Not Found\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Not Found\n");
log_request("GET", request_uri, "none", 404);
}
} else if (strcmp(request_method, "DELETE") == 0) {
// Handle DELETE /<sha256> requests with pre-validated auth
const char *sha256 = extract_sha256_from_uri(request_uri);
+17 -1
View File
@@ -263,14 +263,30 @@ int relay_client_start(void) {
}
app_log(LOG_INFO, "Starting relay client...");
// Use a larger stack for websocket/TLS internals to avoid stack overflow in static builds
pthread_attr_t thread_attr;
size_t stack_size = 8 * 1024 * 1024; // 8MB
if (pthread_attr_init(&thread_attr) != 0) {
app_log(LOG_ERROR, "Failed to initialize thread attributes");
return -1;
}
if (pthread_attr_setstacksize(&thread_attr, stack_size) != 0) {
app_log(LOG_WARN, "Failed to set relay management thread stack size, using default");
}
// Start management thread
g_relay_state.running = 1;
if (pthread_create(&g_relay_state.management_thread, NULL, relay_management_thread, NULL) != 0) {
if (pthread_create(&g_relay_state.management_thread, &thread_attr, relay_management_thread, NULL) != 0) {
app_log(LOG_ERROR, "Failed to create relay management thread");
g_relay_state.running = 0;
pthread_attr_destroy(&thread_attr);
return -1;
}
pthread_attr_destroy(&thread_attr);
app_log(LOG_INFO, "Relay client started successfully");
return 0;
+44 -1
View File
@@ -296,7 +296,50 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
// PHASE 2: NOSTR EVENT VALIDATION (CPU Intensive ~2ms)
/////////////////////////////////////////////////////////////////////
// Check if authentication is disabled first (regardless of header presence)
// Global authentication bypass for all non-admin operations.
// This makes blob upload/download/list/delete anonymous by default while
// keeping admin endpoints gated by the normal validation flow below.
int is_admin_operation =
(request->operation &&
(strcmp(request->operation, "admin") == 0 ||
strcmp(request->operation, "admin_event") == 0));
if (!is_admin_operation) {
validator_debug_log(
"VALIDATOR_DEBUG: STEP 4 PASSED - Authentication bypassed for non-admin operation\n");
result->valid = 1;
result->error_code = NOSTR_SUCCESS;
strcpy(result->reason, "Authentication bypassed for non-admin operation");
// Preserve uploader attribution when a valid auth header is optionally
// provided by extracting the pubkey without requiring it.
if (request->auth_header) {
char optional_event_json[4096];
int optional_parse = parse_authorization_header(
request->auth_header, optional_event_json, sizeof(optional_event_json));
if (optional_parse == NOSTR_SUCCESS) {
cJSON *optional_event = cJSON_Parse(optional_event_json);
if (optional_event) {
if (nostr_validate_event(optional_event) == NOSTR_SUCCESS) {
char optional_pubkey[65] = {0};
int optional_extract = extract_pubkey_from_event(
optional_event, optional_pubkey, sizeof(optional_pubkey));
if (optional_extract == NOSTR_SUCCESS && strlen(optional_pubkey) == 64) {
strncpy(result->pubkey, optional_pubkey, 64);
result->pubkey[64] = '\0';
strcpy(result->reason,
"Authentication bypassed (pubkey extracted from optional auth header)");
}
}
cJSON_Delete(optional_event);
}
}
}
return NOSTR_SUCCESS;
}
// For admin operations, keep config-driven authentication behavior.
if (!g_auth_cache.auth_required) {
validator_debug_log("VALIDATOR_DEBUG: STEP 4 PASSED - Authentication "
"disabled, allowing request\n");