diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs index 51db986..9a9e2a2 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs @@ -331,6 +331,15 @@ namespace WelsonJS.Launcher.Properties { } } + /// + /// 과(와) 유사한 지역화된 문자열을 찾습니다. + /// + internal static string ResourceServerAllowOrigins { + get { + return ResourceManager.GetString("ResourceServerAllowOrigins", resourceCulture); + } + } + /// /// true과(와) 유사한 지역화된 문자열을 찾습니다. /// diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx index b733285..c682ff9 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx @@ -220,4 +220,7 @@ https://api.abuseipdb.com/api/v2/ + + + \ No newline at end of file diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs index e9830b0..29d5fb1 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs @@ -34,12 +34,16 @@ namespace WelsonJS.Launcher private static readonly HttpClient _httpClient = new HttpClient(); private static readonly string _defaultMimeType = "application/octet-stream"; + private static string[] _allowedOrigins; static ResourceServer() { // Set timeout int timeout = int.TryParse(Program.GetAppConfig("HttpClientTimeout"), out timeout) ? timeout : 90; _httpClient.Timeout = TimeSpan.FromSeconds(timeout); + + // Set allowed origins (CORS policy) + TryParseAllowedOrigins(); } public ResourceServer(string prefix, string resourceName, ICompatibleLogger logger = null) @@ -353,6 +357,11 @@ namespace WelsonJS.Launcher public async Task ServeResource(HttpListenerContext context, byte[] data, string mimeType = "text/html", int statusCode = 200) { + if (HandleCorsPreflight(context)) + return; + + TryApplyCors(context); + string xmlHeader = ""; if (data == null) @@ -461,6 +470,92 @@ namespace WelsonJS.Launcher _logger?.Error($"Failed to fetch a blob config. Exception: {ex}"); } } + + private static void TryParseAllowedOrigins(ICompatibleLogger logger = null) + { + var raw = Program.GetAppConfig("ResourceServerAllowOrigins"); + + if (!string.IsNullOrEmpty(raw)) + { + _allowedOrigins = raw.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .Where(s => !string.IsNullOrEmpty(s)) + .ToArray(); + return; + } + + var prefix = Program.GetAppConfig("ResourceServerPrefix"); + if (!string.IsNullOrEmpty(prefix)) + { + try + { + var uri = new Uri(prefix); + _allowedOrigins = new[] { uri.GetLeftPart(UriPartial.Authority) }; // protocol + host + port + return; + } + catch (Exception ex) + { + logger?.Warn($"Invalid ResourceServerPrefix '{prefix}'. It must be a valid absolute URI. Error: {ex.Message}"); + // fall through to set empty + } + } + + _allowedOrigins = Array.Empty(); + } + + private static bool TryApplyCors(HttpListenerContext context) + { + var origin = context.Request.Headers["Origin"]; + if (string.IsNullOrEmpty(origin)) + return false; + + var allowed = _allowedOrigins; + if (allowed.Length == 0) + return false; + + var respHeaders = context.Response.Headers; + + if (allowed.Any(a => a == "*")) + { + respHeaders["Access-Control-Allow-Origin"] = "*"; + respHeaders["Vary"] = "Origin"; + return true; + } + + // only perform a single, case-sensitive origin check + if (allowed.Contains(origin, StringComparer.Ordinal)) + { + respHeaders["Access-Control-Allow-Origin"] = origin; + respHeaders["Access-Control-Allow-Credentials"] = "true"; + respHeaders["Vary"] = "Origin"; + return true; + } + + return false; + } + + private static bool HandleCorsPreflight(HttpListenerContext context) + { + if (!string.Equals(context.Request.HttpMethod, "OPTIONS", StringComparison.OrdinalIgnoreCase)) + return false; + + if (string.IsNullOrEmpty(context.Request.Headers["Origin"])) + return false; + + // Apply CORS headers once here + TryApplyCors(context); + + var respHeaders = context.Response.Headers; + respHeaders["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"; + respHeaders["Access-Control-Allow-Headers"] = "Content-Type, Authorization, X-Requested-With"; + respHeaders["Access-Control-Max-Age"] = "600"; + + context.Response.StatusCode = 204; + context.Response.ContentLength64 = 0; + context.Response.Close(); + + return true; + } } [XmlRoot("blobConfig")] diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config index 7902a42..b74b7fe 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config @@ -1,8 +1,9 @@ - + +