diff --git a/index.html b/index.html
index 8f785da..f730f2d 100644
--- a/index.html
+++ b/index.html
@@ -12,6 +12,42 @@
Portfolio Florian Sylvain - Développeur web fullstack
+
+
diff --git a/server.js b/server.js
index 9a09150..81d9606 100644
--- a/server.js
+++ b/server.js
@@ -7,654 +7,380 @@ const crypto = require("crypto");
const PORT = process.env.PORT || 3000;
const ROOT = __dirname;
-const cspHashes = {
- scripts: new Set(),
- styles: new Set(),
-};
-
-function calculateHash(content, algorithm = "sha256") {
- return crypto
- .createHash(algorithm)
- .update(content, "utf8")
- .digest("base64");
-}
-
-function extractInlineContent(html) {
- const scriptMatches =
- html.match(/`
+ ``
);
}
- extractInlineContent(processedContent);
+ if (minifier) htmlContent = minifier(htmlContent);
+ this.extractCSPHashes(htmlContent);
+ processedContent = Buffer.from(htmlContent, "utf8");
+ } else if (minifier) {
+ processedContent = Buffer.from(
+ minifier(content.toString("utf8")),
+ "utf8"
+ );
}
- body = minify
- ? Buffer.from(minify(processedContent), "utf8")
- : Buffer.from(processedContent, "utf8");
- }
+ const compressed = await this.compress(processedContent);
- let sriHash = null;
- if (relPath === "style.css" || relPath === "script.js") {
- sriHash = generateSRIHash(body.toString("utf8"));
- console.log(`SRI hash for ${relPath}: sha384-${sriHash}`);
- }
-
- const compressed = await preCompress(body);
- const stats = await fs.promises.stat(abs);
-
- const originalSize = src.length;
- const processedSize = body.length;
- const brSize = compressed.br?.length || 0;
- const gzSize = compressed.gz?.length || 0;
-
- console.log(`${relPath}:`);
- console.log(` Original: ${originalSize} bytes`);
- if (!isBinaryFile && minify)
- console.log(
- ` Minified: ${processedSize} bytes (${(
- ((originalSize - processedSize) / originalSize) *
- 100
- ).toFixed(1)}% reduction)`
- );
- if (brSize)
- console.log(
- ` Brotli: ${brSize} bytes (${(
- (brSize / originalSize) *
- 100
- ).toFixed(1)}% of original)`
- );
- if (gzSize)
- console.log(
- ` Gzip: ${gzSize} bytes (${((gzSize / originalSize) * 100).toFixed(
- 1
- )}% of original)`
- );
- console.log("");
-
- const entry = {
- path: relPath,
- mtimeMs: stats.mtimeMs,
- type: contentTypeFor(relPath),
- cache: cacheControlFor(relPath),
- body,
- etag: etag(body),
- br: compressed.br,
- brEtag: compressed.br ? etag(compressed.br) : undefined,
- gz: compressed.gz,
- gzEtag: compressed.gz ? etag(compressed.gz) : undefined,
- sri: sriHash,
- };
-
- files.set("/" + relPath.replace(/\\/g, "/"), entry);
-}
-
-async function build() {
- console.log("Building and compressing files...\n");
-
- await loadAndMinify("index.html", minifyHTML);
- await loadAndMinify("style.css", minifyCSS);
- await loadAndMinify("script.js", minifyJS);
-
- for (const f of [
- "favicon.webp",
- "profile.webp",
- "robots.txt",
- "sitemap.xml",
- ]) {
- if (fs.existsSync(path.join(ROOT, f))) {
- await loadAndMinify(f, null);
- }
- }
- const cspPolicy = generateCSP();
- files.set("__csp__", cspPolicy);
-
- console.log("CSS and JavaScript have been inlined into HTML automatically");
- console.log(
- "CSP policy generated with secure hashes for all inline content"
- );
-}
-
-function setSecurityHeaders(res) {
- const cspPolicy = files.get("__csp__");
-
- res.setHeader("X-Content-Type-Options", "nosniff");
- res.setHeader("X-Frame-Options", "DENY");
- res.setHeader("X-XSS-Protection", "1; mode=block");
- res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
- res.setHeader(
- "Strict-Transport-Security",
- "max-age=31536000; includeSubDomains; preload"
- );
- res.setHeader(
- "Permissions-Policy",
- "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), speaker=()"
- );
-
- if (cspPolicy) {
- res.setHeader("Content-Security-Policy", cspPolicy);
- }
-}
-
-function negotiateEncoding(req) {
- const ae = req.headers["accept-encoding"] || "";
- const hasBr = /\bbr\b/.test(ae);
- const hasGz = /\bgzip\b/.test(ae);
- return { br: hasBr, gz: hasGz };
-}
-
-function serveEntry(req, res, entry) {
- setSecurityHeaders(res);
- res.setHeader("Content-Type", entry.type);
- res.setHeader("Cache-Control", entry.cache);
- res.setHeader("Vary", "Accept-Encoding");
-
- const enc = negotiateEncoding(req);
- let body = entry.body;
- let tag = entry.etag;
-
- if (enc.br && entry.br) {
- body = entry.br;
- tag = entry.brEtag;
- res.setHeader("Content-Encoding", "br");
- } else if (enc.gz && entry.gz) {
- body = entry.gz;
- tag = entry.gzEtag;
- res.setHeader("Content-Encoding", "gzip");
- }
-
- res.setHeader("ETag", tag);
- if (req.headers["if-none-match"] === tag) {
- res.statusCode = 304;
- return res.end();
- }
-
- res.statusCode = 200;
- res.end(body);
-}
-
-function notFound(req, res) {
- setSecurityHeaders(res);
- res.statusCode = 404;
- res.setHeader("Content-Type", "text/plain; charset=utf-8");
- res.end("404 Not Found");
-}
-
-function handler(req, res) {
- const url = new URL(req.url, `http://${req.headers.host}`);
- let pathname = url.pathname;
- if (pathname === "/") pathname = "/index.html";
- const entry = files.get(pathname);
- if (entry) return serveEntry(req, res, entry);
-
- const fallback = path.join(ROOT, pathname);
- if (fs.existsSync(fallback) && fs.statSync(fallback).isFile()) {
- const raw = fs.readFileSync(fallback);
- const tmpEntry = {
- type: contentTypeFor(fallback),
- cache: cacheControlFor(fallback),
- body: raw,
- etag: etag(raw),
+ return {
+ path: "/" + relPath.replace(/\\/g, "/"),
+ mtimeMs: stats.mtimeMs,
+ type: this.getContentType(relPath),
+ cache: this.getCacheControl(relPath),
+ body: processedContent,
+ etag: this.etag(processedContent),
+ br: compressed.br,
+ brEtag: compressed.br ? this.etag(compressed.br) : undefined,
+ gz: compressed.gz,
+ gzEtag: compressed.gz ? this.etag(compressed.gz) : undefined,
};
- return serveEntry(req, res, tmpEntry);
}
- return notFound(req, res);
+ async build() {
+ console.log("Building assets...");
+
+ const tasks = [
+ { file: "index.html", minifier: this.minifyHTML.bind(this) },
+ { file: "style.css", minifier: this.minifyCSS.bind(this) },
+ { file: "script.js", minifier: this.minifyJS.bind(this) },
+ { file: "favicon.webp" },
+ { file: "profile.webp" },
+ { file: "robots.txt" },
+ { file: "sitemap.xml" },
+ ];
+
+ await Promise.allSettled(
+ tasks.map(async ({ file, minifier }) => {
+ if (fs.existsSync(path.join(ROOT, file))) {
+ const entry = await this.processFile(file, minifier);
+ this.files.set(entry.path, entry);
+ return `${file}: processed`;
+ }
+ return `${file}: skipped`;
+ })
+ );
+
+ this.files.set("__csp__", this.generateCSP());
+ console.log("Build complete");
+ }
+
+ setSecurityHeaders(res) {
+ const headers = {
+ "X-Content-Type-Options": "nosniff",
+ "X-Frame-Options": "DENY",
+ "X-XSS-Protection": "1; mode=block",
+ "Referrer-Policy": "strict-origin-when-cross-origin",
+ "Strict-Transport-Security":
+ "max-age=31536000; includeSubDomains; preload",
+ "Permissions-Policy":
+ "geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), speaker=()",
+ "Content-Security-Policy": this.files.get("__csp__"),
+ };
+
+ Object.entries(headers).forEach(([key, value]) => {
+ if (value) res.setHeader(key, value);
+ });
+ }
+
+ serveFile(req, res, entry) {
+ this.setSecurityHeaders(res);
+ res.setHeader("Content-Type", entry.type);
+ res.setHeader("Cache-Control", entry.cache);
+ res.setHeader("Vary", "Accept-Encoding");
+
+ const acceptEncoding = req.headers["accept-encoding"] || "";
+ let { body, etag: tag } = entry;
+
+ if (/\bbr\b/.test(acceptEncoding) && entry.br) {
+ body = entry.br;
+ tag = entry.brEtag;
+ res.setHeader("Content-Encoding", "br");
+ } else if (/\bgzip\b/.test(acceptEncoding) && entry.gz) {
+ body = entry.gz;
+ tag = entry.gzEtag;
+ res.setHeader("Content-Encoding", "gzip");
+ }
+
+ res.setHeader("ETag", tag);
+
+ if (req.headers["if-none-match"] === tag) {
+ res.statusCode = 304;
+ return res.end();
+ }
+
+ res.statusCode = 200;
+ res.end(body);
+ }
+
+ handleRequest(req, res) {
+ const url = new URL(req.url, `http://${req.headers.host}`);
+ let pathname = url.pathname === "/" ? "/index.html" : url.pathname;
+
+ const entry = this.files.get(pathname);
+ if (entry) return this.serveFile(req, res, entry);
+
+ const fallbackPath = path.join(ROOT, pathname);
+ if (fs.existsSync(fallbackPath) && fs.statSync(fallbackPath).isFile()) {
+ const content = fs.readFileSync(fallbackPath);
+ const tempEntry = {
+ type: this.getContentType(fallbackPath),
+ cache: this.getCacheControl(fallbackPath),
+ body: content,
+ etag: this.etag(content),
+ };
+ return this.serveFile(req, res, tempEntry);
+ }
+
+ this.setSecurityHeaders(res);
+ res.statusCode = 404;
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
+ res.end("404 Not Found");
+ }
}
async function start() {
- await build();
- const server = http.createServer(handler);
+ const processor = new AssetProcessor();
+ await processor.build();
+
+ const server = http.createServer((req, res) =>
+ processor.handleRequest(req, res)
+ );
server.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
}
start().catch((err) => {
- console.error("Failed to start server:", err);
+ console.error("Server failed:", err);
process.exit(1);
});