Selaa lähdekoodia

Replaced servez with custom server.js. (#32654)

mrdoob 1 viikko sitten
vanhempi
sitoutus
2b45d4c1a2
5 muutettua tiedostoa jossa 394 lisäystä ja 839 poistoa
  1. 5 744
      package-lock.json
  2. 5 5
      package.json
  3. 3 1
      test/e2e/puppeteer.js
  4. 0 89
      test/e2e/server.js
  5. 381 0
      utils/server.js

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 5 - 744
package-lock.json


+ 5 - 5
package.json

@@ -48,10 +48,10 @@
     "build": "rollup -c utils/build/rollup.config.js",
     "build-module": "rollup -c utils/build/rollup.config.js --configOnlyModule",
     "build-docs": "jsdoc -c utils/docs/jsdoc.config.json",
-    "dev": "node utils/build/dev.js && servez -p 8080",
-    "dev-ssl": "node utils/build/dev.js && servez -p 8080 --ssl",
-    "preview": "concurrently --names \"ROLLUP,HTTP\" -c \"bgBlue.bold,bgGreen.bold\" \"rollup -c utils/build/rollup.config.js -w -m inline\" \"servez -p 8080\"",
-    "preview-ssl": "concurrently --names \"ROLLUP,HTTPS\" -c \"bgBlue.bold,bgGreen.bold\" \"rollup -c utils/build/rollup.config.js -w -m inline\" \"servez -p 8080 --ssl\"",
+    "dev": "node utils/build/dev.js && node utils/server.js -p 8080",
+    "dev-ssl": "node utils/build/dev.js && node utils/server.js -p 8080 --ssl",
+    "preview": "concurrently --names \"ROLLUP,HTTP\" -c \"bgBlue.bold,bgGreen.bold\" \"rollup -c utils/build/rollup.config.js -w -m inline\" \"node utils/server.js -p 8080\"",
+    "preview-ssl": "concurrently --names \"ROLLUP,HTTPS\" -c \"bgBlue.bold,bgGreen.bold\" \"rollup -c utils/build/rollup.config.js -w -m inline\" \"node utils/server.js -p 8080 --ssl\"",
     "lint-core": "eslint src",
     "lint-addons": "eslint examples/jsm",
     "lint-examples": "eslint examples",
@@ -111,7 +111,7 @@
     "puppeteer": "^24.25.0",
     "qunit": "^2.19.4",
     "rollup": "^4.6.0",
-    "servez": "^2.2.4"
+    "selfsigned": "^2.4.1"
   },
   "jspm": {
     "files": [

+ 3 - 1
test/e2e/puppeteer.js

@@ -2,7 +2,9 @@ import puppeteer from 'puppeteer';
 import pixelmatch from 'pixelmatch';
 import { Image } from './image.js';
 import * as fs from 'fs/promises';
-import server from './server.js';
+import { createServer } from '../../utils/server.js';
+
+const server = createServer();
 
 const exceptionList = [
 

+ 0 - 89
test/e2e/server.js

@@ -1,89 +0,0 @@
-import http from 'http';
-import path from 'path';
-import { createReadStream, existsSync, statSync } from 'fs';
-
-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',
-	'.mp3': 'audio/mpeg',
-	'.mp4': 'video/mp4',
-	'.webm': 'video/webm',
-	'.ogv': 'video/ogg',
-	'.ogg': 'audio/ogg',
-	'.woff': 'font/woff',
-	'.woff2': 'font/woff2',
-	'.ttf': 'font/ttf',
-	'.glb': 'model/gltf-binary',
-	'.gltf': 'model/gltf+json',
-	'.hdr': 'application/octet-stream',
-	'.exr': 'application/octet-stream',
-	'.fbx': 'application/octet-stream',
-	'.bin': 'application/octet-stream',
-	'.cube': 'text/plain'
-};
-
-const rootDirectory = path.resolve();
-
-const server = http.createServer( ( req, res ) => {
-
-	const pathname = decodeURIComponent( req.url.split( '?' )[ 0 ] );
-	const filePath = path.normalize( path.join( rootDirectory, pathname ) );
-
-	// Prevent path traversal attacks
-	if ( ! filePath.startsWith( rootDirectory ) ) {
-
-		res.writeHead( 403 );
-		res.end( 'Forbidden' );
-		return;
-
-	}
-
-	if ( ! existsSync( filePath ) ) {
-
-		res.writeHead( 404 );
-		res.end( 'File not found' );
-		return;
-
-	}
-
-	const ext = path.extname( filePath ).toLowerCase();
-	const contentType = mimeTypes[ ext ] || 'application/octet-stream';
-	const stat = statSync( filePath );
-	const fileSize = stat.size;
-	const range = req.headers.range;
-
-	if ( range ) {
-
-		const parts = range.replace( /bytes=/, '' ).split( '-' );
-		const start = parseInt( parts[ 0 ], 10 );
-		const end = parts[ 1 ] ? parseInt( parts[ 1 ], 10 ) : fileSize - 1;
-
-		res.writeHead( 206, {
-			'Content-Range': `bytes ${start}-${end}/${fileSize}`,
-			'Accept-Ranges': 'bytes',
-			'Content-Length': end - start + 1,
-			'Content-Type': contentType
-		} );
-
-		createReadStream( filePath, { start, end } ).pipe( res );
-
-	} else {
-
-		res.writeHead( 200, {
-			'Content-Length': fileSize,
-			'Content-Type': contentType
-		} );
-
-		createReadStream( filePath ).pipe( res );
-
-	}
-
-} );
-
-export default server;

+ 381 - 0
utils/server.js

@@ -0,0 +1,381 @@
+import http from 'http';
+import https from 'https';
+import path from 'path';
+import os from 'os';
+import { createReadStream, existsSync, statSync, readFileSync, mkdirSync, readdirSync, openSync, writeSync, closeSync, fstatSync, constants } from 'fs';
+
+function escapeHtml( str ) {
+
+	return str
+		.replace( /&/g, '&' )
+		.replace( /</g, '&lt;' )
+		.replace( />/g, '&gt;' )
+		.replace( /"/g, '&quot;' );
+
+}
+
+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',
+	'.mp3': 'audio/mpeg',
+	'.mp4': 'video/mp4',
+	'.webm': 'video/webm',
+	'.ogv': 'video/ogg',
+	'.ogg': 'audio/ogg',
+	'.woff': 'font/woff',
+	'.woff2': 'font/woff2',
+	'.ttf': 'font/ttf',
+	'.glb': 'model/gltf-binary',
+	'.gltf': 'model/gltf+json',
+	'.hdr': 'application/octet-stream',
+	'.exr': 'application/octet-stream',
+	'.fbx': 'application/octet-stream',
+	'.bin': 'application/octet-stream',
+	'.cube': 'text/plain',
+	'.wasm': 'application/wasm',
+	'.ktx2': 'image/ktx2'
+};
+
+function createHandler( rootDirectory ) {
+
+	return ( req, res ) => {
+
+		const pathname = decodeURIComponent( req.url.split( '?' )[ 0 ] );
+		let filePath = path.normalize( path.join( rootDirectory, pathname ) );
+
+		// Prevent path traversal attacks
+		if ( ! filePath.startsWith( rootDirectory ) ) {
+
+			res.writeHead( 403 );
+			res.end( 'Forbidden' );
+			return;
+
+		}
+
+		// Handle directories
+		if ( existsSync( filePath ) && statSync( filePath ).isDirectory() ) {
+
+			const indexPath = path.join( filePath, 'index.html' );
+
+			if ( existsSync( indexPath ) ) {
+
+				filePath = indexPath;
+
+			} else {
+
+				// Show directory listing
+				const files = readdirSync( filePath )
+					.filter( f => ! f.startsWith( '.' ) )
+					.sort( ( a, b ) => {
+
+						const aIsDir = statSync( path.join( filePath, a ) ).isDirectory();
+						const bIsDir = statSync( path.join( filePath, b ) ).isDirectory();
+						if ( aIsDir && ! bIsDir ) return - 1;
+						if ( ! aIsDir && bIsDir ) return 1;
+						return a.localeCompare( b );
+
+					} );
+
+				const base = pathname.endsWith( '/' ) ? pathname : pathname + '/';
+				const items = files.map( file => {
+
+					const fullPath = path.join( filePath, file );
+					const isDir = statSync( fullPath ).isDirectory();
+					const safeFile = escapeHtml( file );
+					const safeHref = escapeHtml( base + file + ( isDir ? '/' : '' ) );
+					const icon = isDir ? '📁' : '📄';
+					return `<a href="${safeHref}"><span class="i">${icon}</span>${safeFile}</a>`;
+
+				} ).join( '\n' );
+
+				const safePath = escapeHtml( pathname );
+				const html = `<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width">
+<meta name="color-scheme" content="light dark">
+<title>Index of ${safePath}</title>
+<style>
+body { font-family: system-ui, sans-serif; margin: 20px; }
+h1 { font-weight: normal; font-size: 1.2em; margin-bottom: 10px; }
+a { display: block; padding: 6px 8px; color: inherit; text-decoration: none; border-radius: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+a:hover { background: rgba(128,128,128,0.2); }
+.i { display: inline-block; width: 1.5em; }
+</style>
+</head>
+<body>
+<h1>Index of ${safePath}</h1>
+${pathname !== '/' ? '<a href="../"><span class="i">📁</span>..</a>' : ''}
+${items}
+</body>
+</html>`;
+
+				res.writeHead( 200, { 'Content-Type': 'text/html' } );
+				res.end( html );
+				return;
+
+			}
+
+		}
+
+		if ( ! existsSync( filePath ) ) {
+
+			res.writeHead( 404 );
+			res.end( 'File not found' );
+			return;
+
+		}
+
+		const ext = path.extname( filePath ).toLowerCase();
+		const contentType = mimeTypes[ ext ] || 'application/octet-stream';
+		const stat = statSync( filePath );
+		const fileSize = stat.size;
+		const range = req.headers.range;
+
+		if ( range ) {
+
+			const parts = range.replace( /bytes=/, '' ).split( '-' );
+			const start = parseInt( parts[ 0 ], 10 );
+			const end = parts[ 1 ] ? parseInt( parts[ 1 ], 10 ) : fileSize - 1;
+
+			res.writeHead( 206, {
+				'Content-Range': `bytes ${start}-${end}/${fileSize}`,
+				'Accept-Ranges': 'bytes',
+				'Content-Length': end - start + 1,
+				'Content-Type': contentType
+			} );
+
+			createReadStream( filePath, { start, end } ).pipe( res );
+
+		} else {
+
+			res.writeHead( 200, {
+				'Content-Length': fileSize,
+				'Content-Type': contentType
+			} );
+
+			createReadStream( filePath ).pipe( res );
+
+		}
+
+	};
+
+}
+
+function getCacheDir() {
+
+	const appName = 'three-dev-server';
+
+	if ( process.platform === 'darwin' ) {
+
+		return path.join( os.homedir(), 'Library', 'Application Support', appName );
+
+	} else if ( process.platform === 'win32' ) {
+
+		return path.join( process.env.LOCALAPPDATA || process.env.APPDATA, appName );
+
+	} else {
+
+		return path.join( os.homedir(), '.config', appName );
+
+	}
+
+}
+
+async function getCertificate() {
+
+	// Cache certificate in platform-specific data directory
+	const cacheDir = getCacheDir();
+	const certPath = path.join( cacheDir, 'cert.pem' );
+	const keyPath = path.join( cacheDir, 'key.pem' );
+
+	// Try to use cached certificate (valid for 7 days)
+	try {
+
+		const certFd = openSync( certPath, constants.O_RDONLY );
+		const stat = fstatSync( certFd );
+		const age = Date.now() - stat.mtimeMs;
+		const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days
+
+		if ( age < maxAge ) {
+
+			const cert = readFileSync( certFd, 'utf8' );
+			closeSync( certFd );
+			const key = readFileSync( keyPath, 'utf8' );
+			return { cert, key };
+
+		}
+
+		closeSync( certFd );
+
+	} catch ( e ) {
+
+		// Cache miss or invalid, generate new certificate
+
+	}
+
+	// Generate new self-signed certificate using selfsigned
+	let selfsigned;
+	try {
+
+		selfsigned = ( await import( 'selfsigned' ) ).default;
+
+	} catch ( e ) {
+
+		console.error( 'For HTTPS support, install selfsigned: npm install selfsigned' );
+		process.exit( 1 );
+
+	}
+
+	const attrs = [ { name: 'commonName', value: 'localhost' } ];
+	const pems = selfsigned.generate( attrs, {
+		algorithm: 'sha256',
+		days: 30,
+		keySize: 2048,
+		extensions: [
+			{ name: 'keyUsage', keyCertSign: true, digitalSignature: true, keyEncipherment: true },
+			{ name: 'subjectAltName', altNames: [
+				{ type: 2, value: 'localhost' },
+				{ type: 7, ip: '127.0.0.1' }
+			] }
+		]
+	} );
+
+	// Cache the certificate with restrictive permissions
+	try {
+
+		mkdirSync( cacheDir, { recursive: true, mode: 0o700 } );
+
+		const certFd = openSync( certPath, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC, 0o600 );
+		writeSync( certFd, pems.cert );
+		closeSync( certFd );
+
+		const keyFd = openSync( keyPath, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC, 0o600 );
+		writeSync( keyFd, pems.private );
+		closeSync( keyFd );
+
+	} catch ( e ) {
+
+		// Caching failed, but certificate is still valid for this session
+
+	}
+
+	return { cert: pems.cert, key: pems.private };
+
+}
+
+export function createServer( options = {} ) {
+
+	const rootDirectory = options.root || path.resolve();
+	const handler = createHandler( rootDirectory );
+
+	return http.createServer( handler );
+
+}
+
+function tryListen( server, port, maxAttempts = 20 ) {
+
+	return new Promise( ( resolve, reject ) => {
+
+		let attempts = 0;
+
+		const onError = ( err ) => {
+
+			if ( err.code === 'EADDRINUSE' && attempts < maxAttempts ) {
+
+				attempts ++;
+				server.listen( port + attempts );
+
+			} else {
+
+				reject( err );
+
+			}
+
+		};
+
+		const onListening = () => {
+
+			server.off( 'error', onError );
+			resolve( server.address().port );
+
+		};
+
+		server.once( 'error', onError );
+		server.once( 'listening', onListening );
+		server.listen( port );
+
+	} );
+
+}
+
+// CLI mode
+const isMain = process.argv[ 1 ] && path.resolve( process.argv[ 1 ] ) === path.resolve( import.meta.url.replace( 'file://', '' ) );
+
+if ( isMain ) {
+
+	const args = process.argv.slice( 2 );
+	const requestedPort = parseInt( args.find( ( _, i, arr ) => arr[ i - 1 ] === '-p' ) || '8080', 10 );
+	const useSSL = args.includes( '--ssl' );
+	const rootDirectory = path.resolve();
+
+	const protocol = useSSL ? 'https' : 'http';
+	const handler = createHandler( rootDirectory );
+
+	let server;
+
+	if ( useSSL ) {
+
+		const credentials = await getCertificate();
+		server = https.createServer( credentials, handler );
+
+	} else {
+
+		server = http.createServer( handler );
+
+	}
+
+	const port = await tryListen( server, requestedPort );
+
+	if ( port !== requestedPort ) {
+
+		console.log( `\x1b[33mPort ${requestedPort} in use, using ${port} instead.\x1b[0m` );
+
+	}
+
+	console.log( `\x1b[32mServer running at ${protocol}://localhost:${port}/\x1b[0m` );
+
+	// Show network addresses
+	const interfaces = os.networkInterfaces();
+	for ( const name of Object.keys( interfaces ) ) {
+
+		for ( const net of interfaces[ name ] ) {
+
+			if ( net.family === 'IPv4' && ! net.internal ) {
+
+				console.log( `  ${protocol}://${net.address}:${port}/` );
+
+			}
+
+		}
+
+	}
+
+	console.log( '\nPress Ctrl+C to stop.' );
+
+	process.on( 'SIGINT', () => {
+
+		console.log( '\nShutting down...' );
+		server.close();
+		process.exit( 0 );
+
+	} );
+
+}

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä

粤ICP备19079148号