UNPKG

f2e-server3

Version:

f2e-server 3.0

261 lines (247 loc) 9.22 kB
import { HttpRequest, HttpResponse } from "uWebSockets.js" import { APIContext, F2EConfigResult } from "../interface" import { createHash } from "node:crypto" import * as _ from './misc' import * as zlib from "node:zlib" import logger from "./logger" import { ENGINE_TYPE } from "../server-engine" import { VERSION } from "./engine" import { RouteItem } from "../routes/interface" import { OutgoingHttpHeaders } from "node:http" import { IncomingMessage } from "node:http"; export type HttpHeaders = OutgoingHttpHeaders & { [key: string]: string | number | string[] | undefined } export const getIpAddress = (resp: HttpResponse) => { const bf1 = resp.getProxiedRemoteAddressAsText() if (bf1.byteLength > 0) { return Buffer.from(bf1).toString('utf-8') } const bf2 = resp.getRemoteAddressAsText() if (bf2.byteLength > 0) { return Buffer.from(bf2).toString('utf-8') } return '' } export const getHttpHeaders = (req: HttpRequest | IncomingMessage) => { let headers: HttpHeaders = {} if (req instanceof IncomingMessage) { if (req.headers) { headers = req.headers } else { for (let i = 0; i < req.rawHeaders.length; i += 2) { headers[req.rawHeaders[i].trim().toLowerCase()] = req.rawHeaders[i + 1] } } } else if (req.forEach) { req.forEach((key, value) => { headers[key.trim().toLowerCase()] = (value || '').toString().trim() }) } return headers } const gzipSync = ENGINE_TYPE === 'bun' ? Bun.gzipSync : zlib.gzipSync export const etag = (entity: Buffer | string) => { if (entity.length === 0) { return '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"' } const hash = createHash('sha1') .update(entity) .digest('base64') .substring(0, 27) const len = Buffer.byteLength(entity) return '"' + len.toString(16) + '-' + hash + '"' } const gzipStore = new Map<string, { etag: string; data: Buffer | Uint8Array; }>() export const commonWriteHeaders = (resp: HttpResponse, headers: OutgoingHttpHeaders = {}) => { Object.assign(headers, { 'X-Powered-By': VERSION, }) for (const key in headers) { if (Object.prototype.hasOwnProperty.call(headers, key)) { const value = headers[key] if (typeof value != 'undefined') { resp.writeHeader(key, value.toString()) } } } } export const createResponseHelper = (conf: F2EConfigResult) => { const { gzip, gzip_filter, range_size, page_404, page_50x, page_dir, cache_filter, } = conf const handleNotFound = (resp: HttpResponse, pathname: string) => { const body = _.renderHTML(page_404, { title: 'Page Not Found!', pathname }) resp.cork(() => { resp.writeStatus('404 Not Found') commonWriteHeaders(resp, {'Content-Type': 'text/html; charset=utf-8'}) resp.end(body) }) } const handleError = (resp: HttpResponse, error: string) => { const error_body = _.renderHTML(page_50x, { error, title: 'Server Error!' }) logger.error(error) resp.cork(() => { resp.writeStatus('500 Internal Server Error') commonWriteHeaders(resp, {'Content-Type': 'text/html; charset=utf-8'}) resp.end(error_body) }) } const handleRedirect = (resp: HttpResponse, location: string) => { resp.cork(() => { resp.writeStatus('302 Found') commonWriteHeaders(resp, { location, }) resp.end() }) } const handleSuccess = (ctx: Pick<APIContext, 'headers' | 'resp' | 'responseHeaders'>, pathname: string, data: string | Buffer) => { const { resp, headers = {}, responseHeaders = {} } = ctx const tag = headers['if-none-match'] const newTag = data && etag(data) const txt = _.isText(pathname) const gz = txt && gzip && gzip_filter(pathname, data?.length) const type = _.getMimeType(pathname) + (txt ? '; charset=utf-8' : '') const range = headers['range']?.toString() if (tag && data && tag === newTag) { resp.cork(() => { resp.writeStatus("304 Not Modified") commonWriteHeaders(resp, responseHeaders) resp.endWithoutBody() }) return } if (range && data instanceof Buffer) { let [start = 0, end = 0] = range.replace(/[^\-\d]+/g, '').split('-').map(Number) end = end || (start + range_size) const d = Uint8Array.prototype.slice.call(data, start, end) end = Math.min(end, start + d.length) resp.cork(() => { resp.writeStatus('206 Partial Content') commonWriteHeaders(resp, { 'Content-Type': type, 'Content-Range': `bytes ${start}-${end - 1}/${data.length}`, 'Content-Length': d.length, 'Accept-Ranges': 'bytes', ...responseHeaders, }) resp.end(d) }) return } resp.cork(() => { resp.writeStatus('200 OK') const headers: OutgoingHttpHeaders = { 'Content-Type': type, 'Content-Encoding': gz ? 'gzip' : 'utf-8', 'Etag': newTag, ...responseHeaders, } if (cache_filter(pathname, data?.length)) { headers['Cache-Control'] = 'public, max-age=3600' headers['Last-Modified'] = new Date().toUTCString() } commonWriteHeaders(resp, headers) if (gz) { const temp = gzipStore.get(pathname) if (temp && temp.etag === newTag) { resp.end(temp.data) } else { const res = gzipSync(data) gzipStore.set(pathname, { data: res, etag: newTag }) resp.end(res) } } else { resp.end(data) } }) } /** * 处理目录响应 * @param resp 响应对象 * @param pathname 当前路径 * @param obj 当前路径映射内存对象 */ const handleDirectory = (resp: HttpResponse, pathname: string, obj: any) => { if (page_dir === false) { return handleNotFound(resp, pathname) } const files = [] if (_.isPlainObject(obj)) { for (let key in obj) { const isDir = _.isPlainObject(obj[key]) files.push({ name: key, path: _.pathname_arr(pathname).concat(key).join('/'), isDir, size: isDir ? 0 : (obj[key] && obj[key].length) }) } } const dir_body = _.renderHTML(page_dir, { title: '/' + pathname, pathname: _.pathname_dirname(pathname), files }) resp.cork(() => { resp.writeStatus('200 OK') commonWriteHeaders(resp, { 'Content-Type': 'text/html; charset=utf-8' }) resp.end(dir_body) }) } const handleSSE = (ctx: APIContext, item: RouteItem, body: any) => { const { resp } = ctx const { interval = 1000, interval_beat = 30000, default_content = '', } = item resp.cork(() => { resp.writeStatus('200 OK') commonWriteHeaders(resp, { 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Content-Type': 'text/event-stream', }) }) let interval1: Timer const heartBeat = function heartBeat () { resp.cork(() => { resp.write(`data:${default_content}\n\n`) }) if (interval_beat) { interval1 = setTimeout(heartBeat, interval_beat) } } let interval2: Timer const loop = async function loop () { try { const res = await item.handler(body, ctx) if (res) { resp.cork(() => { resp.write(`data:${JSON.stringify(res)}\n\n`) }) } } catch (e) { logger.error('SSE LOOP:', e) } if (interval) { interval2 = setTimeout(loop, interval) } } resp.onAborted(() => { clearTimeout(interval1) clearTimeout(interval2) }) loop() heartBeat() return false } return { handleSuccess, handleError, handleNotFound, handleDirectory, handleSSE, handleRedirect } }