feat: serve local files

This commit is contained in:
complex 2025-04-11 14:21:06 +02:00
parent 89666ac4c7
commit ba9912d5ad
4 changed files with 72 additions and 16 deletions
client/src
server

@ -21,11 +21,10 @@ export function setDefaultHttpRequest(): void {
if (httpRequestBox) {
// Only set default if the textarea is empty
if (!httpRequestBox.value.trim()) {
const defaultRequest = `GET /index.html HTTP/1.1
Host: example.com
User-Agent: NostrClient/1.0
Accept: text/html,application/xhtml+xml,application/xml
Connection: keep-alive
const defaultRequest = `GET /hello.txt HTTP/1.1
Host: localhost
User-Agent: Nostr-HTTP-Client
Accept: text/plain
`;
httpRequestBox.value = defaultRequest;
@ -51,16 +50,16 @@ export function processTags(tags: string[][]): string[][] {
if (!tags || !Array.isArray(tags)) {
return tags;
}
// Make a deep copy of tags to avoid modifying the original
const processedTags = JSON.parse(JSON.stringify(tags));
for (let i = 0; i < processedTags.length; i++) {
const tag = processedTags[i];
if (!tag || !Array.isArray(tag) || tag.length < 2) {
continue;
}
// Convert npub in "p" tags to hex pubkeys
if (tag[0] === 'p' && typeof tag[1] === 'string' && tag[1].startsWith('npub')) {
try {
@ -72,13 +71,13 @@ export function processTags(tags: string[][]): string[][] {
// Silent error handling
}
}
// Ensure expiration tag value is a string
if (tag[0] === 'expiration' && tag[1]) {
processedTags[i][1] = String(tag[1]);
}
}
return processedTags;
}
@ -91,7 +90,7 @@ export function standardizeEvent(event: Record<string, unknown>): NostrEvent {
if (!event || typeof event !== 'object') {
throw new Error('Invalid event: not an object');
}
// Type assertions needed for raw object properties
return {
id: String(event.id || ''),

@ -0,0 +1 @@
Goodbye!

6
server/public/hello.txt Normal file

@ -0,0 +1,6 @@
Hello from the Nostr HTTP Server!
This is a sample file that can be served by the server.
You can request this file by making a request to /hello.txt
The server will read this file from the local filesystem and return its contents.

@ -7,9 +7,11 @@ import NDK from '@nostr-dev-kit/ndk';
import { NDKUser, NDKEvent, NDKFilter, NDKPrivateKeySigner, NDKSubscription } from '@nostr-dev-kit/ndk';
import express from 'express';
import { createServer } from 'http';
import { ServerConfig, HttpRequest, RateLimitConfig, RateLimitState } from './types';
import { ServerConfig, HttpRequest, RateLimitConfig, RateLimitState } from './types.js';
import WebSocket from 'ws';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
// Custom subscription interface for our WebSocket implementation
interface CustomSubscription {
@ -198,7 +200,7 @@ export class NostrHttpServer {
*/
private async publishToRelays(event: any): Promise<Map<string, { success: boolean; message: string }>> {
const results = new Map<string, { success: boolean; message: string }>();
const publishPromises = this.config.relayUrls.map(async (relayUrl) => {
const publishPromises = this.config.relayUrls.map(async (relayUrl: string) => {
try {
const result = await this.publishToRelay(event, relayUrl);
results.set(relayUrl, result);
@ -839,7 +841,35 @@ export class NostrHttpServer {
* @param request The HTTP request to execute
*/
private async executeHttpRequest(request: HttpRequest): Promise<string> {
// Return a simple Hello World response instead of executing the actual request
// Check if the request is for a file in the public directory
const publicDir = path.join(process.cwd(), 'public');
const requestedPath = path.join(publicDir, request.path.replace(/^\//, ''));
// log the requested path
console.log('requestedPath', requestedPath);
console.log('publicDir', publicDir);
try {
// Check if the file exists and is within the public directory
if (fs.existsSync(requestedPath)) {
const filePath = publicDir + request.path;
const fileContent = fs.readFileSync(filePath, 'utf8');
const contentType = this.getContentType(filePath);
// Build response string with file content
return `HTTP/1.1 200 OK
Content-Type: ${contentType}
Content-Length: ${fileContent.length}
${fileContent}`;
}
} catch (error) {
console.error('Error reading file:', error);
// If there's an error reading the file, fall back to the default response
}
// Return the default response if no file is found or there's an error
const responseBody = `
<!DOCTYPE html>
<html>
@ -878,13 +908,33 @@ export class NostrHttpServer {
</html>`;
// Build response string
let responseStr = `HTTP/1.1 200 OK
return `HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: ${responseBody.length}
${responseBody}`;
}
return responseStr;
/**
* Get the content type for a file based on its extension
* @param filePath The path to the file
* @returns The content type string
*/
private getContentType(filePath: string): string {
const ext = path.extname(filePath).toLowerCase();
const contentTypes: Record<string, string> = {
'.txt': 'text/plain',
'.html': 'text/html',
'.css': 'text/css',
'.js': 'text/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml'
};
return contentTypes[ext] || 'text/plain';
}
/**