diff --git a/lib/configStorageTypes.ts b/lib/configStorageTypes.ts index 6817397..e04fc9a 100644 --- a/lib/configStorageTypes.ts +++ b/lib/configStorageTypes.ts @@ -40,38 +40,17 @@ export const DEFAULT_RELAYS: RelayConfig[] = [ export const DEFAULT_NIP95_APIS: Nip95Config[] = [ { - id: 'voidcat', - url: 'https://void.cat/upload', + id: 'nostrimg', + url: 'https://nostrimg.com/api/upload', enabled: true, priority: 1, createdAt: Date.now(), }, - { - id: 'nostrbuild', - url: 'https://nostr.build/api/v2/upload', - enabled: true, - priority: 2, - createdAt: Date.now(), - }, - { - id: 'picstr', - url: 'https://picstr.build/api/v1/upload', - enabled: true, - priority: 3, - createdAt: Date.now(), - }, { id: 'nostrcheck', url: 'https://nostrcheck.me/api/v1/media', enabled: true, - priority: 4, - createdAt: Date.now(), - }, - { - id: 'nostrimg', - url: 'https://nostrimg.com/api/upload', - enabled: true, - priority: 5, + priority: 2, createdAt: Date.now(), }, ] diff --git a/pages/api/nip95-upload.ts b/pages/api/nip95-upload.ts index 2349af1..073ba1d 100644 --- a/pages/api/nip95-upload.ts +++ b/pages/api/nip95-upload.ts @@ -55,78 +55,118 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return res.status(400).json({ error: 'No file provided' }) } - // Create FormData for the target endpoint - const formData = new FormData() - const fileStream = fs.createReadStream(fileField.filepath) - formData.append('file', fileStream, { - filename: fileField.originalFilename || fileField.newFilename || 'upload', - contentType: fileField.mimetype || 'application/octet-stream', - }) - // Forward to target endpoint using https/http native modules - const targetUrl = new URL(targetEndpoint) - const isHttps = targetUrl.protocol === 'https:' - const clientModule = isHttps ? https : http + // Support redirects (301, 302, 307, 308) + const currentUrl = new URL(targetEndpoint) + const MAX_REDIRECTS = 5 let response: { statusCode: number; statusMessage: string; body: string } try { response = await new Promise<{ statusCode: number; statusMessage: string; body: string }>((resolve, reject) => { - const headers = formData.getHeaders() - const requestOptions = { - hostname: targetUrl.hostname, - port: targetUrl.port || (isHttps ? 443 : 80), - path: targetUrl.pathname + targetUrl.search, - method: 'POST', - headers: headers, - timeout: 30000, // 30 seconds timeout + function makeRequest(url: URL, redirectCount: number, fileField: FormidableFile): void { + if (redirectCount > MAX_REDIRECTS) { + reject(new Error(`Too many redirects (max ${MAX_REDIRECTS})`)) + return + } + + // Recreate FormData for each request (needed for redirects) + const requestFormData = new FormData() + const fileStream = fs.createReadStream(fileField.filepath) + requestFormData.append('file', fileStream, { + filename: fileField.originalFilename || fileField.newFilename || 'upload', + contentType: fileField.mimetype || 'application/octet-stream', + }) + + const isHttps = url.protocol === 'https:' + const clientModule = isHttps ? https : http + const headers = requestFormData.getHeaders() + const requestOptions = { + hostname: url.hostname, + port: url.port || (isHttps ? 443 : 80), + path: url.pathname + url.search, + method: 'POST', + headers: headers, + timeout: 30000, // 30 seconds timeout + } + + const proxyRequest = clientModule.request(requestOptions, (proxyResponse) => { + // Handle redirects (301, 302, 307, 308) + const statusCode = proxyResponse.statusCode || 500 + if ((statusCode === 301 || statusCode === 302 || statusCode === 307 || statusCode === 308) && proxyResponse.headers.location) { + const location = proxyResponse.headers.location + let redirectUrl: URL + try { + // Handle relative and absolute URLs + redirectUrl = new URL(location, url.toString()) + console.log('NIP-95 proxy redirect:', { + from: url.toString(), + to: redirectUrl.toString(), + statusCode, + redirectCount: redirectCount + 1, + }) + // Drain the response before redirecting + proxyResponse.resume() + // Make new request to redirect location + makeRequest(redirectUrl, redirectCount + 1, fileField) + return + } catch (urlError) { + console.error('NIP-95 proxy invalid redirect URL:', { + location, + error: urlError instanceof Error ? urlError.message : 'Unknown error', + }) + reject(new Error(`Invalid redirect URL: ${location}`)) + return + } + } + + let body = '' + proxyResponse.setEncoding('utf8') + proxyResponse.on('data', (chunk) => { + body += chunk + }) + proxyResponse.on('end', () => { + resolve({ + statusCode: statusCode, + statusMessage: proxyResponse.statusMessage || 'Internal Server Error', + body: body, + }) + }) + proxyResponse.on('error', (error) => { + reject(error) + }) + }) + + // Set timeout on the request + proxyRequest.setTimeout(30000, () => { + proxyRequest.destroy() + reject(new Error('Request timeout after 30 seconds')) + }) + + proxyRequest.on('error', (error) => { + // Check for DNS errors specifically + const errorCode = (error as NodeJS.ErrnoException).code + if (errorCode === 'ENOTFOUND' || errorCode === 'EAI_AGAIN') { + console.error('NIP-95 proxy DNS error:', { + targetEndpoint, + hostname: url.hostname, + errorCode, + errorMessage: error.message, + suggestion: 'Check DNS resolution or network connectivity on the server', + }) + reject(new Error(`DNS resolution failed for ${url.hostname}: ${error.message}`)) + } else { + reject(error) + } + }) + + requestFormData.on('error', (error) => { + reject(error) + }) + + requestFormData.pipe(proxyRequest) } - const proxyRequest = clientModule.request(requestOptions, (proxyResponse) => { - let body = '' - proxyResponse.setEncoding('utf8') - proxyResponse.on('data', (chunk) => { - body += chunk - }) - proxyResponse.on('end', () => { - resolve({ - statusCode: proxyResponse.statusCode || 500, - statusMessage: proxyResponse.statusMessage || 'Internal Server Error', - body: body, - }) - }) - proxyResponse.on('error', (error) => { - reject(error) - }) - }) - - // Set timeout on the request - proxyRequest.setTimeout(30000, () => { - proxyRequest.destroy() - reject(new Error('Request timeout after 30 seconds')) - }) - - proxyRequest.on('error', (error) => { - // Check for DNS errors specifically - const errorCode = (error as NodeJS.ErrnoException).code - if (errorCode === 'ENOTFOUND' || errorCode === 'EAI_AGAIN') { - console.error('NIP-95 proxy DNS error:', { - targetEndpoint, - hostname: targetUrl.hostname, - errorCode, - errorMessage: error.message, - suggestion: 'Check DNS resolution or network connectivity on the server', - }) - reject(new Error(`DNS resolution failed for ${targetUrl.hostname}: ${error.message}`)) - } else { - reject(error) - } - }) - - formData.on('error', (error) => { - reject(error) - }) - - formData.pipe(proxyRequest) + makeRequest(currentUrl, 0, fileField) }) } catch (requestError) { // Clean up temporary file before returning error @@ -140,7 +180,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) console.error('NIP-95 proxy request error:', { targetEndpoint, - hostname: targetUrl.hostname, + hostname: currentUrl.hostname, error: errorMessage, isDnsError, fileSize: fileField.size, @@ -151,7 +191,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // Return a more specific error message for DNS issues if (isDnsError) { return res.status(500).json({ - error: `DNS resolution failed for ${targetUrl.hostname}. The server cannot resolve the domain name. Please check DNS configuration and network connectivity.`, + error: `DNS resolution failed for ${currentUrl.hostname}. The server cannot resolve the domain name. Please check DNS configuration and network connectivity.`, }) } @@ -171,12 +211,28 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const errorText = response.body.substring(0, 200) // Limit log size console.error('NIP-95 proxy response error:', { targetEndpoint, + finalUrl: currentUrl.toString(), status: response.statusCode, statusText: response.statusMessage, errorText: errorText, }) + + // Provide more specific error messages for common HTTP status codes + let userFriendlyError = errorText || `Upload failed: ${response.statusCode} ${response.statusMessage}` + if (response.statusCode === 401) { + userFriendlyError = 'Authentication required. This endpoint requires authorization headers.' + } else if (response.statusCode === 403) { + userFriendlyError = 'Access forbidden. This endpoint may require authentication or have restrictions.' + } else if (response.statusCode === 405) { + userFriendlyError = 'Method not allowed. This endpoint may not support POST requests or the URL may be incorrect.' + } else if (response.statusCode === 413) { + userFriendlyError = 'File too large. The file exceeds the maximum size allowed by this endpoint.' + } else if (response.statusCode >= 500) { + userFriendlyError = `Server error (${response.statusCode}). The endpoint server encountered an error.` + } + return res.status(response.statusCode).json({ - error: errorText || `Upload failed: ${response.statusCode} ${response.statusMessage}`, + error: userFriendlyError, }) }