mirror of
https://github.com/denoland/deno.git
synced 2025-01-03 04:48:52 -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.
|
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
use crate::auth_tokens::AuthTokens;
|
||||||
use crate::colors;
|
use crate::colors;
|
||||||
use crate::http_cache::HttpCache;
|
use crate::http_cache::HttpCache;
|
||||||
use crate::http_util::create_http_client;
|
use crate::http_util::create_http_client;
|
||||||
use crate::http_util::fetch_once;
|
use crate::http_util::fetch_once;
|
||||||
|
use crate::http_util::FetchOnceArgs;
|
||||||
use crate::http_util::FetchOnceResult;
|
use crate::http_util::FetchOnceResult;
|
||||||
use crate::media_type::MediaType;
|
use crate::media_type::MediaType;
|
||||||
use crate::text_encoding;
|
use crate::text_encoding;
|
||||||
|
@ -19,6 +21,7 @@ use deno_core::futures::future::FutureExt;
|
||||||
use deno_core::ModuleSpecifier;
|
use deno_core::ModuleSpecifier;
|
||||||
use deno_runtime::deno_fetch::reqwest;
|
use deno_runtime::deno_fetch::reqwest;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
@ -27,6 +30,7 @@ use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
static DENO_AUTH_TOKENS: &str = "DENO_AUTH_TOKENS";
|
||||||
pub const SUPPORTED_SCHEMES: [&str; 4] = ["data", "file", "http", "https"];
|
pub const SUPPORTED_SCHEMES: [&str; 4] = ["data", "file", "http", "https"];
|
||||||
|
|
||||||
/// A structure representing a source file.
|
/// 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.
|
/// A structure for resolving, fetching and caching source files.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FileFetcher {
|
pub struct FileFetcher {
|
||||||
|
auth_tokens: AuthTokens,
|
||||||
allow_remote: bool,
|
allow_remote: bool,
|
||||||
cache: FileCache,
|
cache: FileCache,
|
||||||
cache_setting: CacheSetting,
|
cache_setting: CacheSetting,
|
||||||
|
@ -323,8 +328,9 @@ impl FileFetcher {
|
||||||
ca_data: Option<Vec<u8>>,
|
ca_data: Option<Vec<u8>>,
|
||||||
) -> Result<Self, AnyError> {
|
) -> Result<Self, AnyError> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
auth_tokens: AuthTokens::new(env::var(DENO_AUTH_TOKENS).ok()),
|
||||||
allow_remote,
|
allow_remote,
|
||||||
cache: FileCache::default(),
|
cache: Default::default(),
|
||||||
cache_setting,
|
cache_setting,
|
||||||
http_cache,
|
http_cache,
|
||||||
http_client: create_http_client(get_user_agent(), ca_data)?,
|
http_client: create_http_client(get_user_agent(), ca_data)?,
|
||||||
|
@ -488,17 +494,25 @@ impl FileFetcher {
|
||||||
|
|
||||||
info!("{} {}", colors::green("Download"), specifier);
|
info!("{} {}", colors::green("Download"), specifier);
|
||||||
|
|
||||||
let file_fetcher = self.clone();
|
let maybe_etag = match self.http_cache.get(specifier.as_url()) {
|
||||||
let cached_etag = match self.http_cache.get(specifier.as_url()) {
|
|
||||||
Ok((_, headers)) => headers.get("etag").cloned(),
|
Ok((_, headers)) => headers.get("etag").cloned(),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
let maybe_auth_token = self.auth_tokens.get(&specifier);
|
||||||
let specifier = specifier.clone();
|
let specifier = specifier.clone();
|
||||||
let permissions = permissions.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.
|
// A single pass of fetch either yields code or yields a redirect.
|
||||||
async move {
|
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 => {
|
FetchOnceResult::NotModified => {
|
||||||
let file = file_fetcher.fetch_cached(&specifier, 10)?.unwrap();
|
let file = file_fetcher.fetch_cached(&specifier, 10)?.unwrap();
|
||||||
Ok(file)
|
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_DIR Set the cache directory
|
||||||
DENO_INSTALL_ROOT Set deno install's output directory
|
DENO_INSTALL_ROOT Set deno install's output directory
|
||||||
(defaults to $HOME/.deno/bin)
|
(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
|
HTTP_PROXY Proxy address for HTTP requests
|
||||||
(module downloads, fetch)
|
(module downloads, fetch)
|
||||||
HTTPS_PROXY Proxy address for HTTPS requests
|
HTTPS_PROXY Proxy address for HTTPS requests
|
||||||
(module downloads, fetch)
|
(module downloads, fetch)
|
||||||
|
NO_COLOR Set to disable color
|
||||||
NO_PROXY Comma-separated list of hosts which do not use a proxy
|
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
|
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.
|
// 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::generic_error;
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
use deno_core::url::Url;
|
use deno_core::url::Url;
|
||||||
use deno_runtime::deno_fetch::reqwest;
|
use deno_runtime::deno_fetch::reqwest;
|
||||||
use deno_runtime::deno_fetch::reqwest::header::HeaderMap;
|
use deno_runtime::deno_fetch::reqwest::header::HeaderMap;
|
||||||
use deno_runtime::deno_fetch::reqwest::header::HeaderValue;
|
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::IF_NONE_MATCH;
|
||||||
use deno_runtime::deno_fetch::reqwest::header::LOCATION;
|
use deno_runtime::deno_fetch::reqwest::header::LOCATION;
|
||||||
use deno_runtime::deno_fetch::reqwest::header::USER_AGENT;
|
use deno_runtime::deno_fetch::reqwest::header::USER_AGENT;
|
||||||
|
@ -76,24 +79,33 @@ pub enum FetchOnceResult {
|
||||||
Redirect(Url, HeadersMap),
|
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.
|
/// Asynchronously fetches the given HTTP URL one pass only.
|
||||||
/// If no redirect is present and no error occurs,
|
/// If no redirect is present and no error occurs,
|
||||||
/// yields Code(ResultPayload).
|
/// yields Code(ResultPayload).
|
||||||
/// If redirect occurs, does not follow and
|
/// If redirect occurs, does not follow and
|
||||||
/// yields Redirect(url).
|
/// yields Redirect(url).
|
||||||
pub async fn fetch_once(
|
pub async fn fetch_once(
|
||||||
client: Client,
|
args: FetchOnceArgs,
|
||||||
url: &Url,
|
|
||||||
cached_etag: Option<String>,
|
|
||||||
) -> Result<FetchOnceResult, AnyError> {
|
) -> 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) = args.maybe_etag {
|
||||||
|
|
||||||
if let Some(etag) = cached_etag {
|
|
||||||
let if_none_match_val = HeaderValue::from_str(&etag).unwrap();
|
let if_none_match_val = HeaderValue::from_str(&etag).unwrap();
|
||||||
request = request.header(IF_NONE_MATCH, if_none_match_val);
|
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?;
|
let response = request.send().await?;
|
||||||
|
|
||||||
if response.status() == StatusCode::NOT_MODIFIED {
|
if response.status() == StatusCode::NOT_MODIFIED {
|
||||||
|
@ -126,20 +138,23 @@ pub async fn fetch_once(
|
||||||
if let Some(location) = response.headers().get(LOCATION) {
|
if let Some(location) = response.headers().get(LOCATION) {
|
||||||
let location_string = location.to_str().unwrap();
|
let location_string = location.to_str().unwrap();
|
||||||
debug!("Redirecting to {:?}...", &location_string);
|
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_));
|
return Ok(FetchOnceResult::Redirect(new_url, headers_));
|
||||||
} else {
|
} else {
|
||||||
return Err(generic_error(format!(
|
return Err(generic_error(format!(
|
||||||
"Redirection from '{}' did not provide location header",
|
"Redirection from '{}' did not provide location header",
|
||||||
url
|
args.url
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.status().is_client_error() || response.status().is_server_error()
|
if response.status().is_client_error() || response.status().is_server_error()
|
||||||
{
|
{
|
||||||
let err =
|
let err = generic_error(format!(
|
||||||
generic_error(format!("Import '{}' failed: {}", &url, response.status()));
|
"Import '{}' failed: {}",
|
||||||
|
args.url,
|
||||||
|
response.status()
|
||||||
|
));
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,7 +180,13 @@ mod tests {
|
||||||
let url =
|
let url =
|
||||||
Url::parse("http://127.0.0.1:4545/cli/tests/fixture.json").unwrap();
|
Url::parse("http://127.0.0.1:4545/cli/tests/fixture.json").unwrap();
|
||||||
let client = create_test_client(None);
|
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 {
|
if let Ok(FetchOnceResult::Code(body, headers)) = result {
|
||||||
assert!(!body.is_empty());
|
assert!(!body.is_empty());
|
||||||
assert_eq!(headers.get("content-type").unwrap(), "application/json");
|
assert_eq!(headers.get("content-type").unwrap(), "application/json");
|
||||||
|
@ -185,7 +206,13 @@ mod tests {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let client = create_test_client(None);
|
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 {
|
if let Ok(FetchOnceResult::Code(body, headers)) = result {
|
||||||
assert_eq!(String::from_utf8(body).unwrap(), "console.log('gzip')");
|
assert_eq!(String::from_utf8(body).unwrap(), "console.log('gzip')");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -204,7 +231,13 @@ mod tests {
|
||||||
let _http_server_guard = test_util::http_server();
|
let _http_server_guard = test_util::http_server();
|
||||||
let url = Url::parse("http://127.0.0.1:4545/etag_script.ts").unwrap();
|
let url = Url::parse("http://127.0.0.1:4545/etag_script.ts").unwrap();
|
||||||
let client = create_test_client(None);
|
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 {
|
if let Ok(FetchOnceResult::Code(body, headers)) = result {
|
||||||
assert!(!body.is_empty());
|
assert!(!body.is_empty());
|
||||||
assert_eq!(String::from_utf8(body).unwrap(), "console.log('etag')");
|
assert_eq!(String::from_utf8(body).unwrap(), "console.log('etag')");
|
||||||
|
@ -217,8 +250,13 @@ mod tests {
|
||||||
panic!();
|
panic!();
|
||||||
}
|
}
|
||||||
|
|
||||||
let res =
|
let res = fetch_once(FetchOnceArgs {
|
||||||
fetch_once(client, &url, Some("33a64df551425fcc55e".to_string())).await;
|
client,
|
||||||
|
url,
|
||||||
|
maybe_etag: Some("33a64df551425fcc55e".to_string()),
|
||||||
|
maybe_auth_token: None,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
assert_eq!(res.unwrap(), FetchOnceResult::NotModified);
|
assert_eq!(res.unwrap(), FetchOnceResult::NotModified);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,7 +269,13 @@ mod tests {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let client = create_test_client(None);
|
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 {
|
if let Ok(FetchOnceResult::Code(body, headers)) = result {
|
||||||
assert!(!body.is_empty());
|
assert!(!body.is_empty());
|
||||||
assert_eq!(String::from_utf8(body).unwrap(), "console.log('brotli');");
|
assert_eq!(String::from_utf8(body).unwrap(), "console.log('brotli');");
|
||||||
|
@ -256,7 +300,13 @@ mod tests {
|
||||||
let target_url =
|
let target_url =
|
||||||
Url::parse("http://localhost:4545/cli/tests/fixture.json").unwrap();
|
Url::parse("http://localhost:4545/cli/tests/fixture.json").unwrap();
|
||||||
let client = create_test_client(None);
|
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 {
|
if let Ok(FetchOnceResult::Redirect(url, _)) = result {
|
||||||
assert_eq!(url, target_url);
|
assert_eq!(url, target_url);
|
||||||
} else {
|
} else {
|
||||||
|
@ -322,7 +372,13 @@ mod tests {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.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 {
|
if let Ok(FetchOnceResult::Code(body, headers)) = result {
|
||||||
assert!(!body.is_empty());
|
assert!(!body.is_empty());
|
||||||
assert_eq!(headers.get("content-type").unwrap(), "application/json");
|
assert_eq!(headers.get("content-type").unwrap(), "application/json");
|
||||||
|
@ -354,7 +410,13 @@ mod tests {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.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 {
|
if let Ok(FetchOnceResult::Code(body, headers)) = result {
|
||||||
assert_eq!(String::from_utf8(body).unwrap(), "console.log('gzip')");
|
assert_eq!(String::from_utf8(body).unwrap(), "console.log('gzip')");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -385,7 +447,13 @@ mod tests {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.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 {
|
if let Ok(FetchOnceResult::Code(body, headers)) = result {
|
||||||
assert!(!body.is_empty());
|
assert!(!body.is_empty());
|
||||||
assert_eq!(String::from_utf8(body).unwrap(), "console.log('etag')");
|
assert_eq!(String::from_utf8(body).unwrap(), "console.log('etag')");
|
||||||
|
@ -399,8 +467,13 @@ mod tests {
|
||||||
panic!();
|
panic!();
|
||||||
}
|
}
|
||||||
|
|
||||||
let res =
|
let res = fetch_once(FetchOnceArgs {
|
||||||
fetch_once(client, &url, Some("33a64df551425fcc55e".to_string())).await;
|
client,
|
||||||
|
url,
|
||||||
|
maybe_etag: Some("33a64df551425fcc55e".to_string()),
|
||||||
|
maybe_auth_token: None,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
assert_eq!(res.unwrap(), FetchOnceResult::NotModified);
|
assert_eq!(res.unwrap(), FetchOnceResult::NotModified);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -425,7 +498,13 @@ mod tests {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.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 {
|
if let Ok(FetchOnceResult::Code(body, headers)) = result {
|
||||||
assert!(!body.is_empty());
|
assert!(!body.is_empty());
|
||||||
assert_eq!(String::from_utf8(body).unwrap(), "console.log('brotli');");
|
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_str = "http://127.0.0.1:4545/bad_redirect";
|
||||||
let url = Url::parse(url_str).unwrap();
|
let url = Url::parse(url_str).unwrap();
|
||||||
let client = create_test_client(None);
|
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());
|
assert!(result.is_err());
|
||||||
let err = result.unwrap_err();
|
let err = result.unwrap_err();
|
||||||
// Check that the error message contains the original URL
|
// Check that the error message contains the original URL
|
||||||
|
|
|
@ -8,6 +8,7 @@ extern crate lazy_static;
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
|
||||||
mod ast;
|
mod ast;
|
||||||
|
mod auth_tokens;
|
||||||
mod checksum;
|
mod checksum;
|
||||||
mod colors;
|
mod colors;
|
||||||
mod deno_dir;
|
mod deno_dir;
|
||||||
|
|
|
@ -208,6 +208,42 @@ mod integration {
|
||||||
assert_eq!("noColor false", util::strip_ansi_codes(stdout_str));
|
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)]
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
pub fn test_raw_tty() {
|
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",
|
"reloading_modules": "Reloading modules",
|
||||||
"integrity_checking": "Integrity checking",
|
"integrity_checking": "Integrity checking",
|
||||||
"proxies": "Proxies",
|
"proxies": "Proxies",
|
||||||
|
"private": "Private modules",
|
||||||
"import_maps": "Import maps"
|
"import_maps": "Import maps"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -49,11 +49,13 @@ use tokio_rustls::TlsAcceptor;
|
||||||
use tokio_tungstenite::accept_async;
|
use tokio_tungstenite::accept_async;
|
||||||
|
|
||||||
const PORT: u16 = 4545;
|
const PORT: u16 = 4545;
|
||||||
|
const TEST_AUTH_TOKEN: &str = "abcdef123456789";
|
||||||
const REDIRECT_PORT: u16 = 4546;
|
const REDIRECT_PORT: u16 = 4546;
|
||||||
const ANOTHER_REDIRECT_PORT: u16 = 4547;
|
const ANOTHER_REDIRECT_PORT: u16 = 4547;
|
||||||
const DOUBLE_REDIRECTS_PORT: u16 = 4548;
|
const DOUBLE_REDIRECTS_PORT: u16 = 4548;
|
||||||
const INF_REDIRECTS_PORT: u16 = 4549;
|
const INF_REDIRECTS_PORT: u16 = 4549;
|
||||||
const REDIRECT_ABSOLUTE_PORT: u16 = 4550;
|
const REDIRECT_ABSOLUTE_PORT: u16 = 4550;
|
||||||
|
const AUTH_REDIRECT_PORT: u16 = 4551;
|
||||||
const HTTPS_PORT: u16 = 5545;
|
const HTTPS_PORT: u16 = 5545;
|
||||||
const WS_PORT: u16 = 4242;
|
const WS_PORT: u16 = 4242;
|
||||||
const WSS_PORT: u16 = 4243;
|
const WSS_PORT: u16 = 4243;
|
||||||
|
@ -201,6 +203,25 @@ async fn another_redirect(req: Request<Body>) -> hyper::Result<Response<Body>> {
|
||||||
Ok(redirect_resp(url))
|
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) {
|
async fn run_ws_server(addr: &SocketAddr) {
|
||||||
let listener = TcpListener::bind(addr).await.unwrap();
|
let listener = TcpListener::bind(addr).await.unwrap();
|
||||||
while let Ok((stream, _addr)) = listener.accept().await {
|
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() {
|
async fn wrap_abs_redirect_server() {
|
||||||
let abs_redirect_svc = make_service_fn(|_| async {
|
let abs_redirect_svc = make_service_fn(|_| async {
|
||||||
Ok::<_, Infallible>(service_fn(absolute_redirect))
|
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 double_redirects_server_fut = wrap_double_redirect_server();
|
||||||
let inf_redirects_server_fut = wrap_inf_redirect_server();
|
let inf_redirects_server_fut = wrap_inf_redirect_server();
|
||||||
let another_redirect_server_fut = wrap_another_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 abs_redirect_server_fut = wrap_abs_redirect_server();
|
||||||
|
|
||||||
let ws_addr = SocketAddr::from(([127, 0, 0, 1], WS_PORT));
|
let ws_addr = SocketAddr::from(([127, 0, 0, 1], WS_PORT));
|
||||||
|
@ -756,6 +791,7 @@ pub async fn run_all_servers() {
|
||||||
ws_server_fut,
|
ws_server_fut,
|
||||||
wss_server_fut,
|
wss_server_fut,
|
||||||
another_redirect_server_fut,
|
another_redirect_server_fut,
|
||||||
|
auth_redirect_server_fut,
|
||||||
inf_redirects_server_fut,
|
inf_redirects_server_fut,
|
||||||
double_redirects_server_fut,
|
double_redirects_server_fut,
|
||||||
abs_redirect_server_fut,
|
abs_redirect_server_fut,
|
||||||
|
|
Loading…
Reference in a new issue