jquery/test/middleware-mockserver.js
Michał Gołębiowski-Owczarek b14b62c8a2 Tests: Strip untypical callback parameter characters from mock.php
Only allow alphanumeric characters & underscores for callback parameters.
The change is done both for the PHP server as well as the Node.js-based version.
This is only test code so we're not fixing any security issue but it happens
often enough that the whole jQuery repository directory structure is deployed
onto the server with PHP enabled that it makes is easy to introduce security
issues if this cleanup is not done.

Ref gh-4764
Closes gh-4871

(cherry picked from a70274632d)
2021-04-13 22:36:19 +02:00

316 lines
8.9 KiB
JavaScript

/* eslint-env node */
var url = require( "url" );
var fs = require( "fs" );
var getRawBody = require( "raw-body" );
var cspLog = "";
/**
* Keep in sync with /test/mock.php
*/
function cleanCallback( callback ) {
return callback.replace( /[^a-z0-9_]/gi, "" );
}
var mocks = {
contentType: function( req, resp ) {
resp.writeHead( 200, {
"content-type": req.query.contentType
} );
resp.end( req.query.response );
},
wait: function( req, resp ) {
var wait = Number( req.query.wait ) * 1000;
setTimeout( function() {
if ( req.query.script ) {
resp.writeHead( 200, { "content-type": "text/javascript" } );
} else {
resp.writeHead( 200, { "content-type": "text/html" } );
resp.end( "ERROR <script>QUnit.assert.ok( true, \"mock executed\" );</script>" );
}
}, wait );
},
name: function( req, resp, next ) {
resp.writeHead( 200 );
if ( req.query.name === "foo" ) {
resp.end( "bar" );
return;
}
getBody( req ).then( function( body ) {
if ( body === "name=peter" ) {
resp.end( "pan" );
} else {
resp.end( "ERROR" );
}
}, next );
},
xml: function( req, resp, next ) {
var content = "<math><calculation>5-2</calculation><result>3</result></math>";
resp.writeHead( 200, { "content-type": "text/xml" } );
if ( req.query.cal === "5-2" ) {
resp.end( content );
return;
}
getBody( req ).then( function( body ) {
if ( body === "cal=5-2" ) {
resp.end( content );
} else {
resp.end( "<error>ERROR</error>" );
}
}, next );
},
atom: function( req, resp, next ) {
resp.writeHead( 200, { "content-type": "atom+xml" } );
resp.end( "<root><element /></root>" );
},
script: function( req, resp ) {
if ( req.query.header === "ecma" ) {
resp.writeHead( 200, { "content-type": "application/ecmascript" } );
} else if ( "header" in req.query ) {
resp.writeHead( 200, { "content-type": "text/javascript" } );
} else {
resp.writeHead( 200, { "content-type": "text/html" } );
}
resp.end( "QUnit.assert.ok( true, \"mock executed\" );" );
},
testbar: function( req, resp ) {
resp.writeHead( 200 );
resp.end(
"this.testBar = 'bar'; " +
"jQuery('#ap').html('bar'); " +
"QUnit.assert.ok( true, 'mock executed');"
);
},
json: function( req, resp ) {
if ( req.query.header ) {
resp.writeHead( 200, { "content-type": "application/json" } );
}
if ( req.query.array ) {
resp.end( JSON.stringify(
[ { name: "John", age: 21 }, { name: "Peter", age: 25 } ]
) );
} else {
resp.end( JSON.stringify(
{ data: { lang: "en", length: 25 } }
) );
}
},
jsonp: function( req, resp, next ) {
var callback;
if ( Array.isArray( req.query.callback ) ) {
callback = Promise.resolve( req.query.callback[ req.query.callback.length - 1 ] );
} else if ( req.query.callback ) {
callback = Promise.resolve( req.query.callback );
} else if ( req.method === "GET" ) {
callback = Promise.resolve( req.url.match( /^.+\/([^\/?]+)\?.+$/ )[ 1 ] );
} else {
callback = getBody( req ).then( function( body ) {
return body.trim().replace( "callback=", "" );
} );
}
var json = req.query.array ?
JSON.stringify(
[ { name: "John", age: 21 }, { name: "Peter", age: 25 } ]
) :
JSON.stringify(
{ data: { lang: "en", length: 25 } }
);
callback.then( function( cb ) {
resp.end( cleanCallback( cb ) + "(" + json + ")" );
}, next );
},
xmlOverJsonp: function( req, resp ) {
var callback = req.query.callback;
var body = fs.readFileSync( __dirname + "/data/with_fries.xml" ).toString();
resp.writeHead( 200 );
resp.end( cleanCallback( callback ) + "(" + JSON.stringify( body ) + ")\n" );
},
error: function( req, resp ) {
if ( req.query.json ) {
resp.writeHead( 400, { "content-type": "application/json" } );
resp.end( "{ \"code\": 40, \"message\": \"Bad Request\" }" );
} else {
resp.writeHead( 400 );
resp.end( "plain text message" );
}
},
headers: function( req, resp ) {
resp.writeHead( 200, {
"Sample-Header": "Hello World",
"Empty-Header": "",
"Sample-Header2": "Hello World 2",
"List-Header": "Item 1",
"list-header": "Item 2",
"constructor": "prototype collision (constructor)"
} );
req.query.keys.split( "|" ).forEach( function( key ) {
if ( req.headers[ key.toLowerCase() ] ) {
resp.write( key + ": " + req.headers[ key.toLowerCase() ] + "\n" );
}
} );
resp.end();
},
echoData: function( req, resp, next ) {
getBody( req ).then( function( body ) {
resp.end( body );
}, next );
},
echoQuery: function( req, resp ) {
resp.end( req.parsed.search.slice( 1 ) );
},
echoMethod: function( req, resp ) {
resp.end( req.method );
},
echoHtml: function( req, resp, next ) {
resp.writeHead( 200, { "Content-Type": "text/html" } );
resp.write( "<div id='method'>" + req.method + "</div>" );
resp.write( "<div id='query'>" + req.parsed.search.slice( 1 ) + "</div>" );
getBody( req ).then( function( body ) {
resp.write( "<div id='data'>" + body + "</div>" );
resp.end( body );
}, next );
},
etag: function( req, resp ) {
var hash = Number( req.query.ts ).toString( 36 );
var etag = "W/\"" + hash + "\"";
if ( req.headers[ "if-none-match" ] === etag ) {
resp.writeHead( 304 );
resp.end();
return;
}
resp.writeHead( 200, {
"Etag": etag
} );
resp.end();
},
ims: function( req, resp, next ) {
var ts = req.query.ts;
if ( req.headers[ "if-modified-since" ] === ts ) {
resp.writeHead( 304 );
resp.end();
return;
}
resp.writeHead( 200, {
"Last-Modified": ts
} );
resp.end();
},
status: function( req, resp, next ) {
resp.writeHead( Number( req.query.code ) );
resp.end();
},
testHTML: function( req, resp ) {
resp.writeHead( 200, { "Content-Type": "text/html" } );
var body = fs.readFileSync( __dirname + "/data/test.include.html" ).toString();
body = body.replace( /{{baseURL}}/g, req.query.baseURL );
resp.end( body );
},
cspFrame: function( req, resp ) {
resp.writeHead( 200, {
"Content-Type": "text/html",
"Content-Security-Policy": "default-src 'self'; report-uri /base/test/data/mock.php?action=cspLog"
} );
var body = fs.readFileSync( __dirname + "/data/csp.include.html" ).toString();
resp.end( body );
},
cspNonce: function( req, resp ) {
var testParam = req.query.test ? "-" + req.query.test : "";
resp.writeHead( 200, {
"Content-Type": "text/html",
"Content-Security-Policy": "script-src 'nonce-jquery+hardcoded+nonce'; report-uri /base/test/data/mock.php?action=cspLog"
} );
var body = fs.readFileSync(
__dirname + "/data/csp-nonce" + testParam + ".html" ).toString();
resp.end( body );
},
cspLog: function( req, resp ) {
cspLog = "error";
resp.writeHead( 200 );
resp.end();
},
cspClean: function( req, resp ) {
cspLog = "";
resp.writeHead( 200 );
resp.end();
},
errorWithScript: function( req, resp ) {
if ( req.query.withScriptContentType ) {
resp.writeHead( 404, { "Content-Type": "application/javascript" } );
} else {
resp.writeHead( 404, { "Content-Type": "text/html; charset=UTF-8" } );
}
if ( req.query.callback ) {
resp.end( cleanCallback( req.query.callback ) +
"( {\"status\": 404, \"msg\": \"Not Found\"} )" );
} else {
resp.end( "QUnit.assert.ok( false, \"Mock return erroneously executed\" );" );
}
}
};
var handlers = {
"test/data/mock.php": function( req, resp, next ) {
if ( !mocks[ req.query.action ] ) {
resp.writeHead( 400 );
resp.end( "Invalid action query.\n" );
console.log( "Invalid action query:", req.method, req.url );
return;
}
mocks[ req.query.action ]( req, resp, next );
},
"test/data/support/csp.log": function( req, resp ) {
resp.writeHead( 200 );
resp.end( cspLog );
},
"test/data/404.txt": function( req, resp ) {
resp.writeHead( 404 );
resp.end( "" );
}
};
/**
* Connect-compatible middleware factory for mocking server responses.
* Used by Ajax unit tests when run via Karma.
*
* Despite Karma using Express, it uses Connect to deal with custom middleware,
* which passes the raw Node Request and Response objects instead of the
* Express versions of these (e.g. no req.path, req.query, resp.set).
*/
function MockserverMiddlewareFactory() {
/**
* @param {http.IncomingMessage} req
* @param {http.ServerResponse} resp
* @param {Function} next Continue request handling
*/
return function( req, resp, next ) {
var parsed = url.parse( req.url, /* parseQuery */ true ),
path = parsed.pathname.replace( /^\/base\//, "" ),
query = parsed.query,
subReq = Object.assign( Object.create( req ), {
query: query,
parsed: parsed
} );
if ( /^test\/data\/mock.php\//.test( path ) ) {
// Support REST-like Apache PathInfo
path = "test\/data\/mock.php";
}
if ( !handlers[ path ] ) {
next();
return;
}
handlers[ path ]( subReq, resp, next );
};
}
function getBody( req ) {
return req.method !== "POST" ?
Promise.resolve( "" ) :
getRawBody( req, {
encoding: true
} );
}
module.exports = MockserverMiddlewareFactory;