forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			152 lines
		
	
	
	
		
			5.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			152 lines
		
	
	
	
		
			5.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import http from "node:http"
 | |
| 
 | |
| export interface Handler {
 | |
|     mustMatch: string | RegExp
 | |
|     mimetype: string
 | |
|     addHeaders?: Record<string, string>
 | |
|     handle: (
 | |
|         path: string,
 | |
|         queryParams: URLSearchParams,
 | |
|         req: http.IncomingMessage,
 | |
|         body: string | undefined
 | |
|     ) => Promise<string>
 | |
| }
 | |
| 
 | |
| class ServerUtils {
 | |
|     public static getBody(req: http.IncomingMessage): Promise<string> {
 | |
|         return new Promise<string>((resolve) => {
 | |
|             let body = ""
 | |
|             req.on("data", (chunk) => {
 | |
|                 body += chunk
 | |
|             })
 | |
|             req.on("end", () => {
 | |
|                 resolve(body)
 | |
|             })
 | |
|         })
 | |
|     }
 | |
| }
 | |
| 
 | |
| export class Server {
 | |
|     constructor(
 | |
|         port: number,
 | |
|         options: {
 | |
|             ignorePathPrefix?: string[]
 | |
|         },
 | |
|         handle: Handler[]
 | |
|     ) {
 | |
|         handle.push({
 | |
|             mustMatch: "",
 | |
|             mimetype: "text/html",
 | |
|             handle: async () => {
 | |
|                 return `<html><body>Supported endpoints are <ul>${handle
 | |
|                     .filter((h) => h.mustMatch !== "")
 | |
|                     .map((h) => {
 | |
|                         let l = h.mustMatch
 | |
|                         if (typeof h.mustMatch === "string") {
 | |
|                             l = `<a href='${l}'>${l}</a>`
 | |
|                         }
 | |
|                         return "<li>" + l + "</li>"
 | |
|                     })
 | |
|                     .join("")}</ul></body></html>`
 | |
|             },
 | |
|         })
 | |
|         http.createServer(async (req: http.IncomingMessage, res) => {
 | |
|             try {
 | |
|                 const url = new URL(`http://127.0.0.1/` + req.url)
 | |
|                 let path = url.pathname
 | |
|                 while (path.startsWith("/")) {
 | |
|                     path = path.substring(1)
 | |
|                 }
 | |
|                 console.log(
 | |
|                     req.method + " " + req.url,
 | |
|                     "from:",
 | |
|                     req.headers.origin,
 | |
|                     new Date().toISOString(),
 | |
|                     path
 | |
|                 )
 | |
|                 if (options?.ignorePathPrefix) {
 | |
|                     for (const toIgnore of options.ignorePathPrefix) {
 | |
|                         if (path.startsWith(toIgnore)) {
 | |
|                             path = path.substring(toIgnore.length + 1)
 | |
|                             break
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|                 const handler = handle.find((h) => {
 | |
|                     if (typeof h.mustMatch === "string") {
 | |
|                         return h.mustMatch === path
 | |
|                     }
 | |
|                     if (path.match(h.mustMatch)) {
 | |
|                         return true
 | |
|                     }
 | |
|                 })
 | |
| 
 | |
|                 if (handler === undefined || handler === null) {
 | |
|                     res.writeHead(404, { "Content-Type": "text/html" })
 | |
|                     res.write("<html><body><p>Not found...</p></body></html>")
 | |
|                     res.end()
 | |
|                     return
 | |
|                 }
 | |
| 
 | |
|                 res.setHeader(
 | |
|                     "Access-Control-Allow-Headers",
 | |
|                     "Origin, X-Requested-With, Content-Type, Accept"
 | |
|                 )
 | |
|                 res.setHeader("Access-Control-Allow-Origin", req.headers.origin ?? "*")
 | |
|                 if (req.method === "OPTIONS") {
 | |
|                     res.setHeader(
 | |
|                         "Access-Control-Allow-Methods",
 | |
|                         "POST, GET, OPTIONS, DELETE, UPDATE"
 | |
|                     )
 | |
|                     res.writeHead(204, { "Content-Type": handler.mimetype })
 | |
|                     res.end()
 | |
|                     return
 | |
|                 }
 | |
|                 let body: string | undefined = undefined
 | |
|                 if (req.method === "POST" || req.method === "UPDATE") {
 | |
|                     body = await ServerUtils.getBody(req)
 | |
|                 }
 | |
| 
 | |
|                 if (req.method === "DELETE") {
 | |
|                     return
 | |
|                 }
 | |
| 
 | |
|                 try {
 | |
|                     const result = await handler.handle(path, url.searchParams, req, body)
 | |
|                     if (result === undefined) {
 | |
|                         res.writeHead(500)
 | |
|                         res.write("Could not fetch this website, probably blocked by them")
 | |
|                         res.end()
 | |
|                         return
 | |
|                     }
 | |
|                     if (typeof result !== "string") {
 | |
|                         console.error(
 | |
|                             "Internal server error: handling",
 | |
|                             url,
 | |
|                             "resulted in a ",
 | |
|                             typeof result,
 | |
|                             " instead of a string:",
 | |
|                             result
 | |
|                         )
 | |
|                     }
 | |
|                     const extraHeaders = handler.addHeaders ?? {}
 | |
|                     res.writeHead(200, { "Content-Type": handler.mimetype, ...extraHeaders })
 | |
|                     res.write("" + result)
 | |
|                     res.end()
 | |
|                 } catch (e) {
 | |
|                     console.error("Could not handle request:", e)
 | |
|                     res.writeHead(500)
 | |
|                     res.write(e)
 | |
|                     res.end()
 | |
|                 }
 | |
|             } catch (e) {
 | |
|                 console.error("FATAL:", e)
 | |
|                 res.end()
 | |
|             }
 | |
|         }).listen(port)
 | |
|         console.log(
 | |
|             "Server is running on port " + port,
 | |
|             ". Supported endpoints are: " + handle.map((h) => h.mustMatch).join(", ")
 | |
|         )
 | |
|     }
 | |
| }
 |