feat(cli): support auth tokens for accessing private modules (#9508)

Closes #5239
This commit is contained in:
Kitson Kelly 2021-02-16 13:50:27 +11:00 committed by GitHub
parent ccd6ee5c23
commit 879897ada6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 430 additions and 34 deletions

144
cli/auth_tokens.rs Normal file
View File

@ -0,0 +1,144 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
use deno_core::ModuleSpecifier;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthToken {
host: String,
token: String,
}
impl fmt::Display for AuthToken {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Bearer {}", self.token)
}
}
/// A structure which contains bearer tokens that can be used when sending
/// requests to websites, intended to authorize access to private resources
/// such as remote modules.
#[derive(Debug, Clone)]
pub struct AuthTokens(Vec<AuthToken>);
impl AuthTokens {
/// Create a new set of tokens based on the provided string. It is intended
/// that the string be the value of an environment variable and the string is
/// parsed for token values. The string is expected to be a semi-colon
/// separated string, where each value is `{token}@{hostname}`.
pub fn new(maybe_tokens_str: Option<String>) -> Self {
let mut tokens = Vec::new();
if let Some(tokens_str) = maybe_tokens_str {
for token_str in tokens_str.split(';') {
if token_str.contains('@') {
let pair: Vec<&str> = token_str.rsplitn(2, '@').collect();
let token = pair[1].to_string();
let host = pair[0].to_lowercase();
tokens.push(AuthToken { host, token });
} else {
error!("Badly formed auth token discarded.");
}
}
debug!("Parsed {} auth token(s).", tokens.len());
}
Self(tokens)
}
/// Attempt to match the provided specifier to the tokens in the set. The
/// matching occurs from the right of the hostname plus port, irrespective of
/// scheme. For example `https://www.deno.land:8080/` would match a token
/// with a host value of `deno.land:8080` but not match `www.deno.land`. The
/// matching is case insensitive.
pub fn get(&self, specifier: &ModuleSpecifier) -> Option<AuthToken> {
self.0.iter().find_map(|t| {
let url = specifier.as_url();
let hostname = if let Some(port) = url.port() {
format!("{}:{}", url.host_str()?, port)
} else {
url.host_str()?.to_string()
};
if hostname.to_lowercase().ends_with(&t.host) {
Some(t.clone())
} else {
None
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_auth_token() {
let auth_tokens = AuthTokens::new(Some("abc123@deno.land".to_string()));
let fixture =
ModuleSpecifier::resolve_url("https://deno.land/x/mod.ts").unwrap();
assert_eq!(
auth_tokens.get(&fixture).unwrap().to_string(),
"Bearer abc123"
);
let fixture =
ModuleSpecifier::resolve_url("https://www.deno.land/x/mod.ts").unwrap();
assert_eq!(
auth_tokens.get(&fixture).unwrap().to_string(),
"Bearer abc123".to_string()
);
let fixture =
ModuleSpecifier::resolve_url("http://127.0.0.1:8080/x/mod.ts").unwrap();
assert_eq!(auth_tokens.get(&fixture), None);
let fixture =
ModuleSpecifier::resolve_url("https://deno.land.example.com/x/mod.ts")
.unwrap();
assert_eq!(auth_tokens.get(&fixture), None);
let fixture =
ModuleSpecifier::resolve_url("https://deno.land:8080/x/mod.ts").unwrap();
assert_eq!(auth_tokens.get(&fixture), None);
}
#[test]
fn test_auth_tokens_multiple() {
let auth_tokens =
AuthTokens::new(Some("abc123@deno.land;def456@example.com".to_string()));
let fixture =
ModuleSpecifier::resolve_url("https://deno.land/x/mod.ts").unwrap();
assert_eq!(
auth_tokens.get(&fixture).unwrap().to_string(),
"Bearer abc123".to_string()
);
let fixture =
ModuleSpecifier::resolve_url("http://example.com/a/file.ts").unwrap();
assert_eq!(
auth_tokens.get(&fixture).unwrap().to_string(),
"Bearer def456".to_string()
);
}
#[test]
fn test_auth_tokens_port() {
let auth_tokens =
AuthTokens::new(Some("abc123@deno.land:8080".to_string()));
let fixture =
ModuleSpecifier::resolve_url("https://deno.land/x/mod.ts").unwrap();
assert_eq!(auth_tokens.get(&fixture), None);
let fixture =
ModuleSpecifier::resolve_url("http://deno.land:8080/x/mod.ts").unwrap();
assert_eq!(
auth_tokens.get(&fixture).unwrap().to_string(),
"Bearer abc123".to_string()
);
}
#[test]
fn test_auth_tokens_contain_at() {
let auth_tokens = AuthTokens::new(Some("abc@123@deno.land".to_string()));
let fixture =
ModuleSpecifier::resolve_url("https://deno.land/x/mod.ts").unwrap();
assert_eq!(
auth_tokens.get(&fixture).unwrap().to_string(),
"Bearer abc@123".to_string()
);
}
}

View File

@ -1,9 +1,11 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
use crate::auth_tokens::AuthTokens;
use crate::colors;
use crate::http_cache::HttpCache;
use crate::http_util::create_http_client;
use crate::http_util::fetch_once;
use crate::http_util::FetchOnceArgs;
use crate::http_util::FetchOnceResult;
use crate::media_type::MediaType;
use crate::text_encoding;
@ -19,6 +21,7 @@ use deno_core::futures::future::FutureExt;
use deno_core::ModuleSpecifier;
use deno_runtime::deno_fetch::reqwest;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::future::Future;
use std::io::Read;
@ -27,6 +30,7 @@ use std::pin::Pin;
use std::sync::Arc;
use std::sync::Mutex;
static DENO_AUTH_TOKENS: &str = "DENO_AUTH_TOKENS";
pub const SUPPORTED_SCHEMES: [&str; 4] = ["data", "file", "http", "https"];
/// A structure representing a source file.
@ -308,6 +312,7 @@ fn strip_shebang(mut value: String) -> String {
/// A structure for resolving, fetching and caching source files.
#[derive(Clone)]
pub struct FileFetcher {
auth_tokens: AuthTokens,
allow_remote: bool,
cache: FileCache,
cache_setting: CacheSetting,
@ -323,8 +328,9 @@ impl FileFetcher {
ca_data: Option<Vec<u8>>,
) -> Result<Self, AnyError> {
Ok(Self {
auth_tokens: AuthTokens::new(env::var(DENO_AUTH_TOKENS).ok()),
allow_remote,
cache: FileCache::default(),
cache: Default::default(),
cache_setting,
http_cache,
http_client: create_http_client(get_user_agent(), ca_data)?,
@ -488,17 +494,25 @@ impl FileFetcher {
info!("{} {}", colors::green("Download"), specifier);
let file_fetcher = self.clone();
let cached_etag = match self.http_cache.get(specifier.as_url()) {
let maybe_etag = match self.http_cache.get(specifier.as_url()) {
Ok((_, headers)) => headers.get("etag").cloned(),
_ => None,
};
let maybe_auth_token = self.auth_tokens.get(&specifier);
let specifier = specifier.clone();
let permissions = permissions.clone();
let http_client = self.http_client.clone();
let client = self.http_client.clone();
let file_fetcher = self.clone();
// A single pass of fetch either yields code or yields a redirect.
async move {
match fetch_once(http_client, specifier.as_url(), cached_etag).await? {
match fetch_once(FetchOnceArgs {
client,
url: specifier.as_url().clone(),
maybe_etag,
maybe_auth_token,
})
.await?
{
FetchOnceResult::NotModified => {
let file = file_fetcher.fetch_cached(&specifier, 10)?.unwrap();
Ok(file)

View File

@ -219,18 +219,22 @@ impl From<Flags> for PermissionsOptions {
}
}
static ENV_VARIABLES_HELP: &str = "ENVIRONMENT VARIABLES:
static ENV_VARIABLES_HELP: &str = r#"ENVIRONMENT VARIABLES:
DENO_AUTH_TOKENS A semi-colon separated list of bearer tokens and
hostnames to use when fetching remote modules from
private repositories
(e.g. "abcde12345@deno.land;54321edcba@github.com")
DENO_CERT Load certificate authority from PEM encoded file
DENO_DIR Set the cache directory
DENO_INSTALL_ROOT Set deno install's output directory
(defaults to $HOME/.deno/bin)
DENO_CERT Load certificate authority from PEM encoded file
NO_COLOR Set to disable color
HTTP_PROXY Proxy address for HTTP requests
(module downloads, fetch)
HTTPS_PROXY Proxy address for HTTPS requests
(module downloads, fetch)
NO_COLOR Set to disable color
NO_PROXY Comma-separated list of hosts which do not use a proxy
(module downloads, fetch)";
(module downloads, fetch)"#;
static DENO_HELP: &str = "A secure JavaScript and TypeScript runtime

View File

@ -1,11 +1,14 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
use crate::auth_tokens::AuthToken;
use deno_core::error::generic_error;
use deno_core::error::AnyError;
use deno_core::url::Url;
use deno_runtime::deno_fetch::reqwest;
use deno_runtime::deno_fetch::reqwest::header::HeaderMap;
use deno_runtime::deno_fetch::reqwest::header::HeaderValue;
use deno_runtime::deno_fetch::reqwest::header::AUTHORIZATION;
use deno_runtime::deno_fetch::reqwest::header::IF_NONE_MATCH;
use deno_runtime::deno_fetch::reqwest::header::LOCATION;
use deno_runtime::deno_fetch::reqwest::header::USER_AGENT;
@ -76,24 +79,33 @@ pub enum FetchOnceResult {
Redirect(Url, HeadersMap),
}
#[derive(Debug)]
pub struct FetchOnceArgs {
pub client: Client,
pub url: Url,
pub maybe_etag: Option<String>,
pub maybe_auth_token: Option<AuthToken>,
}
/// Asynchronously fetches the given HTTP URL one pass only.
/// If no redirect is present and no error occurs,
/// yields Code(ResultPayload).
/// If redirect occurs, does not follow and
/// yields Redirect(url).
pub async fn fetch_once(
client: Client,
url: &Url,
cached_etag: Option<String>,
args: FetchOnceArgs,
) -> Result<FetchOnceResult, AnyError> {
let url = url.clone();
let mut request = args.client.get(args.url.clone());
let mut request = client.get(url.clone());
if let Some(etag) = cached_etag {
if let Some(etag) = args.maybe_etag {
let if_none_match_val = HeaderValue::from_str(&etag).unwrap();
request = request.header(IF_NONE_MATCH, if_none_match_val);
}
if let Some(auth_token) = args.maybe_auth_token {
let authorization_val =
HeaderValue::from_str(&auth_token.to_string()).unwrap();
request = request.header(AUTHORIZATION, authorization_val);
}
let response = request.send().await?;
if response.status() == StatusCode::NOT_MODIFIED {
@ -126,20 +138,23 @@ pub async fn fetch_once(
if let Some(location) = response.headers().get(LOCATION) {
let location_string = location.to_str().unwrap();
debug!("Redirecting to {:?}...", &location_string);
let new_url = resolve_url_from_location(&url, location_string);
let new_url = resolve_url_from_location(&args.url, location_string);
return Ok(FetchOnceResult::Redirect(new_url, headers_));
} else {
return Err(generic_error(format!(
"Redirection from '{}' did not provide location header",
url
args.url
)));
}
}
if response.status().is_client_error() || response.status().is_server_error()
{
let err =
generic_error(format!("Import '{}' failed: {}", &url, response.status()));
let err = generic_error(format!(
"Import '{}' failed: {}",
args.url,
response.status()
));
return Err(err);
}
@ -165,7 +180,13 @@ mod tests {
let url =
Url::parse("http://127.0.0.1:4545/cli/tests/fixture.json").unwrap();
let client = create_test_client(None);
let result = fetch_once(client, &url, None).await;
let result = fetch_once(FetchOnceArgs {
client,
url,
maybe_etag: None,
maybe_auth_token: None,
})
.await;
if let Ok(FetchOnceResult::Code(body, headers)) = result {
assert!(!body.is_empty());
assert_eq!(headers.get("content-type").unwrap(), "application/json");
@ -185,7 +206,13 @@ mod tests {
)
.unwrap();
let client = create_test_client(None);
let result = fetch_once(client, &url, None).await;
let result = fetch_once(FetchOnceArgs {
client,
url,
maybe_etag: None,
maybe_auth_token: None,
})
.await;
if let Ok(FetchOnceResult::Code(body, headers)) = result {
assert_eq!(String::from_utf8(body).unwrap(), "console.log('gzip')");
assert_eq!(
@ -204,7 +231,13 @@ mod tests {
let _http_server_guard = test_util::http_server();
let url = Url::parse("http://127.0.0.1:4545/etag_script.ts").unwrap();
let client = create_test_client(None);
let result = fetch_once(client.clone(), &url, None).await;
let result = fetch_once(FetchOnceArgs {
client: client.clone(),
url: url.clone(),
maybe_etag: None,
maybe_auth_token: None,
})
.await;
if let Ok(FetchOnceResult::Code(body, headers)) = result {
assert!(!body.is_empty());
assert_eq!(String::from_utf8(body).unwrap(), "console.log('etag')");
@ -217,8 +250,13 @@ mod tests {
panic!();
}
let res =
fetch_once(client, &url, Some("33a64df551425fcc55e".to_string())).await;
let res = fetch_once(FetchOnceArgs {
client,
url,
maybe_etag: Some("33a64df551425fcc55e".to_string()),
maybe_auth_token: None,
})
.await;
assert_eq!(res.unwrap(), FetchOnceResult::NotModified);
}
@ -231,7 +269,13 @@ mod tests {
)
.unwrap();
let client = create_test_client(None);
let result = fetch_once(client, &url, None).await;
let result = fetch_once(FetchOnceArgs {
client,
url,
maybe_etag: None,
maybe_auth_token: None,
})
.await;
if let Ok(FetchOnceResult::Code(body, headers)) = result {
assert!(!body.is_empty());
assert_eq!(String::from_utf8(body).unwrap(), "console.log('brotli');");
@ -256,7 +300,13 @@ mod tests {
let target_url =
Url::parse("http://localhost:4545/cli/tests/fixture.json").unwrap();
let client = create_test_client(None);
let result = fetch_once(client, &url, None).await;
let result = fetch_once(FetchOnceArgs {
client,
url,
maybe_etag: None,
maybe_auth_token: None,
})
.await;
if let Ok(FetchOnceResult::Redirect(url, _)) = result {
assert_eq!(url, target_url);
} else {
@ -322,7 +372,13 @@ mod tests {
),
)
.unwrap();
let result = fetch_once(client, &url, None).await;
let result = fetch_once(FetchOnceArgs {
client,
url,
maybe_etag: None,
maybe_auth_token: None,
})
.await;
if let Ok(FetchOnceResult::Code(body, headers)) = result {
assert!(!body.is_empty());
assert_eq!(headers.get("content-type").unwrap(), "application/json");
@ -354,7 +410,13 @@ mod tests {
),
)
.unwrap();
let result = fetch_once(client, &url, None).await;
let result = fetch_once(FetchOnceArgs {
client,
url,
maybe_etag: None,
maybe_auth_token: None,
})
.await;
if let Ok(FetchOnceResult::Code(body, headers)) = result {
assert_eq!(String::from_utf8(body).unwrap(), "console.log('gzip')");
assert_eq!(
@ -385,7 +447,13 @@ mod tests {
),
)
.unwrap();
let result = fetch_once(client.clone(), &url, None).await;
let result = fetch_once(FetchOnceArgs {
client: client.clone(),
url: url.clone(),
maybe_etag: None,
maybe_auth_token: None,
})
.await;
if let Ok(FetchOnceResult::Code(body, headers)) = result {
assert!(!body.is_empty());
assert_eq!(String::from_utf8(body).unwrap(), "console.log('etag')");
@ -399,8 +467,13 @@ mod tests {
panic!();
}
let res =
fetch_once(client, &url, Some("33a64df551425fcc55e".to_string())).await;
let res = fetch_once(FetchOnceArgs {
client,
url,
maybe_etag: Some("33a64df551425fcc55e".to_string()),
maybe_auth_token: None,
})
.await;
assert_eq!(res.unwrap(), FetchOnceResult::NotModified);
}
@ -425,7 +498,13 @@ mod tests {
),
)
.unwrap();
let result = fetch_once(client, &url, None).await;
let result = fetch_once(FetchOnceArgs {
client,
url,
maybe_etag: None,
maybe_auth_token: None,
})
.await;
if let Ok(FetchOnceResult::Code(body, headers)) = result {
assert!(!body.is_empty());
assert_eq!(String::from_utf8(body).unwrap(), "console.log('brotli');");
@ -446,7 +525,13 @@ mod tests {
let url_str = "http://127.0.0.1:4545/bad_redirect";
let url = Url::parse(url_str).unwrap();
let client = create_test_client(None);
let result = fetch_once(client, &url, None).await;
let result = fetch_once(FetchOnceArgs {
client,
url,
maybe_etag: None,
maybe_auth_token: None,
})
.await;
assert!(result.is_err());
let err = result.unwrap_err();
// Check that the error message contains the original URL

View File

@ -8,6 +8,7 @@ extern crate lazy_static;
extern crate log;
mod ast;
mod auth_tokens;
mod checksum;
mod colors;
mod deno_dir;

View File

@ -208,6 +208,42 @@ mod integration {
assert_eq!("noColor false", util::strip_ansi_codes(stdout_str));
}
#[test]
fn auth_tokens() {
let _g = util::http_server();
let output = util::deno_cmd()
.current_dir(util::root_path())
.arg("run")
.arg("http://127.0.0.1:4551/cli/tests/001_hello.js")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap()
.wait_with_output()
.unwrap();
assert!(!output.status.success());
let stdout_str = std::str::from_utf8(&output.stdout).unwrap().trim();
assert!(stdout_str.is_empty());
let stderr_str = std::str::from_utf8(&output.stderr).unwrap().trim();
eprintln!("{}", stderr_str);
assert!(stderr_str.contains("Import 'http://127.0.0.1:4551/cli/tests/001_hello.js' failed: 404 Not Found"));
let output = util::deno_cmd()
.current_dir(util::root_path())
.arg("run")
.arg("http://127.0.0.1:4551/cli/tests/001_hello.js")
.env("DENO_AUTH_TOKENS", "abcdef123456789@127.0.0.1:4551")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap()
.wait_with_output()
.unwrap();
assert!(output.status.success());
let stdout_str = std::str::from_utf8(&output.stdout).unwrap().trim();
assert_eq!(util::strip_ansi_codes(stdout_str), "Hello World");
}
#[cfg(unix)]
#[test]
pub fn test_raw_tty() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
docs/images/private-pat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,75 @@
## Private modules and repositories
There maybe instances where you want to load a remote module that is located in
a _private_ repository, like a private repository on GitHub.
Deno supports sending bearer tokens when requesting a remote module. Bearer
tokens are the predominate type of access token used with OAuth 2.0 and is
broadly supported by hosting services (e.g. GitHub, Gitlab, BitBucket,
Cloudsmith, etc.).
### DENO_AUTH_TOKENS
The Deno CLI will look for an environment variable named `DENO_AUTH_TOKENS` to
determine what authentication tokens it should consider using when requesting
remote modules. The value of the environment variable is in the format of a _n_
number of tokens deliminated by a semi-colon (`;`) where each token is in the
format of `{token}@{hostname[:port]}`.
For example a single token for would look something like this:
```sh
DENO_AUTH_TOKENS=a1b2c3d4e5f6@deno.land
```
And multiple tokens would look like this:
```sh
DENO_AUTH_TOKENS=a1b2c3d4e5f6@deno.land;f1e2d3c4b5a6@example.com:8080
```
When Deno goes to fetch a remote module, where the hostname matches the hostname
of the remote module, Deno will set the `Authorization` header of the request to
the value of `Bearer {token}`. This allows the remote server to recognize that
the request is an authorized request tied to a specific authenticated user, and
provide access to the appropriate resources and modules on the server.
### GitHub
To be able to access private repositories on GitHub, you would need to issue
yourself a _personal access token_. You do this by logging into GitHub and going
under _Settings -> Developer settings -> Personal access tokens_:
![Personal access tokens settings on GitHub](../images/private-pat.png)
You would then choose to _Generate new token_ and give your token a description
and appropriate access:
![Creating a new personal access token on GitHub](../images/private-github-new-token.png)
And once created GitHub will display the new token a single time, the value of
which you would want to use in the environment variable:
![Display of newly created token on GitHub](../images/private-github-token-display.png)
In order to access modules that are contained in a private repository on GitHub,
you would want to use the generated token in the `DENO_AUTH_TOKENS` environment
variable scoped to the `raw.githubusercontent.com` hostname. For example:
```sh
DENO_AUTH_TOKENS=a1b2c3d4e5f6@raw.githubusercontent.com
```
This should allow Deno to access any modules that the user who the token was
issued for has access to.
When the token is incorrect, or the user does not have access to the module,
GitHub will issue a `404 Not Found` status, instead of an unauthorized status.
So if you are getting errors that the modules you are trying to access are not
found on the command line, check the environment variable settings and the
personal access token settings.
In addition, `deno run -L debug` should print out a debug message about the
number of tokens that are parsed out of the environment variable. It will print
an error message if it feels any of the tokens are malformed. It won't print any
details about the tokens for security purposes.

View File

@ -31,6 +31,7 @@
"reloading_modules": "Reloading modules",
"integrity_checking": "Integrity checking",
"proxies": "Proxies",
"private": "Private modules",
"import_maps": "Import maps"
}
},

View File

@ -49,11 +49,13 @@ use tokio_rustls::TlsAcceptor;
use tokio_tungstenite::accept_async;
const PORT: u16 = 4545;
const TEST_AUTH_TOKEN: &str = "abcdef123456789";
const REDIRECT_PORT: u16 = 4546;
const ANOTHER_REDIRECT_PORT: u16 = 4547;
const DOUBLE_REDIRECTS_PORT: u16 = 4548;
const INF_REDIRECTS_PORT: u16 = 4549;
const REDIRECT_ABSOLUTE_PORT: u16 = 4550;
const AUTH_REDIRECT_PORT: u16 = 4551;
const HTTPS_PORT: u16 = 5545;
const WS_PORT: u16 = 4242;
const WSS_PORT: u16 = 4243;
@ -201,6 +203,25 @@ async fn another_redirect(req: Request<Body>) -> hyper::Result<Response<Body>> {
Ok(redirect_resp(url))
}
async fn auth_redirect(req: Request<Body>) -> hyper::Result<Response<Body>> {
if let Some(auth) = req
.headers()
.get("authorization")
.map(|v| v.to_str().unwrap())
{
if auth.to_lowercase() == format!("bearer {}", TEST_AUTH_TOKEN) {
let p = req.uri().path();
assert_eq!(&p[0..1], "/");
let url = format!("http://localhost:{}{}", PORT, p);
return Ok(redirect_resp(url));
}
}
let mut resp = Response::new(Body::empty());
*resp.status_mut() = StatusCode::NOT_FOUND;
Ok(resp)
}
async fn run_ws_server(addr: &SocketAddr) {
let listener = TcpListener::bind(addr).await.unwrap();
while let Ok((stream, _addr)) = listener.accept().await {
@ -666,6 +687,19 @@ async fn wrap_another_redirect_server() {
}
}
async fn wrap_auth_redirect_server() {
let auth_redirect_svc = make_service_fn(|_| async {
Ok::<_, Infallible>(service_fn(auth_redirect))
});
let auth_redirect_addr =
SocketAddr::from(([127, 0, 0, 1], AUTH_REDIRECT_PORT));
let auth_redirect_server =
Server::bind(&auth_redirect_addr).serve(auth_redirect_svc);
if let Err(e) = auth_redirect_server.await {
eprintln!("Auth redirect error: {:?}", e);
}
}
async fn wrap_abs_redirect_server() {
let abs_redirect_svc = make_service_fn(|_| async {
Ok::<_, Infallible>(service_fn(absolute_redirect))
@ -740,6 +774,7 @@ pub async fn run_all_servers() {
let double_redirects_server_fut = wrap_double_redirect_server();
let inf_redirects_server_fut = wrap_inf_redirect_server();
let another_redirect_server_fut = wrap_another_redirect_server();
let auth_redirect_server_fut = wrap_auth_redirect_server();
let abs_redirect_server_fut = wrap_abs_redirect_server();
let ws_addr = SocketAddr::from(([127, 0, 0, 1], WS_PORT));
@ -756,6 +791,7 @@ pub async fn run_all_servers() {
ws_server_fut,
wss_server_fut,
another_redirect_server_fut,
auth_redirect_server_fut,
inf_redirects_server_fut,
double_redirects_server_fut,
abs_redirect_server_fut,