mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-29 03:22:42 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 953f85ec74 | |||
| 94ed2cb1b7 | |||
| 0e43138324 | |||
| c981c23f7e | |||
| 9f0c5e974e | |||
| 292c470ada | |||
| 12453fdc00 |
@@ -75,6 +75,7 @@ domainhere
|
|||||||
dracula
|
dracula
|
||||||
dronebl
|
dronebl
|
||||||
droneblresponse
|
droneblresponse
|
||||||
|
dropin
|
||||||
duckduckbot
|
duckduckbot
|
||||||
eerror
|
eerror
|
||||||
ellenjoe
|
ellenjoe
|
||||||
@@ -237,6 +238,7 @@ risc
|
|||||||
ruleset
|
ruleset
|
||||||
runlevels
|
runlevels
|
||||||
RUnlock
|
RUnlock
|
||||||
|
runtimedir
|
||||||
sas
|
sas
|
||||||
sasl
|
sasl
|
||||||
Scumm
|
Scumm
|
||||||
@@ -328,5 +330,4 @@ yoursite
|
|||||||
Zenos
|
Zenos
|
||||||
zizmor
|
zizmor
|
||||||
zombocom
|
zombocom
|
||||||
Zonbocom
|
|
||||||
zos
|
zos
|
||||||
|
|||||||
@@ -32,3 +32,7 @@ const APIPrefix = "/.within.website/x/cmd/anubis/api/"
|
|||||||
// DefaultDifficulty is the default "difficulty" (number of leading zeroes)
|
// DefaultDifficulty is the default "difficulty" (number of leading zeroes)
|
||||||
// that must be met by the client in order to pass the challenge.
|
// that must be met by the client in order to pass the challenge.
|
||||||
const DefaultDifficulty = 4
|
const DefaultDifficulty = 4
|
||||||
|
|
||||||
|
// ForcedLanguage is the language being used instead of the one of the request's Accept-Language header
|
||||||
|
// if being set.
|
||||||
|
var ForcedLanguage = ""
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ var (
|
|||||||
cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for")
|
cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for")
|
||||||
cookiePrefix = flag.String("cookie-prefix", "techaro.lol-anubis", "prefix for browser cookies created by Anubis")
|
cookiePrefix = flag.String("cookie-prefix", "techaro.lol-anubis", "prefix for browser cookies created by Anubis")
|
||||||
cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support")
|
cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support")
|
||||||
|
forcedLanguage = flag.String("forced-language", "", "if set, this language is being used instead of the one from the request's Accept-Language header")
|
||||||
hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set")
|
hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set")
|
||||||
|
cookieSecure = flag.Bool("cookie-secure", true, "if true, sets the secure flag on Anubis cookies")
|
||||||
ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned")
|
ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned")
|
||||||
ed25519PrivateKeyHexFile = flag.String("ed25519-private-key-hex-file", "", "file name containing value for ed25519-private-key-hex")
|
ed25519PrivateKeyHexFile = flag.String("ed25519-private-key-hex-file", "", "file name containing value for ed25519-private-key-hex")
|
||||||
metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to")
|
metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to")
|
||||||
@@ -377,6 +379,7 @@ func main() {
|
|||||||
|
|
||||||
anubis.CookieName = *cookiePrefix + "-auth"
|
anubis.CookieName = *cookiePrefix + "-auth"
|
||||||
anubis.TestCookieName = *cookiePrefix + "-cookie-verification"
|
anubis.TestCookieName = *cookiePrefix + "-cookie-verification"
|
||||||
|
anubis.ForcedLanguage = *forcedLanguage
|
||||||
|
|
||||||
// If OpenGraph configuration values are not set in the config file, use the
|
// If OpenGraph configuration values are not set in the config file, use the
|
||||||
// values from flags / envvars.
|
// values from flags / envvars.
|
||||||
@@ -403,6 +406,7 @@ func main() {
|
|||||||
Target: *target,
|
Target: *target,
|
||||||
WebmasterEmail: *webmasterEmail,
|
WebmasterEmail: *webmasterEmail,
|
||||||
OpenGraph: policy.OpenGraph,
|
OpenGraph: policy.OpenGraph,
|
||||||
|
CookieSecure: *cookieSecure,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("can't construct libanubis.Server: %v", err)
|
log.Fatalf("can't construct libanubis.Server: %v", err)
|
||||||
|
|||||||
@@ -11,11 +11,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
<!-- This changes the project to: -->
|
||||||
|
|
||||||
|
- Add `COOKIE_SECURE` option to set the cookie [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies)
|
||||||
|
- Sets cookie defaults to use [SameSite: None](https://web.dev/articles/samesite-cookies-explained)
|
||||||
- Determine the `BIND_NETWORK`/`--bind-network` value from the bind address ([#677](https://github.com/TecharoHQ/anubis/issues/677)).
|
- Determine the `BIND_NETWORK`/`--bind-network` value from the bind address ([#677](https://github.com/TecharoHQ/anubis/issues/677)).
|
||||||
- Implement localization system. Find locale files in lib/localization/locales/.
|
- Implement localization system. Find locale files in lib/localization/locales/.
|
||||||
- Implement a [development container](https://containers.dev/) manifest to make contributions easier.
|
- Implement a [development container](https://containers.dev/) manifest to make contributions easier.
|
||||||
- Fix dynamic cookie domains functionality ([#731](https://github.com/TecharoHQ/anubis/pull/731))
|
- Fix dynamic cookie domains functionality ([#731](https://github.com/TecharoHQ/anubis/pull/731))
|
||||||
- Add option for custom cookie prefix ([#732](https://github.com/TecharoHQ/anubis/pull/732))
|
- Add option for custom cookie prefix ([#732](https://github.com/TecharoHQ/anubis/pull/732))
|
||||||
|
- Remove the "Success" interstitial after a proof of work challenge is concluded.
|
||||||
|
- Add option for forcing a specific language ([#742](https://github.com/TecharoHQ/anubis/pull/742))
|
||||||
|
|
||||||
|
### Potentially breaking changes
|
||||||
|
|
||||||
|
The following potentially breaking change applies to native installs with systemd only:
|
||||||
|
|
||||||
|
Each instance of systemd service template now has a unique `RuntimeDirectory`, as opposed to each instance of the service sharing a `RuntimeDirectory`. This change was made to avoid [the `RuntimeDirectory` getting nuked any time one of the Anubis instances restarts](https://github.com/TecharoHQ/anubis/issues/748).
|
||||||
|
|
||||||
|
If you configured Anubis' unix sockets to listen on `/run/anubis/foo.sock` for instance `anubis@foo`, you will need to configure Anubis to listen on `/run/anubis/foo/sock` and additionally configure your HTTP load balancer as appropriate.
|
||||||
|
|
||||||
|
If you need the legacy behaviour, install this [systemd unit dropin](https://www.flatcar.org/docs/latest/setup/systemd/drop-in-units/):
|
||||||
|
|
||||||
|
```systemd
|
||||||
|
# /etc/systemd/system/anubis@.service.d/50-runtimedir.conf
|
||||||
|
[Service]
|
||||||
|
RuntimeDirectory=anubis
|
||||||
|
```
|
||||||
|
|
||||||
## v1.20.0: Thancred Waters
|
## v1.20.0: Thancred Waters
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ Anubis uses these environment variables for configuration:
|
|||||||
| `COOKIE_DYNAMIC_DOMAIN` | false | If set to true, automatically set cookie domain fields based on the hostname of the request. EG: if you are making a request to `anubis.techaro.lol`, the Anubis cookie will be valid for any subdomain of `techaro.lol`. |
|
| `COOKIE_DYNAMIC_DOMAIN` | false | If set to true, automatically set cookie domain fields based on the hostname of the request. EG: if you are making a request to `anubis.techaro.lol`, the Anubis cookie will be valid for any subdomain of `techaro.lol`. |
|
||||||
| `COOKIE_EXPIRATION_TIME` | `168h` | The amount of time the authorization cookie is valid for. |
|
| `COOKIE_EXPIRATION_TIME` | `168h` | The amount of time the authorization cookie is valid for. |
|
||||||
| `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. |
|
| `COOKIE_PARTITIONED` | `false` | If set to `true`, enables the [partitioned (CHIPS) flag](https://developers.google.com/privacy-sandbox/cookies/chips), meaning that Anubis inside an iframe has a different set of cookies than the domain hosting the iframe. |
|
||||||
|
| `COOKIE_SECURE` | `true` | If set to `true`, enables the [Secure flag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#block_access_to_your_cookies), meaning that the cookies will only be transmitted over HTTPS. If Anubis is used in an unsecure context (plain HTTP), this will be need to be set to false |
|
||||||
| `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
|
| `DIFFICULTY` | `4` | The difficulty of the challenge, or the number of leading zeroes that must be in successful responses. |
|
||||||
| `ED25519_PRIVATE_KEY_HEX` | unset | The hex-encoded ed25519 private key used to sign Anubis responses. If this is not set, Anubis will generate one for you. This should be exactly 64 characters long. See below for details. |
|
| `ED25519_PRIVATE_KEY_HEX` | unset | The hex-encoded ed25519 private key used to sign Anubis responses. If this is not set, Anubis will generate one for you. This should be exactly 64 characters long. See below for details. |
|
||||||
| `ED25519_PRIVATE_KEY_HEX_FILE` | unset | Path to a file containing the hex-encoded ed25519 private key. Only one of this or its sister option may be set. |
|
| `ED25519_PRIVATE_KEY_HEX_FILE` | unset | Path to a file containing the hex-encoded ed25519 private key. Only one of this or its sister option may be set. |
|
||||||
|
|||||||
@@ -262,6 +262,7 @@ func TestCookieSettings(t *testing.T) {
|
|||||||
|
|
||||||
CookieDomain: "127.0.0.1",
|
CookieDomain: "127.0.0.1",
|
||||||
CookiePartitioned: true,
|
CookiePartitioned: true,
|
||||||
|
CookieSecure: true,
|
||||||
CookieExpiration: anubis.CookieDefaultExpirationTime,
|
CookieExpiration: anubis.CookieDefaultExpirationTime,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -309,6 +310,10 @@ func TestCookieSettings(t *testing.T) {
|
|||||||
if ckie.Partitioned != srv.opts.CookiePartitioned {
|
if ckie.Partitioned != srv.opts.CookiePartitioned {
|
||||||
t.Errorf("wanted partitioned flag %v, got: %v", srv.opts.CookiePartitioned, ckie.Partitioned)
|
t.Errorf("wanted partitioned flag %v, got: %v", srv.opts.CookiePartitioned, ckie.Partitioned)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ckie.Secure != srv.opts.CookieSecure {
|
||||||
|
t.Errorf("wanted secure flag %v, got: %v", srv.opts.CookieSecure, ckie.Secure)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) {
|
func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.898
|
// templ: version: v0.3.906
|
||||||
package metarefresh
|
package metarefresh
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ type Options struct {
|
|||||||
StripBasePrefix bool
|
StripBasePrefix bool
|
||||||
OpenGraph config.OpenGraph
|
OpenGraph config.OpenGraph
|
||||||
ServeRobotsTXT bool
|
ServeRobotsTXT bool
|
||||||
|
CookieSecure bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
|
func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int) (*policy.ParsedConfig, error) {
|
||||||
|
|||||||
+14
-6
@@ -27,6 +27,7 @@ type CookieOpts struct {
|
|||||||
Host string
|
Host string
|
||||||
Path string
|
Path string
|
||||||
Name string
|
Name string
|
||||||
|
Expiry time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) SetCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
|
func (s *Server) SetCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
|
||||||
@@ -45,12 +46,17 @@ func (s *Server) SetCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cookieOpts.Expiry == 0 {
|
||||||
|
cookieOpts.Expiry = s.opts.CookieExpiration
|
||||||
|
}
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: name,
|
Name: name,
|
||||||
Value: cookieOpts.Value,
|
Value: cookieOpts.Value,
|
||||||
Expires: time.Now().Add(s.opts.CookieExpiration),
|
Expires: time.Now().Add(cookieOpts.Expiry),
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteNoneMode,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
|
Secure: s.opts.CookieSecure,
|
||||||
Partitioned: s.opts.CookiePartitioned,
|
Partitioned: s.opts.CookiePartitioned,
|
||||||
Path: path,
|
Path: path,
|
||||||
})
|
})
|
||||||
@@ -77,9 +83,10 @@ func (s *Server) ClearCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
|
|||||||
Value: "",
|
Value: "",
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
Expires: time.Now().Add(-1 * time.Minute),
|
Expires: time.Now().Add(-1 * time.Minute),
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteNoneMode,
|
||||||
Partitioned: s.opts.CookiePartitioned,
|
Partitioned: s.opts.CookiePartitioned,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
|
Secure: s.opts.CookieSecure,
|
||||||
Path: path,
|
Path: path,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -132,11 +139,12 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *polic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
s.SetCookie(w, CookieOpts{
|
||||||
Name: anubis.TestCookieName,
|
|
||||||
Value: challengeStr,
|
Value: challengeStr,
|
||||||
Expires: time.Now().Add(30 * time.Minute),
|
Host: r.Host,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
|
Name: anubis.TestCookieName,
|
||||||
|
Expiry: 30 * time.Minute,
|
||||||
})
|
})
|
||||||
|
|
||||||
impl, ok := challenge.Get(rule.Challenge.Algorithm)
|
impl, ok := challenge.Get(rule.Challenge.Algorithm)
|
||||||
|
|||||||
@@ -27,10 +27,10 @@
|
|||||||
"static_check_endpoint": "Este é apenas um ponto de verificação para seu proxy reverso usar.",
|
"static_check_endpoint": "Este é apenas um ponto de verificação para seu proxy reverso usar.",
|
||||||
"authorization_required": "Autorização necessária",
|
"authorization_required": "Autorização necessária",
|
||||||
"cookies_disabled": "Seu navegador está configurado para desabilitar cookies. O Anubis requer cookies para o interesse legítimo de garantir que você seja um cliente válido. Habilite os cookies para este domínio.",
|
"cookies_disabled": "Seu navegador está configurado para desabilitar cookies. O Anubis requer cookies para o interesse legítimo de garantir que você seja um cliente válido. Habilite os cookies para este domínio.",
|
||||||
"access_denied": "Acesso negado: código de errado",
|
"access_denied": "Acesso negado: código de erro",
|
||||||
"dronebl_entry": "DroneBL relatou uma entrada",
|
"dronebl_entry": "DroneBL relatou uma entrada",
|
||||||
"see_dronebl_lookup": "consulte",
|
"see_dronebl_lookup": "consulte",
|
||||||
"internal_server_error": "Erro interno do servidor: o administrador configurou incorretamente o Anubis. Entre em contato com o administrador e peça para ele procurar os logs em torno dele.",
|
"internal_server_error": "Erro interno do servidor: o administrador configurou incorretamente o Anubis. Entre em contato com o administrador e peça para analisar os logs relacionados.",
|
||||||
"invalid_redirect": "Redirecionamento inválido",
|
"invalid_redirect": "Redirecionamento inválido",
|
||||||
"redirect_not_parseable": "URL de redirecionamento não analisável",
|
"redirect_not_parseable": "URL de redirecionamento não analisável",
|
||||||
"redirect_domain_not_allowed": "Domínio de redirecionamento não permitido",
|
"redirect_domain_not_allowed": "Domínio de redirecionamento não permitido",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package localization
|
|||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"github.com/TecharoHQ/anubis"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -93,8 +94,13 @@ func (sl *SimpleLocalizer) T(messageID string) string {
|
|||||||
return sl.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID})
|
return sl.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLocalizer creates a localizer based on the request's Accept-Language header
|
// GetLocalizer creates a localizer based on the request's Accept-Language header or forcedLanguage option
|
||||||
func GetLocalizer(r *http.Request) *SimpleLocalizer {
|
func GetLocalizer(r *http.Request) *SimpleLocalizer {
|
||||||
localizer := NewLocalizationService().GetLocalizerFromRequest(r)
|
var localizer *i18n.Localizer
|
||||||
|
if anubis.ForcedLanguage == "" {
|
||||||
|
localizer = NewLocalizationService().GetLocalizerFromRequest(r)
|
||||||
|
} else {
|
||||||
|
localizer = NewLocalizationService().GetLocalizer(anubis.ForcedLanguage)
|
||||||
|
}
|
||||||
return &SimpleLocalizer{Localizer: localizer}
|
return &SimpleLocalizer{Localizer: localizer}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ CacheDirectory=anubis/%i
|
|||||||
CacheDirectoryMode=0755
|
CacheDirectoryMode=0755
|
||||||
StateDirectory=anubis/%i
|
StateDirectory=anubis/%i
|
||||||
StateDirectoryMode=0755
|
StateDirectoryMode=0755
|
||||||
RuntimeDirectory=anubis
|
RuntimeDirectory=anubis/%i
|
||||||
RuntimeDirectoryMode=0755
|
RuntimeDirectoryMode=0755
|
||||||
ReadWritePaths=/run
|
ReadWritePaths=/run
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.898
|
// templ: version: v0.3.906
|
||||||
package web
|
package web
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|||||||
@@ -212,11 +212,6 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
|
|||||||
const t1 = Date.now();
|
const t1 = Date.now();
|
||||||
console.log({ hash, nonce });
|
console.log({ hash, nonce });
|
||||||
|
|
||||||
title.innerHTML = t('success');
|
|
||||||
status.innerHTML = `${t('done_took')} ${t1 - t0}ms, ${nonce} ${t('iterations')}`;
|
|
||||||
image.src = imageURL("happy", anubisVersion, basePrefix);
|
|
||||||
progress.style.display = "none";
|
|
||||||
|
|
||||||
if (userReadDetails) {
|
if (userReadDetails) {
|
||||||
const container = document.getElementById("progress");
|
const container = document.getElementById("progress");
|
||||||
|
|
||||||
@@ -251,7 +246,6 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
|
|||||||
container.onclick = onDetailsExpand;
|
container.onclick = onDetailsExpand;
|
||||||
setTimeout(onDetailsExpand, 30000);
|
setTimeout(onDetailsExpand, 30000);
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => {
|
|
||||||
const redir = window.location.href;
|
const redir = window.location.href;
|
||||||
window.location.replace(
|
window.location.replace(
|
||||||
u(`${basePrefix}/.within.website/x/cmd/anubis/api/pass-challenge`, {
|
u(`${basePrefix}/.within.website/x/cmd/anubis/api/pass-challenge`, {
|
||||||
@@ -261,7 +255,6 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
|
|||||||
elapsedTime: t1 - t0,
|
elapsedTime: t1 - t0,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}, 250);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ohNoes({
|
ohNoes({
|
||||||
|
|||||||
Reference in New Issue
Block a user