mirror of
https://github.com/wahyd4/loginsrv.git
synced 2026-08-09 04:46:29 +10:00
rename redirect config parameters
allow-redirects -> redirect redirect-query-parameter -> redirect-query-parameter check-referer-on-redirects -> redirect-check-referer whitelist-domains-file -> redirect-host-file
This commit is contained in:
+2
-2
@@ -43,8 +43,8 @@ func setup(c *caddy.Controller) error {
|
||||
config.Template = filepath.Join(httpserver.GetConfig(c).Root, config.Template)
|
||||
}
|
||||
|
||||
if config.WhitelistDomainsFile != "" && !filepath.IsAbs(config.WhitelistDomainsFile) {
|
||||
config.WhitelistDomainsFile = filepath.Join(httpserver.GetConfig(c).Root, config.WhitelistDomainsFile)
|
||||
if config.RedirectHostFile != "" && !filepath.IsAbs(config.RedirectHostFile) {
|
||||
config.RedirectHostFile = filepath.Join(httpserver.GetConfig(c).Root, config.RedirectHostFile)
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
|
||||
+53
-53
@@ -28,15 +28,15 @@ func TestSetup(t *testing.T) {
|
||||
}`,
|
||||
shouldErr: false,
|
||||
config: login.Config{
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 24 * time.Hour,
|
||||
SuccessURL: "/",
|
||||
AllowRedirects: true,
|
||||
RedirectQueryParameter: "backTo",
|
||||
CheckRefererOnRedirects: true,
|
||||
LoginPath: "/login",
|
||||
CookieName: "jwt_token",
|
||||
CookieHTTPOnly: true,
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 24 * time.Hour,
|
||||
SuccessURL: "/",
|
||||
Redirect: true,
|
||||
RedirectQueryParameter: "backTo",
|
||||
RedirectCheckReferer: true,
|
||||
LoginPath: "/login",
|
||||
CookieName: "jwt_token",
|
||||
CookieHTTPOnly: true,
|
||||
Backends: login.Options{
|
||||
"simple": map[string]string{
|
||||
"bob": "secret",
|
||||
@@ -50,10 +50,10 @@ func TestSetup(t *testing.T) {
|
||||
success_url successurl
|
||||
jwt_expiry 42h
|
||||
login_path /foo/bar
|
||||
allow_redirects true
|
||||
redirect true
|
||||
redirect_query_parameter comingFrom
|
||||
check_referer_on_redirects true
|
||||
whitelist_domains_file domainWhitelist.txt
|
||||
redirect_check_referer true
|
||||
redirect_host_file domainWhitelist.txt
|
||||
cookie_name cookiename
|
||||
cookie_http_only false
|
||||
cookie_domain example.com
|
||||
@@ -63,18 +63,18 @@ func TestSetup(t *testing.T) {
|
||||
}`,
|
||||
shouldErr: false,
|
||||
config: login.Config{
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 42 * time.Hour,
|
||||
SuccessURL: "successurl",
|
||||
AllowRedirects: true,
|
||||
RedirectQueryParameter: "comingFrom",
|
||||
CheckRefererOnRedirects: true,
|
||||
WhitelistDomainsFile: "domainWhitelist.txt",
|
||||
LoginPath: "/foo/bar",
|
||||
CookieName: "cookiename",
|
||||
CookieDomain: "example.com",
|
||||
CookieExpiry: 23*time.Hour + 23*time.Minute,
|
||||
CookieHTTPOnly: false,
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 42 * time.Hour,
|
||||
SuccessURL: "successurl",
|
||||
Redirect: true,
|
||||
RedirectQueryParameter: "comingFrom",
|
||||
RedirectCheckReferer: true,
|
||||
RedirectHostFile: "domainWhitelist.txt",
|
||||
LoginPath: "/foo/bar",
|
||||
CookieName: "cookiename",
|
||||
CookieDomain: "example.com",
|
||||
CookieExpiry: 23*time.Hour + 23*time.Minute,
|
||||
CookieHTTPOnly: false,
|
||||
Backends: login.Options{
|
||||
"simple": map[string]string{
|
||||
"bob": "secret",
|
||||
@@ -98,15 +98,15 @@ func TestSetup(t *testing.T) {
|
||||
}`,
|
||||
shouldErr: false,
|
||||
config: login.Config{
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 24 * time.Hour,
|
||||
SuccessURL: "/",
|
||||
AllowRedirects: true,
|
||||
RedirectQueryParameter: "backTo",
|
||||
CheckRefererOnRedirects: true,
|
||||
LoginPath: "/context/login",
|
||||
CookieName: "cookiename",
|
||||
CookieHTTPOnly: true,
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 24 * time.Hour,
|
||||
SuccessURL: "/",
|
||||
Redirect: true,
|
||||
RedirectQueryParameter: "backTo",
|
||||
RedirectCheckReferer: true,
|
||||
LoginPath: "/context/login",
|
||||
CookieName: "cookiename",
|
||||
CookieHTTPOnly: true,
|
||||
Backends: login.Options{
|
||||
"simple": map[string]string{
|
||||
"bob": "secret",
|
||||
@@ -125,15 +125,15 @@ func TestSetup(t *testing.T) {
|
||||
}`,
|
||||
shouldErr: false,
|
||||
config: login.Config{
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 24 * time.Hour,
|
||||
SuccessURL: "/",
|
||||
AllowRedirects: true,
|
||||
RedirectQueryParameter: "backTo",
|
||||
CheckRefererOnRedirects: true,
|
||||
LoginPath: "/login",
|
||||
CookieName: "cookiename",
|
||||
CookieHTTPOnly: true,
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 24 * time.Hour,
|
||||
SuccessURL: "/",
|
||||
Redirect: true,
|
||||
RedirectQueryParameter: "backTo",
|
||||
RedirectCheckReferer: true,
|
||||
LoginPath: "/login",
|
||||
CookieName: "cookiename",
|
||||
CookieHTTPOnly: true,
|
||||
Backends: login.Options{
|
||||
"simple": map[string]string{
|
||||
"bob": "secret",
|
||||
@@ -150,15 +150,15 @@ func TestSetup(t *testing.T) {
|
||||
}`,
|
||||
shouldErr: false,
|
||||
config: login.Config{
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 24 * time.Hour,
|
||||
SuccessURL: "/",
|
||||
AllowRedirects: true,
|
||||
RedirectQueryParameter: "backTo",
|
||||
CheckRefererOnRedirects: true,
|
||||
LoginPath: "/login",
|
||||
CookieName: "jwt_token",
|
||||
CookieHTTPOnly: true,
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 24 * time.Hour,
|
||||
SuccessURL: "/",
|
||||
Redirect: true,
|
||||
RedirectQueryParameter: "backTo",
|
||||
RedirectCheckReferer: true,
|
||||
LoginPath: "/login",
|
||||
CookieName: "jwt_token",
|
||||
CookieHTTPOnly: true,
|
||||
Backends: login.Options{
|
||||
"simple": map[string]string{
|
||||
"bob": "secret",
|
||||
@@ -197,7 +197,7 @@ func TestSetup(t *testing.T) {
|
||||
func TestSetup_RelativeFiles(t *testing.T) {
|
||||
caddyfile := `loginsrv {
|
||||
template myTemplate.tpl
|
||||
whitelist_domains_file redirectDomains.txt
|
||||
redirect_host_file redirectDomains.txt
|
||||
simple bob=secret
|
||||
}`
|
||||
root, _ := ioutil.TempDir("", "")
|
||||
@@ -216,5 +216,5 @@ func TestSetup_RelativeFiles(t *testing.T) {
|
||||
middleware := mids[len(mids)-1](nil).(*CaddyHandler)
|
||||
|
||||
Equal(t, root+"/myTemplate.tpl", middleware.config.Template)
|
||||
Equal(t, root+"/redirectDomains.txt", middleware.config.WhitelistDomainsFile)
|
||||
Equal(t, root+"/redirectDomains.txt", middleware.config.RedirectHostFile)
|
||||
}
|
||||
|
||||
+44
-44
@@ -23,24 +23,24 @@ func init() {
|
||||
// DefaultConfig for the loginsrv handler
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Host: "localhost",
|
||||
Port: "6789",
|
||||
LogLevel: "info",
|
||||
JwtSecret: jwtDefaultSecret,
|
||||
JwtExpiry: 24 * time.Hour,
|
||||
JwtRefreshes: 0,
|
||||
SuccessURL: "/",
|
||||
AllowRedirects: true,
|
||||
RedirectQueryParameter: "backTo",
|
||||
CheckRefererOnRedirects: true,
|
||||
WhitelistDomainsFile: "",
|
||||
LogoutURL: "",
|
||||
LoginPath: "/login",
|
||||
CookieName: "jwt_token",
|
||||
CookieHTTPOnly: true,
|
||||
Backends: Options{},
|
||||
Oauth: Options{},
|
||||
GracePeriod: 5 * time.Second,
|
||||
Host: "localhost",
|
||||
Port: "6789",
|
||||
LogLevel: "info",
|
||||
JwtSecret: jwtDefaultSecret,
|
||||
JwtExpiry: 24 * time.Hour,
|
||||
JwtRefreshes: 0,
|
||||
SuccessURL: "/",
|
||||
Redirect: true,
|
||||
RedirectQueryParameter: "backTo",
|
||||
RedirectCheckReferer: true,
|
||||
RedirectHostFile: "",
|
||||
LogoutURL: "",
|
||||
LoginPath: "/login",
|
||||
CookieName: "jwt_token",
|
||||
CookieHTTPOnly: true,
|
||||
Backends: Options{},
|
||||
Oauth: Options{},
|
||||
GracePeriod: 5 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,28 +48,28 @@ const envPrefix = "LOGINSRV_"
|
||||
|
||||
// Config for the loginsrv handler
|
||||
type Config struct {
|
||||
Host string
|
||||
Port string
|
||||
LogLevel string
|
||||
TextLogging bool
|
||||
JwtSecret string
|
||||
JwtExpiry time.Duration
|
||||
JwtRefreshes int
|
||||
SuccessURL string
|
||||
AllowRedirects bool
|
||||
RedirectQueryParameter string
|
||||
CheckRefererOnRedirects bool
|
||||
WhitelistDomainsFile string
|
||||
LogoutURL string
|
||||
Template string
|
||||
LoginPath string
|
||||
CookieName string
|
||||
CookieExpiry time.Duration
|
||||
CookieDomain string
|
||||
CookieHTTPOnly bool
|
||||
Backends Options
|
||||
Oauth Options
|
||||
GracePeriod time.Duration
|
||||
Host string
|
||||
Port string
|
||||
LogLevel string
|
||||
TextLogging bool
|
||||
JwtSecret string
|
||||
JwtExpiry time.Duration
|
||||
JwtRefreshes int
|
||||
SuccessURL string
|
||||
Redirect bool
|
||||
RedirectQueryParameter string
|
||||
RedirectCheckReferer bool
|
||||
RedirectHostFile string
|
||||
LogoutURL string
|
||||
Template string
|
||||
LoginPath string
|
||||
CookieName string
|
||||
CookieExpiry time.Duration
|
||||
CookieDomain string
|
||||
CookieHTTPOnly bool
|
||||
Backends Options
|
||||
Oauth Options
|
||||
GracePeriod time.Duration
|
||||
}
|
||||
|
||||
// Options is the configuration structure for oauth and backend provider
|
||||
@@ -112,10 +112,10 @@ func (c *Config) ConfigureFlagSet(f *flag.FlagSet) {
|
||||
f.DurationVar(&c.CookieExpiry, "cookie-expiry", c.CookieExpiry, "The expiry duration for the cookie, e.g. 2h or 3h30m. Default is browser session")
|
||||
f.StringVar(&c.CookieDomain, "cookie-domain", c.CookieDomain, "The optional domain parameter for the cookie")
|
||||
f.StringVar(&c.SuccessURL, "success-url", c.SuccessURL, "The url to redirect after login")
|
||||
f.BoolVar(&c.AllowRedirects, "allow-redirects", c.AllowRedirects, "Allow dynamic redirects by parameter")
|
||||
f.StringVar(&c.RedirectQueryParameter, "redirect-query-parameter", c.RedirectQueryParameter, "Allow dynamic redirects by parameter")
|
||||
f.BoolVar(&c.CheckRefererOnRedirects, "check-referer-on-redirects", c.CheckRefererOnRedirects, "When redirecting check that the referer is the same domain")
|
||||
f.StringVar(&c.WhitelistDomainsFile, "whitelist-domains-file", c.WhitelistDomainsFile, "A file containing a list of domains that redirects are allowed to, one domain per line")
|
||||
f.BoolVar(&c.Redirect, "redirect", c.Redirect, "Allow dynamic overwriting of the the success by query parameter")
|
||||
f.StringVar(&c.RedirectQueryParameter, "redirect-query-parameter", c.RedirectQueryParameter, "URL parameter for the redirect target")
|
||||
f.BoolVar(&c.RedirectCheckReferer, "redirect-check-referer", c.RedirectCheckReferer, "When redirecting check that the referer is the same domain")
|
||||
f.StringVar(&c.RedirectHostFile, "redirect-host-file", c.RedirectHostFile, "A file containing a list of domains that redirects are allowed to, one domain per line")
|
||||
|
||||
f.StringVar(&c.LogoutURL, "logout-url", c.LogoutURL, "The url or path to redirect after logout")
|
||||
f.StringVar(&c.Template, "template", c.Template, "An alternative template for the login form")
|
||||
|
||||
+42
-42
@@ -29,10 +29,10 @@ func TestConfig_ReadConfig(t *testing.T) {
|
||||
"--jwt-secret=jwtsecret",
|
||||
"--jwt-expiry=42h42m",
|
||||
"--success-url=successurl",
|
||||
"--allow-redirects=false",
|
||||
"--redirect=false",
|
||||
"--redirect-query-parameter=comingFrom",
|
||||
"--check-referer-on-redirects=false",
|
||||
"--whitelist-domains-file=File",
|
||||
"--redirect-check-referer=false",
|
||||
"--redirect-host-file=File",
|
||||
"--logout-url=logouturl",
|
||||
"--template=template",
|
||||
"--login-path=loginpath",
|
||||
@@ -47,24 +47,24 @@ func TestConfig_ReadConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
expected := &Config{
|
||||
Host: "host",
|
||||
Port: "port",
|
||||
LogLevel: "loglevel",
|
||||
TextLogging: true,
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 42*time.Hour + 42*time.Minute,
|
||||
SuccessURL: "successurl",
|
||||
AllowRedirects: false,
|
||||
RedirectQueryParameter: "comingFrom",
|
||||
CheckRefererOnRedirects: false,
|
||||
WhitelistDomainsFile: "File",
|
||||
LogoutURL: "logouturl",
|
||||
Template: "template",
|
||||
LoginPath: "loginpath",
|
||||
CookieName: "cookiename",
|
||||
CookieExpiry: 23 * time.Minute,
|
||||
CookieDomain: "*.example.com",
|
||||
CookieHTTPOnly: false,
|
||||
Host: "host",
|
||||
Port: "port",
|
||||
LogLevel: "loglevel",
|
||||
TextLogging: true,
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 42*time.Hour + 42*time.Minute,
|
||||
SuccessURL: "successurl",
|
||||
Redirect: false,
|
||||
RedirectQueryParameter: "comingFrom",
|
||||
RedirectCheckReferer: false,
|
||||
RedirectHostFile: "File",
|
||||
LogoutURL: "logouturl",
|
||||
Template: "template",
|
||||
LoginPath: "loginpath",
|
||||
CookieName: "cookiename",
|
||||
CookieExpiry: 23 * time.Minute,
|
||||
CookieDomain: "*.example.com",
|
||||
CookieHTTPOnly: false,
|
||||
Backends: Options{
|
||||
"simple": map[string]string{},
|
||||
"foo": map[string]string{},
|
||||
@@ -91,10 +91,10 @@ func TestConfig_ReadConfigFromEnv(t *testing.T) {
|
||||
NoError(t, os.Setenv("LOGINSRV_JWT_SECRET", "jwtsecret"))
|
||||
NoError(t, os.Setenv("LOGINSRV_JWT_EXPIRY", "42h42m"))
|
||||
NoError(t, os.Setenv("LOGINSRV_SUCCESS_URL", "successurl"))
|
||||
NoError(t, os.Setenv("LOGINSRV_ALLOW_REDIRECTS", "false"))
|
||||
NoError(t, os.Setenv("LOGINSRV_REDIRECT", "false"))
|
||||
NoError(t, os.Setenv("LOGINSRV_REDIRECT_QUERY_PARAMETER", "comingFrom"))
|
||||
NoError(t, os.Setenv("LOGINSRV_CHECK_REFERER_ON_REDIRECTS", "false"))
|
||||
NoError(t, os.Setenv("LOGINSRV_WHITELIST_DOMAINS_FILE", "File"))
|
||||
NoError(t, os.Setenv("LOGINSRV_REDIRECT_CHECK_REFERER", "false"))
|
||||
NoError(t, os.Setenv("LOGINSRV_REDIRECT_HOST_FILE", "File"))
|
||||
NoError(t, os.Setenv("LOGINSRV_LOGOUT_URL", "logouturl"))
|
||||
NoError(t, os.Setenv("LOGINSRV_TEMPLATE", "template"))
|
||||
NoError(t, os.Setenv("LOGINSRV_LOGIN_PATH", "loginpath"))
|
||||
@@ -107,24 +107,24 @@ func TestConfig_ReadConfigFromEnv(t *testing.T) {
|
||||
NoError(t, os.Setenv("LOGINSRV_GRACE_PERIOD", "4s"))
|
||||
|
||||
expected := &Config{
|
||||
Host: "host",
|
||||
Port: "port",
|
||||
LogLevel: "loglevel",
|
||||
TextLogging: true,
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 42*time.Hour + 42*time.Minute,
|
||||
SuccessURL: "successurl",
|
||||
AllowRedirects: false,
|
||||
RedirectQueryParameter: "comingFrom",
|
||||
CheckRefererOnRedirects: false,
|
||||
WhitelistDomainsFile: "File",
|
||||
LogoutURL: "logouturl",
|
||||
Template: "template",
|
||||
LoginPath: "loginpath",
|
||||
CookieName: "cookiename",
|
||||
CookieExpiry: 23 * time.Minute,
|
||||
CookieDomain: "*.example.com",
|
||||
CookieHTTPOnly: false,
|
||||
Host: "host",
|
||||
Port: "port",
|
||||
LogLevel: "loglevel",
|
||||
TextLogging: true,
|
||||
JwtSecret: "jwtsecret",
|
||||
JwtExpiry: 42*time.Hour + 42*time.Minute,
|
||||
SuccessURL: "successurl",
|
||||
Redirect: false,
|
||||
RedirectQueryParameter: "comingFrom",
|
||||
RedirectCheckReferer: false,
|
||||
RedirectHostFile: "File",
|
||||
LogoutURL: "logouturl",
|
||||
Template: "template",
|
||||
LoginPath: "loginpath",
|
||||
CookieName: "cookiename",
|
||||
CookieExpiry: 23 * time.Minute,
|
||||
CookieDomain: "*.example.com",
|
||||
CookieHTTPOnly: false,
|
||||
Backends: Options{
|
||||
"simple": map[string]string{
|
||||
"foo": "bar",
|
||||
|
||||
+11
-6
@@ -35,10 +35,10 @@ func (h *Handler) deleteRedirectCookie(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *Handler) allowRedirect(r *http.Request) bool {
|
||||
if !h.config.AllowRedirects {
|
||||
if !h.config.Redirect {
|
||||
return false
|
||||
}
|
||||
if !h.config.CheckRefererOnRedirects {
|
||||
if !h.config.RedirectCheckReferer {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func (h *Handler) allowRedirect(r *http.Request) bool {
|
||||
|
||||
func (h *Handler) redirectURL(r *http.Request, w http.ResponseWriter) string {
|
||||
targetURL, foundTarget := h.getRedirectTarget(r)
|
||||
if foundTarget && h.config.AllowRedirects {
|
||||
if foundTarget && h.config.Redirect {
|
||||
sameHost := targetURL.Host == "" || r.Host == targetURL.Host
|
||||
if sameHost && targetURL.Path != "" {
|
||||
return targetURL.Path
|
||||
@@ -93,9 +93,14 @@ func (h *Handler) getRedirectTarget(r *http.Request) (*url.URL, bool) {
|
||||
}
|
||||
|
||||
func (h *Handler) isRedirectDomainWhitelisted(r *http.Request, host string) bool {
|
||||
f, err := os.Open(h.config.WhitelistDomainsFile)
|
||||
if h.config.RedirectHostFile == "" {
|
||||
logging.Application(r.Header).Warnf("redirect attempt to '%s', but no whitelist domain file given", host)
|
||||
return false
|
||||
}
|
||||
|
||||
f, err := os.Open(h.config.RedirectHostFile)
|
||||
if err != nil {
|
||||
logging.Application(r.Header).Warnf("can't open redirect whitelist domains file '%s'", h.config.WhitelistDomainsFile)
|
||||
logging.Application(r.Header).Warnf("can't open redirect whitelist domains file '%s'", h.config.RedirectHostFile)
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
@@ -106,6 +111,6 @@ func (h *Handler) isRedirectDomainWhitelisted(r *http.Request, host string) bool
|
||||
return true
|
||||
}
|
||||
}
|
||||
logging.Application(r.Header).Warnf("domain '%s' not in redirect whitelist", host)
|
||||
logging.Application(r.Header).Warnf("redirect attempt to '%s', but not in redirect whitelist", host)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@ func TestRedirect(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRedirect_NotAllowed(t *testing.T) {
|
||||
// redirect to SuccessURL if AllowRedirects is false
|
||||
// redirect to SuccessURL if Redirect is false
|
||||
cfg := DefaultConfig()
|
||||
cfg.AllowRedirects = false
|
||||
cfg.Redirect = false
|
||||
h := &Handler{
|
||||
backends: []Backend{
|
||||
NewSimpleBackend(map[string]string{"bob": "secret"}),
|
||||
@@ -55,9 +55,9 @@ func TestRedirect_NonMatchingReferrer(t *testing.T) {
|
||||
setCookieList = readSetCookies(recorder.Header())
|
||||
Equal(t, 0, len(setCookieList))
|
||||
|
||||
// set redirect cookie with mismatch referer if CheckRefererOnRedirects is false
|
||||
// set redirect cookie with mismatch referer if RedirectCheckReferer is false
|
||||
cfg := DefaultConfig()
|
||||
cfg.CheckRefererOnRedirects = false
|
||||
cfg.RedirectCheckReferer = false
|
||||
h := &Handler{
|
||||
backends: []Backend{
|
||||
NewSimpleBackend(map[string]string{"bob": "secret"}),
|
||||
@@ -93,7 +93,7 @@ func TestRedirect_Whitelisting(t *testing.T) {
|
||||
|
||||
// redirect to success url if domains whitelist file doesn't exist
|
||||
cfg := DefaultConfig()
|
||||
cfg.WhitelistDomainsFile = whitelistFile.Name()
|
||||
cfg.RedirectHostFile = whitelistFile.Name()
|
||||
h := &Handler{
|
||||
backends: []Backend{
|
||||
NewSimpleBackend(map[string]string{"bob": "secret"}),
|
||||
|
||||
Reference in New Issue
Block a user