Daniele Teti 2023-08-16 11:40:36 +02:00
parent 1b9abb39d6
commit ec799cf573
3 changed files with 106 additions and 26 deletions

View File

@ -59,17 +59,24 @@ begin
Config[TMVCConfigKey.ExposeServerSignature] := 'true';
// Max request size in bytes
Config[TMVCConfigKey.MaxRequestSize] := IntToStr(TMVCConstants.DEFAULT_MAX_REQUEST_SIZE);
Config[TMVCConfigKey.LoadSystemControllers] := 'false';
end);
FMVC
.AddController(TMyController);
{ // Allows all origins -> * }
//FMVC.AddMiddleware(TMVCCORSMiddleware.Create);
{ **************************************************************************** }
{ Enable one of the following lines to configure CORS support in different way }
{ **************************************************************************** }
{ // Allows all origins -> * }
FMVC.AddMiddleware(TMVCCORSMiddleware.Create('https://anotherserver.com,http://localhost:9090'));
FMVC.AddMiddleware(TMVCCORSMiddleware.Create);
{ // Allows 2 origins }
// FMVC.AddMiddleware(TMVCCORSMiddleware.Create('https://anotherserver.com,http://localhost:9090'));
{ // Disallow localhost:9090 }
// FMVC.AddMiddleware(TMVCCORSMiddleware.Create('https://anotherserver.com'));
end;
procedure TMyWebModule.WebModuleDestroy(Sender: TObject);

View File

@ -9,20 +9,37 @@
</head>
<body>
<button id="btn">Click Here!</button>
<h1>DMVCFramework CORS Support Sample</h1>
<h3>This page must be served by "SimpleWebServer.exe" project, otherwise the demo doesn't work correctly.</h3>
<p>Change CORS configuration in "middleware_cors\WebModuleU.pas" (there are commented lines)</p>
<button id="btnCORS">POST request with CORS enabled!</button>
<button id="btnNoCORS">POST request with No-CORS!</button>
<div id="output"></div>
<script defer>
let output = document.getElementById('output');
document.getElementById('btn').onclick = () => {
document.getElementById('btnNoCORS').onclick = () => {
fetch('http://localhost:8080/api/customers', {
"mode":"no-cors",
"method": 'POST',
"body": JSON.stringify({ "hello": "world" })
})
.then((res) => {
output.innerHTML = "Request done but response is opaque to javascript";
});
};
document.getElementById('btnCORS').onclick = () => {
fetch('http://localhost:8080/api/customers', {
"method": 'POST',
"body": JSON.stringify({ "hello": "world" })
})
.then((res) => res.json())
.then((json) => {
output.innerHTML = json['data']["message"]
output.innerHTML = JSON.stringify(json);
});
}
};
</script>
</body>

View File

@ -58,6 +58,11 @@ type
/// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
/// </summary>
ALLOWS_METHODS = 'POST,GET,OPTIONS,PUT,DELETE';
/// <summary>
/// Indicates the number of seconds (60 by default) the information provided by
/// the `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers` headers can be cached.
/// </summary>
ACCESS_CONTROL_MAX_AGE = 60;
end;
TMVCCORSMiddleware = class(TInterfacedObject, IMVCMiddleware)
@ -68,8 +73,12 @@ type
FAllowsMethods: string;
FExposeHeaders: string;
FAllowsHeaders: string;
FAccessControlMaxAge: string;
protected
function GetAllowedOriginURL(AContext: TWebContext): String; virtual;
procedure FillCommonHeaders(AContext: TWebContext; const AAllowOrigin: String); virtual;
procedure HandlePreflightRequest(AContext: TWebContext; var AHandled: Boolean); virtual;
procedure HandleRequest(AContext: TWebContext; var AHandled: Boolean); virtual;
procedure OnBeforeRouting(
AContext: TWebContext;
@ -99,7 +108,8 @@ type
const AAllowsCredentials: Boolean = TMVCCORSDefaults.ALLOWS_CREDENTIALS;
const AExposeHeaders: String = TMVCCORSDefaults.EXPOSE_HEADERS;
const AAllowsHeaders: String = TMVCCORSDefaults.ALLOWS_HEADERS;
const AAllowsMethods: string = TMVCCORSDefaults.ALLOWS_METHODS
const AAllowsMethods: string = TMVCCORSDefaults.ALLOWS_METHODS;
const AAccessControlMaxAge: Integer = TMVCCORSDefaults.ACCESS_CONTROL_MAX_AGE
); virtual;
end;
@ -108,7 +118,7 @@ type
implementation
uses
System.SysUtils;
System.SysUtils, System.Classes;
{ TMVCCORSMiddleware }
@ -117,14 +127,28 @@ constructor TMVCCORSMiddleware.Create(
const AAllowsCredentials: Boolean;
const AExposeHeaders: String;
const AAllowsHeaders: String;
const AAllowsMethods: string
const AAllowsMethods: string;
const AAccessControlMaxAge: Integer
);
begin
inherited Create;
FAllowedOriginURLs := AAllowedOriginURLs.Split([',']);
FAllowsCredentials := AAllowsCredentials;
FExposeHeaders := AExposeHeaders;
FAllowsHeaders := AAllowsHeaders;
FAllowsMethods := AAllowsMethods;
FAccessControlMaxAge := IntToStr(AAccessControlMaxAge);
end;
procedure TMVCCORSMiddleware.FillCommonHeaders(AContext: TWebContext; const AAllowOrigin: String);
var
lCustomHeaders: TStrings;
begin
lCustomHeaders := AContext.Response.RawWebResponse.CustomHeaders;
lCustomHeaders.Values['Access-Control-Allow-Origin'] := AAllowOrigin;
lCustomHeaders.Values['Access-Control-Allow-Methods'] := FAllowsMethods;
lCustomHeaders.Values['Access-Control-Allow-Headers'] := FAllowsHeaders;
lCustomHeaders.Values['Access-Control-Max-Age'] := FAccessControlMaxAge;
end;
function TMVCCORSMiddleware.GetAllowedOriginURL(AContext: TWebContext): String;
@ -147,6 +171,44 @@ begin
end;
end;
procedure TMVCCORSMiddleware.HandlePreflightRequest(AContext: TWebContext;
var AHandled: Boolean);
var
lAllowOrigin: String;
begin
// https://fetch.spec.whatwg.org/#cors-preflight-request
lAllowOrigin := GetAllowedOriginURL(AContext);
AContext.Response.StatusCode := HTTP_STATUS.NoContent;
if not lAllowOrigin.IsEmpty then
begin
FillCommonHeaders(AContext, lAllowOrigin);
end;
AHandled := True;
end;
procedure TMVCCORSMiddleware.HandleRequest(AContext: TWebContext;
var AHandled: Boolean);
var
lAllowOrigin: String;
lCustomHeaders: TStrings;
begin
// https://fetch.spec.whatwg.org/#http-responses
lAllowOrigin := GetAllowedOriginURL(AContext);
if not lAllowOrigin.IsEmpty then
begin
FillCommonHeaders(AContext, lAllowOrigin);
lCustomHeaders := AContext.Response.RawWebResponse.CustomHeaders;
lCustomHeaders.Values['Access-Control-Expose-Headers'] := FExposeHeaders; {only for not preflight requests}
if FAllowsCredentials then
begin
// Omit Access-Control-Allow-Credentials if <> true
// https://github.com/danieleteti/delphimvcframework/issues/679#issuecomment-1676535853
lCustomHeaders.Values['Access-Control-Allow-Credentials'] := 'true';
end;
end;
AHandled := False;
end;
procedure TMVCCORSMiddleware.OnAfterControllerAction(
AContext: TWebContext;
const AControllerQualifiedClassName: string; const AActionName: string;
@ -168,24 +230,18 @@ begin
end;
procedure TMVCCORSMiddleware.OnBeforeRouting(AContext: TWebContext; var AHandled: Boolean);
var
lAllowOrigin: String;
begin
AContext.Response.RawWebResponse.CustomHeaders.Values['Access-Control-Allow-Origin'] := GetAllowedOriginURL(AContext);
AContext.Response.RawWebResponse.CustomHeaders.Values['Access-Control-Allow-Methods'] := FAllowsMethods;
AContext.Response.RawWebResponse.CustomHeaders.Values['Access-Control-Allow-Headers'] := FAllowsHeaders;
if FAllowsCredentials then
if AContext.Request.HTTPMethod <> httpOPTIONS then
begin
// Omit Access-Control-Allow-Credentials if <> true
// https://github.com/danieleteti/delphimvcframework/issues/679#issuecomment-1676535853
AContext.Response.RawWebResponse.CustomHeaders.Values['Access-Control-Allow-Credentials'] := 'true';
end;
AContext.Response.RawWebResponse.CustomHeaders.Values['Access-Control-Expose-Headers'] := FExposeHeaders;
// allows preflight requests
if (AContext.Request.HTTPMethod = httpOPTIONS) then
//normal request, no preflight request
HandleRequest(AContext, AHandled);
end
else
begin
AContext.Response.StatusCode := HTTP_STATUS.OK;
AHandled := True;
//preflight
HandlePreflightRequest(AContext, AHandled);
end;
end;