morpress.js
Version:
A simple API framework without dependencies
268 lines (235 loc) • 9.73 kB
JavaScript
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 };