From b9e39dd9c7dd1b7d82c6b145684bee5fb123f135 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Fri, 3 Oct 2025 17:35:34 +0900 Subject: [PATCH 1/8] Add CORS support to ResourceServer Implemented CORS handling in ResourceServer, including preflight (OPTIONS) request handling and configurable allowed origins via the new ResourceServerAllowOrigins app setting. Updated resources and configuration files to support the new setting. --- .../Properties/Resources.Designer.cs | 9 ++ .../Properties/Resources.resx | 3 + .../WelsonJS.Launcher/ResourceServer.cs | 97 +++++++++++++++++++ WelsonJS.Toolkit/WelsonJS.Launcher/app.config | 3 +- 4 files changed, 111 insertions(+), 1 deletion(-) 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..32fa56f 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs @@ -353,6 +353,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 +466,98 @@ namespace WelsonJS.Launcher _logger?.Error($"Failed to fetch a blob config. Exception: {ex}"); } } + + + private static string[] GetAllowedOrigins() + { + // 1. Try explicit ResourceServerAllowOrigins config + var raw = Program.GetAppConfig("ResourceServerAllowOrigins"); + + if (string.IsNullOrEmpty(raw)) + { + // 2. Fallback: parse from ResourceServerPrefix + var prefix = Program.GetAppConfig("ResourceServerPrefix"); + if (!string.IsNullOrEmpty(prefix)) + { + try + { + var uri = new Uri(prefix); + var origin = uri.GetLeftPart(UriPartial.Authority); // protocol + host + port + return new[] { origin }; + } + catch + { + return Array.Empty(); + } + } + return Array.Empty(); + } + + // Split configured list + var parts = raw.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .Where(s => !string.IsNullOrEmpty(s)) + .ToArray(); + return parts; + } + + private static bool TryApplyCors(HttpListenerContext context) + { + var origin = context.Request.Headers["Origin"]; + if (string.IsNullOrEmpty(origin)) + return false; + + var allowed = GetAllowedOrigins(); + if (allowed.Length == 0) + return false; + + var respHeaders = context.Response.Headers; + respHeaders["Vary"] = "Origin"; + + if (allowed.Any(a => a == "*")) + { + respHeaders["Access-Control-Allow-Origin"] = "*"; + return true; + } + + if (allowed.Contains(origin, StringComparer.OrdinalIgnoreCase)) + { + respHeaders["Access-Control-Allow-Origin"] = origin; + respHeaders["Access-Control-Allow-Credentials"] = "true"; + 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 requestHeaders = context.Request.Headers["Access-Control-Request-Headers"]; + var requestMethod = context.Request.Headers["Access-Control-Request-Method"]; + + var h = context.Response.Headers; + h["Access-Control-Allow-Methods"] = string.IsNullOrEmpty(requestMethod) + ? "GET, POST, PUT, DELETE, OPTIONS" + : requestMethod; + h["Access-Control-Allow-Headers"] = string.IsNullOrEmpty(requestHeaders) + ? "Content-Type, Authorization, X-Requested-With" + : requestHeaders; + h["Access-Control-Max-Age"] = "600"; + + context.Response.StatusCode = 204; + context.Response.ContentLength64 = 0; + context.Response.OutputStream.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 @@ - + + From a745b6d0a7adbae4e0f6b61d0c896057455cc68a Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Fri, 3 Oct 2025 17:47:01 +0900 Subject: [PATCH 2/8] Update WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs Co-authored-by: qodo-merge-pro[bot] <151058649+qodo-merge-pro[bot]@users.noreply.github.com> --- WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs index 32fa56f..38d3ca1 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs @@ -545,12 +545,8 @@ namespace WelsonJS.Launcher var requestMethod = context.Request.Headers["Access-Control-Request-Method"]; var h = context.Response.Headers; - h["Access-Control-Allow-Methods"] = string.IsNullOrEmpty(requestMethod) - ? "GET, POST, PUT, DELETE, OPTIONS" - : requestMethod; - h["Access-Control-Allow-Headers"] = string.IsNullOrEmpty(requestHeaders) - ? "Content-Type, Authorization, X-Requested-With" - : requestHeaders; + h["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"; + h["Access-Control-Allow-Headers"] = "Content-Type, Authorization, X-Requested-With"; h["Access-Control-Max-Age"] = "600"; context.Response.StatusCode = 204; From 8ab70208d0aec972101da8d5f24111ff4e644138 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Fri, 3 Oct 2025 17:48:25 +0900 Subject: [PATCH 3/8] Update WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs Co-authored-by: qodo-merge-pro[bot] <151058649+qodo-merge-pro[bot]@users.noreply.github.com> --- WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs index 38d3ca1..214675d 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs @@ -512,11 +512,11 @@ namespace WelsonJS.Launcher return false; var respHeaders = context.Response.Headers; - respHeaders["Vary"] = "Origin"; if (allowed.Any(a => a == "*")) { respHeaders["Access-Control-Allow-Origin"] = "*"; + respHeaders["Vary"] = "Origin"; return true; } @@ -524,6 +524,7 @@ namespace WelsonJS.Launcher { respHeaders["Access-Control-Allow-Origin"] = origin; respHeaders["Access-Control-Allow-Credentials"] = "true"; + respHeaders["Vary"] = "Origin"; return true; } From 6e499bcedf8a670efc5aa7a4f8ca43f269c9c316 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Fri, 3 Oct 2025 17:49:10 +0900 Subject: [PATCH 4/8] Update WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs index 214675d..0fc4e24 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs @@ -521,10 +521,10 @@ namespace WelsonJS.Launcher } if (allowed.Contains(origin, StringComparer.OrdinalIgnoreCase)) + if (allowed.Contains(origin, StringComparer.Ordinal)) { respHeaders["Access-Control-Allow-Origin"] = origin; respHeaders["Access-Control-Allow-Credentials"] = "true"; - respHeaders["Vary"] = "Origin"; return true; } From 49263bb0ac7344dfe5e2d070960c963d645ea8e6 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sat, 4 Oct 2025 18:09:03 +0900 Subject: [PATCH 5/8] Refactor CORS allowed origins initialization Replaces GetAllowedOrigins with TryParseAllowedOrigins to initialize allowed origins once during static construction. Adds logging for invalid ResourceServerPrefix values and stores allowed origins in a static field for improved efficiency. --- .../WelsonJS.Launcher/ResourceServer.cs | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs index 32fa56f..87d5ea1 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) @@ -467,38 +471,36 @@ namespace WelsonJS.Launcher } } - - private static string[] GetAllowedOrigins() + private static void TryParseAllowedOrigins(ICompatibleLogger logger = null) { - // 1. Try explicit ResourceServerAllowOrigins config var raw = Program.GetAppConfig("ResourceServerAllowOrigins"); - if (string.IsNullOrEmpty(raw)) + if (!string.IsNullOrEmpty(raw)) { - // 2. Fallback: parse from ResourceServerPrefix - var prefix = Program.GetAppConfig("ResourceServerPrefix"); - if (!string.IsNullOrEmpty(prefix)) - { - try - { - var uri = new Uri(prefix); - var origin = uri.GetLeftPart(UriPartial.Authority); // protocol + host + port - return new[] { origin }; - } - catch - { - return Array.Empty(); - } - } - return Array.Empty(); + _allowedOrigins = raw.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .Where(s => !string.IsNullOrEmpty(s)) + .ToArray(); + return; } - // Split configured list - var parts = raw.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(s => s.Trim()) - .Where(s => !string.IsNullOrEmpty(s)) - .ToArray(); - return parts; + 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) @@ -507,7 +509,7 @@ namespace WelsonJS.Launcher if (string.IsNullOrEmpty(origin)) return false; - var allowed = GetAllowedOrigins(); + var allowed = _allowedOrigins; if (allowed.Length == 0) return false; From 33d8fadc8fed710d2d0c7db87d5262137cd2cf18 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sat, 4 Oct 2025 19:39:58 +0900 Subject: [PATCH 6/8] Update WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs index 6da51ac..9119b30 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs @@ -522,13 +522,18 @@ namespace WelsonJS.Launcher return true; } - if (allowed.Contains(origin, StringComparer.OrdinalIgnoreCase)) + // 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"; return true; } + { + respHeaders["Access-Control-Allow-Origin"] = origin; + respHeaders["Access-Control-Allow-Credentials"] = "true"; + return true; + } return false; } From b32801c1c9170c1b34a082532224ab874a3e6c83 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Thu, 9 Oct 2025 21:14:41 +0900 Subject: [PATCH 7/8] clean up the code clean up the code --- .../WelsonJS.Launcher/ResourceServer.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs index 9119b30..6ceec16 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs @@ -529,11 +529,6 @@ namespace WelsonJS.Launcher respHeaders["Access-Control-Allow-Credentials"] = "true"; return true; } - { - respHeaders["Access-Control-Allow-Origin"] = origin; - respHeaders["Access-Control-Allow-Credentials"] = "true"; - return true; - } return false; } @@ -549,17 +544,15 @@ namespace WelsonJS.Launcher // Apply CORS headers once here TryApplyCors(context); - var requestHeaders = context.Request.Headers["Access-Control-Request-Headers"]; - var requestMethod = context.Request.Headers["Access-Control-Request-Method"]; - - var h = context.Response.Headers; - h["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"; - h["Access-Control-Allow-Headers"] = "Content-Type, Authorization, X-Requested-With"; - h["Access-Control-Max-Age"] = "600"; + 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.OutputStream.Close(); + return true; } } From ebe7b605cb3bff38f56694557521a27329f6e094 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Thu, 9 Oct 2025 22:04:27 +0900 Subject: [PATCH 8/8] Add Vary: Origin header and use Response.Close Sets the 'Vary' header to 'Origin' for CORS responses to improve cache behavior. Replaces OutputStream.Close with Response.Close for proper response handling. --- WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs index 6ceec16..29d5fb1 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs @@ -527,6 +527,7 @@ namespace WelsonJS.Launcher { respHeaders["Access-Control-Allow-Origin"] = origin; respHeaders["Access-Control-Allow-Credentials"] = "true"; + respHeaders["Vary"] = "Origin"; return true; } @@ -551,7 +552,7 @@ namespace WelsonJS.Launcher context.Response.StatusCode = 204; context.Response.ContentLength64 = 0; - context.Response.OutputStream.Close(); + context.Response.Close(); return true; }