UNPKG

morpress.js

Version:

A simple API framework without dependencies

268 lines (235 loc) 9.73 kB
const http = require('http'); const url = require('url'); const querystring = require('querystring'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); class Morpress { constructor() { this.routes = { GET: {}, POST: {}, PUT: {}, DELETE: {}, PATCH: {}, ALL: {} }; this.middlewares = []; this.routers = []; this.staticDirs = []; this.errorHandler = null; this.server = http.createServer(this.requestHandler.bind(this)); this.session = { store: new Map() }; // Session storage this.trustProxy = false; this.rateLimits = new Map(); // Rate limits storage this.rateLimitOptions = { windowMs: 15 * 60 * 1000, // 15 minutes max: 100 // Maximum 100 requests }; this.csrfTokens = new Map(); this.templateEngine = null; this.csrfProtection = false; } use(path, middlewareOrRouter) { if (typeof path === 'string' && middlewareOrRouter instanceof Router) { this.routers.push({ path, router: middlewareOrRouter }); } else if (typeof path === 'string' && typeof middlewareOrRouter === 'function') { this.routes.ALL[path] = middlewareOrRouter; } else if (typeof path === 'function') { this.middlewares.push(path); // Global middleware } else if (middlewareOrRouter instanceof Router) { this.routers.push({ path, router: middlewareOrRouter }); } } get(path, handler) { this.routes.GET[this.normalizePath(path)] = handler; } post(path, handler) { this.routes.POST[this.normalizePath(path)] = handler; } put(path, handler) { this.routes.PUT[this.normalizePath(path)] = handler; } delete(path, handler) { this.routes.DELETE[this.normalizePath(path)] = handler; } patch(path, handler) { this.routes.PATCH[this.normalizePath(path)] = handler; } normalizePath(path) { return path.endsWith('/') ? path.slice(0, -1) : path; } static(path) { this.staticDirs.push(path); } trustProxy(value) { this.trustProxy = value; } sessionOptions(options) { this.session = { ...this.session, ...options }; } rateLimit(options) { this.rateLimitOptions = { ...this.rateLimitOptions, ...options }; } csrfProtection() { this.csrfProtection = true; } templateEngine(engine) { this.templateEngine = engine; } async requestHandler(req, res) { const { method, url: requestUrl } = req; const { pathname, query } = url.parse(requestUrl); req.query = querystring.parse(query); req.params = {}; // Apply body parsers req.body = ''; req.on('data', chunk => { req.body += chunk.toString(); }); req.on('end', async () => { this.setupResponseHelpers(res); // Security Measures const sessionID = req.headers['x-session-id']; if (this.csrfProtection && req.method === 'POST') { const csrfToken = req.headers['x-csrf-token']; const validToken = this.csrfTokens.get(sessionID); if (!csrfToken || csrfToken !== validToken) { return res.status(403).json({ error: 'Forbidden' }); } } // Rate Limiting if (this.rateLimitOptions) { const ip = this.trustProxy ? req.headers['x-forwarded-for'] || req.connection.remoteAddress : req.connection.remoteAddress; const now = Date.now(); const windowMs = this.rateLimitOptions.windowMs; const max = this.rateLimitOptions.max; if (!this.rateLimits.has(ip)) { this.rateLimits.set(ip, []); } const timestamps = this.rateLimits.get(ip); timestamps.push(now); this.rateLimits.set(ip, timestamps.filter(timestamp => now - timestamp < windowMs)); if (timestamps.length > max) { res.status(429).json({ error: 'Too Many Requests' }); return; } } try { // Global Middlewares for (const middleware of this.middlewares) { await new Promise((resolve, reject) => { middleware(req, res, err => (err ? reject(err) : resolve())); }); } // Static File Serving for (const dir of this.staticDirs) { const filePath = path.join(dir, pathname); if (fs.existsSync(filePath)) { return res.sendFile(filePath); } } // Check Routers for (const { path, router } of this.routers) { if (pathname === path || pathname.startsWith(`${path}/`)) { const subPath = pathname.slice(path.length) || '/'; req.url = subPath; return router.handle(req, res); } } // Route Handling const normalizedPath = this.normalizePath(pathname); const handler = this.routes[method][normalizedPath] || this.routes.ALL[normalizedPath]; if (handler) { return handler(req, res); } else { res.status(404).json({ error: 'Not Found' }); } } catch (err) { if (this.errorHandler) { this.errorHandler(err, req, res); } else { res.status(500).json({ error: 'Internal Server Error' }); } } }); } setupResponseHelpers(res) { res.statusCode = 200; res.json = (obj) => { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(obj)); }; res.send = (body) => { res.setHeader('Content-Type', typeof body === 'string' ? 'text/plain' : 'application/json'); res.end(typeof body === 'string' ? body : JSON.stringify(body)); }; res.status = (code) => { res.statusCode = code; return res; }; res.redirect = (location) => { res.statusCode = 302; res.setHeader('Location', location); res.end(); }; res.sendFile = (filePath) => { fs.readFile(filePath, (err, data) => { if (err) { res.status(404).json({ error: 'File Not Found' }); } else { const ext = path.extname(filePath).slice(1); const mimeTypes = { html: 'text/html', js: 'application/javascript', css: 'text/css', json: 'application/json', png: 'image/png', jpg: 'image/jpeg', gif: 'image/gif', svg: 'image/svg+xml' }; res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream'); res.end(data); } }); }; res.render = (view, locals) => { if (!this.templateEngine) { throw new Error('No template engine configured'); } this.templateEngine.render(view, locals, (err, str) => { if (err) { res.status(500).json({ error: 'Template rendering error' }); } else { res.setHeader('Content-Type', 'text/html'); res.end(str); } }); }; } listen(port, callback) { this.server.listen(port, callback); } setErrorHandler(handler) { this.errorHandler = handler; } } class Router { constructor() { this.routes = { GET: {}, POST: {}, PUT: {}, DELETE: {}, PATCH: {}, ALL: {} }; this.middlewares = []; } use(path, middleware) { if (typeof path === 'string') { this.routes.ALL[this.normalizePath(path)] = middleware; } else { this.middlewares.push(path); } } get(path, handler) { this.routes.GET[this.normalizePath(path)] = handler; } post(path, handler) { this.routes.POST[this.normalizePath(path)] = handler; } put(path, handler) { this.routes.PUT[this.normalizePath(path)] = handler; } delete(path, handler) { this.routes.DELETE[this.normalizePath(path)] = handler; } patch(path, handler) { this.routes.PATCH[this.normalizePath(path)] = handler; } normalizePath(path) { return path.endsWith('/') ? path.slice(0, -1) : path; } async handle(req, res) { const { method, url: requestUrl } = req; const { pathname } = url.parse(requestUrl); // Apply middlewares for (const middleware of this.middlewares) { await new Promise((resolve, reject) => { middleware(req, res, err => (err ? reject(err) : resolve())); }); } // Route handling const normalizedPath = this.normalizePath(pathname); const handler = this.routes[method][normalizedPath] || this.routes.ALL[normalizedPath]; if (handler) { handler(req, res); } else { res.status(404).json({ error: 'Not Found' }); } } } module.exports = { Morpress, Router };