mirror of
https://github.com/denoland/deno.git
synced 2024-12-22 07:14:47 -05:00
feat(cli): support auth tokens for accessing private modules (#9508)
Closes #5239
This commit is contained in:
parent
ccd6ee5c23
commit
879897ada6
12 changed files with 430 additions and 34 deletions
144
cli/auth_tokens.rs
Normal file
144
cli/auth_tokens.rs
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
12
cli/flags.rs
12
cli/flags.rs
|
@ -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
|
||||
|
||||
|
|
135
cli/http_util.rs
135
cli/http_util.rs
|
@ -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
|
||||
|
|
|
@ -8,6 +8,7 @@ extern crate lazy_static;
|
|||
extern crate log;
|
||||
|
||||
mod ast;
|
||||
mod auth_tokens;
|
||||
mod checksum;
|
||||
mod colors;
|
||||
mod deno_dir;
|
||||
|
|
|
@ -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() {
|
||||
|
|
BIN
docs/images/private-github-new-token.png
Normal file
BIN
docs/images/private-github-new-token.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
BIN
docs/images/private-github-token-display.png
Normal file
BIN
docs/images/private-github-token-display.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
BIN
docs/images/private-pat.png
Normal file
BIN
docs/images/private-pat.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
75
docs/linking_to_external_code/private.md
Normal file
75
docs/linking_to_external_code/private.md
Normal 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.
|
|
@ -31,6 +31,7 @@
|
|||
"reloading_modules": "Reloading modules",
|
||||
"integrity_checking": "Integrity checking",
|
||||
"proxies": "Proxies",
|
||||
"private": "Private modules",
|
||||
"import_maps": "Import maps"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue