diff --git a/packages/next/src/compiled/http-proxy/index.js b/packages/next/src/compiled/http-proxy/index.js index d7d41b9d698b4d..2e67249ef32a14 100644 --- a/packages/next/src/compiled/http-proxy/index.js +++ b/packages/next/src/compiled/http-proxy/index.js @@ -1,4 +1,4 @@ -(()=>{var e={993:e=>{"use strict";var t=Object.prototype.hasOwnProperty,r="~";function Events(){}if(Object.create){Events.prototype=Object.create(null);if(!(new Events).__proto__)r=false}function EE(e,t,r){this.fn=e;this.context=t;this.once=r||false}function addListener(e,t,o,s,n){if(typeof o!=="function"){throw new TypeError("The listener must be a function")}var i=new EE(o,s||e,n),a=r?r+t:t;if(!e._events[a])e._events[a]=i,e._eventsCount++;else if(!e._events[a].fn)e._events[a].push(i);else e._events[a]=[e._events[a],i];return e}function clearEvent(e,t){if(--e._eventsCount===0)e._events=new Events;else delete e._events[t]}function EventEmitter(){this._events=new Events;this._eventsCount=0}EventEmitter.prototype.eventNames=function eventNames(){var e=[],o,s;if(this._eventsCount===0)return e;for(s in o=this._events){if(t.call(o,s))e.push(r?s.slice(1):s)}if(Object.getOwnPropertySymbols){return e.concat(Object.getOwnPropertySymbols(o))}return e};EventEmitter.prototype.listeners=function listeners(e){var t=r?r+e:e,o=this._events[t];if(!o)return[];if(o.fn)return[o.fn];for(var s=0,n=o.length,i=new Array(n);s{var o=r(310);var s=o.URL;var n=r(685);var i=r(687);var a=r(491);var c=r(781).Writable;var u=r(937)("follow-redirects");var f={GET:true,HEAD:true,OPTIONS:true,TRACE:true};var h=Object.create(null);["abort","aborted","connect","error","socket","timeout"].forEach((function(e){h[e]=function(t,r,o){this._redirectable.emit(e,t,r,o)}}));function RedirectableRequest(e,t){c.call(this);this._sanitizeOptions(e);this._options=e;this._ended=false;this._ending=false;this._redirectCount=0;this._redirects=[];this._requestBodyLength=0;this._requestBodyBuffers=[];if(t){this.on("response",t)}var r=this;this._onNativeResponse=function(e){r._processResponse(e)};this._performRequest()}RedirectableRequest.prototype=Object.create(c.prototype);RedirectableRequest.prototype.write=function(e,t,r){if(this._ending){throw new Error("write after end")}if(!(typeof e==="string"||typeof e==="object"&&"length"in e)){throw new Error("data should be a string, Buffer or Uint8Array")}if(typeof t==="function"){r=t;t=null}if(e.length===0){if(r){r()}return}if(this._requestBodyLength+e.length<=this._options.maxBodyLength){this._requestBodyLength+=e.length;this._requestBodyBuffers.push({data:e,encoding:t});this._currentRequest.write(e,t,r)}else{this.emit("error",new Error("Request body larger than maxBodyLength limit"));this.abort()}};RedirectableRequest.prototype.end=function(e,t,r){if(typeof e==="function"){r=e;e=t=null}else if(typeof t==="function"){r=t;t=null}if(!e){this._ended=this._ending=true;this._currentRequest.end(null,null,r)}else{var o=this;var s=this._currentRequest;this.write(e,t,(function(){o._ended=true;s.end(null,null,r)}));this._ending=true}};RedirectableRequest.prototype.setHeader=function(e,t){this._options.headers[e]=t;this._currentRequest.setHeader(e,t)};RedirectableRequest.prototype.removeHeader=function(e){delete this._options.headers[e];this._currentRequest.removeHeader(e)};RedirectableRequest.prototype.setTimeout=function(e,t){if(t){this.once("timeout",t)}if(this.socket){startTimer(this,e)}else{var r=this;this._currentRequest.once("socket",(function(){startTimer(r,e)}))}this.once("response",clearTimer);this.once("error",clearTimer);return this};function startTimer(e,t){clearTimeout(e._timeout);e._timeout=setTimeout((function(){e.emit("timeout")}),t)}function clearTimer(){clearTimeout(this._timeout)}["abort","flushHeaders","getHeader","setNoDelay","setSocketKeepAlive"].forEach((function(e){RedirectableRequest.prototype[e]=function(t,r){return this._currentRequest[e](t,r)}}));["aborted","connection","socket"].forEach((function(e){Object.defineProperty(RedirectableRequest.prototype,e,{get:function(){return this._currentRequest[e]}})}));RedirectableRequest.prototype._sanitizeOptions=function(e){if(!e.headers){e.headers={}}if(e.host){if(!e.hostname){e.hostname=e.host}delete e.host}if(!e.pathname&&e.path){var t=e.path.indexOf("?");if(t<0){e.pathname=e.path}else{e.pathname=e.path.substring(0,t);e.search=e.path.substring(t)}}};RedirectableRequest.prototype._performRequest=function(){var e=this._options.protocol;var t=this._options.nativeProtocols[e];if(!t){this.emit("error",new Error("Unsupported protocol "+e));return}if(this._options.agents){var r=e.substr(0,e.length-1);this._options.agent=this._options.agents[r]}var s=this._currentRequest=t.request(this._options,this._onNativeResponse);this._currentUrl=o.format(this._options);s._redirectable=this;for(var n in h){if(n){s.on(n,h[n])}}if(this._isRedirect){var i=0;var a=this;var c=this._requestBodyBuffers;(function writeNext(e){if(s===a._currentRequest){if(e){a.emit("error",e)}else if(i=300&&t<400){this._currentRequest.removeAllListeners();this._currentRequest.on("error",noop);this._currentRequest.abort();e.destroy();if(++this._redirectCount>this._options.maxRedirects){this.emit("error",new Error("Max redirects exceeded."));return}var s;var n=this._options.headers;if(t!==307&&!(this._options.method in f)){this._options.method="GET";this._requestBodyBuffers=[];for(s in n){if(/^content-/i.test(s)){delete n[s]}}}if(!this._isRedirect){for(s in n){if(/^host$/i.test(s)){delete n[s]}}}var i=o.resolve(this._currentUrl,r);u("redirecting to",i);Object.assign(this._options,o.parse(i));if(typeof this._options.beforeRedirect==="function"){try{this._options.beforeRedirect.call(null,this._options)}catch(e){this.emit("error",e);return}this._sanitizeOptions(this._options)}this._isRedirect=true;this._performRequest()}else{e.responseUrl=this._currentUrl;e.redirects=this._redirects;this.emit("response",e);this._requestBodyBuffers=[]}};function wrap(e){var t={maxRedirects:21,maxBodyLength:10*1024*1024};var r={};Object.keys(e).forEach((function(n){var i=n+":";var c=r[i]=e[n];var f=t[n]=Object.create(c);f.request=function(e,n,c){if(typeof e==="string"){var f=e;try{e=urlToOptions(new s(f))}catch(t){e=o.parse(f)}}else if(s&&e instanceof s){e=urlToOptions(e)}else{c=n;n=e;e={protocol:i}}if(typeof n==="function"){c=n;n=null}n=Object.assign({maxRedirects:t.maxRedirects,maxBodyLength:t.maxBodyLength},e,n);n.nativeProtocols=r;a.equal(n.protocol,i,"protocol mismatch");u("options",n);return new RedirectableRequest(n,c)};f.get=function(e,t,r){var o=f.request(e,t,r);o.end();return o}}));return t}function noop(){}function urlToOptions(e){var t={protocol:e.protocol,hostname:e.hostname.startsWith("[")?e.hostname.slice(1,-1):e.hostname,hash:e.hash,search:e.search,pathname:e.pathname,path:e.pathname+e.search,href:e.href};if(e.port!==""){t.port=Number(e.port)}return t}e.exports=wrap({http:n,https:i});e.exports.wrap=wrap},768:(e,t,r)=>{ +(()=>{var e={993:e=>{"use strict";var t=Object.prototype.hasOwnProperty,r="~";function Events(){}if(Object.create){Events.prototype=Object.create(null);if(!(new Events).__proto__)r=false}function EE(e,t,r){this.fn=e;this.context=t;this.once=r||false}function addListener(e,t,o,n,s){if(typeof o!=="function"){throw new TypeError("The listener must be a function")}var i=new EE(o,n||e,s),a=r?r+t:t;if(!e._events[a])e._events[a]=i,e._eventsCount++;else if(!e._events[a].fn)e._events[a].push(i);else e._events[a]=[e._events[a],i];return e}function clearEvent(e,t){if(--e._eventsCount===0)e._events=new Events;else delete e._events[t]}function EventEmitter(){this._events=new Events;this._eventsCount=0}EventEmitter.prototype.eventNames=function eventNames(){var e=[],o,n;if(this._eventsCount===0)return e;for(n in o=this._events){if(t.call(o,n))e.push(r?n.slice(1):n)}if(Object.getOwnPropertySymbols){return e.concat(Object.getOwnPropertySymbols(o))}return e};EventEmitter.prototype.listeners=function listeners(e){var t=r?r+e:e,o=this._events[t];if(!o)return[];if(o.fn)return[o.fn];for(var n=0,s=o.length,i=new Array(s);n{var o=r(310);var n=o.URL;var s=r(685);var i=r(687);var a=r(491);var c=r(781).Writable;var u=r(937)("follow-redirects");var f={GET:true,HEAD:true,OPTIONS:true,TRACE:true};var h=Object.create(null);["abort","aborted","connect","error","socket","timeout"].forEach((function(e){h[e]=function(t,r,o){this._redirectable.emit(e,t,r,o)}}));function RedirectableRequest(e,t){c.call(this);this._sanitizeOptions(e);this._options=e;this._ended=false;this._ending=false;this._redirectCount=0;this._redirects=[];this._requestBodyLength=0;this._requestBodyBuffers=[];if(t){this.on("response",t)}var r=this;this._onNativeResponse=function(e){r._processResponse(e)};this._performRequest()}RedirectableRequest.prototype=Object.create(c.prototype);RedirectableRequest.prototype.write=function(e,t,r){if(this._ending){throw new Error("write after end")}if(!(typeof e==="string"||typeof e==="object"&&"length"in e)){throw new Error("data should be a string, Buffer or Uint8Array")}if(typeof t==="function"){r=t;t=null}if(e.length===0){if(r){r()}return}if(this._requestBodyLength+e.length<=this._options.maxBodyLength){this._requestBodyLength+=e.length;this._requestBodyBuffers.push({data:e,encoding:t});this._currentRequest.write(e,t,r)}else{this.emit("error",new Error("Request body larger than maxBodyLength limit"));this.abort()}};RedirectableRequest.prototype.end=function(e,t,r){if(typeof e==="function"){r=e;e=t=null}else if(typeof t==="function"){r=t;t=null}if(!e){this._ended=this._ending=true;this._currentRequest.end(null,null,r)}else{var o=this;var n=this._currentRequest;this.write(e,t,(function(){o._ended=true;n.end(null,null,r)}));this._ending=true}};RedirectableRequest.prototype.setHeader=function(e,t){this._options.headers[e]=t;this._currentRequest.setHeader(e,t)};RedirectableRequest.prototype.removeHeader=function(e){delete this._options.headers[e];this._currentRequest.removeHeader(e)};RedirectableRequest.prototype.setTimeout=function(e,t){if(t){this.once("timeout",t)}if(this.socket){startTimer(this,e)}else{var r=this;this._currentRequest.once("socket",(function(){startTimer(r,e)}))}this.once("response",clearTimer);this.once("error",clearTimer);return this};function startTimer(e,t){clearTimeout(e._timeout);e._timeout=setTimeout((function(){e.emit("timeout")}),t)}function clearTimer(){clearTimeout(this._timeout)}["abort","flushHeaders","getHeader","setNoDelay","setSocketKeepAlive"].forEach((function(e){RedirectableRequest.prototype[e]=function(t,r){return this._currentRequest[e](t,r)}}));["aborted","connection","socket"].forEach((function(e){Object.defineProperty(RedirectableRequest.prototype,e,{get:function(){return this._currentRequest[e]}})}));RedirectableRequest.prototype._sanitizeOptions=function(e){if(!e.headers){e.headers={}}if(e.host){if(!e.hostname){e.hostname=e.host}delete e.host}if(!e.pathname&&e.path){var t=e.path.indexOf("?");if(t<0){e.pathname=e.path}else{e.pathname=e.path.substring(0,t);e.search=e.path.substring(t)}}};RedirectableRequest.prototype._performRequest=function(){var e=this._options.protocol;var t=this._options.nativeProtocols[e];if(!t){this.emit("error",new Error("Unsupported protocol "+e));return}if(this._options.agents){var r=e.substr(0,e.length-1);this._options.agent=this._options.agents[r]}var n=this._currentRequest=t.request(this._options,this._onNativeResponse);this._currentUrl=o.format(this._options);n._redirectable=this;for(var s in h){if(s){n.on(s,h[s])}}if(this._isRedirect){var i=0;var a=this;var c=this._requestBodyBuffers;(function writeNext(e){if(n===a._currentRequest){if(e){a.emit("error",e)}else if(i=300&&t<400){this._currentRequest.removeAllListeners();this._currentRequest.on("error",noop);this._currentRequest.abort();e.destroy();if(++this._redirectCount>this._options.maxRedirects){this.emit("error",new Error("Max redirects exceeded."));return}var n;var s=this._options.headers;if(t!==307&&!(this._options.method in f)){this._options.method="GET";this._requestBodyBuffers=[];for(n in s){if(/^content-/i.test(n)){delete s[n]}}}if(!this._isRedirect){for(n in s){if(/^host$/i.test(n)){delete s[n]}}}var i=o.resolve(this._currentUrl,r);u("redirecting to",i);Object.assign(this._options,o.parse(i));if(typeof this._options.beforeRedirect==="function"){try{this._options.beforeRedirect.call(null,this._options)}catch(e){this.emit("error",e);return}this._sanitizeOptions(this._options)}this._isRedirect=true;this._performRequest()}else{e.responseUrl=this._currentUrl;e.redirects=this._redirects;this.emit("response",e);this._requestBodyBuffers=[]}};function wrap(e){var t={maxRedirects:21,maxBodyLength:10*1024*1024};var r={};Object.keys(e).forEach((function(s){var i=s+":";var c=r[i]=e[s];var f=t[s]=Object.create(c);f.request=function(e,s,c){if(typeof e==="string"){var f=e;try{e=urlToOptions(new n(f))}catch(t){e=o.parse(f)}}else if(n&&e instanceof n){e=urlToOptions(e)}else{c=s;s=e;e={protocol:i}}if(typeof s==="function"){c=s;s=null}s=Object.assign({maxRedirects:t.maxRedirects,maxBodyLength:t.maxBodyLength},e,s);s.nativeProtocols=r;a.equal(s.protocol,i,"protocol mismatch");u("options",s);return new RedirectableRequest(s,c)};f.get=function(e,t,r){var o=f.request(e,t,r);o.end();return o}}));return t}function noop(){}function urlToOptions(e){var t={protocol:e.protocol,hostname:e.hostname.startsWith("[")?e.hostname.slice(1,-1):e.hostname,hash:e.hash,search:e.search,pathname:e.pathname,path:e.pathname+e.search,href:e.href};if(e.port!==""){t.port=Number(e.port)}return t}e.exports=wrap({http:s,https:i});e.exports.wrap=wrap},204:(e,t,r)=>{ /*! * Caron dimonio, con occhi di bragia * loro accennando, tutte le raccoglie; @@ -10,25 +10,25 @@ * * Dante - The Divine Comedy (Canto III) */ -e.exports=r(852)},852:(e,t,r)=>{var o=r(949).Server;function createProxyServer(e){return new o(e)}o.createProxyServer=createProxyServer;o.createServer=createProxyServer;o.createProxy=createProxyServer;e.exports=o},26:(e,t,r)=>{var o=t,s=r(310),n=r(85);var i=/(^|,)\s*upgrade\s*($|,)/i,a=/^https|wss/;o.isSSL=a;o.setupOutgoing=function(e,t,r,c){e.port=t[c||"target"].port||(a.test(t[c||"target"].protocol)?443:80);["host","hostname","socketPath","pfx","key","passphrase","cert","ca","ciphers","secureProtocol"].forEach((function(r){e[r]=t[c||"target"][r]}));e.method=t.method||r.method;e.headers=Object.assign({},r.headers);if(t.headers){Object.assign(e.headers,t.headers)}if(t.auth){e.auth=t.auth}if(t.ca){e.ca=t.ca}if(a.test(t[c||"target"].protocol)){e.rejectUnauthorized=typeof t.secure==="undefined"?true:t.secure}e.agent=t.agent||false;e.localAddress=t.localAddress;if(!e.agent){e.headers=e.headers||{};if(typeof e.headers.connection!=="string"||!i.test(e.headers.connection)){e.headers.connection="close"}}var u=t[c||"target"];var f=u&&t.prependPath!==false?u.path||"":"";var h=!t.toProxy?s.parse(r.url).path||"":r.url;h=!t.ignorePath?h:"";e.path=o.urlJoin(f,h);if(t.changeOrigin){e.headers.host=n(e.port,t[c||"target"].protocol)&&!hasPort(e.host)?e.host+":"+e.port:e.host}return e};o.setupSocket=function(e){e.setTimeout(0);e.setNoDelay(true);e.setKeepAlive(true,0);return e};o.getPort=function(e){var t=e.headers.host?e.headers.host.match(/:(\d+)/):"";return t?t[1]:o.hasEncryptedConnection(e)?"443":"80"};o.hasEncryptedConnection=function(e){return Boolean(e.connection.encrypted||e.connection.pair)};o.urlJoin=function(){var e=Array.prototype.slice.call(arguments),t=e.length-1,r=e[t],o=r.split("?"),s;e[t]=o.shift();s=[e.filter(Boolean).join("/").replace(/\/+/g,"/").replace("http:/","http://").replace("https:/","https://")];s.push.apply(s,o);return s.join("?")};o.rewriteCookieProperty=function rewriteCookieProperty(e,t,r){if(Array.isArray(e)){return e.map((function(e){return rewriteCookieProperty(e,t,r)}))}return e.replace(new RegExp("(;\\s*"+r+"=)([^;]+)","i"),(function(e,r,o){var s;if(o in t){s=t[o]}else if("*"in t){s=t["*"]}else{return e}if(s){return r+s}else{return""}}))};function hasPort(e){return!!~e.indexOf(":")}},949:(e,t,r)=>{var o=e.exports,s=r(310).parse,n=r(993),i=r(685),a=r(687),c=r(244),u=r(893);o.Server=ProxyServer;function createRightProxy(e){return function(t){return function(r,o){var n=e==="ws"?this.wsPasses:this.webPasses,i=[].slice.call(arguments),a=i.length-1,c,u;if(typeof i[a]==="function"){u=i[a];a--}var f=t;if(!(i[a]instanceof Buffer)&&i[a]!==o){f=Object.assign({},t);Object.assign(f,i[a]);a--}if(i[a]instanceof Buffer){c=i[a]}["target","forward"].forEach((function(e){if(typeof f[e]==="string")f[e]=s(f[e])}));if(!f.target&&!f.forward){return this.emit("error",new Error("Must provide a proper URL as target"))}for(var h=0;h{var o=r(685),s=r(687),n=r(417),i=r(26),a=r(900);n=Object.keys(n).map((function(e){return n[e]}));var c={http:o,https:s}; +e.exports=r(763)},763:(e,t,r)=>{var o=r(458).Server;function createProxyServer(e){return new o(e)}o.createProxyServer=createProxyServer;o.createServer=createProxyServer;o.createProxy=createProxyServer;e.exports=o},341:(e,t,r)=>{var o=t,n=r(310),s=r(85);var i=/(^|,)\s*upgrade\s*($|,)/i,a=/(^|,)\s*transfer-encoding\s*($|,)/i,c=/^https|wss/;o.isSSL=c;o.setupOutgoing=function(e,t,r,u){e.port=t[u||"target"].port||(c.test(t[u||"target"].protocol)?443:80);["host","hostname","socketPath","pfx","key","passphrase","cert","ca","ciphers","secureProtocol"].forEach((function(r){e[r]=t[u||"target"][r]}));e.method=t.method||r.method;e.headers=Object.assign({},r.headers);if(t.headers){Object.assign(e.headers,t.headers)}if(t.auth){e.auth=t.auth}if(t.ca){e.ca=t.ca}if(c.test(t[u||"target"].protocol)){e.rejectUnauthorized=typeof t.secure==="undefined"?true:t.secure}e.agent=t.agent||false;e.localAddress=t.localAddress;e.headers=e.headers||{};var f=Object.keys(e.headers).some((function(t){return t.toLowerCase()==="transfer-encoding"&&typeof e.headers[t]!=="undefined"}));if(f||typeof e.headers.connection==="string"&&a.test(e.headers.connection)){e.headers.connection="close"}if(!e.agent){if(typeof e.headers.connection!=="string"||!i.test(e.headers.connection)){e.headers.connection="close"}}var h=t[u||"target"];var p=h&&t.prependPath!==false?h.path||"":"";var d=!t.toProxy?n.parse(r.url).path||"":r.url;d=!t.ignorePath?d:"";e.path=o.urlJoin(p,d);if(t.changeOrigin){e.headers.host=s(e.port,t[u||"target"].protocol)&&!hasPort(e.host)?e.host+":"+e.port:e.host}return e};o.setupSocket=function(e){e.setTimeout(0);e.setNoDelay(true);e.setKeepAlive(true,0);return e};o.getPort=function(e){var t=e.headers.host?e.headers.host.match(/:(\d+)/):"";return t?t[1]:o.hasEncryptedConnection(e)?"443":"80"};o.hasEncryptedConnection=function(e){return Boolean(e.connection.encrypted||e.connection.pair)};o.urlJoin=function(){var e=Array.prototype.slice.call(arguments),t=e.length-1,r=e[t],o=r.split("?"),n;e[t]=o.shift();n=[e.filter(Boolean).join("/").replace(/\/+/g,"/").replace("http:/","http://").replace("https:/","https://")];n.push.apply(n,o);return n.join("?")};o.rewriteCookieProperty=function rewriteCookieProperty(e,t,r){if(Array.isArray(e)){return e.map((function(e){return rewriteCookieProperty(e,t,r)}))}return e.replace(new RegExp("(;\\s*"+r+"=)([^;]+)","i"),(function(e,r,o){var n;if(o in t){n=t[o]}else if("*"in t){n=t["*"]}else{return e}if(n){return r+n}else{return""}}))};function hasPort(e){return!!~e.indexOf(":")}},458:(e,t,r)=>{var o=e.exports,n=r(310).parse,s=r(993),i=r(685),a=r(687),c=r(101),u=r(761);o.Server=ProxyServer;function createRightProxy(e){return function(t){return function(r,o){var s=e==="ws"?this.wsPasses:this.webPasses,i=[].slice.call(arguments),a=i.length-1,c,u;if(typeof i[a]==="function"){u=i[a];a--}var f=t;if(!(i[a]instanceof Buffer)&&i[a]!==o){f=Object.assign({},t);Object.assign(f,i[a]);a--}if(i[a]instanceof Buffer){c=i[a]}["target","forward"].forEach((function(e){if(typeof f[e]==="string")f[e]=n(f[e])}));if(!f.target&&!f.forward){return this.emit("error",new Error("Must provide a proper URL as target"))}for(var h=0;h{var o=r(685),n=r(687),s=r(445),i=r(341),a=r(900);s=Object.keys(s).map((function(e){return s[e]}));var c={http:o,https:n}; /*! * Array of passes. * * A `pass` is just a function that is executed on `req, res, options` * so that you can easily add new checks while still keeping the base * flexible. - */e.exports={deleteLength:function deleteLength(e,t,r){if((e.method==="DELETE"||e.method==="OPTIONS")&&!e.headers["content-length"]){e.headers["content-length"]="0";delete e.headers["transfer-encoding"]}},timeout:function timeout(e,t,r){if(r.timeout){e.socket.setTimeout(r.timeout)}},XHeaders:function XHeaders(e,t,r){if(!r.xfwd)return;var o=e.isSpdy||i.hasEncryptedConnection(e);var s={for:e.connection.remoteAddress||e.socket.remoteAddress,port:i.getPort(e),proto:o?"https":"http"};["for","port","proto"].forEach((function(t){e.headers["x-forwarded-"+t]=(e.headers["x-forwarded-"+t]||"")+(e.headers["x-forwarded-"+t]?",":"")+s[t]}));e.headers["x-forwarded-host"]=e.headers["x-forwarded-host"]||e.headers["host"]||""},stream:function stream(e,t,r,o,s,u){s.emit("start",e,t,r.target||r.forward);var f=r.followRedirects?a:c;var h=f.http;var p=f.https;if(r.forward){var d=(r.forward.protocol==="https:"?p:h).request(i.setupOutgoing(r.ssl||{},r,e,"forward"));var l=createErrorHandler(d,r.forward);e.on("error",l);d.on("error",l);(r.buffer||e).pipe(d);if(!r.target){return t.end()}}var v=(r.target.protocol==="https:"?p:h).request(i.setupOutgoing(r.ssl||{},r,e));v.on("socket",(function(o){if(s&&!v.getHeader("expect")){s.emit("proxyReq",v,e,t,r)}}));if(r.proxyTimeout){v.setTimeout(r.proxyTimeout,(function(){v.abort()}))}e.on("aborted",(function(){v.abort()}));var m=createErrorHandler(v,r.target);e.on("error",m);v.on("error",m);function createErrorHandler(r,o){return function proxyError(n){if(e.socket.destroyed&&n.code==="ECONNRESET"){s.emit("econnreset",n,e,t,o);return r.abort()}if(u){u(n,e,t,o)}else{s.emit("error",n,e,t,o)}}}(r.buffer||e).pipe(v);v.on("response",(function(o){if(s){s.emit("proxyRes",o,e,t)}if(!t.headersSent&&!r.selfHandleResponse){for(var i=0;i{var o=r(310),s=r(26);var n=/^201|30(1|2|7|8)$/; + */e.exports={deleteLength:function deleteLength(e,t,r){if((e.method==="DELETE"||e.method==="OPTIONS")&&typeof e.headers["content-length"]==="undefined"&&typeof e.headers["transfer-encoding"]==="undefined"){e.headers["content-length"]="0"}},timeout:function timeout(e,t,r){if(r.timeout){e.socket.setTimeout(r.timeout)}},XHeaders:function XHeaders(e,t,r){if(!r.xfwd)return;var o=e.isSpdy||i.hasEncryptedConnection(e);var n={for:e.connection.remoteAddress||e.socket.remoteAddress,port:i.getPort(e),proto:o?"https":"http"};["for","port","proto"].forEach((function(t){e.headers["x-forwarded-"+t]=(e.headers["x-forwarded-"+t]||"")+(e.headers["x-forwarded-"+t]?",":"")+n[t]}));e.headers["x-forwarded-host"]=e.headers["x-forwarded-host"]||e.headers["host"]||""},stream:function stream(e,t,r,o,n,u){n.emit("start",e,t,r.target||r.forward);var f=r.followRedirects?a:c;var h=f.http;var p=f.https;if(r.forward){var d=(r.forward.protocol==="https:"?p:h).request(i.setupOutgoing(r.ssl||{},r,e,"forward"));var l=createErrorHandler(d,r.forward);e.on("error",l);d.on("error",l);(r.buffer||e).pipe(d);if(!r.target){return t.end()}}var v=(r.target.protocol==="https:"?p:h).request(i.setupOutgoing(r.ssl||{},r,e));v.on("socket",(function(o){if(n&&!v.getHeader("expect")){n.emit("proxyReq",v,e,t,r)}}));if(r.proxyTimeout){v.setTimeout(r.proxyTimeout,(function(){v.abort()}))}e.on("aborted",(function(){v.abort()}));var m=createErrorHandler(v,r.target);e.on("error",m);v.on("error",m);function createErrorHandler(r,o){return function proxyError(s){if(e.socket.destroyed&&s.code==="ECONNRESET"){n.emit("econnreset",s,e,t,o);return r.abort()}if(u){u(s,e,t,o)}else{n.emit("error",s,e,t,o)}}}(r.buffer||e).pipe(v);v.on("response",(function(o){if(n){n.emit("proxyRes",o,e,t)}if(!t.headersSent&&!r.selfHandleResponse){for(var i=0;i{var o=r(310),n=r(341);var s=/^201|30(1|2|7|8)$/; /*! * Array of passes. * * A `pass` is just a function that is executed on `req, res, options` * so that you can easily add new checks while still keeping the base * flexible. - */e.exports={removeChunked:function removeChunked(e,t,r){if(e.httpVersion==="1.0"){delete r.headers["transfer-encoding"]}},setConnection:function setConnection(e,t,r){if(e.httpVersion==="1.0"){r.headers.connection=e.headers.connection||"close"}else if(e.httpVersion!=="2.0"&&!r.headers.connection){r.headers.connection=e.headers.connection||"keep-alive"}},setRedirectHostRewrite:function setRedirectHostRewrite(e,t,r,s){if((s.hostRewrite||s.autoRewrite||s.protocolRewrite)&&r.headers["location"]&&n.test(r.statusCode)){var i=o.parse(s.target);var a=o.parse(r.headers["location"]);if(i.host!=a.host){return}if(s.hostRewrite){a.host=s.hostRewrite}else if(s.autoRewrite){a.host=e.headers["host"]}if(s.protocolRewrite){a.protocol=s.protocolRewrite}r.headers["location"]=a.format()}},writeHeaders:function writeHeaders(e,t,r,o){var n=o.cookieDomainRewrite,i=o.cookiePathRewrite,a=o.preserveHeaderKeyCase,c,setHeader=function(e,r){if(r==undefined)return;if(n&&e.toLowerCase()==="set-cookie"){r=s.rewriteCookieProperty(r,n,"domain")}if(i&&e.toLowerCase()==="set-cookie"){r=s.rewriteCookieProperty(r,i,"path")}t.setHeader(String(e).trim(),r)};if(typeof n==="string"){n={"*":n}}if(typeof i==="string"){i={"*":i}}if(a&&r.rawHeaders!=undefined){c={};for(var u=0;u{var o=r(685),s=r(687),n=r(26); + */e.exports={removeChunked:function removeChunked(e,t,r){if(e.httpVersion==="1.0"){delete r.headers["transfer-encoding"]}},setConnection:function setConnection(e,t,r){if(e.httpVersion==="1.0"){r.headers.connection=e.headers.connection||"close"}else if(e.httpVersion!=="2.0"&&!r.headers.connection){r.headers.connection=e.headers.connection||"keep-alive"}},setRedirectHostRewrite:function setRedirectHostRewrite(e,t,r,n){if((n.hostRewrite||n.autoRewrite||n.protocolRewrite)&&r.headers["location"]&&s.test(r.statusCode)){var i=o.parse(n.target);var a=o.parse(r.headers["location"]);if(i.host!=a.host){return}if(n.hostRewrite){a.host=n.hostRewrite}else if(n.autoRewrite){a.host=e.headers["host"]}if(n.protocolRewrite){a.protocol=n.protocolRewrite}r.headers["location"]=a.format()}},writeHeaders:function writeHeaders(e,t,r,o){var s=o.cookieDomainRewrite,i=o.cookiePathRewrite,a=o.preserveHeaderKeyCase,c,setHeader=function(e,r){if(r==undefined)return;if(s&&e.toLowerCase()==="set-cookie"){r=n.rewriteCookieProperty(r,s,"domain")}if(i&&e.toLowerCase()==="set-cookie"){r=n.rewriteCookieProperty(r,i,"path")}t.setHeader(String(e).trim(),r)};if(typeof s==="string"){s={"*":s}}if(typeof i==="string"){i={"*":i}}if(a&&r.rawHeaders!=undefined){c={};for(var u=0;u{var o=r(685),n=r(687),s=r(341); /*! * Array of passes. * * A `pass` is just a function that is executed on `req, socket, options` * so that you can easily add new checks while still keeping the base * flexible. - */e.exports={checkMethodAndHeader:function checkMethodAndHeader(e,t){if(e.method!=="GET"||!e.headers.upgrade){t.destroy();return true}if(e.headers.upgrade.toLowerCase()!=="websocket"){t.destroy();return true}},XHeaders:function XHeaders(e,t,r){if(!r.xfwd)return;var o={for:e.connection.remoteAddress||e.socket.remoteAddress,port:n.getPort(e),proto:n.hasEncryptedConnection(e)?"wss":"ws"};["for","port","proto"].forEach((function(t){e.headers["x-forwarded-"+t]=(e.headers["x-forwarded-"+t]||"")+(e.headers["x-forwarded-"+t]?",":"")+o[t]}))},stream:function stream(e,t,r,i,a,c){var createHttpHeader=function(e,t){return Object.keys(t).reduce((function(e,r){var o=t[r];if(!Array.isArray(o)){e.push(r+": "+o);return e}for(var s=0;s{"use strict";e.exports=function required(e,t){t=t.split(":")[0];e=+e;if(!e)return false;switch(t){case"http":case"ws":return e!==80;case"https":case"wss":return e!==443;case"ftp":return e!==21;case"gopher":return e!==70;case"file":return false}return e!==0}},491:e=>{"use strict";e.exports=require("assert")},685:e=>{"use strict";e.exports=require("http")},687:e=>{"use strict";e.exports=require("https")},937:e=>{"use strict";e.exports=require("next/dist/compiled/debug")},781:e=>{"use strict";e.exports=require("stream")},310:e=>{"use strict";e.exports=require("url")},837:e=>{"use strict";e.exports=require("util")}};var t={};function __nccwpck_require__(r){var o=t[r];if(o!==undefined){return o.exports}var s=t[r]={exports:{}};var n=true;try{e[r](s,s.exports,__nccwpck_require__);n=false}finally{if(n)delete t[r]}return s.exports}if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=__dirname+"/";var r=__nccwpck_require__(768);module.exports=r})(); \ No newline at end of file + */e.exports={checkMethodAndHeader:function checkMethodAndHeader(e,t){if(e.method!=="GET"||!e.headers.upgrade){t.destroy();return true}if(e.headers.upgrade.toLowerCase()!=="websocket"){t.destroy();return true}},XHeaders:function XHeaders(e,t,r){if(!r.xfwd)return;var o={for:e.connection.remoteAddress||e.socket.remoteAddress,port:s.getPort(e),proto:s.hasEncryptedConnection(e)?"wss":"ws"};["for","port","proto"].forEach((function(t){e.headers["x-forwarded-"+t]=(e.headers["x-forwarded-"+t]||"")+(e.headers["x-forwarded-"+t]?",":"")+o[t]}))},stream:function stream(e,t,r,i,a,c){var createHttpHeader=function(e,t){return Object.keys(t).reduce((function(e,r){var o=t[r];if(!Array.isArray(o)){e.push(r+": "+o);return e}for(var n=0;n{"use strict";e.exports=function required(e,t){t=t.split(":")[0];e=+e;if(!e)return false;switch(t){case"http":case"ws":return e!==80;case"https":case"wss":return e!==443;case"ftp":return e!==21;case"gopher":return e!==70;case"file":return false}return e!==0}},491:e=>{"use strict";e.exports=require("assert")},685:e=>{"use strict";e.exports=require("http")},687:e=>{"use strict";e.exports=require("https")},937:e=>{"use strict";e.exports=require("next/dist/compiled/debug")},781:e=>{"use strict";e.exports=require("stream")},310:e=>{"use strict";e.exports=require("url")},837:e=>{"use strict";e.exports=require("util")}};var t={};function __nccwpck_require__(r){var o=t[r];if(o!==undefined){return o.exports}var n=t[r]={exports:{}};var s=true;try{e[r](n,n.exports,__nccwpck_require__);s=false}finally{if(s)delete t[r]}return n.exports}if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=__dirname+"/";var r=__nccwpck_require__(204);module.exports=r})(); \ No newline at end of file diff --git a/packages/next/src/server/app-render/action-handler.ts b/packages/next/src/server/app-render/action-handler.ts index b183998acd1904..2ebc053b741838 100644 --- a/packages/next/src/server/app-render/action-handler.ts +++ b/packages/next/src/server/app-render/action-handler.ts @@ -616,9 +616,14 @@ export async function handleAction({ workStore.fetchCache = 'default-no-store' const originHeader = req.headers['origin'] - const originDomain = - typeof originHeader === 'string' && originHeader !== 'null' - ? new URL(originHeader).host + const originHost = + typeof originHeader === 'string' + ? // 'null' is a valid origin e.g. from privacy-sensitive contexts like sandboxed iframes. + // However, these contexts can still send along credentials like cookies, + // so we need to check if they're allowed cross-origin requests. + originHeader === 'null' + ? 'null' + : new URL(originHeader).host : undefined const host = parseHostHeader(req.headers) @@ -631,15 +636,17 @@ export async function handleAction({ } // This is to prevent CSRF attacks. If `x-forwarded-host` is set, we need to // ensure that the request is coming from the same host. - if (!originDomain) { - // This might be an old browser that doesn't send `host` header. We ignore - // this case. + if (!originHost) { + // This is a handcrafted request without an origin or a request from an unsafe browser. + // We'll let this through but log a warning. + // We can't guard against unsafe browsers and handcrafted requests can't contain + // user credentials that haven't been shared willingly. warning = 'Missing `origin` header from a forwarded Server Actions request.' - } else if (!host || originDomain !== host.value) { + } else if (!host || originHost !== host.value) { // If the customer sets a list of allowed origins, we'll allow the request. // These are considered safe but might be different from forwarded host set // by the infra (i.e. reverse proxies). - if (isCsrfOriginAllowed(originDomain, serverActions?.allowedOrigins)) { + if (isCsrfOriginAllowed(originHost, serverActions?.allowedOrigins)) { // Ignore it } else { if (host) { @@ -650,7 +657,7 @@ export async function handleAction({ }\` header with value \`${limitUntrustedHeaderValueForLogs( host.value )}\` does not match \`origin\` header with value \`${limitUntrustedHeaderValueForLogs( - originDomain + originHost )}\` from a forwarded Server Actions request. Aborting the action.` ) } else { diff --git a/packages/next/src/server/lib/incremental-cache/memory-cache.external.ts b/packages/next/src/server/lib/incremental-cache/memory-cache.external.ts index 33ce99e47ed4b7..e44665640973a1 100644 --- a/packages/next/src/server/lib/incremental-cache/memory-cache.external.ts +++ b/packages/next/src/server/lib/incremental-cache/memory-cache.external.ts @@ -4,6 +4,24 @@ import { LRUCache } from '../lru-cache' let memoryCache: LRUCache | undefined +function getBufferSize(buffer: Buffer | undefined) { + return buffer?.length || 0 +} + +function getSegmentDataSize(segmentData: Map | undefined) { + if (!segmentData) { + return 0 + } + + let size = 0 + + for (const [segmentPath, buffer] of segmentData) { + size += segmentPath.length + getBufferSize(buffer) + } + + return size +} + export function getMemoryCache(maxMemoryCacheSize: number) { if (!memoryCache) { memoryCache = new LRUCache(maxMemoryCacheSize, function length({ value }) { @@ -19,14 +37,17 @@ export function getMemoryCache(maxMemoryCacheSize: number) { return value.body.length } // rough estimate of size of cache value - return ( - value.html.length + - (JSON.stringify( - value.kind === CachedRouteKind.APP_PAGE - ? value.rscData - : value.pageData - )?.length || 0) - ) + if (value.kind === CachedRouteKind.APP_PAGE) { + return Math.max( + 1, + value.html.length + + getBufferSize(value.rscData) + + (value.postponed?.length || 0) + + getSegmentDataSize(value.segmentData) + ) + } + + return value.html.length + (JSON.stringify(value.pageData)?.length || 0) }) } diff --git a/patches/http-proxy@1.18.1.patch b/patches/http-proxy@1.18.1.patch index 04e179137026a3..e30900ebf339d3 100644 --- a/patches/http-proxy@1.18.1.patch +++ b/patches/http-proxy@1.18.1.patch @@ -1,15 +1,19 @@ diff --git a/lib/http-proxy/common.js b/lib/http-proxy/common.js -index 6513e81d80d5250ea249ea833f819ece67897c7e..486d4c896d65a3bb7cf63307af68facb3ddb886b 100644 +index 6513e81d80d5250ea249ea833f819ece67897c7e..09143dd1fe4e67885f40ea916a6ea1ef3e3afa19 100644 --- a/lib/http-proxy/common.js +++ b/lib/http-proxy/common.js -@@ -1,6 +1,5 @@ +@@ -1,9 +1,9 @@ var common = exports, url = require('url'), - extend = require('util')._extend, required = require('requires-port'); var upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i, -@@ -40,10 +39,10 @@ common.setupOutgoing = function(outgoing, options, req, forward) { ++ hopByHopTransferEncodingHeader = /(^|,)\s*transfer-encoding\s*($|,)/i, + isSSL = /^https|wss/; + + /** +@@ -40,10 +40,10 @@ common.setupOutgoing = function(outgoing, options, req, forward) { ); outgoing.method = options.method || req.method; @@ -22,6 +26,30 @@ index 6513e81d80d5250ea249ea833f819ece67897c7e..486d4c896d65a3bb7cf63307af68facb } if (options.auth) { +@@ -61,13 +61,22 @@ common.setupOutgoing = function(outgoing, options, req, forward) { + + outgoing.agent = options.agent || false; + outgoing.localAddress = options.localAddress; ++ outgoing.headers = outgoing.headers || {}; ++ var hasTransferEncodingHeader = Object.keys(outgoing.headers).some(function (header) { ++ return header.toLowerCase() === 'transfer-encoding' ++ && typeof outgoing.headers[header] !== 'undefined'; ++ }); ++ ++ if (hasTransferEncodingHeader ++ || (typeof outgoing.headers.connection === 'string' ++ && hopByHopTransferEncodingHeader.test(outgoing.headers.connection)) ++ ) { outgoing.headers.connection = 'close'; } + + // + // Remark: If we are false and not upgrading, set the connection: close. This is the right thing to do + // as node core doesn't handle this COMPLETELY properly yet. + // + if (!outgoing.agent) { +- outgoing.headers = outgoing.headers || {}; + if (typeof outgoing.headers.connection !== 'string' + || !upgradeHeader.test(outgoing.headers.connection) + ) { outgoing.headers.connection = 'close'; } diff --git a/lib/http-proxy/index.js b/lib/http-proxy/index.js index 977a4b3622b9eaac27689f06347ea4c5173a96cd..88b2d0fcfa03c3aafa47c7e6d38e64412c45a7cc 100644 --- a/lib/http-proxy/index.js @@ -44,3 +72,19 @@ index 977a4b3622b9eaac27689f06347ea4c5173a96cd..88b2d0fcfa03c3aafa47c7e6d38e6441 cntr--; } +diff --git a/lib/http-proxy/passes/web-incoming.js b/lib/http-proxy/passes/web-incoming.js +index 7ae735514190eea569c605fff7d27c045fe8d601..c7c25e7228b21c76b3c7115af82ddcbf13a8e3ec 100644 +--- a/lib/http-proxy/passes/web-incoming.js ++++ b/lib/http-proxy/passes/web-incoming.js +@@ -33,9 +33,9 @@ module.exports = { + + deleteLength: function deleteLength(req, res, options) { + if((req.method === 'DELETE' || req.method === 'OPTIONS') +- && !req.headers['content-length']) { ++ && typeof req.headers['content-length'] === 'undefined' ++ && typeof req.headers['transfer-encoding'] === 'undefined') { + req.headers['content-length'] = '0'; +- delete req.headers['transfer-encoding']; + } + }, + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32b38260424056..6607a3d4b40c43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,7 +29,7 @@ patchedDependencies: hash: rvl3vkomen3tospgr67bzubfyu path: patches/@types__node@20.17.6.patch http-proxy@1.18.1: - hash: qqiqxx62zlcu62nljjmhlvexni + hash: eyqcxg3pntyhqyqr5zytxa7pbi path: patches/http-proxy@1.18.1.patch minizlib@3.1.0: hash: o4nv5mg6kfnuzlknbdln3azhvu @@ -371,7 +371,7 @@ importers: version: 5.1.18 http-proxy: specifier: 1.18.1 - version: 1.18.1(patch_hash=qqiqxx62zlcu62nljjmhlvexni) + version: 1.18.1(patch_hash=eyqcxg3pntyhqyqr5zytxa7pbi) husky: specifier: 9.0.11 version: 9.0.11 @@ -1523,7 +1523,7 @@ importers: version: 5.1.1 http-proxy: specifier: 1.18.1 - version: 1.18.1(patch_hash=qqiqxx62zlcu62nljjmhlvexni) + version: 1.18.1(patch_hash=eyqcxg3pntyhqyqr5zytxa7pbi) http-proxy-agent: specifier: 5.0.0 version: 5.0.0 @@ -30554,7 +30554,7 @@ snapshots: http-proxy-middleware@2.0.7(@types/express@4.17.21): dependencies: '@types/http-proxy': 1.17.17 - http-proxy: 1.18.1(patch_hash=qqiqxx62zlcu62nljjmhlvexni) + http-proxy: 1.18.1(patch_hash=eyqcxg3pntyhqyqr5zytxa7pbi) is-glob: 4.0.3 is-plain-obj: 3.0.0 micromatch: 4.0.8 @@ -30563,7 +30563,7 @@ snapshots: transitivePeerDependencies: - supports-color - http-proxy@1.18.1(patch_hash=qqiqxx62zlcu62nljjmhlvexni): + http-proxy@1.18.1(patch_hash=eyqcxg3pntyhqyqr5zytxa7pbi): dependencies: eventemitter3: 4.0.7 follow-redirects: 1.9.0 diff --git a/test/e2e/app-dir/actions-allowed-origins/app-action-allowed-origins.test.ts b/test/e2e/app-dir/actions-allowed-origins/app-action-allowed-origins.test.ts index 8cf75c75860fcc..7a055bb2fc5a92 100644 --- a/test/e2e/app-dir/actions-allowed-origins/app-action-allowed-origins.test.ts +++ b/test/e2e/app-dir/actions-allowed-origins/app-action-allowed-origins.test.ts @@ -26,17 +26,4 @@ describe('app-dir action allowed origins', () => { return await browser.elementByCss('#res').text() }, 'hi') }) - - it('should not crash for requests from privacy sensitive contexts', async function () { - const res = await next.fetch('/', { - method: 'POST', - headers: { - Origin: 'null', - 'Content-type': 'application/x-www-form-urlencoded', - 'Sec-Fetch-Site': 'same-origin', - }, - }) - - expect({ status: res.status }).toEqual({ status: 200 }) - }) }) diff --git a/test/e2e/app-dir/actions-allowed-origins/app-action-opaque-origin.test.ts b/test/e2e/app-dir/actions-allowed-origins/app-action-opaque-origin.test.ts new file mode 100644 index 00000000000000..b4126ae632d0c2 --- /dev/null +++ b/test/e2e/app-dir/actions-allowed-origins/app-action-opaque-origin.test.ts @@ -0,0 +1,71 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' +import { join } from 'path' + +describe('app-dir action allowed from opaque origins', () => { + const { next, skipped } = nextTestSetup({ + files: join(__dirname, 'opaque-origin'), + skipDeployment: true, + env: { + NEXT_TEST_ALLOW_OPAQUE_ORIGIN: '1', + }, + }) + + if (skipped) { + return + } + + it('should succeed on submission', async function () { + const browser = await next.browser('/sandboxed') + + await browser.elementByCss('input[type="submit"]').click() + + await retry(async () => { + expect(await browser.elementByCss('output').text()).toEqual( + 'Action Invoked' + ) + }) + }) +}) + +describe('app-dir action disallowed from opaque origins', () => { + const { isNextDev, next, skipped } = nextTestSetup({ + files: join(__dirname, 'opaque-origin'), + skipDeployment: true, + env: { + NEXT_TEST_ALLOW_OPAQUE_ORIGIN: '', + }, + }) + + if (skipped) { + return + } + + it('should fail on submission', async function () { + const browser = await next.browser('/sandboxed') + const beforeSubmissionLogOffset = (await browser.log()).length + + await browser.elementByCss('input[type="submit"]').click() + + await retry(async () => { + const logs = await browser.log() + const newLogs = logs.slice(beforeSubmissionLogOffset) + expect(newLogs).toEqual( + expect.arrayContaining([ + { + source: 'error', + message: + 'Failed to load resource: the server responded with a status of 500 (Internal Server Error)', + }, + ]) + ) + }) + if (isNextDev) { + // page is borked at this point. Nothing interesting to assert on. + } else { + expect(await browser.elementByCss('body').text()).toEqual( + 'Internal Server Error' + ) + } + }) +}) diff --git a/test/e2e/app-dir/actions-allowed-origins/opaque-origin/app/action.js b/test/e2e/app-dir/actions-allowed-origins/opaque-origin/app/action.js new file mode 100644 index 00000000000000..956cb1edc08854 --- /dev/null +++ b/test/e2e/app-dir/actions-allowed-origins/opaque-origin/app/action.js @@ -0,0 +1,10 @@ +'use server' + +import { cookies } from 'next/headers' + +export async function log() { + console.log('action invoked') + const cookieStore = await cookies() + cookieStore.set('log-action-invoked', '1') + return 'hi' +} diff --git a/test/e2e/app-dir/actions-allowed-origins/opaque-origin/app/layout.js b/test/e2e/app-dir/actions-allowed-origins/opaque-origin/app/layout.js new file mode 100644 index 00000000000000..1b89d467cf3ec8 --- /dev/null +++ b/test/e2e/app-dir/actions-allowed-origins/opaque-origin/app/layout.js @@ -0,0 +1,21 @@ +import { Suspense } from 'react' + +export default function RootLayout({ children }) { + return ( + // Needs to be above html since we can't allow scripts in sandbox + Loading...}> + + + +
    + {/* These need to be MPAs so that the appropriate headers are applied */} +
  • + Sandboxed Page +
  • +
+ {children} + + +
+ ) +} diff --git a/test/e2e/app-dir/actions-allowed-origins/opaque-origin/app/sandboxed/page.js b/test/e2e/app-dir/actions-allowed-origins/opaque-origin/app/sandboxed/page.js new file mode 100644 index 00000000000000..cfc916eb2f13cc --- /dev/null +++ b/test/e2e/app-dir/actions-allowed-origins/opaque-origin/app/sandboxed/page.js @@ -0,0 +1,14 @@ +import { cookies } from 'next/headers' +import { log } from '../action' + +export default async function Page() { + const cookieStore = await cookies() + const cookie = cookieStore.get('log-action-invoked') + const hasLogged = cookie?.value === '1' + return ( +
+ + {hasLogged ? 'Action Invoked' : 'Action Not Invoked'} +
+ ) +} diff --git a/test/e2e/app-dir/actions-allowed-origins/opaque-origin/next.config.js b/test/e2e/app-dir/actions-allowed-origins/opaque-origin/next.config.js new file mode 100644 index 00000000000000..1fade4aa2f130b --- /dev/null +++ b/test/e2e/app-dir/actions-allowed-origins/opaque-origin/next.config.js @@ -0,0 +1,27 @@ +const allowOpaqueOrigin = process.env.NEXT_TEST_ALLOW_OPAQUE_ORIGIN === '1' + +/** @type {import('next').NextConfig} */ +module.exports = { + productionBrowserSourceMaps: true, + logging: { + fetches: {}, + }, + headers() { + return [ + { + source: '/sandboxed', + headers: [ + { + key: 'Content-Security-Policy', + value: 'sandbox allow-forms', + }, + ], + }, + ] + }, + experimental: { + serverActions: { + allowedOrigins: allowOpaqueOrigin ? ['null'] : [], + }, + }, +} diff --git a/test/production/app-dir/empty-shell-route-cache/app/layout.tsx b/test/production/app-dir/empty-shell-route-cache/app/layout.tsx new file mode 100644 index 00000000000000..716a8db36f52c5 --- /dev/null +++ b/test/production/app-dir/empty-shell-route-cache/app/layout.tsx @@ -0,0 +1,9 @@ +import { ReactNode } from 'react' + +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/production/app-dir/empty-shell-route-cache/app/page.tsx b/test/production/app-dir/empty-shell-route-cache/app/page.tsx new file mode 100644 index 00000000000000..4a395e2d31eadc --- /dev/null +++ b/test/production/app-dir/empty-shell-route-cache/app/page.tsx @@ -0,0 +1,17 @@ +import Link from 'next/link' + +export default function Page() { + return ( +
+

Empty Shell Route Cache

+
    +
  • + With Suspense +
  • +
  • + Without Suspense +
  • +
+
+ ) +} diff --git a/test/production/app-dir/empty-shell-route-cache/app/with-suspense/page.tsx b/test/production/app-dir/empty-shell-route-cache/app/with-suspense/page.tsx new file mode 100644 index 00000000000000..675cdb5586e6b5 --- /dev/null +++ b/test/production/app-dir/empty-shell-route-cache/app/with-suspense/page.tsx @@ -0,0 +1,18 @@ +import { Suspense } from 'react' +import { connection } from 'next/server' + +async function DynamicContent() { + await connection() + return

Dynamic content rendered at request time

+} + +export default function Page() { + return ( +
+

With Suspense

+ Loading...

}> + +
+
+ ) +} diff --git a/test/production/app-dir/empty-shell-route-cache/app/without-suspense/page.tsx b/test/production/app-dir/empty-shell-route-cache/app/without-suspense/page.tsx new file mode 100644 index 00000000000000..e2c79bc63f998d --- /dev/null +++ b/test/production/app-dir/empty-shell-route-cache/app/without-suspense/page.tsx @@ -0,0 +1,13 @@ +import { connection } from 'next/server' + +export const unstable_instant = false + +export default async function Page() { + await connection() + return ( +
+

Without Suspense

+

Dynamic content rendered at request time

+
+ ) +} diff --git a/test/production/app-dir/empty-shell-route-cache/empty-shell-route-cache.test.ts b/test/production/app-dir/empty-shell-route-cache/empty-shell-route-cache.test.ts new file mode 100644 index 00000000000000..13a0b25090a3f6 --- /dev/null +++ b/test/production/app-dir/empty-shell-route-cache/empty-shell-route-cache.test.ts @@ -0,0 +1,92 @@ +import fs from 'fs/promises' +import { nextTestSetup } from 'e2e-utils' +import path from 'path' + +describe('empty-shell-route-cache', () => { + const { next } = nextTestSetup({ + files: __dirname, + skipStart: true, + // This regression inspects on-disk .next artifacts, so it only applies to + // self-hosted build + start and is not portable to deployment mode. + skipDeployment: true, + }) + + beforeAll(async () => { + await next.build() + }) + + async function getRouteArtifactMtimes(route: string) { + const routeMeta = JSON.parse( + await next.readFile(`.next/server/app/${route}.meta`) + ) + const files = [ + `${route}.html`, + `${route}.meta`, + ...routeMeta.segmentPaths.map( + (segmentPath: string) => `${route}.segments${segmentPath}.segment.rsc` + ), + ] + + return Object.fromEntries( + await Promise.all( + files.map(async (file) => { + const stat = await fs.stat( + path.join(next.testDir, '.next/server/app', file) + ) + + return [file, stat.mtimeMs] + }) + ) + ) + } + + it('should emit both routes as partially static build artifacts', async () => { + const prerenderManifest = JSON.parse( + await next.readFile('.next/prerender-manifest.json') + ) + + expect(prerenderManifest.routes['/with-suspense'].renderingMode).toBe( + 'PARTIALLY_STATIC' + ) + expect(prerenderManifest.routes['/without-suspense'].renderingMode).toBe( + 'PARTIALLY_STATIC' + ) + + expect(await next.readFile('.next/server/app/with-suspense.html')).not.toBe( + '' + ) + expect(await next.readFile('.next/server/app/without-suspense.html')).toBe( + '' + ) + + const withoutSuspenseMeta = JSON.parse( + await next.readFile('.next/server/app/without-suspense.meta') + ) + expect(withoutSuspenseMeta.postponed).toBeTruthy() + }) + + describe('after next start', () => { + beforeAll(async () => { + await next.start({ skipBuild: true }) + }) + + afterAll(async () => { + await next.stop() + }) + + it.each([ + ['/with-suspense', 'with-suspense'], + ['/without-suspense', 'without-suspense'], + ])( + 'should not rewrite %s build artifacts on the first request', + async (pathname, route) => { + const before = await getRouteArtifactMtimes(route) + const response = await next.fetch(pathname) + const after = await getRouteArtifactMtimes(route) + + expect(response.status).toBe(200) + expect(after).toEqual(before) + } + ) + }) +}) diff --git a/test/production/app-dir/empty-shell-route-cache/next.config.js b/test/production/app-dir/empty-shell-route-cache/next.config.js new file mode 100644 index 00000000000000..e64bae22d65803 --- /dev/null +++ b/test/production/app-dir/empty-shell-route-cache/next.config.js @@ -0,0 +1,8 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + cacheComponents: true, +} + +module.exports = nextConfig diff --git a/test/production/rewrite-request-smuggling/next.config.js b/test/production/rewrite-request-smuggling/next.config.js new file mode 100644 index 00000000000000..d0471a4691ece2 --- /dev/null +++ b/test/production/rewrite-request-smuggling/next.config.js @@ -0,0 +1,13 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + async rewrites() { + return [ + { + source: '/rewrites/:path*', + destination: `http://127.0.0.1:${process.env.TEST_INTERMEDIARY_PORT}/rewrites/:path*`, + }, + ] + }, +} + +module.exports = nextConfig diff --git a/test/production/rewrite-request-smuggling/pages/index.tsx b/test/production/rewrite-request-smuggling/pages/index.tsx new file mode 100644 index 00000000000000..ff7159d9149fee --- /dev/null +++ b/test/production/rewrite-request-smuggling/pages/index.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/production/rewrite-request-smuggling/rewrite-request-smuggling.test.ts b/test/production/rewrite-request-smuggling/rewrite-request-smuggling.test.ts new file mode 100644 index 00000000000000..e53984ab1f54b2 --- /dev/null +++ b/test/production/rewrite-request-smuggling/rewrite-request-smuggling.test.ts @@ -0,0 +1,229 @@ +import net from 'net' +import http from 'http' +import { createNext, NextInstance } from 'e2e-utils' +import { findPort, retry } from 'next-test-utils' + +describe('rewrite-request-smuggling', () => { + let backend: http.Server + let backendPort: number + let intermediary: http.Server + let intermediaryPort: number + let next: NextInstance + const backendRequests: string[] = [] + + async function sendSmugglingPayload({ + nextPort, + connectionHeader, + method = 'DELETE', + rewritePath = '/rewrites/poc', + }: { + nextPort: number + connectionHeader: string + method?: 'DELETE' | 'OPTIONS' + rewritePath?: string + }) { + const smuggledRequest = Buffer.from( + `GET /secret HTTP/1.1\r\nHost: 127.0.0.1:${nextPort}\r\n\r\n`, + 'latin1' + ) + const chunkSize = Buffer.from( + `${smuggledRequest.length.toString(16).toUpperCase()}\r\n`, + 'latin1' + ) + + const payload = Buffer.concat([ + Buffer.from( + `${method} ${rewritePath} HTTP/1.1\r\nHost: 127.0.0.1:${nextPort}\r\nTransfer-Encoding: chunked\r\nConnection: ${connectionHeader}\r\n\r\n`, + 'latin1' + ), + chunkSize, + smuggledRequest, + Buffer.from('\r\n0\r\n\r\n', 'latin1'), + ]) + + await new Promise((resolve, reject) => { + const socket = net.createConnection({ + host: '127.0.0.1', + port: nextPort, + }) + + socket.once('connect', () => { + socket.write(payload) + }) + socket.once('error', reject) + socket.setTimeout(1000, () => socket.destroy()) + socket.once('close', () => resolve()) + }) + } + + beforeAll(async () => { + backendPort = await findPort() + intermediaryPort = await findPort() + + backend = http.createServer((req, res) => { + backendRequests.push(`${req.method} ${req.url}`) + + if (req.url?.startsWith('/rewrites/')) { + res.statusCode = 200 + res.end('rewrite-ok') + return + } + + if (req.url === '/secret') { + res.statusCode = 200 + res.end('secret') + return + } + + res.statusCode = 404 + res.end('not-found') + }) + + intermediary = http.createServer((req, res) => { + const connectionHeader = Array.isArray(req.headers['connection']) + ? req.headers['connection'].join(',') + : req.headers['connection'] || '' + const hopByHopHeaders = connectionHeader + .split(',') + .map((h) => h.trim().toLowerCase()) + .filter(Boolean) + const stripTransferEncodingUnconditionally = + req.url?.startsWith('/rewrites/non-rfc-strip') || false + + const forwardHeaders: Record = {} + for (const [key, value] of Object.entries(req.headers)) { + if (key === 'connection') continue + if (stripTransferEncodingUnconditionally && key === 'transfer-encoding') + continue + if (hopByHopHeaders.includes(key)) continue + if (value !== undefined) { + forwardHeaders[key] = value + } + } + forwardHeaders.connection = stripTransferEncodingUnconditionally + ? connectionHeader.toLowerCase().includes('close') + ? 'close' + : 'keep-alive' + : 'keep-alive' + + const proxyReq = http.request( + { + hostname: '127.0.0.1', + port: backendPort, + method: req.method, + path: req.url, + headers: forwardHeaders, + }, + (proxyRes) => { + res.writeHead(proxyRes.statusCode || 500, proxyRes.headers) + proxyRes.pipe(res) + } + ) + + proxyReq.on('error', () => { + res.statusCode = 502 + res.end('Bad Gateway') + }) + + req.pipe(proxyReq) + }) + + await new Promise((resolve, reject) => { + backend.listen(backendPort, '127.0.0.1', resolve) + backend.once('error', reject) + }) + + await new Promise((resolve, reject) => { + intermediary.listen(intermediaryPort, '127.0.0.1', resolve) + intermediary.once('error', reject) + }) + + next = await createNext({ + files: __dirname, + env: { + TEST_INTERMEDIARY_PORT: String(intermediaryPort), + }, + }) + }) + + afterAll(async () => { + await next?.destroy() + await new Promise((resolve) => intermediary.close(() => resolve())) + await new Promise((resolve) => backend.close(() => resolve())) + }) + + it('does not smuggle a second request when using keep-alive only', async () => { + backendRequests.length = 0 + + const nextPort = Number(new URL(next.url).port) + await sendSmugglingPayload({ nextPort, connectionHeader: 'keep-alive' }) + + await retry(async () => { + expect(backendRequests).toContain('DELETE /rewrites/poc') + }) + expect(backendRequests).not.toContain('GET /secret') + }) + + it('does not smuggle a second request with keep-alive, upgrade', async () => { + backendRequests.length = 0 + + const nextPort = Number(new URL(next.url).port) + await sendSmugglingPayload({ + nextPort, + connectionHeader: 'keep-alive, upgrade', + }) + + await retry(async () => { + expect(backendRequests).toContain('DELETE /rewrites/poc') + }) + expect(backendRequests).not.toContain('GET /secret') + }) + + it('does not smuggle a second request with Transfer-Encoding, upgrade', async () => { + backendRequests.length = 0 + + const nextPort = Number(new URL(next.url).port) + await sendSmugglingPayload({ + nextPort, + connectionHeader: 'Transfer-Encoding, upgrade', + }) + + await retry(async () => { + expect(backendRequests).toContain('DELETE /rewrites/poc') + }) + expect(backendRequests).not.toContain('GET /secret') + }) + + it('does not smuggle a second request for OPTIONS with Transfer-Encoding, upgrade', async () => { + backendRequests.length = 0 + + const nextPort = Number(new URL(next.url).port) + await sendSmugglingPayload({ + nextPort, + method: 'OPTIONS', + connectionHeader: 'Transfer-Encoding, upgrade', + }) + + await retry(async () => { + expect(backendRequests).toContain('OPTIONS /rewrites/poc') + }) + expect(backendRequests).not.toContain('GET /secret') + }) + + it('does not smuggle a second request when an intermediary strips transfer-encoding unconditionally', async () => { + backendRequests.length = 0 + + const nextPort = Number(new URL(next.url).port) + await sendSmugglingPayload({ + nextPort, + method: 'OPTIONS', + rewritePath: '/rewrites/non-rfc-strip', + connectionHeader: 'keep-alive, upgrade', + }) + + await retry(async () => { + expect(backendRequests).toContain('OPTIONS /rewrites/non-rfc-strip') + }) + expect(backendRequests).not.toContain('GET /secret') + }) +})