Improve the concept to access a blob sources

This commit is contained in:
Namhyeon Go 2025-05-10 16:07:36 +09:00
parent 14c8b12df7
commit a001471451
6 changed files with 273 additions and 193 deletions

View File

@ -60,15 +60,6 @@ namespace WelsonJS.Launcher.Properties {
} }
} }
/// <summary>
/// https://ajax.aspnetcdn.com/과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string AspNetCdnPrefix {
get {
return ResourceManager.GetString("AspNetCdnPrefix", resourceCulture);
}
}
/// <summary> /// <summary>
/// 과(와) 유사한 지역화된 문자열을 찾습니다. /// 과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary> /// </summary>
@ -96,6 +87,15 @@ namespace WelsonJS.Launcher.Properties {
} }
} }
/// <summary>
/// https://catswords.blob.core.windows.net/welsonjs/blob.config.xml과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string BlobConfigUrl {
get {
return ResourceManager.GetString("BlobConfigUrl", resourceCulture);
}
}
/// <summary> /// <summary>
/// https://catswords.blob.core.windows.net/welsonjs/과(와) 유사한 지역화된 문자열을 찾습니다. /// https://catswords.blob.core.windows.net/welsonjs/과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary> /// </summary>
@ -105,15 +105,6 @@ namespace WelsonJS.Launcher.Properties {
} }
} }
/// <summary>
/// https://cdnjs.cloudflare.com/과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string CdnJsPrefix {
get {
return ResourceManager.GetString("CdnJsPrefix", resourceCulture);
}
}
/// <summary> /// <summary>
/// https://copilot.microsoft.com/과(와) 유사한 지역화된 문자열을 찾습니다. /// https://copilot.microsoft.com/과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary> /// </summary>
@ -141,24 +132,6 @@ namespace WelsonJS.Launcher.Properties {
} }
} }
/// <summary>
/// https://esm.run/과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string EsmRunPrefix {
get {
return ResourceManager.GetString("EsmRunPrefix", resourceCulture);
}
}
/// <summary>
/// https://esm.sh/과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string EsmShPrefix {
get {
return ResourceManager.GetString("EsmShPrefix", resourceCulture);
}
}
/// <summary> /// <summary>
/// (아이콘)과(와) 유사한 System.Drawing.Icon 형식의 지역화된 리소스를 찾습니다. /// (아이콘)과(와) 유사한 System.Drawing.Icon 형식의 지역화된 리소스를 찾습니다.
/// </summary> /// </summary>
@ -170,11 +143,11 @@ namespace WelsonJS.Launcher.Properties {
} }
/// <summary> /// <summary>
/// https://ajax.googleapis.com/과(와) 유사한 지역화된 문자열을 찾습니다. /// 90과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary> /// </summary>
internal static string GoogleApisPrefix { internal static string HttpClientTimeout {
get { get {
return ResourceManager.GetString("GoogleApisPrefix", resourceCulture); return ResourceManager.GetString("HttpClientTimeout", resourceCulture);
} }
} }
@ -268,42 +241,6 @@ namespace WelsonJS.Launcher.Properties {
} }
} }
/// <summary>
/// https://code.jquery.com/과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string JqueryCdnPrefix {
get {
return ResourceManager.GetString("JqueryCdnPrefix", resourceCulture);
}
}
/// <summary>
/// https://cdn.jsdelivr.net/과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string JsDeliverPrefix {
get {
return ResourceManager.GetString("JsDeliverPrefix", resourceCulture);
}
}
/// <summary>
/// https://polyfill-fastly.io/과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string PolyfillPrefix {
get {
return ResourceManager.GetString("PolyfillPrefix", resourceCulture);
}
}
/// <summary>
/// https://raw.githubusercontent.com/과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string RawGitHubPrefix {
get {
return ResourceManager.GetString("RawGitHubPrefix", resourceCulture);
}
}
/// <summary> /// <summary>
/// https://github.com/gnh1201/welsonjs과(와) 유사한 지역화된 문자열을 찾습니다. /// https://github.com/gnh1201/welsonjs과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary> /// </summary>
@ -322,24 +259,6 @@ namespace WelsonJS.Launcher.Properties {
} }
} }
/// <summary>
/// https://www.skypack.dev/과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string SkypackPrefix {
get {
return ResourceManager.GetString("SkypackPrefix", resourceCulture);
}
}
/// <summary>
/// https://unpkg.com/과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string UnpkgPrefix {
get {
return ResourceManager.GetString("UnpkgPrefix", resourceCulture);
}
}
/// <summary> /// <summary>
/// 141.101.82.1과(와) 유사한 지역화된 문자열을 찾습니다. /// 141.101.82.1과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary> /// </summary>

View File

@ -184,37 +184,10 @@
<data name="AzureAiServiceApiVersion" xml:space="preserve"> <data name="AzureAiServiceApiVersion" xml:space="preserve">
<value>2024-05-01-preview</value> <value>2024-05-01-preview</value>
</data> </data>
<data name="CdnJsPrefix" xml:space="preserve"> <data name="BlobConfigUrl" xml:space="preserve">
<value>https://cdnjs.cloudflare.com/</value> <value>https://catswords.blob.core.windows.net/welsonjs/blob.config.xml</value>
</data> </data>
<data name="EsmRunPrefix" xml:space="preserve"> <data name="HttpClientTimeout" xml:space="preserve">
<value>https://esm.run/</value> <value>90</value>
</data>
<data name="EsmShPrefix" xml:space="preserve">
<value>https://esm.sh/</value>
</data>
<data name="JqueryCdnPrefix" xml:space="preserve">
<value>https://code.jquery.com/</value>
</data>
<data name="JsDeliverPrefix" xml:space="preserve">
<value>https://cdn.jsdelivr.net/</value>
</data>
<data name="SkypackPrefix" xml:space="preserve">
<value>https://www.skypack.dev/</value>
</data>
<data name="UnpkgPrefix" xml:space="preserve">
<value>https://unpkg.com/</value>
</data>
<data name="AspNetCdnPrefix" xml:space="preserve">
<value>https://ajax.aspnetcdn.com/</value>
</data>
<data name="GoogleApisPrefix" xml:space="preserve">
<value>https://ajax.googleapis.com/</value>
</data>
<data name="PolyfillPrefix" xml:space="preserve">
<value>https://polyfill-fastly.io/</value>
</data>
<data name="RawGitHubPrefix" xml:space="preserve">
<value>https://raw.githubusercontent.com/</value>
</data> </data>
</root> </root>

View File

@ -17,6 +17,7 @@ using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using System.Xml.Serialization;
namespace WelsonJS.Launcher namespace WelsonJS.Launcher
{ {
@ -29,26 +30,19 @@ namespace WelsonJS.Launcher
private string _prefix; private string _prefix;
private string _resourceName; private string _resourceName;
private List<IResourceTool> _tools = new List<IResourceTool>(); private List<IResourceTool> _tools = new List<IResourceTool>();
private readonly HttpClient _httpClient = new HttpClient(); private static readonly HttpClient _httpClient = new HttpClient();
private static readonly string _defaultMimeType = "application/octet-stream"; private static readonly string _defaultMimeType = "application/octet-stream";
private static readonly Regex _nodePackageRegex = new Regex(@"^[^/@]+@[^/]+/", RegexOptions.Compiled); private static BlobConfig _blobConfig;
private static readonly List<string[]> CDN_PREFIXES = new List<string[]> {
new[] { "ajax/libs/" }, static ResourceServer()
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
{ {
Cloudflare = 0, // Set timeout
JsDeliver = 1, int timeout = int.TryParse(Program.GetAppConfig("HttpClientTimeout"), out timeout) ? timeout : 90;
Jquery = 2, _httpClient.Timeout = TimeSpan.FromSeconds(timeout);
Polyfill = 3,
Microsoft = 4, // Fetch a blob config from Internet
GitHub = 5 FetchBlobConfig();
}; }
public ResourceServer(string prefix, string resourceName) public ResourceServer(string prefix, string resourceName)
{ {
@ -56,7 +50,6 @@ namespace WelsonJS.Launcher
_listener = new HttpListener(); _listener = new HttpListener();
_listener.Prefixes.Add(prefix); _listener.Prefixes.Add(prefix);
_resourceName = resourceName; _resourceName = resourceName;
_httpClient.Timeout = TimeSpan.FromSeconds(30);
// Add resource tools // Add resource tools
_tools.Add(new ResourceTools.Completion(this, _httpClient)); _tools.Add(new ResourceTools.Completion(this, _httpClient));
@ -207,8 +200,8 @@ namespace WelsonJS.Launcher
} }
} }
// use CDN sources // use a blob source
if (await TryServeFromCdn(context, path)) if (await TryServeFromBlob(context, path))
{ {
return true; return true;
} }
@ -217,47 +210,32 @@ namespace WelsonJS.Launcher
return false; return false;
} }
private async Task<bool> TryServeFromCdn(HttpListenerContext context, string path) private async Task<bool> TryServeFromBlob(HttpListenerContext context, string path)
{ {
bool isNodePackageExpression = _nodePackageRegex.IsMatch(path); if (_blobConfig != null)
bool isPrefixMatched(CDN_TYPES type)
{ {
if (CDN_PREFIXES[(int)type].Any(prefix => path.StartsWith(prefix))) foreach (var route in _blobConfig.Routes)
{ {
return true; foreach (var (regex, index) in route.RegexConditions.Select((r, i) => (r, i)))
} {
if (!regex.Compiled.IsMatch(path)) continue;
return false; var match = (index < route.Matches.Count) ? route.Matches[index] : route.Matches.First();
} var _path = route.StripPrefix ? path.Substring(match.Length) : path;
var sources = new (bool isMatch, string configKey, Func<string, string> transform)[] foreach (var prefixUrl in route.PrefixUrls)
{
(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))
{ {
if (await ServeBlob(context, _path, prefixUrl))
return true; return true;
} }
} }
} }
}
// fallback
string prefix = Program.GetAppConfig("BlobStoragePrefix");
if (await ServeBlob(context, path, prefix))
return true;
return false; return false;
} }
@ -360,7 +338,8 @@ namespace WelsonJS.Launcher
{ {
string xmlHeader = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"; string xmlHeader = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
if (data == null) { if (data == null)
{
data = Encoding.UTF8.GetBytes(xmlHeader + "\r\n<error>Could not find the resource.</error>"); data = Encoding.UTF8.GetBytes(xmlHeader + "\r\n<error>Could not find the resource.</error>");
mimeType = "application/xml"; mimeType = "application/xml";
statusCode = 404; statusCode = 404;
@ -442,5 +421,80 @@ namespace WelsonJS.Launcher
return memoryStream.ToArray(); 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<BlobRoute> Routes { get; set; } = new List<BlobRoute>();
public void Compile()
{
foreach (var route in Routes)
{
if (route.Matches == null) continue;
route.RegexConditions = new List<RegexCondition>();
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<string> Matches { get; set; }
[XmlArray("prefixUrls")]
[XmlArrayItem("url")]
public List<string> PrefixUrls { get; set; }
[XmlAttribute("stripPrefix")]
public bool StripPrefix { get; set; }
[XmlIgnore]
public List<RegexCondition> RegexConditions { get; set; }
}
public class RegexCondition
{
[XmlIgnore]
public string Pattern { get; set; }
[XmlIgnore]
public Regex Compiled { get; set; }
} }
} }

View File

@ -13,16 +13,8 @@
<add key="WhoisClientAddress" value="141.101.82.1"/> <add key="WhoisClientAddress" value="141.101.82.1"/>
<add key="DnsServerAddress" value="1.1.1.1"/> <add key="DnsServerAddress" value="1.1.1.1"/>
<add key="BlobStoragePrefix" value="https://catswords.blob.core.windows.net/welsonjs/"/> <add key="BlobStoragePrefix" value="https://catswords.blob.core.windows.net/welsonjs/"/>
<add key="CdnJsPrefix" value="https://cdnjs.cloudflare.com/"/> <add key="BlobConfigUrl" value="https://catswords.blob.core.windows.net/welsonjs/blob.config.xml"/>
<add key="JsDeliverPrefix" value="https://cdn.jsdelivr.net/"/> <add key="HttpClientTimeout" value="90"/>
<add key="UnpkgPrefix" value="https://unpkg.com/"/>
<add key="SkypackPrefix" value="https://www.skypack.dev/"/>
<add key="EsmShPrefix" value="https://esm.sh/"/>
<add key="EsmRunPrefix" value="https://esm.run/"/>
<add key="JqueryCdnPrefix" value="https://code.jquery.com/"/>
<add key="AspNetCdnPrefix" value="https://ajax.aspnetcdn.com/"/>
<add key="GoogleApisPrefix" value="https://ajax.googleapis.com/"/>
<add key="RawGitHubPrefix" value="https://raw.githubusercontent.com/"/>
</appSettings> </appSettings>
<startup> <startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/>

74
data/blob.config.json Normal file
View File

@ -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
}
]
}

68
data/blob.config.xml Normal file
View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<blobConfig>
<routes>
<route stripPrefix="false">
<matches>
<match>ajax/libs/</match>
</matches>
<prefixUrls>
<url>https://cdnjs.cloudflare.com/</url>
<url>https://ajax.googleapis.com/</url>
</prefixUrls>
</route>
<route stripPrefix="false">
<matches>
<match>npm/</match>
<match>gh/</match>
<match>wp/</match>
</matches>
<prefixUrls>
<url>https://cdn.jsdelivr.net/</url>
</prefixUrls>
</route>
<route stripPrefix="false">
<matches>
<match>^[^/@]+@[^/]+/</match>
</matches>
<prefixUrls>
<url>https://unpkg.com/</url>
<url>https://www.skypack.dev/</url>
<url>https://esm.sh/</url>
<url>https://esm.run/</url>
</prefixUrls>
</route>
<route stripPrefix="true">
<matches>
<match>jquery/</match>
</matches>
<prefixUrls>
<url>https://code.jquery.com/</url>
</prefixUrls>
</route>
<route stripPrefix="true">
<matches>
<match>polyfill/</match>
</matches>
<prefixUrls>
<url>https://cdnjs.cloudflare.com/</url>
<url>https://polyfill-fastly.io/</url>
</prefixUrls>
</route>
<route stripPrefix="true">
<matches>
<match>ajax/</match>
</matches>
<prefixUrls>
<url>https://ajax.aspnetcdn.com/</url>
</prefixUrls>
</route>
<route stripPrefix="true">
<matches>
<match>raw/gh/</match>
</matches>
<prefixUrls>
<url>https://raw.githubusercontent.com/</url>
</prefixUrls>
</route>
</routes>
</blobConfig>