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>
@ -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>
/// https://catswords.blob.core.windows.net/welsonjs/과(와) 유사한 지역화된 문자열을 찾습니다.
/// </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>
/// https://copilot.microsoft.com/과(와) 유사한 지역화된 문자열을 찾습니다.
/// </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>
/// (아이콘)과(와) 유사한 System.Drawing.Icon 형식의 지역화된 리소스를 찾습니다.
/// </summary>
@ -170,11 +143,11 @@ namespace WelsonJS.Launcher.Properties {
}
/// <summary>
/// https://ajax.googleapis.com/과(와) 유사한 지역화된 문자열을 찾습니다.
/// 90과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
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 {
}
}
/// <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>
/// https://github.com/gnh1201/welsonjs과(와) 유사한 지역화된 문자열을 찾습니다.
/// </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>
/// 141.101.82.1과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>

View File

@ -184,37 +184,10 @@
<data name="AzureAiServiceApiVersion" xml:space="preserve">
<value>2024-05-01-preview</value>
</data>
<data name="CdnJsPrefix" xml:space="preserve">
<value>https://cdnjs.cloudflare.com/</value>
<data name="BlobConfigUrl" xml:space="preserve">
<value>https://catswords.blob.core.windows.net/welsonjs/blob.config.xml</value>
</data>
<data name="EsmRunPrefix" xml:space="preserve">
<value>https://esm.run/</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 name="HttpClientTimeout" xml:space="preserve">
<value>90</value>
</data>
</root>

View File

@ -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<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 Regex _nodePackageRegex = new Regex(@"^[^/@]+@[^/]+/", RegexOptions.Compiled);
private static readonly List<string[]> CDN_PREFIXES = new List<string[]> {
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));
@ -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,47 +210,32 @@ namespace WelsonJS.Launcher
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);
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;
}
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)[]
{
(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 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 = "<?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>");
mimeType = "application/xml";
statusCode = 404;
@ -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<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="DnsServerAddress" value="1.1.1.1"/>
<add key="BlobStoragePrefix" value="https://catswords.blob.core.windows.net/welsonjs/"/>
<add key="CdnJsPrefix" value="https://cdnjs.cloudflare.com/"/>
<add key="JsDeliverPrefix" value="https://cdn.jsdelivr.net/"/>
<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/"/>
<add key="BlobConfigUrl" value="https://catswords.blob.core.windows.net/welsonjs/blob.config.xml"/>
<add key="HttpClientTimeout" value="90"/>
</appSettings>
<startup>
<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>