@jsenv/server 17.0.3 → 17.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/jsenv_server.js
CHANGED
|
@@ -3863,7 +3863,7 @@ const serverPluginOpenFile = () => {
|
|
|
3863
3863
|
const routeInspectorHtmlFileUrl = import.meta
|
|
3864
3864
|
.resolve("./client/route_inspector/route_inspector.html");
|
|
3865
3865
|
|
|
3866
|
-
const serverPluginRouteInspector = () => {
|
|
3866
|
+
const serverPluginRouteInspector = ({ canExposeSensitiveData }) => {
|
|
3867
3867
|
return {
|
|
3868
3868
|
name: "jsenv:route_inspector",
|
|
3869
3869
|
routes: [
|
|
@@ -3889,7 +3889,9 @@ const serverPluginRouteInspector = () => {
|
|
|
3889
3889
|
description: "Get the routes available on this server in JSON.",
|
|
3890
3890
|
declarationSource: import.meta.url,
|
|
3891
3891
|
fetch: (request, helpers) => {
|
|
3892
|
-
const routeJSON = helpers.router.inspect(
|
|
3892
|
+
const routeJSON = helpers.router.inspect({
|
|
3893
|
+
canExposeSensitiveData,
|
|
3894
|
+
});
|
|
3893
3895
|
return Response.json(routeJSON);
|
|
3894
3896
|
},
|
|
3895
3897
|
},
|
|
@@ -5378,13 +5380,18 @@ const createResourcePattern = (pattern) => {
|
|
|
5378
5380
|
let decodedPathname = decodeURIComponent(pathname);
|
|
5379
5381
|
let result;
|
|
5380
5382
|
if (patternEndsWithSlash && !searchPattern && !hashPattern) {
|
|
5381
|
-
|
|
5383
|
+
// also match the path without trailing slash (e.g. /foo matches /foo/)
|
|
5384
|
+
const pathnamePatternWithoutSlash = pathnamePatternString.slice(0, -1);
|
|
5385
|
+
if (decodedPathname === pathnamePatternWithoutSlash) {
|
|
5386
|
+
result = { named: {}, stars: [""] };
|
|
5387
|
+
} else if (!decodedPathname.startsWith(pathnamePatternString)) {
|
|
5382
5388
|
return null;
|
|
5389
|
+
} else {
|
|
5390
|
+
result = {
|
|
5391
|
+
named: {},
|
|
5392
|
+
stars: [decodedPathname.slice(pathnamePatternString.length)],
|
|
5393
|
+
};
|
|
5383
5394
|
}
|
|
5384
|
-
result = {
|
|
5385
|
-
named: {},
|
|
5386
|
-
stars: [decodedPathname.slice(pathnamePatternString.length)],
|
|
5387
|
-
};
|
|
5388
5395
|
} else {
|
|
5389
5396
|
result = pathnamePattern.match(decodedPathname);
|
|
5390
5397
|
}
|
|
@@ -5664,7 +5671,7 @@ const HTTP_METHODS = [
|
|
|
5664
5671
|
|
|
5665
5672
|
const createRouter = (
|
|
5666
5673
|
routeDescriptionArray,
|
|
5667
|
-
{ optionsFallback, logLevel } = {},
|
|
5674
|
+
{ optionsFallback, logLevel, redirect = "manual" } = {},
|
|
5668
5675
|
) => {
|
|
5669
5676
|
const logger = createLogger({ logLevel });
|
|
5670
5677
|
const routeSet = new Set();
|
|
@@ -5813,16 +5820,6 @@ const createRouter = (
|
|
|
5813
5820
|
|
|
5814
5821
|
const match = async (request, fetchSecondArg) => {
|
|
5815
5822
|
fetchSecondArg.router = router;
|
|
5816
|
-
const wouldHaveMatched = {
|
|
5817
|
-
// in case nothing matches we can produce a response with Allow: GET, POST, PUT for example
|
|
5818
|
-
methodSet: new Set(),
|
|
5819
|
-
requestMediaTypeSet: new Set(),
|
|
5820
|
-
responseMediaTypeSet: new Set(),
|
|
5821
|
-
responseLanguageSet: new Set(),
|
|
5822
|
-
responseVersionSet: new Set(),
|
|
5823
|
-
responseEncodingSet: new Set(),
|
|
5824
|
-
upgrade: false,
|
|
5825
|
-
};
|
|
5826
5823
|
|
|
5827
5824
|
let currentServerPlugin;
|
|
5828
5825
|
let currentRoutingTiming;
|
|
@@ -5897,7 +5894,6 @@ It should be set to one of route.${routePropertyName}: ${availableValues.join(",
|
|
|
5897
5894
|
break;
|
|
5898
5895
|
}
|
|
5899
5896
|
}
|
|
5900
|
-
|
|
5901
5897
|
if (!matchesAnAvailableValue) {
|
|
5902
5898
|
request.logger.warn(
|
|
5903
5899
|
`The value "${responseHeaderValue}" found in response header ${responseHeaderName} is strange.
|
|
@@ -5913,217 +5909,267 @@ It should be should be one of route.${routePropertyName}: ${availableValues.join
|
|
|
5913
5909
|
checkResponseContentHeader(route, responseHeaders, "encoding");
|
|
5914
5910
|
};
|
|
5915
5911
|
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5912
|
+
const resolveResponse = async (fetchReturnValue, { request, route }) => {
|
|
5913
|
+
let status;
|
|
5914
|
+
let statusText;
|
|
5915
|
+
let statusMessage;
|
|
5916
|
+
let headers;
|
|
5917
|
+
let body;
|
|
5918
|
+
|
|
5919
|
+
if (fetchReturnValue instanceof Response) {
|
|
5920
|
+
status = fetchReturnValue.status;
|
|
5921
|
+
statusText = fetchReturnValue.statusText;
|
|
5922
|
+
headers = Object.fromEntries(fetchReturnValue.headers);
|
|
5923
|
+
body = fetchReturnValue.body;
|
|
5924
|
+
} else if (
|
|
5925
|
+
fetchReturnValue !== null &&
|
|
5926
|
+
typeof fetchReturnValue === "object"
|
|
5927
|
+
) {
|
|
5928
|
+
status = fetchReturnValue.status || 404;
|
|
5929
|
+
statusText = fetchReturnValue.statusText;
|
|
5930
|
+
statusMessage = fetchReturnValue.statusMessage;
|
|
5931
|
+
headers = fetchReturnValue.headers || {};
|
|
5932
|
+
body = fetchReturnValue.body;
|
|
5933
|
+
} else {
|
|
5934
|
+
throw new TypeError(
|
|
5935
|
+
`response must be a Response, or an Object, received ${fetchReturnValue}`,
|
|
5936
|
+
);
|
|
5927
5937
|
}
|
|
5938
|
+
|
|
5928
5939
|
if (
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5940
|
+
redirect === "follow" &&
|
|
5941
|
+
isRedirectStatus(status) &&
|
|
5942
|
+
headers["location"]
|
|
5932
5943
|
) {
|
|
5933
|
-
const
|
|
5944
|
+
const redirectUrl = new URL(headers["location"], request.url);
|
|
5945
|
+
if (redirectUrl.origin === new URL(request.url).origin) {
|
|
5946
|
+
const redirectRequest = {
|
|
5947
|
+
...request,
|
|
5948
|
+
url: redirectUrl.href,
|
|
5949
|
+
resource:
|
|
5950
|
+
redirectUrl.pathname + redirectUrl.search + redirectUrl.hash,
|
|
5951
|
+
// GET for 301/302/303, preserve method for 307/308
|
|
5952
|
+
method: status === 307 || status === 308 ? request.method : "GET",
|
|
5953
|
+
params: {},
|
|
5954
|
+
};
|
|
5955
|
+
const redirectedResponse = await matchRoutes(redirectRequest);
|
|
5956
|
+
onRouteMatch(route);
|
|
5957
|
+
onResponseHeaders(request, route, redirectedResponse.headers || {});
|
|
5958
|
+
return redirectedResponse;
|
|
5959
|
+
}
|
|
5960
|
+
// redirect to other origins are left to the client to handle
|
|
5961
|
+
}
|
|
5962
|
+
|
|
5963
|
+
onRouteMatch(route);
|
|
5964
|
+
onResponseHeaders(request, route, headers);
|
|
5965
|
+
return { status, statusText, statusMessage, headers, body };
|
|
5966
|
+
};
|
|
5967
|
+
|
|
5968
|
+
const matchRoutes = async (request) => {
|
|
5969
|
+
// in case nothing matches we can produce a response with Allow: GET, POST, PUT for example
|
|
5970
|
+
const wouldHaveMatched = {
|
|
5971
|
+
methodSet: new Set(),
|
|
5972
|
+
requestMediaTypeSet: new Set(),
|
|
5973
|
+
responseMediaTypeSet: new Set(),
|
|
5974
|
+
responseLanguageSet: new Set(),
|
|
5975
|
+
responseVersionSet: new Set(),
|
|
5976
|
+
responseEncodingSet: new Set(),
|
|
5977
|
+
upgrade: false,
|
|
5978
|
+
};
|
|
5979
|
+
|
|
5980
|
+
for (const route of routeSet) {
|
|
5981
|
+
onRouteMatchStart(route);
|
|
5982
|
+
const resourceMatchResult = route.matchResource(request.resource);
|
|
5983
|
+
if (!resourceMatchResult) {
|
|
5984
|
+
continue;
|
|
5985
|
+
}
|
|
5986
|
+
if (!route.matchMethod(request.method)) {
|
|
5987
|
+
if (!route.isFallback) {
|
|
5988
|
+
wouldHaveMatched.methodSet.add(route.method);
|
|
5989
|
+
}
|
|
5990
|
+
continue;
|
|
5991
|
+
}
|
|
5934
5992
|
if (
|
|
5935
|
-
|
|
5936
|
-
|
|
5993
|
+
request.method === "POST" ||
|
|
5994
|
+
request.method === "PATCH" ||
|
|
5995
|
+
request.method === "PUT"
|
|
5937
5996
|
) {
|
|
5938
|
-
|
|
5939
|
-
|
|
5997
|
+
const { acceptedMediaTypes } = route;
|
|
5998
|
+
if (
|
|
5999
|
+
acceptedMediaTypes.length &&
|
|
6000
|
+
!isRequestBodyMediaTypeSupported(request, { acceptedMediaTypes })
|
|
6001
|
+
) {
|
|
6002
|
+
for (const acceptedMediaType of acceptedMediaTypes) {
|
|
6003
|
+
wouldHaveMatched.requestMediaTypeSet.add(acceptedMediaType);
|
|
6004
|
+
}
|
|
6005
|
+
continue;
|
|
5940
6006
|
}
|
|
6007
|
+
}
|
|
6008
|
+
const headersMatchResult = route.matchHeaders(request.headers);
|
|
6009
|
+
if (!headersMatchResult) {
|
|
5941
6010
|
continue;
|
|
5942
6011
|
}
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
6012
|
+
if (
|
|
6013
|
+
route.isForWebSocket &&
|
|
6014
|
+
request.headers["upgrade"] !== "websocket"
|
|
6015
|
+
) {
|
|
6016
|
+
wouldHaveMatched.upgrade = true;
|
|
6017
|
+
continue;
|
|
6018
|
+
}
|
|
6019
|
+
// now we are "good", let's try to generate a response
|
|
6020
|
+
const contentNegotiationResult = {};
|
|
6021
|
+
{
|
|
6022
|
+
// when content nego fails
|
|
6023
|
+
// we will check the remaining accept headers to properly inform client of all the things are failing
|
|
6024
|
+
// Example:
|
|
6025
|
+
// client says "I want text in french"
|
|
6026
|
+
// but server only provide json in english
|
|
6027
|
+
// we want to tell client both text and french are not available
|
|
6028
|
+
let hasFailed = false;
|
|
6029
|
+
const { availableMediaTypes } = route;
|
|
6030
|
+
if (availableMediaTypes.length) {
|
|
6031
|
+
fetchSecondArg.injectResponseHeader("vary", "accept");
|
|
6032
|
+
if (request.headers["accept"]) {
|
|
6033
|
+
const mediaTypeNegotiated = pickContentType(
|
|
6034
|
+
request,
|
|
6035
|
+
availableMediaTypes,
|
|
6036
|
+
);
|
|
6037
|
+
if (!mediaTypeNegotiated) {
|
|
6038
|
+
for (const availableMediaType of availableMediaTypes) {
|
|
6039
|
+
wouldHaveMatched.responseMediaTypeSet.add(availableMediaType);
|
|
6040
|
+
}
|
|
6041
|
+
hasFailed = true;
|
|
5973
6042
|
}
|
|
5974
|
-
|
|
6043
|
+
contentNegotiationResult.mediaType = mediaTypeNegotiated;
|
|
6044
|
+
} else {
|
|
6045
|
+
contentNegotiationResult.mediaType = availableMediaTypes[0];
|
|
5975
6046
|
}
|
|
5976
|
-
contentNegotiationResult.mediaType = mediaTypeNegotiated;
|
|
5977
|
-
} else {
|
|
5978
|
-
contentNegotiationResult.mediaType = availableMediaTypes[0];
|
|
5979
6047
|
}
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
6048
|
+
const { availableLanguages } = route;
|
|
6049
|
+
if (availableLanguages.length) {
|
|
6050
|
+
fetchSecondArg.injectResponseHeader("vary", "accept-language");
|
|
6051
|
+
if (request.headers["accept-language"]) {
|
|
6052
|
+
const languageNegotiated = pickContentLanguage(
|
|
6053
|
+
request,
|
|
6054
|
+
availableLanguages,
|
|
6055
|
+
);
|
|
6056
|
+
if (!languageNegotiated) {
|
|
6057
|
+
for (const availableLanguage of availableLanguages) {
|
|
6058
|
+
wouldHaveMatched.responseLanguageSet.add(availableLanguage);
|
|
6059
|
+
}
|
|
6060
|
+
hasFailed = true;
|
|
5992
6061
|
}
|
|
5993
|
-
|
|
6062
|
+
contentNegotiationResult.language = languageNegotiated;
|
|
6063
|
+
} else {
|
|
6064
|
+
contentNegotiationResult.language = availableLanguages[0];
|
|
5994
6065
|
}
|
|
5995
|
-
contentNegotiationResult.language = languageNegotiated;
|
|
5996
|
-
} else {
|
|
5997
|
-
contentNegotiationResult.language = availableLanguages[0];
|
|
5998
6066
|
}
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6067
|
+
const { availableVersions } = route;
|
|
6068
|
+
if (availableVersions.length) {
|
|
6069
|
+
fetchSecondArg.injectResponseHeader("vary", "accept-version");
|
|
6070
|
+
if (request.headers["accept-version"]) {
|
|
6071
|
+
const versionNegotiated = pickContentVersion(
|
|
6072
|
+
request,
|
|
6073
|
+
availableVersions,
|
|
6074
|
+
);
|
|
6075
|
+
if (!versionNegotiated) {
|
|
6076
|
+
for (const availableVersion of availableVersions) {
|
|
6077
|
+
wouldHaveMatched.responseVersionSet.add(availableVersion);
|
|
6078
|
+
}
|
|
6079
|
+
hasFailed = true;
|
|
6011
6080
|
}
|
|
6012
|
-
|
|
6081
|
+
contentNegotiationResult.version = versionNegotiated;
|
|
6082
|
+
} else {
|
|
6083
|
+
contentNegotiationResult.version = availableVersions[0];
|
|
6013
6084
|
}
|
|
6014
|
-
contentNegotiationResult.version = versionNegotiated;
|
|
6015
|
-
} else {
|
|
6016
|
-
contentNegotiationResult.version = availableVersions[0];
|
|
6017
6085
|
}
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6086
|
+
const { availableEncodings } = route;
|
|
6087
|
+
if (availableEncodings.length) {
|
|
6088
|
+
fetchSecondArg.injectResponseHeader("vary", "accept-encoding");
|
|
6089
|
+
if (request.headers["accept-encoding"]) {
|
|
6090
|
+
const encodingNegotiated = pickContentEncoding(
|
|
6091
|
+
request,
|
|
6092
|
+
availableEncodings,
|
|
6093
|
+
);
|
|
6094
|
+
if (!encodingNegotiated) {
|
|
6095
|
+
for (const availableEncoding of availableEncodings) {
|
|
6096
|
+
wouldHaveMatched.responseEncodingSet.add(availableEncoding);
|
|
6097
|
+
}
|
|
6098
|
+
hasFailed = true;
|
|
6030
6099
|
}
|
|
6031
|
-
|
|
6100
|
+
contentNegotiationResult.encoding = encodingNegotiated;
|
|
6101
|
+
} else {
|
|
6102
|
+
contentNegotiationResult.encoding = availableEncodings[0];
|
|
6032
6103
|
}
|
|
6033
|
-
contentNegotiationResult.encoding = encodingNegotiated;
|
|
6034
|
-
} else {
|
|
6035
|
-
contentNegotiationResult.encoding = availableEncodings[0];
|
|
6036
6104
|
}
|
|
6105
|
+
if (hasFailed) {
|
|
6106
|
+
continue;
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
6109
|
+
const { named, stars = [] } = PATTERN.composeTwoMatchResults(
|
|
6110
|
+
resourceMatchResult,
|
|
6111
|
+
headersMatchResult,
|
|
6112
|
+
);
|
|
6113
|
+
Object.assign(request.params, named, stars);
|
|
6114
|
+
fetchSecondArg.contentNegotiation = contentNegotiationResult;
|
|
6115
|
+
let fetchReturnValue = route.fetch(request, fetchSecondArg);
|
|
6116
|
+
if (
|
|
6117
|
+
fetchReturnValue !== null &&
|
|
6118
|
+
typeof fetchReturnValue === "object" &&
|
|
6119
|
+
typeof fetchReturnValue.then === "function"
|
|
6120
|
+
) {
|
|
6121
|
+
fetchReturnValue = await fetchReturnValue;
|
|
6037
6122
|
}
|
|
6038
|
-
|
|
6123
|
+
// route decided not to handle in the end
|
|
6124
|
+
if (fetchReturnValue === null || fetchReturnValue === undefined) {
|
|
6039
6125
|
continue;
|
|
6040
6126
|
}
|
|
6127
|
+
return resolveResponse(fetchReturnValue, { request, route });
|
|
6128
|
+
}
|
|
6129
|
+
// nothing has matched fully
|
|
6130
|
+
// if nothing matches at all we'll send 404
|
|
6131
|
+
// but if url matched but METHOD was not supported we send 405
|
|
6132
|
+
if (wouldHaveMatched.methodSet.size) {
|
|
6133
|
+
return createMethodNotAllowedResponse(request, {
|
|
6134
|
+
allowedMethods: [...wouldHaveMatched.methodSet],
|
|
6135
|
+
});
|
|
6136
|
+
}
|
|
6137
|
+
if (wouldHaveMatched.requestMediaTypeSet.size) {
|
|
6138
|
+
return createUnsupportedMediaTypeResponse(request, {
|
|
6139
|
+
acceptedMediaTypes: [...wouldHaveMatched.requestMediaTypeSet],
|
|
6140
|
+
});
|
|
6041
6141
|
}
|
|
6042
|
-
const { named, stars = [] } = PATTERN.composeTwoMatchResults(
|
|
6043
|
-
resourceMatchResult,
|
|
6044
|
-
headersMatchResult,
|
|
6045
|
-
);
|
|
6046
|
-
Object.assign(request.params, named, stars);
|
|
6047
|
-
fetchSecondArg.contentNegotiation = contentNegotiationResult;
|
|
6048
|
-
let fetchReturnValue = route.fetch(request, fetchSecondArg);
|
|
6049
6142
|
if (
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6143
|
+
wouldHaveMatched.responseMediaTypeSet.size ||
|
|
6144
|
+
wouldHaveMatched.responseLanguageSet.size ||
|
|
6145
|
+
wouldHaveMatched.responseVersionSet.size ||
|
|
6146
|
+
wouldHaveMatched.responseEncodingSet.size
|
|
6053
6147
|
) {
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
onRouteMatch(route);
|
|
6061
|
-
if (fetchReturnValue instanceof Response) {
|
|
6062
|
-
const headers = Object.fromEntries(fetchReturnValue.headers);
|
|
6063
|
-
onResponseHeaders(request, route, headers);
|
|
6064
|
-
return {
|
|
6065
|
-
status: fetchReturnValue.status,
|
|
6066
|
-
statusText: fetchReturnValue.statusText,
|
|
6067
|
-
headers,
|
|
6068
|
-
body: fetchReturnValue.body,
|
|
6069
|
-
};
|
|
6148
|
+
return createNotAcceptableResponse(request, {
|
|
6149
|
+
availableMediaTypes: [...wouldHaveMatched.responseMediaTypeSet],
|
|
6150
|
+
availableLanguages: [...wouldHaveMatched.responseLanguageSet],
|
|
6151
|
+
availableVersions: [...wouldHaveMatched.responseVersionSet],
|
|
6152
|
+
availableEncodings: [...wouldHaveMatched.responseEncodingSet],
|
|
6153
|
+
});
|
|
6070
6154
|
}
|
|
6071
|
-
if (
|
|
6072
|
-
const headers = fetchReturnValue.headers || {};
|
|
6073
|
-
onResponseHeaders(request, route, headers);
|
|
6155
|
+
if (wouldHaveMatched.upgrade) {
|
|
6074
6156
|
return {
|
|
6075
|
-
status:
|
|
6076
|
-
statusText:
|
|
6077
|
-
statusMessage:
|
|
6078
|
-
headers,
|
|
6079
|
-
body: fetchReturnValue.body,
|
|
6157
|
+
status: 426,
|
|
6158
|
+
statusText: "Upgrade Required",
|
|
6159
|
+
statusMessage: `The request requires the upgrade to a webSocket connection`,
|
|
6080
6160
|
};
|
|
6081
6161
|
}
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
// if nothing matches at all we'll send 404
|
|
6088
|
-
// but if url matched but METHOD was not supported we send 405
|
|
6089
|
-
if (wouldHaveMatched.methodSet.size) {
|
|
6090
|
-
return createMethodNotAllowedResponse(request, {
|
|
6091
|
-
allowedMethods: [...wouldHaveMatched.methodSet],
|
|
6092
|
-
});
|
|
6093
|
-
}
|
|
6094
|
-
if (wouldHaveMatched.requestMediaTypeSet.size) {
|
|
6095
|
-
return createUnsupportedMediaTypeResponse(request, {
|
|
6096
|
-
acceptedMediaTypes: [...wouldHaveMatched.requestMediaTypeSet],
|
|
6097
|
-
});
|
|
6098
|
-
}
|
|
6099
|
-
if (
|
|
6100
|
-
wouldHaveMatched.responseMediaTypeSet.size ||
|
|
6101
|
-
wouldHaveMatched.responseLanguageSet.size ||
|
|
6102
|
-
wouldHaveMatched.responseVersionSet.size ||
|
|
6103
|
-
wouldHaveMatched.responseEncodingSet.size
|
|
6104
|
-
) {
|
|
6105
|
-
return createNotAcceptableResponse(request, {
|
|
6106
|
-
availableMediaTypes: [...wouldHaveMatched.responseMediaTypeSet],
|
|
6107
|
-
availableLanguages: [...wouldHaveMatched.responseLanguageSet],
|
|
6108
|
-
availableVersions: [...wouldHaveMatched.responseVersionSet],
|
|
6109
|
-
availableEncodings: [...wouldHaveMatched.responseEncodingSet],
|
|
6110
|
-
});
|
|
6111
|
-
}
|
|
6112
|
-
if (wouldHaveMatched.upgrade) {
|
|
6113
|
-
return {
|
|
6114
|
-
status: 426,
|
|
6115
|
-
statusText: "Upgrade Required",
|
|
6116
|
-
statusMessage: `The request requires the upgrade to a webSocket connection`,
|
|
6117
|
-
};
|
|
6118
|
-
}
|
|
6119
|
-
constructAvailableEndpoints();
|
|
6120
|
-
return createRouteNotFoundResponse(request);
|
|
6162
|
+
constructAvailableEndpoints();
|
|
6163
|
+
return createRouteNotFoundResponse(request);
|
|
6164
|
+
}; // end matchRoutes
|
|
6165
|
+
|
|
6166
|
+
return matchRoutes(request);
|
|
6121
6167
|
};
|
|
6122
|
-
const inspect = () => {
|
|
6168
|
+
const inspect = ({ canExposeSensitiveData }) => {
|
|
6123
6169
|
// I want all the info I can gather about the routes
|
|
6124
6170
|
const data = [];
|
|
6125
6171
|
for (const route of routeSet) {
|
|
6126
|
-
data.push(route.toJSON());
|
|
6172
|
+
data.push(route.toJSON({ canExposeSensitiveData }));
|
|
6127
6173
|
}
|
|
6128
6174
|
return data;
|
|
6129
6175
|
};
|
|
@@ -6248,10 +6294,10 @@ const createRoute = ({
|
|
|
6248
6294
|
toString: () => {
|
|
6249
6295
|
return `${method} ${resource}`;
|
|
6250
6296
|
},
|
|
6251
|
-
toJSON: () => {
|
|
6297
|
+
toJSON: ({ canExposeSensitiveData }) => {
|
|
6252
6298
|
const meta = {};
|
|
6253
6299
|
|
|
6254
|
-
if (declarationSource) {
|
|
6300
|
+
if (declarationSource && canExposeSensitiveData) {
|
|
6255
6301
|
meta.declarationLink = {
|
|
6256
6302
|
url: `javascript:window.fetch("/.internal/open_file/${encodeURIComponent(declarationSource)}")`,
|
|
6257
6303
|
text: declarationSource,
|
|
@@ -6279,7 +6325,6 @@ const createRoute = ({
|
|
|
6279
6325
|
} catch {}
|
|
6280
6326
|
}
|
|
6281
6327
|
}
|
|
6282
|
-
|
|
6283
6328
|
return {
|
|
6284
6329
|
method,
|
|
6285
6330
|
resource,
|
|
@@ -6296,7 +6341,9 @@ const createRoute = ({
|
|
|
6296
6341
|
: typeof clientCodeExample === "string"
|
|
6297
6342
|
? clientCodeExample
|
|
6298
6343
|
: undefined,
|
|
6299
|
-
declarationSource
|
|
6344
|
+
declarationSource: canExposeSensitiveData
|
|
6345
|
+
? declarationSource
|
|
6346
|
+
: undefined,
|
|
6300
6347
|
meta,
|
|
6301
6348
|
};
|
|
6302
6349
|
},
|
|
@@ -6308,6 +6355,9 @@ const createRoute = ({
|
|
|
6308
6355
|
return route;
|
|
6309
6356
|
};
|
|
6310
6357
|
|
|
6358
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
6359
|
+
const isRedirectStatus = (status) => REDIRECT_STATUSES.has(status);
|
|
6360
|
+
|
|
6311
6361
|
const isRequestBodyMediaTypeSupported = (request, { acceptedMediaTypes }) => {
|
|
6312
6362
|
const requestBodyContentType = request.headers["content-type"];
|
|
6313
6363
|
if (!requestBodyContentType) {
|
|
@@ -6749,6 +6799,12 @@ const startServer = async ({
|
|
|
6749
6799
|
keepProcessAlive = true,
|
|
6750
6800
|
routes = [],
|
|
6751
6801
|
plugins = [],
|
|
6802
|
+
// When enabled, the server gives more power:
|
|
6803
|
+
// - each route show the source where it was declared
|
|
6804
|
+
// - server can be requested to open a file on the machine
|
|
6805
|
+
// - client can subscribe to server to detect when it restarts
|
|
6806
|
+
// This param should be enabled ONLY during development on your machine
|
|
6807
|
+
canExposeSensitiveData = false,
|
|
6752
6808
|
nagle = true,
|
|
6753
6809
|
serverTiming = false,
|
|
6754
6810
|
requestWaitingMs = 0,
|
|
@@ -6823,11 +6879,11 @@ const startServer = async ({
|
|
|
6823
6879
|
}
|
|
6824
6880
|
|
|
6825
6881
|
plugins = [
|
|
6826
|
-
serverPluginOpenFile(),
|
|
6882
|
+
...(canExposeSensitiveData ? [serverPluginOpenFile()] : []),
|
|
6827
6883
|
serverPluginDefaultBody4xx5xx(),
|
|
6828
|
-
serverPluginRouteInspector(),
|
|
6829
|
-
serverPluginInternalClientFiles(),
|
|
6830
|
-
serverPluginAutoreloadOnRestart(),
|
|
6884
|
+
serverPluginRouteInspector({ canExposeSensitiveData }),
|
|
6885
|
+
...(canExposeSensitiveData ? [serverPluginInternalClientFiles()] : []),
|
|
6886
|
+
...(canExposeSensitiveData ? [serverPluginAutoreloadOnRestart()] : []),
|
|
6831
6887
|
...plugins,
|
|
6832
6888
|
];
|
|
6833
6889
|
|
|
@@ -7949,17 +8005,32 @@ const generateAccessControlHeaders = ({
|
|
|
7949
8005
|
} = {}) => {
|
|
7950
8006
|
const vary = [];
|
|
7951
8007
|
|
|
7952
|
-
|
|
7953
|
-
if
|
|
7954
|
-
|
|
7955
|
-
|
|
8008
|
+
// Access-Control-Allow-Origin must be a single value (not a list).
|
|
8009
|
+
// We reflect back the request's origin if it is in the allowed list.
|
|
8010
|
+
// If no origin matches we fall back to "*" (only when not using credentials).
|
|
8011
|
+
let allowOrigin = null;
|
|
8012
|
+
|
|
8013
|
+
const requestOrigin =
|
|
8014
|
+
"origin" in headers && headers.origin !== "null"
|
|
8015
|
+
? headers.origin
|
|
8016
|
+
: "referer" in headers
|
|
8017
|
+
? new URL(headers.referer).origin
|
|
8018
|
+
: null;
|
|
8019
|
+
|
|
8020
|
+
if (requestOrigin) {
|
|
8021
|
+
if (accessControlAllowedOrigins.includes(requestOrigin)) {
|
|
8022
|
+
allowOrigin = requestOrigin;
|
|
8023
|
+
vary.push("origin");
|
|
8024
|
+
} else if (accessControlAllowRequestOrigin) {
|
|
8025
|
+
allowOrigin = requestOrigin;
|
|
7956
8026
|
vary.push("origin");
|
|
7957
|
-
} else if ("referer" in headers) {
|
|
7958
|
-
allowedOriginArray.push(new URL(headers.referer).origin);
|
|
7959
|
-
vary.push("referer");
|
|
7960
|
-
} else {
|
|
7961
|
-
allowedOriginArray.push("*");
|
|
7962
8027
|
}
|
|
8028
|
+
} else if (accessControlAllowRequestOrigin) {
|
|
8029
|
+
allowOrigin = "*";
|
|
8030
|
+
}
|
|
8031
|
+
|
|
8032
|
+
if (allowOrigin === null) {
|
|
8033
|
+
allowOrigin = accessControlAllowedOrigins[0] ?? "*";
|
|
7963
8034
|
}
|
|
7964
8035
|
|
|
7965
8036
|
const allowedMethodArray = [...accessControlAllowedMethods];
|
|
@@ -7993,16 +8064,14 @@ const generateAccessControlHeaders = ({
|
|
|
7993
8064
|
}
|
|
7994
8065
|
|
|
7995
8066
|
return {
|
|
7996
|
-
"access-control-allow-origin":
|
|
8067
|
+
"access-control-allow-origin": allowOrigin,
|
|
7997
8068
|
"access-control-allow-methods": allowedMethodArray.join(", "),
|
|
7998
8069
|
"access-control-allow-headers": allowedHeaderArray.join(", "),
|
|
7999
8070
|
...(accessControlAllowCredentials
|
|
8000
8071
|
? { "access-control-allow-credentials": true }
|
|
8001
8072
|
: {}),
|
|
8002
8073
|
"access-control-max-age": accessControlMaxAge,
|
|
8003
|
-
...(timingAllowOrigin
|
|
8004
|
-
? { "timing-allow-origin": allowedOriginArray.join(", ") }
|
|
8005
|
-
: {}),
|
|
8074
|
+
...(timingAllowOrigin ? { "timing-allow-origin": allowOrigin } : {}),
|
|
8006
8075
|
...(vary.length ? { vary: vary.join(", ") } : {}),
|
|
8007
8076
|
};
|
|
8008
8077
|
};
|
package/package.json
CHANGED
|
@@ -75,17 +75,32 @@ const generateAccessControlHeaders = ({
|
|
|
75
75
|
} = {}) => {
|
|
76
76
|
const vary = [];
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
if
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
// Access-Control-Allow-Origin must be a single value (not a list).
|
|
79
|
+
// We reflect back the request's origin if it is in the allowed list.
|
|
80
|
+
// If no origin matches we fall back to "*" (only when not using credentials).
|
|
81
|
+
let allowOrigin = null;
|
|
82
|
+
|
|
83
|
+
const requestOrigin =
|
|
84
|
+
"origin" in headers && headers.origin !== "null"
|
|
85
|
+
? headers.origin
|
|
86
|
+
: "referer" in headers
|
|
87
|
+
? new URL(headers.referer).origin
|
|
88
|
+
: null;
|
|
89
|
+
|
|
90
|
+
if (requestOrigin) {
|
|
91
|
+
if (accessControlAllowedOrigins.includes(requestOrigin)) {
|
|
92
|
+
allowOrigin = requestOrigin;
|
|
93
|
+
vary.push("origin");
|
|
94
|
+
} else if (accessControlAllowRequestOrigin) {
|
|
95
|
+
allowOrigin = requestOrigin;
|
|
82
96
|
vary.push("origin");
|
|
83
|
-
} else if ("referer" in headers) {
|
|
84
|
-
allowedOriginArray.push(new URL(headers.referer).origin);
|
|
85
|
-
vary.push("referer");
|
|
86
|
-
} else {
|
|
87
|
-
allowedOriginArray.push("*");
|
|
88
97
|
}
|
|
98
|
+
} else if (accessControlAllowRequestOrigin) {
|
|
99
|
+
allowOrigin = "*";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (allowOrigin === null) {
|
|
103
|
+
allowOrigin = accessControlAllowedOrigins[0] ?? "*";
|
|
89
104
|
}
|
|
90
105
|
|
|
91
106
|
const allowedMethodArray = [...accessControlAllowedMethods];
|
|
@@ -119,16 +134,14 @@ const generateAccessControlHeaders = ({
|
|
|
119
134
|
}
|
|
120
135
|
|
|
121
136
|
return {
|
|
122
|
-
"access-control-allow-origin":
|
|
137
|
+
"access-control-allow-origin": allowOrigin,
|
|
123
138
|
"access-control-allow-methods": allowedMethodArray.join(", "),
|
|
124
139
|
"access-control-allow-headers": allowedHeaderArray.join(", "),
|
|
125
140
|
...(accessControlAllowCredentials
|
|
126
141
|
? { "access-control-allow-credentials": true }
|
|
127
142
|
: {}),
|
|
128
143
|
"access-control-max-age": accessControlMaxAge,
|
|
129
|
-
...(timingAllowOrigin
|
|
130
|
-
? { "timing-allow-origin": allowedOriginArray.join(", ") }
|
|
131
|
-
: {}),
|
|
144
|
+
...(timingAllowOrigin ? { "timing-allow-origin": allowOrigin } : {}),
|
|
132
145
|
...(vary.length ? { vary: vary.join(", ") } : {}),
|
|
133
146
|
};
|
|
134
147
|
};
|
|
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
const routeInspectorHtmlFileUrl = import.meta
|
|
4
4
|
.resolve("./client/route_inspector.html");
|
|
5
5
|
|
|
6
|
-
export const serverPluginRouteInspector = () => {
|
|
6
|
+
export const serverPluginRouteInspector = ({ canExposeSensitiveData }) => {
|
|
7
7
|
return {
|
|
8
8
|
name: "jsenv:route_inspector",
|
|
9
9
|
routes: [
|
|
@@ -29,7 +29,9 @@ export const serverPluginRouteInspector = () => {
|
|
|
29
29
|
description: "Get the routes available on this server in JSON.",
|
|
30
30
|
declarationSource: import.meta.url,
|
|
31
31
|
fetch: (request, helpers) => {
|
|
32
|
-
const routeJSON = helpers.router.inspect(
|
|
32
|
+
const routeJSON = helpers.router.inspect({
|
|
33
|
+
canExposeSensitiveData,
|
|
34
|
+
});
|
|
33
35
|
return Response.json(routeJSON);
|
|
34
36
|
},
|
|
35
37
|
},
|
package/src/router/router.js
CHANGED
|
@@ -28,7 +28,7 @@ const HTTP_METHODS = [
|
|
|
28
28
|
|
|
29
29
|
export const createRouter = (
|
|
30
30
|
routeDescriptionArray,
|
|
31
|
-
{ optionsFallback, logLevel } = {},
|
|
31
|
+
{ optionsFallback, logLevel, redirect = "manual" } = {},
|
|
32
32
|
) => {
|
|
33
33
|
const logger = createLogger({ logLevel });
|
|
34
34
|
const routeSet = new Set();
|
|
@@ -177,16 +177,6 @@ export const createRouter = (
|
|
|
177
177
|
|
|
178
178
|
const match = async (request, fetchSecondArg) => {
|
|
179
179
|
fetchSecondArg.router = router;
|
|
180
|
-
const wouldHaveMatched = {
|
|
181
|
-
// in case nothing matches we can produce a response with Allow: GET, POST, PUT for example
|
|
182
|
-
methodSet: new Set(),
|
|
183
|
-
requestMediaTypeSet: new Set(),
|
|
184
|
-
responseMediaTypeSet: new Set(),
|
|
185
|
-
responseLanguageSet: new Set(),
|
|
186
|
-
responseVersionSet: new Set(),
|
|
187
|
-
responseEncodingSet: new Set(),
|
|
188
|
-
upgrade: false,
|
|
189
|
-
};
|
|
190
180
|
|
|
191
181
|
let currentServerPlugin;
|
|
192
182
|
let currentRoutingTiming;
|
|
@@ -261,7 +251,6 @@ It should be set to one of route.${routePropertyName}: ${availableValues.join(",
|
|
|
261
251
|
break;
|
|
262
252
|
}
|
|
263
253
|
}
|
|
264
|
-
|
|
265
254
|
if (!matchesAnAvailableValue) {
|
|
266
255
|
request.logger.warn(
|
|
267
256
|
`The value "${responseHeaderValue}" found in response header ${responseHeaderName} is strange.
|
|
@@ -277,220 +266,270 @@ It should be should be one of route.${routePropertyName}: ${availableValues.join
|
|
|
277
266
|
checkResponseContentHeader(route, responseHeaders, "encoding");
|
|
278
267
|
};
|
|
279
268
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
269
|
+
const resolveResponse = async (fetchReturnValue, { request, route }) => {
|
|
270
|
+
let status;
|
|
271
|
+
let statusText;
|
|
272
|
+
let statusMessage;
|
|
273
|
+
let headers;
|
|
274
|
+
let body;
|
|
275
|
+
|
|
276
|
+
if (fetchReturnValue instanceof Response) {
|
|
277
|
+
status = fetchReturnValue.status;
|
|
278
|
+
statusText = fetchReturnValue.statusText;
|
|
279
|
+
headers = Object.fromEntries(fetchReturnValue.headers);
|
|
280
|
+
body = fetchReturnValue.body;
|
|
281
|
+
} else if (
|
|
282
|
+
fetchReturnValue !== null &&
|
|
283
|
+
typeof fetchReturnValue === "object"
|
|
284
|
+
) {
|
|
285
|
+
status = fetchReturnValue.status || 404;
|
|
286
|
+
statusText = fetchReturnValue.statusText;
|
|
287
|
+
statusMessage = fetchReturnValue.statusMessage;
|
|
288
|
+
headers = fetchReturnValue.headers || {};
|
|
289
|
+
body = fetchReturnValue.body;
|
|
290
|
+
} else {
|
|
291
|
+
throw new TypeError(
|
|
292
|
+
`response must be a Response, or an Object, received ${fetchReturnValue}`,
|
|
293
|
+
);
|
|
291
294
|
}
|
|
295
|
+
|
|
292
296
|
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
297
|
+
redirect === "follow" &&
|
|
298
|
+
isRedirectStatus(status) &&
|
|
299
|
+
headers["location"]
|
|
296
300
|
) {
|
|
297
|
-
const
|
|
301
|
+
const redirectUrl = new URL(headers["location"], request.url);
|
|
302
|
+
if (redirectUrl.origin === new URL(request.url).origin) {
|
|
303
|
+
const redirectRequest = {
|
|
304
|
+
...request,
|
|
305
|
+
url: redirectUrl.href,
|
|
306
|
+
resource:
|
|
307
|
+
redirectUrl.pathname + redirectUrl.search + redirectUrl.hash,
|
|
308
|
+
// GET for 301/302/303, preserve method for 307/308
|
|
309
|
+
method: status === 307 || status === 308 ? request.method : "GET",
|
|
310
|
+
params: {},
|
|
311
|
+
};
|
|
312
|
+
const redirectedResponse = await matchRoutes(redirectRequest);
|
|
313
|
+
onRouteMatch(route);
|
|
314
|
+
onResponseHeaders(request, route, redirectedResponse.headers || {});
|
|
315
|
+
return redirectedResponse;
|
|
316
|
+
}
|
|
317
|
+
// redirect to other origins are left to the client to handle
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
onRouteMatch(route);
|
|
321
|
+
onResponseHeaders(request, route, headers);
|
|
322
|
+
return { status, statusText, statusMessage, headers, body };
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const matchRoutes = async (request) => {
|
|
326
|
+
// in case nothing matches we can produce a response with Allow: GET, POST, PUT for example
|
|
327
|
+
const wouldHaveMatched = {
|
|
328
|
+
methodSet: new Set(),
|
|
329
|
+
requestMediaTypeSet: new Set(),
|
|
330
|
+
responseMediaTypeSet: new Set(),
|
|
331
|
+
responseLanguageSet: new Set(),
|
|
332
|
+
responseVersionSet: new Set(),
|
|
333
|
+
responseEncodingSet: new Set(),
|
|
334
|
+
upgrade: false,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
for (const route of routeSet) {
|
|
338
|
+
onRouteMatchStart(route);
|
|
339
|
+
const resourceMatchResult = route.matchResource(request.resource);
|
|
340
|
+
if (!resourceMatchResult) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (!route.matchMethod(request.method)) {
|
|
344
|
+
if (!route.isFallback) {
|
|
345
|
+
wouldHaveMatched.methodSet.add(route.method);
|
|
346
|
+
}
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
298
349
|
if (
|
|
299
|
-
|
|
300
|
-
|
|
350
|
+
request.method === "POST" ||
|
|
351
|
+
request.method === "PATCH" ||
|
|
352
|
+
request.method === "PUT"
|
|
301
353
|
) {
|
|
302
|
-
|
|
303
|
-
|
|
354
|
+
const { acceptedMediaTypes } = route;
|
|
355
|
+
if (
|
|
356
|
+
acceptedMediaTypes.length &&
|
|
357
|
+
!isRequestBodyMediaTypeSupported(request, { acceptedMediaTypes })
|
|
358
|
+
) {
|
|
359
|
+
for (const acceptedMediaType of acceptedMediaTypes) {
|
|
360
|
+
wouldHaveMatched.requestMediaTypeSet.add(acceptedMediaType);
|
|
361
|
+
}
|
|
362
|
+
continue;
|
|
304
363
|
}
|
|
364
|
+
}
|
|
365
|
+
const headersMatchResult = route.matchHeaders(request.headers);
|
|
366
|
+
if (!headersMatchResult) {
|
|
305
367
|
continue;
|
|
306
368
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
369
|
+
if (
|
|
370
|
+
route.isForWebSocket &&
|
|
371
|
+
request.headers["upgrade"] !== "websocket"
|
|
372
|
+
) {
|
|
373
|
+
wouldHaveMatched.upgrade = true;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
// now we are "good", let's try to generate a response
|
|
377
|
+
const contentNegotiationResult = {};
|
|
378
|
+
content_negotiation: {
|
|
379
|
+
// when content nego fails
|
|
380
|
+
// we will check the remaining accept headers to properly inform client of all the things are failing
|
|
381
|
+
// Example:
|
|
382
|
+
// client says "I want text in french"
|
|
383
|
+
// but server only provide json in english
|
|
384
|
+
// we want to tell client both text and french are not available
|
|
385
|
+
let hasFailed = false;
|
|
386
|
+
const { availableMediaTypes } = route;
|
|
387
|
+
if (availableMediaTypes.length) {
|
|
388
|
+
fetchSecondArg.injectResponseHeader("vary", "accept");
|
|
389
|
+
if (request.headers["accept"]) {
|
|
390
|
+
const mediaTypeNegotiated = pickContentType(
|
|
391
|
+
request,
|
|
392
|
+
availableMediaTypes,
|
|
393
|
+
);
|
|
394
|
+
if (!mediaTypeNegotiated) {
|
|
395
|
+
for (const availableMediaType of availableMediaTypes) {
|
|
396
|
+
wouldHaveMatched.responseMediaTypeSet.add(availableMediaType);
|
|
397
|
+
}
|
|
398
|
+
hasFailed = true;
|
|
337
399
|
}
|
|
338
|
-
|
|
400
|
+
contentNegotiationResult.mediaType = mediaTypeNegotiated;
|
|
401
|
+
} else {
|
|
402
|
+
contentNegotiationResult.mediaType = availableMediaTypes[0];
|
|
339
403
|
}
|
|
340
|
-
contentNegotiationResult.mediaType = mediaTypeNegotiated;
|
|
341
|
-
} else {
|
|
342
|
-
contentNegotiationResult.mediaType = availableMediaTypes[0];
|
|
343
404
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
405
|
+
const { availableLanguages } = route;
|
|
406
|
+
if (availableLanguages.length) {
|
|
407
|
+
fetchSecondArg.injectResponseHeader("vary", "accept-language");
|
|
408
|
+
if (request.headers["accept-language"]) {
|
|
409
|
+
const languageNegotiated = pickContentLanguage(
|
|
410
|
+
request,
|
|
411
|
+
availableLanguages,
|
|
412
|
+
);
|
|
413
|
+
if (!languageNegotiated) {
|
|
414
|
+
for (const availableLanguage of availableLanguages) {
|
|
415
|
+
wouldHaveMatched.responseLanguageSet.add(availableLanguage);
|
|
416
|
+
}
|
|
417
|
+
hasFailed = true;
|
|
356
418
|
}
|
|
357
|
-
|
|
419
|
+
contentNegotiationResult.language = languageNegotiated;
|
|
420
|
+
} else {
|
|
421
|
+
contentNegotiationResult.language = availableLanguages[0];
|
|
358
422
|
}
|
|
359
|
-
contentNegotiationResult.language = languageNegotiated;
|
|
360
|
-
} else {
|
|
361
|
-
contentNegotiationResult.language = availableLanguages[0];
|
|
362
423
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
424
|
+
const { availableVersions } = route;
|
|
425
|
+
if (availableVersions.length) {
|
|
426
|
+
fetchSecondArg.injectResponseHeader("vary", "accept-version");
|
|
427
|
+
if (request.headers["accept-version"]) {
|
|
428
|
+
const versionNegotiated = pickContentVersion(
|
|
429
|
+
request,
|
|
430
|
+
availableVersions,
|
|
431
|
+
);
|
|
432
|
+
if (!versionNegotiated) {
|
|
433
|
+
for (const availableVersion of availableVersions) {
|
|
434
|
+
wouldHaveMatched.responseVersionSet.add(availableVersion);
|
|
435
|
+
}
|
|
436
|
+
hasFailed = true;
|
|
375
437
|
}
|
|
376
|
-
|
|
438
|
+
contentNegotiationResult.version = versionNegotiated;
|
|
439
|
+
} else {
|
|
440
|
+
contentNegotiationResult.version = availableVersions[0];
|
|
377
441
|
}
|
|
378
|
-
contentNegotiationResult.version = versionNegotiated;
|
|
379
|
-
} else {
|
|
380
|
-
contentNegotiationResult.version = availableVersions[0];
|
|
381
442
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
443
|
+
const { availableEncodings } = route;
|
|
444
|
+
if (availableEncodings.length) {
|
|
445
|
+
fetchSecondArg.injectResponseHeader("vary", "accept-encoding");
|
|
446
|
+
if (request.headers["accept-encoding"]) {
|
|
447
|
+
const encodingNegotiated = pickContentEncoding(
|
|
448
|
+
request,
|
|
449
|
+
availableEncodings,
|
|
450
|
+
);
|
|
451
|
+
if (!encodingNegotiated) {
|
|
452
|
+
for (const availableEncoding of availableEncodings) {
|
|
453
|
+
wouldHaveMatched.responseEncodingSet.add(availableEncoding);
|
|
454
|
+
}
|
|
455
|
+
hasFailed = true;
|
|
394
456
|
}
|
|
395
|
-
|
|
457
|
+
contentNegotiationResult.encoding = encodingNegotiated;
|
|
458
|
+
} else {
|
|
459
|
+
contentNegotiationResult.encoding = availableEncodings[0];
|
|
396
460
|
}
|
|
397
|
-
contentNegotiationResult.encoding = encodingNegotiated;
|
|
398
|
-
} else {
|
|
399
|
-
contentNegotiationResult.encoding = availableEncodings[0];
|
|
400
461
|
}
|
|
462
|
+
if (hasFailed) {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const { named, stars = [] } = PATTERN.composeTwoMatchResults(
|
|
467
|
+
resourceMatchResult,
|
|
468
|
+
headersMatchResult,
|
|
469
|
+
);
|
|
470
|
+
Object.assign(request.params, named, stars);
|
|
471
|
+
fetchSecondArg.contentNegotiation = contentNegotiationResult;
|
|
472
|
+
let fetchReturnValue = route.fetch(request, fetchSecondArg);
|
|
473
|
+
if (
|
|
474
|
+
fetchReturnValue !== null &&
|
|
475
|
+
typeof fetchReturnValue === "object" &&
|
|
476
|
+
typeof fetchReturnValue.then === "function"
|
|
477
|
+
) {
|
|
478
|
+
fetchReturnValue = await fetchReturnValue;
|
|
401
479
|
}
|
|
402
|
-
|
|
480
|
+
// route decided not to handle in the end
|
|
481
|
+
if (fetchReturnValue === null || fetchReturnValue === undefined) {
|
|
403
482
|
continue;
|
|
404
483
|
}
|
|
484
|
+
return resolveResponse(fetchReturnValue, { request, route });
|
|
405
485
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (
|
|
414
|
-
fetchReturnValue !== null &&
|
|
415
|
-
typeof fetchReturnValue === "object" &&
|
|
416
|
-
typeof fetchReturnValue.then === "function"
|
|
417
|
-
) {
|
|
418
|
-
fetchReturnValue = await fetchReturnValue;
|
|
486
|
+
// nothing has matched fully
|
|
487
|
+
// if nothing matches at all we'll send 404
|
|
488
|
+
// but if url matched but METHOD was not supported we send 405
|
|
489
|
+
if (wouldHaveMatched.methodSet.size) {
|
|
490
|
+
return createMethodNotAllowedResponse(request, {
|
|
491
|
+
allowedMethods: [...wouldHaveMatched.methodSet],
|
|
492
|
+
});
|
|
419
493
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
494
|
+
if (wouldHaveMatched.requestMediaTypeSet.size) {
|
|
495
|
+
return createUnsupportedMediaTypeResponse(request, {
|
|
496
|
+
acceptedMediaTypes: [...wouldHaveMatched.requestMediaTypeSet],
|
|
497
|
+
});
|
|
423
498
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
499
|
+
if (
|
|
500
|
+
wouldHaveMatched.responseMediaTypeSet.size ||
|
|
501
|
+
wouldHaveMatched.responseLanguageSet.size ||
|
|
502
|
+
wouldHaveMatched.responseVersionSet.size ||
|
|
503
|
+
wouldHaveMatched.responseEncodingSet.size
|
|
504
|
+
) {
|
|
505
|
+
return createNotAcceptableResponse(request, {
|
|
506
|
+
availableMediaTypes: [...wouldHaveMatched.responseMediaTypeSet],
|
|
507
|
+
availableLanguages: [...wouldHaveMatched.responseLanguageSet],
|
|
508
|
+
availableVersions: [...wouldHaveMatched.responseVersionSet],
|
|
509
|
+
availableEncodings: [...wouldHaveMatched.responseEncodingSet],
|
|
510
|
+
});
|
|
434
511
|
}
|
|
435
|
-
if (
|
|
436
|
-
const headers = fetchReturnValue.headers || {};
|
|
437
|
-
onResponseHeaders(request, route, headers);
|
|
512
|
+
if (wouldHaveMatched.upgrade) {
|
|
438
513
|
return {
|
|
439
|
-
status:
|
|
440
|
-
statusText:
|
|
441
|
-
statusMessage:
|
|
442
|
-
headers,
|
|
443
|
-
body: fetchReturnValue.body,
|
|
514
|
+
status: 426,
|
|
515
|
+
statusText: "Upgrade Required",
|
|
516
|
+
statusMessage: `The request requires the upgrade to a webSocket connection`,
|
|
444
517
|
};
|
|
445
518
|
}
|
|
446
|
-
|
|
447
|
-
|
|
519
|
+
const availableEndpoints = constructAvailableEndpoints(
|
|
520
|
+
request,
|
|
521
|
+
fetchSecondArg,
|
|
448
522
|
);
|
|
449
|
-
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
if (wouldHaveMatched.methodSet.size) {
|
|
454
|
-
return createMethodNotAllowedResponse(request, {
|
|
455
|
-
allowedMethods: [...wouldHaveMatched.methodSet],
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
if (wouldHaveMatched.requestMediaTypeSet.size) {
|
|
459
|
-
return createUnsupportedMediaTypeResponse(request, {
|
|
460
|
-
acceptedMediaTypes: [...wouldHaveMatched.requestMediaTypeSet],
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
if (
|
|
464
|
-
wouldHaveMatched.responseMediaTypeSet.size ||
|
|
465
|
-
wouldHaveMatched.responseLanguageSet.size ||
|
|
466
|
-
wouldHaveMatched.responseVersionSet.size ||
|
|
467
|
-
wouldHaveMatched.responseEncodingSet.size
|
|
468
|
-
) {
|
|
469
|
-
return createNotAcceptableResponse(request, {
|
|
470
|
-
availableMediaTypes: [...wouldHaveMatched.responseMediaTypeSet],
|
|
471
|
-
availableLanguages: [...wouldHaveMatched.responseLanguageSet],
|
|
472
|
-
availableVersions: [...wouldHaveMatched.responseVersionSet],
|
|
473
|
-
availableEncodings: [...wouldHaveMatched.responseEncodingSet],
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
if (wouldHaveMatched.upgrade) {
|
|
477
|
-
return {
|
|
478
|
-
status: 426,
|
|
479
|
-
statusText: "Upgrade Required",
|
|
480
|
-
statusMessage: `The request requires the upgrade to a webSocket connection`,
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
const availableEndpoints = constructAvailableEndpoints(
|
|
484
|
-
request,
|
|
485
|
-
fetchSecondArg,
|
|
486
|
-
);
|
|
487
|
-
return createRouteNotFoundResponse(request, { availableEndpoints });
|
|
523
|
+
return createRouteNotFoundResponse(request, { availableEndpoints });
|
|
524
|
+
}; // end matchRoutes
|
|
525
|
+
|
|
526
|
+
return matchRoutes(request);
|
|
488
527
|
};
|
|
489
|
-
const inspect = () => {
|
|
528
|
+
const inspect = ({ canExposeSensitiveData }) => {
|
|
490
529
|
// I want all the info I can gather about the routes
|
|
491
530
|
const data = [];
|
|
492
531
|
for (const route of routeSet) {
|
|
493
|
-
data.push(route.toJSON());
|
|
532
|
+
data.push(route.toJSON({ canExposeSensitiveData }));
|
|
494
533
|
}
|
|
495
534
|
return data;
|
|
496
535
|
};
|
|
@@ -615,10 +654,10 @@ const createRoute = ({
|
|
|
615
654
|
toString: () => {
|
|
616
655
|
return `${method} ${resource}`;
|
|
617
656
|
},
|
|
618
|
-
toJSON: () => {
|
|
657
|
+
toJSON: ({ canExposeSensitiveData }) => {
|
|
619
658
|
const meta = {};
|
|
620
659
|
|
|
621
|
-
if (declarationSource) {
|
|
660
|
+
if (declarationSource && canExposeSensitiveData) {
|
|
622
661
|
meta.declarationLink = {
|
|
623
662
|
url: `javascript:window.fetch("/.internal/open_file/${encodeURIComponent(declarationSource)}")`,
|
|
624
663
|
text: declarationSource,
|
|
@@ -646,7 +685,6 @@ const createRoute = ({
|
|
|
646
685
|
} catch {}
|
|
647
686
|
}
|
|
648
687
|
}
|
|
649
|
-
|
|
650
688
|
return {
|
|
651
689
|
method,
|
|
652
690
|
resource,
|
|
@@ -663,7 +701,9 @@ const createRoute = ({
|
|
|
663
701
|
: typeof clientCodeExample === "string"
|
|
664
702
|
? clientCodeExample
|
|
665
703
|
: undefined,
|
|
666
|
-
declarationSource
|
|
704
|
+
declarationSource: canExposeSensitiveData
|
|
705
|
+
? declarationSource
|
|
706
|
+
: undefined,
|
|
667
707
|
meta,
|
|
668
708
|
};
|
|
669
709
|
},
|
|
@@ -675,6 +715,9 @@ const createRoute = ({
|
|
|
675
715
|
return route;
|
|
676
716
|
};
|
|
677
717
|
|
|
718
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
719
|
+
const isRedirectStatus = (status) => REDIRECT_STATUSES.has(status);
|
|
720
|
+
|
|
678
721
|
const isRequestBodyMediaTypeSupported = (request, { acceptedMediaTypes }) => {
|
|
679
722
|
const requestBodyContentType = request.headers["content-type"];
|
|
680
723
|
if (!requestBodyContentType) {
|
package/src/start_server.js
CHANGED
|
@@ -79,6 +79,12 @@ export const startServer = async ({
|
|
|
79
79
|
keepProcessAlive = true,
|
|
80
80
|
routes = [],
|
|
81
81
|
plugins = [],
|
|
82
|
+
// When enabled, the server gives more power:
|
|
83
|
+
// - each route show the source where it was declared
|
|
84
|
+
// - server can be requested to open a file on the machine
|
|
85
|
+
// - client can subscribe to server to detect when it restarts
|
|
86
|
+
// This param should be enabled ONLY during development on your machine
|
|
87
|
+
canExposeSensitiveData = false,
|
|
82
88
|
nagle = true,
|
|
83
89
|
serverTiming = false,
|
|
84
90
|
requestWaitingMs = 0,
|
|
@@ -153,11 +159,11 @@ export const startServer = async ({
|
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
plugins = [
|
|
156
|
-
serverPluginOpenFile(),
|
|
162
|
+
...(canExposeSensitiveData ? [serverPluginOpenFile()] : []),
|
|
157
163
|
serverPluginDefaultBody4xx5xx(),
|
|
158
|
-
serverPluginRouteInspector(),
|
|
159
|
-
serverPluginInternalClientFiles(),
|
|
160
|
-
serverPluginAutoreloadOnRestart(),
|
|
164
|
+
serverPluginRouteInspector({ canExposeSensitiveData }),
|
|
165
|
+
...(canExposeSensitiveData ? [serverPluginInternalClientFiles()] : []),
|
|
166
|
+
...(canExposeSensitiveData ? [serverPluginAutoreloadOnRestart()] : []),
|
|
161
167
|
...plugins,
|
|
162
168
|
];
|
|
163
169
|
|