#!/usr/bin/env node import http from 'http' ; import fs from 'fs' ; import os from 'os' ; import path from 'path' ; import axios from 'axios'; import url from 'url'; import {string_head, str_headSplit, fs_readDir } from 'sr_common'; main( ) ; // ------------------------------------- main ------------------------------------- async function main( ) { const ipAddr = undefined; const port = parseInt(process.argv[2]) || 9000; // seqnbr assigned to each call to phpProxy. Used to link request and response. let proxy_seqNum = 0 ; // each request and response are saved in the serveHistory array. let serveHistoryArr = [] ; // read server settings from .proxy-server-settings.json const serverSettings = await permSettings_recall( ) ; console.log( `cwd: ${process.cwd( )}`); console.log( `serverSettings: ${JSON.stringify(serverSettings)}`); http.createServer( async function (req, res) { // let { body, isBinary, contentType, body_text } = await gatherRequestBody(req) ; const body = await gatherRequestBody(req) ; console.log(`${req.method} ${req.url} ${body.contentType}`); // parse URL // const parsedUrl = url.parse( str_decodeURI( req.url )); const parsedUrl = url.parse( req.url ); // extract URL path let pathname = `${parsedUrl.pathname}`; // based on the URL path, extract the file extension. e.g. .js, .doc, ... const ext = path.parse(pathname).ext; const {part1, part2 } = str_headSplit(pathname,11) ; if (req.method === "OPTIONS") { const headers = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "*", "Access-Control-Allow-Methods": "OPTIONS, POST, GET", "Access-Control-Max-Age": 2592000, // 30 days }; res.writeHead(204, headers); res.end(); } else if (( part1 == '/sps-config') && ( part2 == '/update')) { let errmsg = '' ; let content_text = body.body_text ; if (body.contentType == 'application/x-www-form-urlencoded') { content_text = decodeURIComponent(content_text); } const content = JSON.parse( content_text) ; if ( content.proxyServerURL ) serverSettings.proxyServerURL = content.proxyServerURL; if ( content.homePath ) { serverSettings.homePath = content.homePath; } serverSettings.logContent = content.logContent || false ; serverSettings.proxyJS = content.proxyJS || false ; await permSettings_store( serverSettings ) ; const rv = {errmsg} ; res.writeHead(200, { 'Content-Type': 'application/json' }); res.write(JSON.stringify(rv)); res.end(); } else if ((part1 == '/sps-config') && (part2 == '/getSettings')) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.write(JSON.stringify( serverSettings )); res.end(); } else if ( part1 == '/sps-config') { const p1 = process.cwd( ) ; const p2 = '/pages/config'; const configPath = path.join(p1, p2, part2); console.log(`configPath:${configPath}`); await staticServe(configPath, req, res); } else if ( part1 == '/sps-tester') { const p1 = process.cwd( ) ; const p2 = '/pages/tester'; const testerPath = path.join(p1, p2, part2); console.log(`testerPath:${testerPath}`); await staticServe(testerPath, req, res); } // request is for a .php file. Proxy the request according to settings // specified in serverSettings. else if ((ext == '.php') || ( ext == '.js' && serverSettings.proxyJS )) { proxy_seqNum += 1 ; await phpProxy(pathname, serverSettings, body, req, res, proxy_seqNum ) ; } // respond to request by serving the static file from home path. else { const rltvPathName = `${serverSettings.homePath}${pathname}`; await staticServe( rltvPathName, req, res); } }).listen( port, ipAddr) ; console.log(`Server listening on port ${port}`); } // ------------------------------- extToContentType ------------------------------- function extToContentType( ext ) { // maps file extension to MIME typere const map = { '.ico': 'image/x-icon', '.html': 'text/html', '.js': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.png': 'image/png', '.jpg': 'image/jpeg', '.wav': 'audio/wav', '.mp3': 'audio/mpeg', '.svg': 'image/svg+xml', '.pdf': 'application/pdf', '.doc': 'application/msword' }; return map[ext] || 'text/plain'; } // ------------------------------- extToDataType ------------------------------- function extToDataType(ext) { // maps file extension to MIME typere const map = { '.ico': 'binary', '.html': 'utf8', '.js': 'utf8', '.json': 'utf8', '.css': 'utf8', '.png': 'binary', '.jpg': 'binary', '.wav': 'binary', '.mp3': 'binary', '.svg': 'utf8', '.pdf': 'binary', '.doc': 'binary' }; return map[ext] || 'binary'; } // ----------------------------------- phpProxy ----------------------------------- // request from browser to be proxied up to actual server. // axios is used to sent the request to that actual server. // Receive response from the server and relay that response to the browser. // type type_gatherReturn = { body: Buffer, isBinary: boolean, contentType: string, body_text: string }; async function phpProxy( pathname:string, serverSettings:any, body : type_gatherReturn, req:any, res:any, proxy_seqNum:number ) { let proxy_url ; if ( string_head(pathname,7) == 'http://') proxy_url = pathname ; else proxy_url = `${serverSettings.proxyServerURL}${pathname}`; if ( serverSettings.logContent ) { if ( req.method == 'POST') { console.log(`phpProxy. proxy_url:${proxy_url}`); console.log(`phpProxy. seqnum:${proxy_seqNum} postText:${body.body_text}`); } else if ( req.method == 'GET') { console.log(`phpProxy. get_url:${proxy_url}`); console.log(`phpProxy. get seqnum:${proxy_seqNum} url:${req.url}`); } else { console.log(`phpProxy. method:${req.method}`); } } // response from axios get or post to the proxy target. let response ; // headers properties to proxy up to the server. // If the request contains cookies ( from the browser ), pass them along and // proxy up to the server. const headers = { }; for( const key in req.headers ) { const vlu = req.headers[key] ; if (( key == 'connection' ) && ( vlu == 'keep-alive')) { console.log('connection: keep-alive removed'); } else { headers[key] = vlu ; } } try { if ( req.method == 'POST' ) { response = await axios.post(proxy_url, body.body_text, {headers, responseType:'arraybuffer', responseEncoding:'binary'}); } else if ( req.method == 'GET' ) { const ix = req.url.indexOf('?') ; const full_proxy_url = (ix == -1) ? proxy_url : `${proxy_url}${req.url.substring(ix)}`; console.log(`proxy to server url:${full_proxy_url}`); response = await axios.get(full_proxy_url, {headers, responseType:'arraybuffer', responseEncoding:'binary'}); } } catch(err) { console.log( `post error. seqnum:${proxy_seqNum} url:${proxy_url} body_data:${body.body_text}`); console.log( `${err.message}`); res.writeHead( '400', err.message ); res.end(); return ; } // response content type is content-type of response returned by axios from the // server that was proxied to. const res_contentType = response?.headers?.['content-type'] ? response.headers['content-type'] : '' ; const binary_data = res_contentType.indexOf('binary') == -1 ? false : true ; const res_data = binary_data ? response.data.toString('binary') : response.data.toString( ) ; const res_status = response.status || 200 ; // response header is an object. Each header value is a property in the header. // Copy each property to response header which is to be written as the response // sent back to the proxy client. const res_header = {} ; for( const key in response?.headers || {} ) { const vlu = response.headers[key] ; res_header[key] = vlu ; } // response from axios get/post contains cookies that are to be returned and "set" // in the browser. // note: cookies accumulate in the browser. // - The browsers sends all of its cookies up to the server in the cookie // property of req.headers // - The response from the server does not have to include any or all of the // cookies it received. // - Only when cookies are being changed, removed or added will the response // header contain the 'set-cookie' property. const set_cookie = response.headers['set-cookie'] ; res.writeHead( res_status, res_header); if (typeof res_data == 'object') { const response_text = JSON.stringify(res_data); if ( serverSettings.logContent && !binary_data ) { console.log(`response from server. ${res_status} seqnum:${proxy_seqNum} data:${response_text}`); } res.write(response_text); } else { if (serverSettings.logContent && !binary_data) { console.log(`response from server. ${res_status} seqnum:${proxy_seqNum} data:${res_data}`); } const encoding = binary_data ? 'binary' : 'utf8' ; res.write(res_data, encoding ); } res.end(); } // ---------------------------- steve_readFile -------------------------------- function steve_readFile( filePath ) { let errmsg = '' ; const promise = new Promise((resolve,reject) => { fs.readFile(filePath, 'utf8', (err, data) => { if ( err ) errmsg = err.message ; resolve({data, errmsg}) ; }) ; }); return promise ; } // ---------------------------------- staticServe ---------------------------------- async function staticServe( in_pathname, req, res ) { let pathname = decodeURI( in_pathname ); let fileExists = await fs_checkFileExists(pathname) ; // does not exist. Check if file exists with a different case. if ( !fileExists ) { let dirPath = path.dirname(pathname) ; const fileName = path.basename(pathname) ; let dirExists = await fs_checkFileExists( dirPath ) ; // first - check that directory exists if ( !dirExists ) { const parentPath = path.dirname(dirPath); const dirName = path.basename(dirPath); const {dirList, errmsg} = await steve_getDirectories(parentPath); if ( dirList ) { for (const item of dirList) { if (item.toLowerCase() == dirName.toLowerCase()) { dirPath = path.join(parentPath, item); pathname = path.join(dirPath, fileName ) ; dirExists = await fs_checkFileExists(dirPath); break; } } } } if ( dirExists ) { const {files} = await fs_readDir(dirPath) ; for( const item of files ) { if ( item.toLowerCase( ) == fileName.toLowerCase( )) { pathname = path.join( dirPath, item ) ; fileExists = await fs_checkFileExists(pathname) ; break ; } } } } if (!fileExists) { console.log(`not found: ${pathname}`); res.statusCode = 404; res.end(`File ${pathname} not found!`); return; } // if is a directory search for index file matching the extension const ext = path.parse(pathname).ext; if (fs.statSync(pathname).isDirectory()) pathname += '/index' + ext; // read file from file system fs.readFile(pathname, function (err, data) { if (err) { res.statusCode = 500; res.end(`Error getting the file: ${err}.`); } else { // if the file is found, set Content-type and send data res.setHeader('Content-type', extToContentType(ext)); const dataType = extToDataType(ext); res.end(data, dataType); } }); } // ---------------------------------- gatherRequestBody ---------------------------------- // gathers and returns request body: // {body, isBinary, contentType, body_buf, body_text} type type_gatherReturn = { body: Buffer, isBinary: boolean, contentType: string, body_text: string }; function gatherRequestBody(req): Promise { const contentType = req.headers['content-type'] ? req.headers['content-type'] as string : ''; const isBinary = contentType.indexOf('/binary') >= 0 ? true : false; const promise = new Promise((resolve, reject) => { let body: Buffer; let body_text: string; let body_arr = []; req.on('error', (err) => { console.error(err); }).on('data', (chunk) => { body_arr.push(chunk); }).on('end', () => { // all buffer chunks have been received. Concat into single buffer. body = Buffer.concat(body_arr); // convert raw buffer bytes to utf-8 text. if (!isBinary) { body_text = body.toString(); } resolve({ body, isBinary, contentType, body_text }); }) }); return promise; } // ---------------------------- str_decodeURIComponent ---------------------------- function str_decodeURIComponent( str ) { // first. replace all "+" with space. const s2 = str.replace(/\+/g, ' ') ; // replace all %## with the character code equivalent character. const res = s2.replace(/%\d\d/g, (match) => { const ch1 = String.fromCharCode( match.substr(1)) ; return String.fromCharCode( match.substr(1)) ; }); return res ; } // -------------------------------- fs_checkFileExists -------------------------------- function fs_checkFileExists(file) { return fs.promises.access(file, fs.constants.F_OK) .then(() => true) .catch(() => false) } // --------------------------------- file_getJSON --------------------------------- async function file_getJSON( filePath ) { const { data, err } = await fs_readFile(filePath) ; if ( data ) { const json = JSON.parse( data ) ; return {json, err } ; } else { return { json:'', err } ; } } // ---------------------------------- fs_readFile ---------------------------------- async function fs_readFile(filePath ) { try { const data = await fs.promises.readFile(filePath, 'utf8') return {data, err:''} ; } catch (err) { return {data:'', err } ; } } // ------------------------------ permSettings_recall ------------------------------ async function permSettings_recall( ) { const filePath = path.join(os.homedir(), '.proxy-server-settings.json'); console.log(`settings path:${filePath}`); const exists = await fs_checkFileExists( filePath ) ; if ( !exists ) { await permSettings_store( {} ) ; } const {json, err } = await file_getJSON( filePath ) ; json.proxyServerURL = json.proxyServerURL || 'http://173.54.20.170:10081' ; json.homePath = json.homePath || process.cwd( ) ; return json ; } // ------------------------------ permSettings_store ------------------------------ async function permSettings_store( settings ) { const filePath = path.join( os.homedir(), '.proxy-server-settings.json'); const jsonText = JSON.stringify(settings) ; await fs.promises.writeFile( filePath, jsonText ); } type getDirectories_returnType = { dirList:string[], errmsg:string } // ----------------------------- steve_getDirectories ----------------------------- function steve_getDirectories( dirPath ) : Promise { const promise = new Promise((resolve, reject) => { let dirList ; let errmsg = '' ; fs.readdir( dirPath, {withFileTypes:true}, (error, items) => { if ( error ) errmsg = error.message ; else { dirList = items.filter(item => item.isDirectory()) .map(item => item.name); } resolve({dirList, errmsg}) ; }) ; }); return promise ; } // ------------------------------ path_itemSplit ---------------------------------- function path_itemSplit( itemPath ) { const itemsArr = [] ; while( itemPath && ( itemPath != '.')) { const dirName = path.dirname( itemPath ); const baseName = path.basename( itemPath ); itemsArr.push(baseName) ; itemPath = dirName ; } itemsArr.reverse( ) ; return itemsArr ; } // -------------------------- path_resolveItems --------------------------- async function path_resolveItems( itemPath ) { let fullPath = '' ; let errmsg = '' ; let itemType = '' ; const itemsArr = path_itemSplit(itemPath) ; for( const item of itemsArr ) { fullPath = path.join( fullPath, item ) ; if ( str_tail(fullPath, 1) == ':') { itemType = 'drive' ; fullPath = '/mnt/c'; } else { const stat = await fs.promises.lstat(fullPath) ; itemType = '' ; if (stat.isFile( )) itemType = 'file' ; else if ( stat.isDirectory( )) itemType = 'directory' ; else errmsg = `${fullPath} not found`; } } return {fullPath, itemType, errmsg }; } // --------------------------------- str_decodeURI --------------------------------- function str_decodeURI( str ) { const decode1 = decodeURI( str ); const decode2 = decode1.replace(/\+/g, (match) => { return ' '; }); const decode3 = decode2.replace(/%\d\d/g, (match) => { const ch1 = String.fromCharCode( Number(match.substring(1)) ); return String.fromCharCode( Number(match.substring(1)) ); }); return decode3; } // const getDirectories = async source => // (await readdir(source, { withFileTypes: true })) // .filter(dirent => dirent.isDirectory()) // .map(dirent => dirent.name) // --------------------------------- strToHex --------------------------------- function strToHex(str, maxLx) { maxLx = maxLx || str.length; maxLx = str.length > maxLx ? maxLx : str.length ; let hex = '' ; for (let ix = 0; ix < maxLx; ++ix) { const ch1 = str[ix]; const buf1 = Buffer.from(ch1, 'binary'); hex += '' + buf1[0].toString(16); } return hex; } // ---------------------- str_tail -------------------------- function str_tail( str, lx ) { if ( lx > str.length ) lx = str.length ; const bx = str.length - lx; return str.substring(bx); } // --------------------------------- axios_tester --------------------------------- async function axios_tester( ) { type User = { id: number; email: string; first_name: string; }; type GetUsersResponse = { data: User[]; }; const { data, status } = await axios.get( 'https://reqres.in/api/users', { headers: { Accept: 'application/json', }, }) ; }