diff --git a/dotnet/src/dotnetcore/GxClasses.Web/Middleware/GXRouting.cs b/dotnet/src/dotnetcore/GxClasses.Web/Middleware/GXRouting.cs index 09d5bbddd..dec57197e 100644 --- a/dotnet/src/dotnetcore/GxClasses.Web/Middleware/GXRouting.cs +++ b/dotnet/src/dotnetcore/GxClasses.Web/Middleware/GXRouting.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Reflection; using System.Runtime.Serialization; using System.Text.Json.Serialization; @@ -20,6 +21,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; namespace GxClasses.Web.Middleware { @@ -254,26 +256,24 @@ public Task ProcessRestRequest(HttpContext context) } else if (HttpMethods.IsOptions(context.Request.Method)) { - string mthheaders = "OPTIONS,HEAD"; + List mthheaders = new List() { $"{HttpMethod.Options.Method},{HttpMethod.Head.Method}" }; if (!String.IsNullOrEmpty(actualPath) && servicesMapData.ContainsKey(actualPath)) { foreach (Tuple t in servicesMapData[actualPath].Keys) { if (t.Item1.Equals(controllerWithParms.ToLower())) { - mthheaders += "," + t.Item2; + mthheaders.Add(t.Item2); } } } else { - mthheaders += ", GET, POST"; + mthheaders.Add(HttpMethod.Get.Method); + mthheaders.Add(HttpMethod.Post.Method); } - context.Response.Headers.Add("Access-Control-Allow-Origin", new[] { (string)context.Request.Headers["Origin"] }); - context.Response.Headers.Add("Access-Control-Allow-Headers", new[] { "Origin, X-Requested-With, Content-Type, Accept" }); - context.Response.Headers.Add("Access-Control-Allow-Methods", new[] { mthheaders }); - context.Response.Headers.Add("Access-Control-Allow-Credentials", new[] { "true" }); - context.Response.Headers.Add("Allow", mthheaders); + HttpHelper.CorsHeaders(context); + HttpHelper.AllowHeader(context, mthheaders); context.Response.StatusCode = (int)HttpStatusCode.OK; } else diff --git a/dotnet/src/dotnetcore/GxClasses/Properties/AssemblyInfo.cs b/dotnet/src/dotnetcore/GxClasses/Properties/AssemblyInfo.cs index 23eed26f7..cb96c93e4 100644 --- a/dotnet/src/dotnetcore/GxClasses/Properties/AssemblyInfo.cs +++ b/dotnet/src/dotnetcore/GxClasses/Properties/AssemblyInfo.cs @@ -7,3 +7,6 @@ [assembly: InternalsVisibleTo("AzureFunctionsTest")] [assembly: InternalsVisibleTo("GXQueue")] [assembly: InternalsVisibleTo("DotNetCoreUnitTest")] +[assembly: InternalsVisibleTo("GeneXus.Deploy.AzureFunctions.Handlers")] +[assembly: InternalsVisibleTo("AzureFunctionsTest")] +[assembly: InternalsVisibleTo("GXMessageBroker")] diff --git a/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs b/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs index ff9f16a7d..10a8138a9 100644 --- a/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs +++ b/dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs @@ -108,6 +108,9 @@ public class Startup const string SWAGGER_DEFAULT_YAML = "default.yaml"; const string DEVELOPER_MENU = "developermenu.html"; const string SWAGGER_SUFFIX = "swagger"; + const string CORS_POLICY_NAME = "AllowSpecificOriginsPolicy"; + const string CORS_ANY_ORIGIN = "*"; + const double CORS_MAX_AGE_SECONDS = 86400; public List servicesBase = new List(); @@ -193,9 +196,41 @@ public void ConfigureServices(IServiceCollection services) options.EnableForHttps = true; }); } + DefineCorsPolicy(services); services.AddMvc(); } + private void DefineCorsPolicy(IServiceCollection services) + { + if (Preferences.CorsEnabled) + { + string corsAllowedOrigins = Preferences.CorsAllowedOrigins(); + if (!string.IsNullOrEmpty(corsAllowedOrigins)) + { + string[] origins = corsAllowedOrigins.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + foreach (string origin in origins) + { + GXLogging.Info(log, $"Adding origin to CORS policy:", origin); + } + services.AddCors(options => + { + options.AddPolicy(name: CORS_POLICY_NAME, + policy => + { + policy.WithOrigins(origins); + if (!corsAllowedOrigins.Contains(CORS_ANY_ORIGIN)) + { + policy.AllowCredentials(); + } + policy.AllowAnyHeader(); + policy.AllowAnyMethod(); + policy.SetPreflightMaxAge(TimeSpan.FromSeconds(CORS_MAX_AGE_SECONDS)); + }); + }); + } + } + } + private void ConfigureSessionService(IServiceCollection services, ISessionService sessionService) { if (sessionService is GxRedisSession) @@ -246,6 +281,7 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos app.UseCookiePolicy(); app.UseSession(); app.UseStaticFiles(); + ConfigureCors(app); ConfigureSwaggerUI(app, baseVirtualPath); if (Directory.Exists(Path.Combine(LocalPath, RESOURCES_FOLDER))) @@ -353,6 +389,14 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos app.UseEnableRequestRewind(); } + private void ConfigureCors(IApplicationBuilder app) + { + if (Preferences.CorsEnabled) + { + app.UseCors(CORS_POLICY_NAME); + } + } + private void ConfigureSwaggerUI(IApplicationBuilder app, string baseVirtualPath) { try diff --git a/dotnet/src/dotnetframework/GxClasses/Core/gxconfig.cs b/dotnet/src/dotnetframework/GxClasses/Core/gxconfig.cs index b2118ce47..8eb2c769b 100644 --- a/dotnet/src/dotnetframework/GxClasses/Core/gxconfig.cs +++ b/dotnet/src/dotnetframework/GxClasses/Core/gxconfig.cs @@ -6,10 +6,12 @@ namespace GeneXus.Configuration #if NETCORE using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; + using System.Text; #else using System.Web; -#endif using System.Configuration; + using System.Collections.Generic; +#endif using System.Collections; using System.Collections.Specialized; using System.Xml; @@ -23,9 +25,7 @@ namespace GeneXus.Configuration using System.Collections.Concurrent; using System.Reflection; using System.Runtime.Serialization.Json; - using System.Collections.Generic; using GxClasses.Helpers; - using System.Text; public class Config { @@ -1312,6 +1312,19 @@ public static string ApplicationPath set { _applicationPath = value; } } + internal static bool CorsEnabled { + get { + return !string.IsNullOrEmpty(CorsAllowedOrigins()); + } + } + + internal static string CorsAllowedOrigins() + { + if (Config.GetValueOf("CORS_ALLOW_ORIGIN", out string corsOrigin)) + return corsOrigin; + else + return string.Empty; + } public static int GetMaximumOpenCursors() { if (maximumOpenCursors == 0) @@ -1372,5 +1385,6 @@ public static int GetHttpClientMaxConnectionPerRoute() return httpclient_max_per_route; } + } } diff --git a/dotnet/src/dotnetframework/GxClasses/GxClasses.csproj b/dotnet/src/dotnetframework/GxClasses/GxClasses.csproj index 266384d36..84dde7cae 100644 --- a/dotnet/src/dotnetframework/GxClasses/GxClasses.csproj +++ b/dotnet/src/dotnetframework/GxClasses/GxClasses.csproj @@ -11,6 +11,7 @@ + diff --git a/dotnet/src/dotnetframework/GxClasses/Helpers/HttpHelper.cs b/dotnet/src/dotnetframework/GxClasses/Helpers/HttpHelper.cs index 5abda3d9b..1d702753d 100644 --- a/dotnet/src/dotnetframework/GxClasses/Helpers/HttpHelper.cs +++ b/dotnet/src/dotnetframework/GxClasses/Helpers/HttpHelper.cs @@ -8,12 +8,11 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; -using System.Net.Http; using Microsoft.AspNetCore.Mvc.Formatters; #else using System.ServiceModel.Web; using System.ServiceModel; - +using System.ServiceModel.Channels; #endif using System; using System.Collections.Generic; @@ -25,6 +24,8 @@ using System.Runtime.Serialization; using GeneXus.Mime; using System.Text.RegularExpressions; +using Microsoft.Net.Http.Headers; +using System.Net.Http; namespace GeneXus.Http { @@ -82,6 +83,99 @@ public class HttpHelper const string GAM_CODE_TOKEN_EXPIRED = "103"; static Regex CapitalsToTitle = new Regex(@"(?<=[A-Z])(?=[A-Z][a-z]) | (?<=[^A-Z])(?=[A-Z]) | (?<=[A-Za-z])(?=[^A-Za-z])", RegexOptions.IgnorePatternWhitespace); + const string CORS_MAX_AGE_SECONDS = "86400"; + internal static void CorsHeaders(HttpContext httpContext) + { + if (Preferences.CorsEnabled) + { + string[] origins = Preferences.CorsAllowedOrigins().Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + if (httpContext != null) + { + string requestHeaders = httpContext.Request.Headers[HeaderNames.AccessControlRequestHeaders]; + string requestMethod = httpContext.Request.Headers[HeaderNames.AccessControlRequestMethod]; + CorsValuesToHeaders(httpContext.Response, origins, requestHeaders, requestMethod); + } + } + } +#if !NETCORE + internal static void CorsHeaders(HttpResponseMessageProperty response, string requestHeaders, string requestMethods) + { + if (Preferences.CorsEnabled) + { + string[] origins = Preferences.CorsAllowedOrigins().Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + CorsValuesToHeaders(response, origins, requestHeaders, requestMethods); + } + } + internal static void CorsHeaders(WebOperationContext wcfContext) + { + if (Preferences.CorsEnabled) + { + string[] origins = Preferences.CorsAllowedOrigins().Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + if (wcfContext != null) + { + string requestHeaders = wcfContext.IncomingRequest.Headers[HeaderNames.AccessControlRequestHeaders]; + string requestMethods = wcfContext.IncomingRequest.Headers[HeaderNames.AccessControlRequestMethod]; + CorsValuesToHeaders(wcfContext.OutgoingResponse, origins, requestHeaders, requestMethods); + } + + } + } + static void CorsValuesToHeaders(OutgoingWebResponseContext httpResponse, string[] origins, string requestHeaders, string requestMethods) + { + foreach (string origin in origins) + { + if (!string.IsNullOrEmpty(origin)) + httpResponse.Headers[HeaderNames.AccessControlAllowOrigin] = origin; + } + httpResponse.Headers[HeaderNames.AccessControlAllowCredentials] = true.ToString(); + + if (!string.IsNullOrEmpty(requestHeaders)) + httpResponse.Headers[HeaderNames.AccessControlAllowHeaders] = requestHeaders; + + if (!string.IsNullOrEmpty(requestMethods)) + httpResponse.Headers[HeaderNames.AccessControlAllowMethods] = requestMethods; + + httpResponse.Headers[HeaderNames.AccessControlMaxAge] = CORS_MAX_AGE_SECONDS; + + } + static void CorsValuesToHeaders(HttpResponseMessageProperty httpResponse, string[] origins, string requestHeaders, string requestMethods) + { + foreach (string origin in origins) + { + if (!string.IsNullOrEmpty(origin)) + httpResponse.Headers[HeaderNames.AccessControlAllowOrigin] = origin; + } + httpResponse.Headers[HeaderNames.AccessControlAllowCredentials] = true.ToString(); + + if (!string.IsNullOrEmpty(requestHeaders)) + httpResponse.Headers[HeaderNames.AccessControlAllowHeaders] = requestHeaders; + + if (!string.IsNullOrEmpty(requestMethods)) + httpResponse.Headers[HeaderNames.AccessControlAllowMethods] = requestMethods; + + httpResponse.Headers[HeaderNames.AccessControlMaxAge] = CORS_MAX_AGE_SECONDS; + } + +#endif + static void CorsValuesToHeaders(HttpResponse httpResponse, string[] origins, string requestHeaders, string requestMethods) + { + //AppendHeader must be used on httpResponse (instead of httpResponse.Headers[]) to support WebDev.WevServer2 + foreach (string origin in origins) + { + if (!string.IsNullOrEmpty(origin)) + httpResponse.AppendHeader(HeaderNames.AccessControlAllowOrigin, origin); + } + httpResponse.AppendHeader(HeaderNames.AccessControlAllowCredentials, true.ToString()); + + if (!string.IsNullOrEmpty(requestHeaders)) + httpResponse.AppendHeader(HeaderNames.AccessControlAllowHeaders, requestHeaders); + + if (!string.IsNullOrEmpty(requestMethods)) + httpResponse.AppendHeader(HeaderNames.AccessControlAllowMethods, requestMethods); + + httpResponse.AppendHeader(HeaderNames.AccessControlMaxAge, CORS_MAX_AGE_SECONDS); + } + public static void SetResponseStatus(HttpContext httpContext, string statusCode, string statusDescription) { HttpStatusCode httpStatusCode = MapStatusCode(statusCode); @@ -282,7 +376,7 @@ public static string RequestPhysicalApplicationPath(HttpContext context = null) #if NETCORE public static byte[] DownloadFile(string url, out HttpStatusCode statusCode) { - var buffer = Array.Empty(); + byte[] buffer = Array.Empty(); using (var client = new HttpClient()) { using (HttpResponseMessage response = client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead).Result) @@ -350,6 +444,10 @@ public static string[] GetParameterValues(string query) } } + internal static void AllowHeader(HttpContext httpContext, List methods) + { + httpContext.Response.AppendHeader(HeaderNames.Allow, string.Join(",", methods)); + } } #if NETCORE public class HttpCookieCollection : Dictionary @@ -671,7 +769,7 @@ public static NameValueCollection GetQueryString(this HttpRequest request) #if NETCORE NameValueCollection paramValues = new NameValueCollection(); - foreach (var key in request.Query.Keys) + foreach (string key in request.Query.Keys) { paramValues.Add(key, request.Query[key].ToString()); } @@ -691,7 +789,7 @@ public static NameValueCollection GetParams(this HttpRequest request) NameValueCollection paramValues = request.GetQueryString(); try { - foreach (var key in request.Form.Keys) + foreach (string key in request.Form.Keys) { paramValues.Add(key, request.Form[key].ToString()); } @@ -699,7 +797,7 @@ public static NameValueCollection GetParams(this HttpRequest request) catch (InvalidOperationException) { //The Form property is populated when the HTTP request Content-Type value is either "application/x-www-form-urlencoded" or "multipart/form-data". } - foreach (var key in request.Cookies.Keys) + foreach (string key in request.Cookies.Keys) { paramValues.Add(key, request.Cookies[key]); } @@ -878,7 +976,7 @@ public static string GetAbsolutePath(this HttpRequest request) public static string GetFilePath(this HttpRequest request) { #if NETCORE - var basePath = string.Empty; + string basePath = string.Empty; if (request.PathBase.HasValue) basePath = request.PathBase.Value; if (request.Path.HasValue) diff --git a/dotnet/src/dotnetframework/GxClasses/Middleware/GXHttp.cs b/dotnet/src/dotnetframework/GxClasses/Middleware/GXHttp.cs index cdadf55fd..8b25e11e1 100644 --- a/dotnet/src/dotnetframework/GxClasses/Middleware/GXHttp.cs +++ b/dotnet/src/dotnetframework/GxClasses/Middleware/GXHttp.cs @@ -13,7 +13,6 @@ namespace GeneXus.Http using GeneXus.Application; using GeneXus.Configuration; - using GeneXus.Data.NTier; using GeneXus.Encryption; using GeneXus.Metadata; using GeneXus.Mime; @@ -24,15 +23,17 @@ namespace GeneXus.Http using log4net; using Jayrock.Json; - using System.Web.SessionState; using Helpers; using System.Collections.Concurrent; + using Microsoft.Net.Http.Headers; + using System.Net.Http; #if NETCORE using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using System.Net; using GeneXus.Web.Security; using System.Linq; + using System.Reflection.PortableExecutable; #else using System.Web; using System.Web.UI; @@ -41,8 +42,8 @@ namespace GeneXus.Http using System.Net; using GeneXus.Notifications; using Web.Security; + using System.Web.SessionState; #endif - #if NETCORE public abstract class GXHttpHandler : GXBaseObject, IHttpHandler #else @@ -1860,9 +1861,7 @@ public void ProcessRequest(HttpContext httpContext) try { #if NETCORE - sendCacheHeaders(); - GXLogging.Debug(log, "HttpHeaders: ", DumpHeaders(httpContext)); - sendAdditionalHeaders(); + SendHeaders(); string clientid = context.ClientID; //Send clientid cookie (before response HasStarted) if necessary, since UseResponseBuffering is not in .netcore3.0 #endif bool validSession = ValidWebSession(); @@ -1900,10 +1899,7 @@ public void ProcessRequest(HttpContext httpContext) } SetCompression(httpContext); #if !NETCORE - sendCacheHeaders(); - - GXLogging.Debug(log, "HttpHeaders: ", DumpHeaders(httpContext)); - sendAdditionalHeaders(); + SendHeaders(); #endif context.ResponseCommited = true; } @@ -2097,6 +2093,14 @@ private string GetGAMNotAuthorizedWebObject() else return formatLink(loginObject); } + private void SendHeaders() + { + sendCacheHeaders(); + GXLogging.Debug(log, "HttpHeaders: ", DumpHeaders(localHttpContext)); + sendAdditionalHeaders(); + HttpHelper.CorsHeaders(localHttpContext); + HttpHelper.AllowHeader(localHttpContext, new List() { $"{HttpMethod.Get.Method},{HttpMethod.Post.Method}" }); + } protected virtual void sendCacheHeaders() { diff --git a/dotnet/src/dotnetframework/GxClasses/Middleware/GXHttpModules.cs b/dotnet/src/dotnetframework/GxClasses/Middleware/GXHttpModules.cs index ea847a093..98ffd93e3 100644 --- a/dotnet/src/dotnetframework/GxClasses/Middleware/GXHttpModules.cs +++ b/dotnet/src/dotnetframework/GxClasses/Middleware/GXHttpModules.cs @@ -14,6 +14,7 @@ using System.Web.SessionState; using System.Web.Configuration; using System.Security; +using System.Net.Http; namespace GeneXus.Http.HttpModules { @@ -74,26 +75,42 @@ void IHttpModule.Init(HttpApplication context) ServicesGroupSetting(GxContext.StaticPhysicalPath()); GXAPIModule.moduleStarted = true; } - context.PostMapRequestHandler += new EventHandler(context_PostMapRequestHandler); + context.PostMapRequestHandler += context_PostMapRequestHandler; + context.PostResolveRequestCache += onPostResolveRequestCache; + } + private void onPostResolveRequestCache(object sender, EventArgs eventArgs) + { + if (string.Equals(HttpContext.Current.Request.HttpMethod, HttpMethod.Options.Method, StringComparison.OrdinalIgnoreCase)) + { + IHttpHandler apiHandler = MapHandler(sender, eventArgs); + if (apiHandler != null) + HttpContext.Current.RemapHandler(apiHandler); + } } - void IHttpModule.Dispose() { } - - private void context_PostMapRequestHandler(object sender, EventArgs e) + + private void context_PostMapRequestHandler(object sender, EventArgs eventArgs) + { + HttpApplication httpApp = sender as HttpApplication; + IHttpHandler apiHandler = MapHandler(sender, eventArgs); + if (apiHandler != null) + httpApp.Context.Handler = apiHandler; + + } + private IHttpHandler MapHandler(object sender, EventArgs e) { HttpApplication httpApp = sender as HttpApplication; HttpContext context = httpApp.Context; - String actualPath = ""; - if (GXAPIModule.serviceInPath(context.Request.FilePath, actualPath: out actualPath)) + if (GXAPIModule.serviceInPath(context.Request.FilePath, actualPath: out _)) { - IHttpHandler myHandler = new GeneXus.HttpHandlerFactory.HandlerFactory().GetHandler(context, context.Request.RequestType, context.Request.Url.AbsoluteUri, context.Request.FilePath); - if (myHandler !=null) - context.Handler = myHandler; + return new GeneXus.HttpHandlerFactory.HandlerFactory().GetHandler(context, context.Request.RequestType, context.Request.Url.AbsoluteUri, context.Request.FilePath); } + else + return null; } - + public static Boolean serviceInPath(String path, out String actualPath) { actualPath = ""; @@ -191,7 +208,7 @@ public class GXSessionModule : IHttpModule public void Init(HttpApplication app) { - App = app; + App = app; try { SessionStateSection sessionStateSection = (SessionStateSection)System.Configuration.ConfigurationManager.GetSection("system.web/sessionState"); diff --git a/dotnet/src/dotnetframework/GxClasses/Middleware/HandlerFactory.cs b/dotnet/src/dotnetframework/GxClasses/Middleware/HandlerFactory.cs index e5a2c9248..b945d0df5 100644 --- a/dotnet/src/dotnetframework/GxClasses/Middleware/HandlerFactory.cs +++ b/dotnet/src/dotnetframework/GxClasses/Middleware/HandlerFactory.cs @@ -9,12 +9,51 @@ using GeneXus.Utils; using GeneXus.Http.HttpModules; using GeneXus.Metadata; -using GeneXus.Procedure; using System.Net; using System.Text.RegularExpressions; +using System.Net.Http; +using GeneXus.Http; namespace GeneXus.HttpHandlerFactory { + internal class OptionsApiObjectRequestHandler : IHttpHandler + { + string actualPath; + string objectName; + internal OptionsApiObjectRequestHandler(string path, string name) + { + actualPath = path; + objectName = name; + } + public void ProcessRequest(HttpContext context) + { + // OPTIONS VERB + List mthheaders = new List() { $"{HttpMethod.Options.Method},{HttpMethod.Head.Method}" }; + bool found = false; + foreach (Tuple t in GXAPIModule.servicesMapData[actualPath].Keys) + { + if (t.Item1.Equals(objectName.ToLower())) + { + mthheaders.Add(t.Item2); + found = true; + } + } + if (found) + { + HttpHelper.CorsHeaders(context); + HttpHelper.AllowHeader(context, mthheaders); + } + else + { + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + } + } + + public bool IsReusable + { + get { return false; } + } + } class HandlerFactory : IHttpHandlerFactory { private static readonly ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); @@ -70,43 +109,19 @@ public IHttpHandler GetHandler(HttpContext context, string requestType, string u asssemblycontroller = addNspace + "." + tmpController; nspace += "." + addNspace; } - var gxContext = GxContext.CreateDefaultInstance(); - var handler = ClassLoader.FindInstance(asssemblycontroller, nspace, tmpController, new Object[] { gxContext }, null); + GxContext gxContext = GxContext.CreateDefaultInstance(); + object handler = ClassLoader.FindInstance(asssemblycontroller, nspace, tmpController, new Object[] { gxContext }, null); gxContext.HttpContext = context; GxRestWrapper restWrapper = new Application.GxRestWrapper(handler as GXBaseObject, context, gxContext, value.ServiceMethod, value.VariableAlias, routeParms); return restWrapper; } - } + } else - { - if ( requestType.Equals("OPTIONS") && !String.IsNullOrEmpty(actualPath) && GXAPIModule.servicesMapData.ContainsKey(actualPath)) + { + if ( requestType.Equals(HttpMethod.Options.Method) && !String.IsNullOrEmpty(actualPath) && GXAPIModule.servicesMapData.ContainsKey(actualPath)) { - // OPTIONS VERB - string mthheaders = "OPTIONS,HEAD"; - bool found = false; - foreach (Tuple t in GXAPIModule.servicesMapData[actualPath].Keys) - { - if (t.Item1.Equals(objectName.ToLower())) - { - mthheaders += "," + t.Item2; - found = true; - } - } - if (found) - { - context.Response.Headers.Add("Allow", mthheaders); - context.Response.Headers.Add("Access-Control-Allow-Headers", "Content-Type"); - context.Response.Headers.Add("Access-Control-Allow-Origin", "*"); - context.Response.Headers.Add("Access-Control-Allow-Methods", mthheaders); - context.Response.End(); - } - else - { - context.Response.StatusCode = (int)HttpStatusCode.NotFound; - context.Response.End(); - } - return null; + return new OptionsApiObjectRequestHandler(actualPath, objectName); } } return null; diff --git a/dotnet/src/dotnetframework/GxClasses/Services/GXRestServices.cs b/dotnet/src/dotnetframework/GxClasses/Services/GXRestServices.cs index a319c5ae3..b47ca5203 100644 --- a/dotnet/src/dotnetframework/GxClasses/Services/GXRestServices.cs +++ b/dotnet/src/dotnetframework/GxClasses/Services/GXRestServices.cs @@ -9,6 +9,7 @@ using System.Runtime.Serialization.Json; using System.ServiceModel; using System.ServiceModel.Channels; +using System.ServiceModel.Configuration; using System.ServiceModel.Description; using System.ServiceModel.Dispatcher; using System.ServiceModel.Web; @@ -21,10 +22,11 @@ using GeneXus.Metadata; using GeneXus.Security; using log4net; +using Microsoft.Net.Http.Headers; namespace GeneXus.Utils { - public class CustomHttpBehaviorExtensionElement : System.ServiceModel.Configuration.BehaviorExtensionElement + public class CustomHttpBehaviorExtensionElement : BehaviorExtensionElement { protected override object CreateBehavior() { @@ -54,7 +56,51 @@ protected override void AddServerErrorHandlers(ServiceEndpoint endpoint, Endpoin endpointDispatcher.ChannelDispatcher.ErrorHandlers.Clear(); endpointDispatcher.ChannelDispatcher.ErrorHandlers.Add(new JsonErrorHandler()); } - } + public override void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) + { + endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new CustomHeaderMessageInspector()); + base.ApplyDispatchBehavior(endpoint, endpointDispatcher); + } + } + internal class CustomHeaderMessageInspector : IDispatchMessageInspector + { + const string HttpResponseProperty = "httpResponse"; + const string PreflightReturn = "PreflightReturn"; + public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext) + { + HttpRequestMessageProperty httpRequest = (HttpRequestMessageProperty)request.Properties[HttpRequestMessageProperty.Name]; + return new CorrelationState() + { + RequestHeaders = httpRequest.Headers[HeaderNames.AccessControlRequestHeaders], + RequestMethods = httpRequest.Headers[HeaderNames.AccessControlRequestMethod], + HandlePreflight = httpRequest.Method.Equals(HttpMethod.Options.Method, StringComparison.InvariantCultureIgnoreCase) + }; + } + + public void BeforeSendReply(ref Message reply, object correlationState) + { + CorrelationState state = correlationState as CorrelationState; + if (state != null && state.HandlePreflight) + { + HttpResponseMessageProperty httpResponse = reply.Properties[HttpResponseProperty] as HttpResponseMessageProperty; + if (httpResponse == null) + { + reply = Message.CreateMessage(MessageVersion.None, PreflightReturn); + httpResponse = new HttpResponseMessageProperty(); + reply.Properties.Add(HttpResponseMessageProperty.Name, httpResponse); + } + HttpHelper.CorsHeaders(httpResponse, state.RequestHeaders, state.RequestMethods); + httpResponse.SuppressEntityBody = true; + httpResponse.StatusCode = HttpStatusCode.OK; + } + } + } + internal class CorrelationState + { + internal string RequestHeaders; + internal string RequestMethods; + internal bool HandlePreflight; + } class CustomOperationSelector : WebHttpDispatchOperationSelector { static readonly ILog log = log4net.LogManager.GetLogger(typeof(CustomOperationSelector)); @@ -199,7 +245,7 @@ public GxRestService() } public void Cleanup() { - SendCacheHeaders(); + ServiceHeaders(); if (runAsMain) context.CloseConnections(); } @@ -545,7 +591,18 @@ private void SendCacheHeaders() if (string.IsNullOrEmpty(context.GetHeader(HttpHeader.CACHE_CONTROL))) AddHeader("Cache-Control", HttpHelper.CACHE_CONTROL_HEADER_NO_CACHE); } - + private void ServiceHeaders() + { + SendCacheHeaders(); + if (httpContext != null) + { + HttpHelper.CorsHeaders(httpContext); + }else if (wcfContext != null) + { + HttpHelper.CorsHeaders(wcfContext); + } + + } DateTime HTMLDateToDatetime(string s) { // Date Format: RFC 1123 diff --git a/dotnet/src/dotnetframework/GxClasses/Services/GxRestWrapper.cs b/dotnet/src/dotnetframework/GxClasses/Services/GxRestWrapper.cs index a788e5b60..0068ac0ac 100644 --- a/dotnet/src/dotnetframework/GxClasses/Services/GxRestWrapper.cs +++ b/dotnet/src/dotnetframework/GxClasses/Services/GxRestWrapper.cs @@ -26,8 +26,8 @@ using GeneXus.Security; using System.Collections; using Jayrock.Json; - - +using Microsoft.Net.Http.Headers; +using System.Net.Http; namespace GeneXus.Application @@ -141,7 +141,7 @@ public virtual Task MethodBodyExecute(object key) _procWorker.cleanup(); RestProcess(outputParameters); wrapped = GetWrappedStatus(_procWorker, wrapped, outputParameters, outputParameters.Count); - SendCacheHeaders(); + ServiceHeaders(); return Serialize(outputParameters, formatParameters, wrapped); } catch (Exception e) @@ -330,7 +330,7 @@ public virtual Task MethodUrlExecute(object key) RestProcess(outputParameters); bool wrapped = false; wrapped = GetWrappedStatus(_procWorker, wrapped, outputParameters, parCount); - SendCacheHeaders(); + ServiceHeaders(); return Serialize(outputParameters, formatParameters, wrapped); } catch (Exception e) @@ -669,6 +669,14 @@ protected bool ProcessHeaders(string queryId) } return true; } + private void ServiceHeaders() + { + SendCacheHeaders(); + HttpHelper.CorsHeaders(_httpContext); + HttpHelper.AllowHeader(_httpContext, new List() { $"{HttpMethod.Get.Method},{HttpMethod.Post.Method}" }); + + } + private void SendCacheHeaders() { if (string.IsNullOrEmpty(_gxContext.GetHeader(HttpHeader.CACHE_CONTROL))) diff --git a/dotnet/test/DotNetCoreUnitTest/DotNetCoreUnitTest.csproj b/dotnet/test/DotNetCoreUnitTest/DotNetCoreUnitTest.csproj index a3b288e4a..818e9f7ae 100644 --- a/dotnet/test/DotNetCoreUnitTest/DotNetCoreUnitTest.csproj +++ b/dotnet/test/DotNetCoreUnitTest/DotNetCoreUnitTest.csproj @@ -92,6 +92,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -159,7 +162,6 @@ - diff --git a/dotnet/test/DotNetCoreUnitTest/Middleware/CorsTest.cs b/dotnet/test/DotNetCoreUnitTest/Middleware/CorsTest.cs new file mode 100644 index 000000000..c7a4094fd --- /dev/null +++ b/dotnet/test/DotNetCoreUnitTest/Middleware/CorsTest.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection; +using System.Threading.Tasks; +using GeneXus.Configuration; +using GeneXus.Metadata; +using Microsoft.Net.Http.Headers; +using Xunit; +using xUnitTesting; + +namespace DotNetCoreUnitTest.Middleware +{ + public class CorsTest : MiddlewareTest + { + const string HttpCorsProgramName = "httpcors"; + const string HttpCorsProgramModule = "apps"; + string Origin = Preferences.CorsAllowedOrigins(); + string[] Headers = { "authorization","cache-control", "deviceid", "devicetype", "genexus-agent", "gxtzoffset" }; + public CorsTest() + { + ClassLoader.FindType($"{HttpCorsProgramModule}.{HttpCorsProgramName}", $"GeneXus.Programs.{HttpCorsProgramModule}", HttpCorsProgramName, Assembly.GetExecutingAssembly(), true);//Force loading assembly + Assert.NotEmpty(Origin); + } + [Fact] + public async Task TestCorsOnPost() + { + server.AllowSynchronousIO = true; + HttpClient client = server.CreateClient(); + string deviceIdHeaderName = "deviceid"; + string deviceIdHeaderValue = "AndroidDevice"; + string contentType = "application/json"; + + client.DefaultRequestHeaders.Add(HeaderNames.Origin, Origin); + client.DefaultRequestHeaders.Add(HeaderNames.AccessControlRequestHeaders, Headers); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(contentType)); + client.DefaultRequestHeaders.Add(deviceIdHeaderName, deviceIdHeaderValue); + + HttpResponseMessage response = await client.PostAsync($"rest/{HttpCorsProgramModule}/{HttpCorsProgramName}", null); + response.EnsureSuccessStatusCode(); + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + + string originHeader = GetHeader(response, HeaderNames.AccessControlAllowOrigin); + Assert.Equal(Origin, originHeader); + + string deviceidHeader = GetHeader(response, deviceIdHeaderName); + Assert.Equal(deviceIdHeaderValue, deviceidHeader); + } + [Fact] + public async Task TestCorsOnPreflightRequest() + { + server.AllowSynchronousIO = true; + HttpClient client = server.CreateClient(); + client.DefaultRequestHeaders.Add(HeaderNames.Origin, Origin); + client.DefaultRequestHeaders.Add(HeaderNames.AccessControlRequestHeaders, Headers); + client.DefaultRequestHeaders.Add(HeaderNames.AccessControlRequestMethod, HttpMethod.Options.Method); + + HttpResponseMessage response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Options, $"rest/{HttpCorsProgramModule}/{HttpCorsProgramName}")); + response.EnsureSuccessStatusCode(); + Assert.Equal(System.Net.HttpStatusCode.NoContent, response.StatusCode); + + string originHeader = GetHeader(response, HeaderNames.AccessControlAllowOrigin); + Assert.Equal(Origin, originHeader); + + string methodsHeader = GetHeader(response, HeaderNames.AccessControlAllowMethods); + Assert.Equal(HttpMethod.Options.Method, methodsHeader); + + string credentialsHeader = GetHeader(response, HeaderNames.AccessControlAllowCredentials); + Assert.Equal("true", credentialsHeader); + + string headersHeader = GetHeader(response, HeaderNames.AccessControlAllowHeaders); + foreach(string header in Headers) + { + Assert.Contains(header, headersHeader, StringComparison.OrdinalIgnoreCase); + } + } + private string GetHeader(HttpResponseMessage response, string headerName) + { + if (response.Headers.TryGetValues(headerName, out IEnumerable value)) + return value.First(); + else + return string.Empty; + + } + + } +} diff --git a/dotnet/test/DotNetCoreUnitTest/Middleware/RestServiceTest.cs b/dotnet/test/DotNetCoreUnitTest/Middleware/RestServiceTest.cs index 507ed7645..639a2ef30 100644 --- a/dotnet/test/DotNetCoreUnitTest/Middleware/RestServiceTest.cs +++ b/dotnet/test/DotNetCoreUnitTest/Middleware/RestServiceTest.cs @@ -126,29 +126,5 @@ private async Task RunController(HttpClient client) return response; } } - public class MultiCallMiddleware - { - private readonly RequestDelegate _next; - - public MultiCallMiddleware(RequestDelegate next) - { - _next = next; - } - - public async Task Invoke(HttpContext context) - { - try - { - GXMultiCall multicall = new GXMultiCall(); - multicall.ProcessRequest(context); - } - catch (Exception ex) - { - Console.WriteLine(ex.Message); - throw; - } - await _next(context); - } - } } diff --git a/dotnet/test/DotNetCoreUnitTest/apps/httpcors.cs b/dotnet/test/DotNetCoreUnitTest/apps/httpcors.cs new file mode 100644 index 000000000..ec095594f --- /dev/null +++ b/dotnet/test/DotNetCoreUnitTest/apps/httpcors.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections; +using GeneXus.Utils; +using GeneXus.Resources; +using GeneXus.Application; +using GeneXus.Metadata; +using GeneXus.Cryptography; +using com.genexus; +using GeneXus.Data.ADO; +using GeneXus.Data.NTier; +using GeneXus.Data.NTier.ADO; +using GeneXus.WebControls; +using GeneXus.Http; +using GeneXus.Procedure; +using GeneXus.XML; +using GeneXus.Search; +using GeneXus.Encryption; +using GeneXus.Http.Client; +using GeneXus.Http.Server; +using System.Threading; +using System.Xml.Serialization; +using System.Runtime.Serialization; + +namespace GeneXus.Programs.apps +{ + public class httpcors : GXWebProcedure + { + + public httpcors() + { + context = new GxContext(); + DataStoreUtil.LoadDataStores(context); + IsMain = true; + context.SetDefaultTheme("Carmine"); + } + + public httpcors(IGxContext context) + { + this.context = context; + IsMain = false; + } + + public void execute() + { + initialize(); + executePrivate(); + } + + void executePrivate() + { + GxHttpRequest AV24httpRequest = new GxHttpRequest(context); + GxHttpResponse AV25httpResponse = new GxHttpResponse(context); + string AV26character = AV24httpRequest.GetHeader("deviceid"); + AV25httpResponse.AppendHeader("deviceid", AV26character); + + this.cleanup(); + } + + public override void cleanup() + { + CloseOpenCursors(); + base.cleanup(); + if (IsMain) + { + context.CloseConnections(); + } + ExitApp(); + } + + protected void CloseOpenCursors() + { + } + + public override void initialize() + { + } + } + +} diff --git a/dotnet/test/DotNetCoreUnitTest/apps/httpcors.svc b/dotnet/test/DotNetCoreUnitTest/apps/httpcors.svc new file mode 100644 index 000000000..23d933d0d --- /dev/null +++ b/dotnet/test/DotNetCoreUnitTest/apps/httpcors.svc @@ -0,0 +1 @@ +<%@ServiceHost Service= "GeneXus.Programs.apps.httpcors,apps.httpcors" %> diff --git a/dotnet/test/DotNetCoreUnitTest/appsettings.json b/dotnet/test/DotNetCoreUnitTest/appsettings.json index 6b27b50ce..f95299aff 100644 --- a/dotnet/test/DotNetCoreUnitTest/appsettings.json +++ b/dotnet/test/DotNetCoreUnitTest/appsettings.json @@ -47,7 +47,8 @@ "wcf:serviceHostingEnvironment:useClassicReadEntityBodyMode": "true", "HTTP_PROTOCOL": "Unsecure", "SAMESITE_COOKIE": "Lax", - "CACHE_INVALIDATION_TOKEN": "20216211291931" + "CACHE_INVALIDATION_TOKEN": "20216211291931", + "CORS_ALLOW_ORIGIN": "https://normal-website.com" }, "languages": { "English": {