diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs index e40782d..66bbb56 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs @@ -60,15 +60,6 @@ namespace WelsonJS.Launcher.Properties { } } - /// - /// https://ajax.aspnetcdn.com/과(와) 유사한 지역화된 문자열을 찾습니다. - /// - internal static string AspNetCdnPrefix { - get { - return ResourceManager.GetString("AspNetCdnPrefix", resourceCulture); - } - } - /// /// 과(와) 유사한 지역화된 문자열을 찾습니다. /// @@ -96,6 +87,15 @@ namespace WelsonJS.Launcher.Properties { } } + /// + /// https://catswords.blob.core.windows.net/welsonjs/blob.config.xml과(와) 유사한 지역화된 문자열을 찾습니다. + /// + internal static string BlobConfigUrl { + get { + return ResourceManager.GetString("BlobConfigUrl", resourceCulture); + } + } + /// /// https://catswords.blob.core.windows.net/welsonjs/과(와) 유사한 지역화된 문자열을 찾습니다. /// @@ -105,15 +105,6 @@ namespace WelsonJS.Launcher.Properties { } } - /// - /// https://cdnjs.cloudflare.com/과(와) 유사한 지역화된 문자열을 찾습니다. - /// - internal static string CdnJsPrefix { - get { - return ResourceManager.GetString("CdnJsPrefix", resourceCulture); - } - } - /// /// https://copilot.microsoft.com/과(와) 유사한 지역화된 문자열을 찾습니다. /// @@ -141,24 +132,6 @@ namespace WelsonJS.Launcher.Properties { } } - /// - /// https://esm.run/과(와) 유사한 지역화된 문자열을 찾습니다. - /// - internal static string EsmRunPrefix { - get { - return ResourceManager.GetString("EsmRunPrefix", resourceCulture); - } - } - - /// - /// https://esm.sh/과(와) 유사한 지역화된 문자열을 찾습니다. - /// - internal static string EsmShPrefix { - get { - return ResourceManager.GetString("EsmShPrefix", resourceCulture); - } - } - /// /// (아이콘)과(와) 유사한 System.Drawing.Icon 형식의 지역화된 리소스를 찾습니다. /// @@ -170,11 +143,11 @@ namespace WelsonJS.Launcher.Properties { } /// - /// https://ajax.googleapis.com/과(와) 유사한 지역화된 문자열을 찾습니다. + /// 90과(와) 유사한 지역화된 문자열을 찾습니다. /// - internal static string GoogleApisPrefix { + internal static string HttpClientTimeout { get { - return ResourceManager.GetString("GoogleApisPrefix", resourceCulture); + return ResourceManager.GetString("HttpClientTimeout", resourceCulture); } } @@ -268,42 +241,6 @@ namespace WelsonJS.Launcher.Properties { } } - /// - /// https://code.jquery.com/과(와) 유사한 지역화된 문자열을 찾습니다. - /// - internal static string JqueryCdnPrefix { - get { - return ResourceManager.GetString("JqueryCdnPrefix", resourceCulture); - } - } - - /// - /// https://cdn.jsdelivr.net/과(와) 유사한 지역화된 문자열을 찾습니다. - /// - internal static string JsDeliverPrefix { - get { - return ResourceManager.GetString("JsDeliverPrefix", resourceCulture); - } - } - - /// - /// https://polyfill-fastly.io/과(와) 유사한 지역화된 문자열을 찾습니다. - /// - internal static string PolyfillPrefix { - get { - return ResourceManager.GetString("PolyfillPrefix", resourceCulture); - } - } - - /// - /// https://raw.githubusercontent.com/과(와) 유사한 지역화된 문자열을 찾습니다. - /// - internal static string RawGitHubPrefix { - get { - return ResourceManager.GetString("RawGitHubPrefix", resourceCulture); - } - } - /// /// https://github.com/gnh1201/welsonjs과(와) 유사한 지역화된 문자열을 찾습니다. /// @@ -322,24 +259,6 @@ namespace WelsonJS.Launcher.Properties { } } - /// - /// https://www.skypack.dev/과(와) 유사한 지역화된 문자열을 찾습니다. - /// - internal static string SkypackPrefix { - get { - return ResourceManager.GetString("SkypackPrefix", resourceCulture); - } - } - - /// - /// https://unpkg.com/과(와) 유사한 지역화된 문자열을 찾습니다. - /// - internal static string UnpkgPrefix { - get { - return ResourceManager.GetString("UnpkgPrefix", resourceCulture); - } - } - /// /// 141.101.82.1과(와) 유사한 지역화된 문자열을 찾습니다. /// diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx index 5e041d3..ed4a71f 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx @@ -184,37 +184,10 @@ 2024-05-01-preview - - https://cdnjs.cloudflare.com/ + + https://catswords.blob.core.windows.net/welsonjs/blob.config.xml - - https://esm.run/ - - - https://esm.sh/ - - - https://code.jquery.com/ - - - https://cdn.jsdelivr.net/ - - - https://www.skypack.dev/ - - - https://unpkg.com/ - - - https://ajax.aspnetcdn.com/ - - - https://ajax.googleapis.com/ - - - https://polyfill-fastly.io/ - - - https://raw.githubusercontent.com/ + + 90 \ No newline at end of file diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs index caf1f72..d972332 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceServer.cs @@ -17,6 +17,7 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; +using System.Xml.Serialization; namespace WelsonJS.Launcher { @@ -29,26 +30,19 @@ namespace WelsonJS.Launcher private string _prefix; private string _resourceName; private List _tools = new List(); - private readonly HttpClient _httpClient = new HttpClient(); + private static readonly HttpClient _httpClient = new HttpClient(); private static readonly string _defaultMimeType = "application/octet-stream"; - private static readonly Regex _nodePackageRegex = new Regex(@"^[^/@]+@[^/]+/", RegexOptions.Compiled); - private static readonly List CDN_PREFIXES = new List { - new[] { "ajax/libs/" }, - new[] { "npm/", "gh/", "wp/" }, - new[] { "jquery/" }, - new[] { "polyfill/" }, - new[] { "ajax/" }, // https://learn.microsoft.com/en-us/aspnet/ajax/cdn/overview - new[] { "raw/gh/"} - }; - private enum CDN_TYPES: int + private static BlobConfig _blobConfig; + + static ResourceServer() { - Cloudflare = 0, - JsDeliver = 1, - Jquery = 2, - Polyfill = 3, - Microsoft = 4, - GitHub = 5 - }; + // Set timeout + int timeout = int.TryParse(Program.GetAppConfig("HttpClientTimeout"), out timeout) ? timeout : 90; + _httpClient.Timeout = TimeSpan.FromSeconds(timeout); + + // Fetch a blob config from Internet + FetchBlobConfig(); + } public ResourceServer(string prefix, string resourceName) { @@ -56,7 +50,6 @@ namespace WelsonJS.Launcher _listener = new HttpListener(); _listener.Prefixes.Add(prefix); _resourceName = resourceName; - _httpClient.Timeout = TimeSpan.FromSeconds(30); // Add resource tools _tools.Add(new ResourceTools.Completion(this, _httpClient)); @@ -100,7 +93,7 @@ namespace WelsonJS.Launcher { return _isRunning; } - + private async Task ListenLoop(CancellationToken token) { while (!token.IsCancellationRequested && _isRunning) @@ -207,8 +200,8 @@ namespace WelsonJS.Launcher } } - // use CDN sources - if (await TryServeFromCdn(context, path)) + // use a blob source + if (await TryServeFromBlob(context, path)) { return true; } @@ -217,48 +210,33 @@ namespace WelsonJS.Launcher return false; } - private async Task TryServeFromCdn(HttpListenerContext context, string path) + private async Task TryServeFromBlob(HttpListenerContext context, string path) { - bool isNodePackageExpression = _nodePackageRegex.IsMatch(path); - bool isPrefixMatched(CDN_TYPES type) + if (_blobConfig != null) { - if (CDN_PREFIXES[(int)type].Any(prefix => path.StartsWith(prefix))) + foreach (var route in _blobConfig.Routes) { - return true; - } - - return false; - } - - var sources = new (bool isMatch, string configKey, Func transform)[] - { - (isPrefixMatched(CDN_TYPES.Cloudflare), "CdnJsPrefix", p => p), // Libraries from Cloudflare - (isPrefixMatched(CDN_TYPES.Cloudflare), "GoogleApisPrefix", p => p), // Libraries from Google - (isNodePackageExpression, "UnpkgPrefix", p => p), - (isNodePackageExpression, "SkypackPrefix", p => p), - (isNodePackageExpression, "EsmShPrefix", p => p), - (isNodePackageExpression, "EsmRunPrefix", p => p), - (isPrefixMatched(CDN_TYPES.JsDeliver), "JsDeliverPrefix", p => p), - (isPrefixMatched(CDN_TYPES.Jquery), "JqueryCdnPrefix", p => p.Substring("jquery/".Length)), - (isPrefixMatched(CDN_TYPES.Polyfill), "CdnJsPrefix", p => p), // polyfill.js from Cloudflare - (isPrefixMatched(CDN_TYPES.Polyfill), "PolyfillPrefix", p => p.Substring("polyfill/".Length)), // polyfill.js from Fastly - (isPrefixMatched(CDN_TYPES.Microsoft), "AspNetCdnPrefix", p => p), // Libraries from Microsoft - (isPrefixMatched(CDN_TYPES.GitHub), "RawGitHubPrefix", p => p.Substring("raw/gh/".Length)), - (true, "BlobStoragePrefix", p => p) // fallback - }; - - foreach (var (isMatch, configKey, transform) in sources) - { - if (isMatch) - { - string prefix = Program.GetAppConfig(configKey); - if (await ServeBlob(context, transform(path), prefix)) + foreach (var (regex, index) in route.RegexConditions.Select((r, i) => (r, i))) { - return true; + if (!regex.Compiled.IsMatch(path)) continue; + + var match = (index < route.Matches.Count) ? route.Matches[index] : route.Matches.First(); + var _path = route.StripPrefix ? path.Substring(match.Length) : path; + + foreach (var prefixUrl in route.PrefixUrls) + { + if (await ServeBlob(context, _path, prefixUrl)) + return true; + } } } } + // fallback + string prefix = Program.GetAppConfig("BlobStoragePrefix"); + if (await ServeBlob(context, path, prefix)) + return true; + return false; } @@ -360,7 +338,8 @@ namespace WelsonJS.Launcher { string xmlHeader = ""; - if (data == null) { + if (data == null) + { data = Encoding.UTF8.GetBytes(xmlHeader + "\r\nCould not find the resource."); mimeType = "application/xml"; statusCode = 404; @@ -375,7 +354,7 @@ namespace WelsonJS.Launcher } } - public void ServeResource(HttpListenerContext context, string data, string mimeType = "text/html", int statusCode = 200) + public void ServeResource(HttpListenerContext context, string data, string mimeType = "text/html", int statusCode = 200) { string xmlHeader = ""; @@ -442,5 +421,80 @@ namespace WelsonJS.Launcher return memoryStream.ToArray(); } } + + private static async void FetchBlobConfig() + { + try + { + string url = Program.GetAppConfig("BlobConfigUrl"); + var response = await _httpClient.GetStreamAsync(url); + + var serializer = new XmlSerializer(typeof(BlobConfig)); + using (var reader = new StreamReader(response)) + { + _blobConfig = (BlobConfig)serializer.Deserialize(reader); + } + + _blobConfig?.Compile(); + } + catch (Exception ex) + { + Trace.TraceError($"Failed to fetch a blob config. Exception: {ex.Message}"); + } + } } -} + + [XmlRoot("blobConfig")] + public class BlobConfig + { + [XmlArray("routes")] + [XmlArrayItem("route")] + public List Routes { get; set; } = new List(); + + public void Compile() + { + foreach (var route in Routes) + { + if (route.Matches == null) continue; + + route.RegexConditions = new List(); + foreach (var match in route.Matches) + { + route.RegexConditions.Add(new RegexCondition + { + Pattern = match, + Compiled = new Regex( + match.StartsWith("^") ? match : "^" + Regex.Escape(match), + RegexOptions.Compiled) + }); + } + } + } + } + + public class BlobRoute + { + [XmlArray("matches")] + [XmlArrayItem("match")] + public List Matches { get; set; } + + [XmlArray("prefixUrls")] + [XmlArrayItem("url")] + public List PrefixUrls { get; set; } + + [XmlAttribute("stripPrefix")] + public bool StripPrefix { get; set; } + + [XmlIgnore] + public List RegexConditions { get; set; } + } + + public class RegexCondition + { + [XmlIgnore] + public string Pattern { get; set; } + + [XmlIgnore] + public Regex Compiled { get; set; } + } +} \ No newline at end of file diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config index 4bd67b0..8c641cf 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config @@ -13,16 +13,8 @@ - - - - - - - - - - + + diff --git a/data/blob.config.json b/data/blob.config.json new file mode 100644 index 0000000..d3c2589 --- /dev/null +++ b/data/blob.config.json @@ -0,0 +1,74 @@ +{ + "routes": [ + { + "matches": [ + "ajax/libs/" + ], + "prefixUrls": [ + "https://cdnjs.cloudflare.com/", + "https://ajax.googleapis.com/" + ], + "stripPrefix": false + }, + { + "matches": [ + "npm/", + "gh/", + "wp/" + ], + "prefixUrls": [ + "https://cdn.jsdelivr.net/" + ], + "stripPrefix": false + }, + { + "matches": [ + "^[^/@]+@[^/]+/" + ], + "prefixUrls": [ + "https://unpkg.com/", + "https://www.skypack.dev/", + "https://esm.sh/", + "https://esm.run/" + ], + "stripPrefix": false + }, + { + "matches": [ + "jquery/" + ], + "prefixUrls": [ + "https://code.jquery.com/" + ], + "stripPrefix": true + }, + { + "matches": [ + "polyfill/" + ], + "prefixUrls": [ + "https://cdnjs.cloudflare.com/", + "https://polyfill-fastly.io/" + ], + "stripPrefix": true + }, + { + "matches": [ + "ajax/" + ], + "prefixUrls": [ + "https://ajax.aspnetcdn.com/" + ], + "stripPrefix": true + }, + { + "matches": [ + "raw/gh/" + ], + "prefixUrls": [ + "https://raw.githubusercontent.com/" + ], + "stripPrefix": true + } + ] +} \ No newline at end of file diff --git a/data/blob.config.xml b/data/blob.config.xml new file mode 100644 index 0000000..3166933 --- /dev/null +++ b/data/blob.config.xml @@ -0,0 +1,68 @@ + + + + + + ajax/libs/ + + + https://cdnjs.cloudflare.com/ + https://ajax.googleapis.com/ + + + + + npm/ + gh/ + wp/ + + + https://cdn.jsdelivr.net/ + + + + + ^[^/@]+@[^/]+/ + + + https://unpkg.com/ + https://www.skypack.dev/ + https://esm.sh/ + https://esm.run/ + + + + + jquery/ + + + https://code.jquery.com/ + + + + + polyfill/ + + + https://cdnjs.cloudflare.com/ + https://polyfill-fastly.io/ + + + + + ajax/ + + + https://ajax.aspnetcdn.com/ + + + + + raw/gh/ + + + https://raw.githubusercontent.com/ + + + +