forked from ebkr/r2modmanPlus
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request ebkr#1130 from ebkr/support-alternative-cdn
Support alternative Thunderstore CDN
- Loading branch information
Showing
8 changed files
with
185 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import R2Error from '../../../model/errors/R2Error'; | ||
import { getAxiosWithTimeouts } from '../../../utils/HttpUtils'; | ||
import { addOrReplaceSearchParams, replaceHost } from '../../../utils/UrlUtils'; | ||
|
||
const CDNS = [ | ||
"gcdn.thunderstore.io", | ||
"hcdn-1.hcdn.thunderstore.io" | ||
] | ||
const TEST_FILE = "healthz"; | ||
|
||
const CONNECTION_ERROR = new R2Error( | ||
"Can't reach content delivery networks", | ||
`All Thunderstore CDNs seem to be currently unreachable from | ||
this computer. You can still use the mod manager, but | ||
downloading mods will not work.`, | ||
`Test another internet connection, if available. For example | ||
using a VPN or connecting to a mobile hotspot might solve the | ||
issue.` | ||
); | ||
|
||
export default class CdnProvider { | ||
private static axios = getAxiosWithTimeouts(5000, 5000); | ||
private static preferredCdn = ""; | ||
|
||
public static async checkCdnConnection() { | ||
const headers = { | ||
"Cache-Control": "no-cache", | ||
"Pragma": "no-cache", | ||
"Expires": "0", | ||
}; | ||
const params = {"disableCache": new Date().getTime()}; | ||
let res; | ||
|
||
for await (const cdn of CDNS) { | ||
const url = `https://${cdn}/${TEST_FILE}`; | ||
|
||
try { | ||
res = await CdnProvider.axios.get(url, {headers, params}); | ||
} catch (e) { | ||
continue; | ||
} | ||
|
||
if (res.status === 200) { | ||
CdnProvider.preferredCdn = cdn; | ||
return; | ||
} | ||
}; | ||
|
||
throw CONNECTION_ERROR; | ||
} | ||
|
||
public static replaceCdnHost(url: string) { | ||
return CdnProvider.preferredCdn | ||
? replaceHost(url, CdnProvider.preferredCdn) | ||
: url; | ||
} | ||
|
||
public static addCdnQueryParameter(url: string) { | ||
return CdnProvider.preferredCdn | ||
? addOrReplaceSearchParams(url, `cdn=${CdnProvider.preferredCdn}`) | ||
: url; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import axios from "axios"; | ||
|
||
const newAbortSignal = (timeoutMs: number) => { | ||
const abortController = new AbortController(); | ||
setTimeout(() => abortController.abort(), timeoutMs); | ||
return abortController.signal; | ||
}; | ||
|
||
/** | ||
* Return Axios instance with timeouts enabled. | ||
* @param responseTimeout Time (in ms) the server has to generate a | ||
* response once a connection is established. Defaults to 5 seconds. | ||
* @param connectionTimeout Time (in ms) the request has in total, | ||
* including opening the connection and receiving the response. | ||
* Defaults to 10 seconds. | ||
* @returns AxiosInstance | ||
*/ | ||
export const getAxiosWithTimeouts = (responseTimeout = 5000, connectionTimeout = 10000) => { | ||
const instance = axios.create({timeout: responseTimeout}); | ||
|
||
// Use interceptors to have a fresh abort signal for each request, | ||
// so the instance can be shared by multiple requests. | ||
instance.interceptors.request.use((config) => { | ||
config.signal = newAbortSignal(connectionTimeout); | ||
return config; | ||
}); | ||
|
||
return instance; | ||
}; | ||
|
||
export const isNetworkError = (responseOrError: unknown) => | ||
responseOrError instanceof Error && responseOrError.message === "Network Error"; | ||
|
||
/** | ||
* Is the Error thrown by Axios request caused by a response timeout? | ||
*/ | ||
export const isResponseTimeout = (error: unknown) => | ||
error instanceof Error && /timeout of (\d+)ms exceeded/i.test(error.message); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/** | ||
* Append given search parameters to an URL which may or may not already | ||
* have search parameters. | ||
* | ||
* Existing search parameters with a key present in the new parameters | ||
* will be overwritten. | ||
*/ | ||
export const addOrReplaceSearchParams = (url: string, paramString: string) => { | ||
const newUrl = new URL(url); | ||
newUrl.search = new URLSearchParams( | ||
Object.assign( | ||
{}, | ||
Object.fromEntries(newUrl.searchParams), | ||
Object.fromEntries(new URLSearchParams(paramString)) | ||
) | ||
).toString(); | ||
return newUrl.href; | ||
} | ||
|
||
/** | ||
* Replace URL host, i.e. the domain and the port number. | ||
* | ||
* @param url e.g. "https://thunderstore.io/foo" | ||
* @param domainAndPort e.g. "thunderstore.dev" or "thunderstore.dev:8080" | ||
* @returns e.g. "https://thunderstore.dev:8080/foo" | ||
*/ | ||
export const replaceHost = (url: string, domainAndPort: string) => { | ||
const newValues = domainAndPort.split(":"); | ||
const newUrl = new URL(url); | ||
newUrl.hostname = newValues[0]; | ||
newUrl.port = newValues[1] || ""; | ||
return newUrl.href; | ||
}; |