This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-xrpc / src / lib.rs
21 kB 653 lines
1mod blocklist; 2mod body; 3mod branches; 4mod cob; 5mod collaborators; 6mod error; 7mod events; 8mod forks; 9pub mod legacy_admin; 10mod lfs; 11mod lists; 12mod locks; 13mod members; 14mod merge; 15mod patchtext; 16mod query; 17mod reads; 18mod receive; 19mod repos; 20mod reservations; 21mod service; 22mod sniff; 23mod wire; 24 25#[cfg(test)] 26mod tests; 27 28pub use error::XrpcError; 29pub use knot_pack::MaxWireBytes; 30pub use knot_postreceive::LanguagesPushBudget; 31pub use knot_resource::{ 32 Burst, GlobalInflight, LimitConfig, PerPeerInflight, PreAuthLimiter, RateLimit, RefillMicros, 33}; 34pub use lfs::LfsWeb; 35pub use locks::CobLocks; 36pub use merge::Committer; 37pub use receive::advertiser as receive_advertiser; 38pub use reservations::{GlobalQuota, PerActorQuota, ReservationTtl, Reservations}; 39 40use std::collections::BTreeSet; 41use std::net::IpAddr; 42use std::path::PathBuf; 43use std::sync::Arc; 44use std::time::{Duration, Instant}; 45 46use axum::Json; 47use axum::Router; 48use axum::body::Bytes; 49use axum::extract::{DefaultBodyLimit, FromRequestParts, MatchedPath, Request, State}; 50use axum::middleware::{Next, from_fn_with_state}; 51use axum::response::{IntoResponse, Response}; 52use axum::routing::{get, post}; 53use http::request::Parts; 54use http::{HeaderMap, HeaderValue, StatusCode, header::AUTHORIZATION}; 55use serde::de::DeserializeOwned; 56use serde_json::json; 57 58use knot_atproto::{Atproto, AtprotoError, ServiceJwt}; 59use knot_events::{EventLog, SubscriberGate}; 60pub use knot_git::ArchiveLimit; 61use knot_git::Layout; 62use knot_index::{Index, Resolved}; 63use knot_maintenance::MaintenanceHandle; 64use knot_resource::Slots; 65use knot_runtime::{Clock, Entropy, HttpTransport}; 66use knot_secrets::SealedStore; 67use knot_types::{ 68 AccountDid, AdmissionPolicy, AppviewEndpoint, CiLogsAddr, ClonePath, KnotHostname, KnotId, 69 KnotServiceUrl, Nsid, OwnerDid, OwnerRef, RepoDid, UnixSeconds, 70}; 71 72use base64::Engine; 73use knot_pack::SocketPeer; 74use knot_resource::{AdmitGuard, Refusal}; 75 76pub(crate) const PUSH_NSID: &str = "sh.tangled.repo.push"; 77 78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 79pub enum ReadBudget { 80 Within(Duration), 81 Unbounded, 82} 83 84impl ReadBudget { 85 pub fn deadline(self) -> Option<Instant> { 86 match self { 87 ReadBudget::Within(budget) => Some(Instant::now() + budget), 88 ReadBudget::Unbounded => None, 89 } 90 } 91} 92 93// `XrpcState` keeps a bunch of these side by side, 94// some usize & some u64. 95// Within each group every one of them typechecked in every other one's slot. 96knot_types::scalar_newtype! { 97 pub struct BodyLimit(usize); 98 pub struct PatchLimit(usize); 99 pub struct PatchDecompressedLimit(u64); 100 pub struct ResponseLimit(usize); 101 pub struct ForkPackLimit(u64); 102 pub struct TreeReadBudget(ReadBudget); 103 pub struct BlobReadBudget(ReadBudget); 104 pub struct LanguagesReadBudget(ReadBudget); 105} 106 107#[derive(Debug, Clone, Copy, PartialEq, Eq)] 108pub struct ByteLimits { 109 pub body: BodyLimit, 110 pub patch: PatchLimit, 111 pub patch_decompressed: PatchDecompressedLimit, 112 pub response: ResponseLimit, 113 pub archive: ArchiveLimit, 114 pub fork_pack: ForkPackLimit, 115 pub pack: MaxWireBytes, 116} 117 118impl Default for ByteLimits { 119 fn default() -> Self { 120 Self { 121 body: BodyLimit::new(64 * 1024), 122 patch: PatchLimit::new(16 * 1024 * 1024), 123 patch_decompressed: PatchDecompressedLimit::new(128 * 1024 * 1024), 124 response: ResponseLimit::new(5 * 1024 * 1024), 125 archive: ArchiveLimit::default(), 126 fork_pack: ForkPackLimit::new(1024 * 1024 * 1024), 127 pack: MaxWireBytes::new(8 * 1024 * 1024 * 1024), 128 } 129 } 130} 131 132#[derive(Debug, Clone, Copy, PartialEq, Eq)] 133pub struct Budgets { 134 pub tree_last_commit: TreeReadBudget, 135 pub blob_last_commit: BlobReadBudget, 136 pub languages: LanguagesReadBudget, 137 pub languages_push: LanguagesPushBudget, 138} 139 140impl Default for Budgets { 141 fn default() -> Self { 142 Self { 143 tree_last_commit: TreeReadBudget::new(ReadBudget::Within(Duration::from_millis(300))), 144 blob_last_commit: BlobReadBudget::new(ReadBudget::Within(Duration::from_millis(2_000))), 145 languages: LanguagesReadBudget::new(ReadBudget::Within(Duration::from_millis(1_000))), 146 languages_push: LanguagesPushBudget::new(Duration::from_millis(2_000)), 147 } 148 } 149} 150 151pub struct XrpcState<H, C> { 152 pub layout: Layout, 153 pub index: Arc<Index>, 154 pub atproto: Arc<Atproto<H, C>>, 155 pub secrets: Arc<SealedStore>, 156 pub entropy: Arc<dyn Entropy>, 157 pub admins: BTreeSet<AccountDid>, 158 pub admission: AdmissionPolicy, 159 pub knot_did: KnotId, 160 pub knot_hostname: KnotHostname, 161 pub ci_logs: Option<CiLogsAddr>, 162 pub meta_path: PathBuf, 163 pub knot_service_url: KnotServiceUrl, 164 pub limiter: Arc<PreAuthLimiter>, 165 pub cob_locks: Arc<CobLocks>, 166 pub reservations: Arc<Reservations>, 167 pub proxy_trust: knot_types::ProxyTrust, 168 pub committer: Committer, 169 pub byte_limits: ByteLimits, 170 pub budgets: Budgets, 171 pub git_http: Arc<dyn HttpTransport>, 172 pub pack_limits: knot_pack::PackLimits, 173 pub service_owner: AccountDid, 174 pub events: Arc<EventLog<C>>, 175 pub subscriber_gate: Arc<SubscriberGate>, 176 pub maintenance: MaintenanceHandle, 177 pub appview: AppviewEndpoint, 178 pub slots: Slots, 179 pub lfs: Option<LfsWeb>, 180 pub catalog: Arc<knot_messages::Catalog>, 181} 182 183impl<H: HttpTransport, C: Clock> XrpcState<H, C> { 184 pub fn now(&self) -> UnixSeconds { 185 UnixSeconds::new((self.atproto.now().get() / 1_000_000) as i64) 186 } 187 188 pub(crate) fn knot_authority(&self) -> &str { 189 self.knot_service_url.authority() 190 } 191 192 pub(crate) async fn authenticate( 193 &self, 194 headers: &HeaderMap, 195 method: &Method, 196 ) -> Result<AccountDid, XrpcError> { 197 let token = bearer(headers)?; 198 self.atproto 199 .verify_service_jwt(&token, method.nsid()) 200 .await 201 .map_err(map_verify_error) 202 } 203 204 pub(crate) async fn authenticate_push( 205 &self, 206 headers: &HeaderMap, 207 ) -> Result<AccountDid, XrpcError> { 208 let token = push_credential(headers)?; 209 let method = Nsid::new_owned(PUSH_NSID).expect("push nsid is always a valid nsid"); 210 self.atproto 211 .verify_service_jwt_guarded( 212 &token, 213 &method, 214 knot_atproto::ReplayGuard::ReusableUntilExpiry, 215 ) 216 .await 217 .map_err(map_verify_error) 218 } 219} 220 221fn map_verify_error(error: AtprotoError) -> XrpcError { 222 if error.is_transient() { 223 XrpcError::upstream_unavailable(error.to_string()) 224 } else { 225 XrpcError::auth_required(error.to_string()) 226 } 227} 228 229pub(crate) struct Method(Nsid); 230 231impl Method { 232 fn nsid(&self) -> &Nsid { 233 &self.0 234 } 235 236 #[cfg(test)] 237 pub(crate) fn from_nsid(nsid: &str) -> Self { 238 Self(Nsid::new_owned(nsid).expect("test route nsid parses")) 239 } 240} 241 242impl<S: Send + Sync> FromRequestParts<S> for Method { 243 type Rejection = XrpcError; 244 245 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { 246 let matched = MatchedPath::from_request_parts(parts, state) 247 .await 248 .map_err(|_| XrpcError::internal("xrpc handler reached without a matched route"))?; 249 let nsid = matched 250 .as_str() 251 .strip_prefix("/xrpc/") 252 .ok_or_else(|| XrpcError::internal("xrpc route paths are prefixed with /xrpc/"))?; 253 Nsid::new_owned(nsid) 254 .map(Self) 255 .map_err(|_| XrpcError::internal("route nsid is always a valid nsid")) 256 } 257} 258 259pub fn router<H: HttpTransport, C: Clock>(state: Arc<XrpcState<H, C>>) -> Router { 260 let merge_routes = Router::new() 261 .route(merge::MERGE_ROUTE, post(merge::merge::<H, C>)) 262 .route(merge::MERGE_CHECK_ROUTE, post(merge::merge_check::<H, C>)) 263 .layer(DefaultBodyLimit::max(state.byte_limits.patch.get())); 264 Router::new() 265 .merge(merge_routes) 266 .route(members::ADD_ROUTE, post(members::add_member::<H, C>)) 267 .route(members::REMOVE_ROUTE, post(members::remove_member::<H, C>)) 268 .route(blocklist::BAN_ROUTE, post(blocklist::ban::<H, C>)) 269 .route(blocklist::UNBAN_ROUTE, post(blocklist::unban::<H, C>)) 270 .route( 271 collaborators::ADD_ROUTE, 272 post(collaborators::add_collaborator::<H, C>), 273 ) 274 .route( 275 collaborators::REMOVE_ROUTE, 276 post(collaborators::remove_collaborator::<H, C>), 277 ) 278 .route(repos::CREATE_ROUTE, post(repos::create_repo::<H, C>)) 279 .route(repos::DELETE_ROUTE, post(repos::delete_repo::<H, C>)) 280 .route(repos::RENAME_ROUTE, post(repos::rename_repo::<H, C>)) 281 .route(repos::RESERVE_ROUTE, post(repos::reserve_key::<H, C>)) 282 .route( 283 branches::SET_DEFAULT_ROUTE, 284 post(branches::set_default_branch::<H, C>), 285 ) 286 .route( 287 branches::DELETE_ROUTE, 288 post(branches::delete_branch::<H, C>), 289 ) 290 .route(forks::STATUS_ROUTE, post(forks::fork_status::<H, C>)) 291 .route(forks::SYNC_ROUTE, post(forks::fork_sync::<H, C>)) 292 .route(forks::HIDDEN_REF_ROUTE, post(forks::hidden_ref::<H, C>)) 293 .route(reads::TREE_ROUTE, get(reads::repo_tree::<H, C>)) 294 .route(reads::LOG_ROUTE, get(reads::repo_log::<H, C>)) 295 .route(reads::BRANCHES_ROUTE, get(reads::repo_branches::<H, C>)) 296 .route(reads::BRANCH_ROUTE, get(reads::repo_branch::<H, C>)) 297 .route(reads::TAGS_ROUTE, get(reads::repo_tags::<H, C>)) 298 .route(reads::TAG_ROUTE, get(reads::repo_tag::<H, C>)) 299 .route(reads::BLOB_ROUTE, get(reads::repo_blob::<H, C>)) 300 .route(reads::DIFF_ROUTE, get(reads::repo_diff::<H, C>)) 301 .route(reads::COMPARE_ROUTE, get(reads::repo_compare::<H, C>)) 302 .route(reads::ARCHIVE_ROUTE, get(reads::repo_archive::<H, C>)) 303 .route(reads::LANGUAGES_ROUTE, get(reads::repo_languages::<H, C>)) 304 .route( 305 reads::GET_DEFAULT_BRANCH_ROUTE, 306 get(reads::repo_get_default_branch::<H, C>), 307 ) 308 .route( 309 reads::DESCRIBE_REPO_ROUTE, 310 get(reads::repo_describe_repo::<H, C>), 311 ) 312 .route(reads::LIST_REFS_ROUTE, get(reads::git_list_refs::<H, C>)) 313 .route(reads::LIST_REPOS_ROUTE, get(reads::sync_list_repos::<H, C>)) 314 .route(lists::LIST_MEMBERS_ROUTE, get(lists::list_members::<H, C>)) 315 .route( 316 lists::LIST_COLLABORATORS_ROUTE, 317 get(lists::list_collaborators::<H, C>), 318 ) 319 .route(service::VERSION_ROUTE, get(service::version)) 320 .route(service::OWNER_ROUTE, get(service::owner::<H, C>)) 321 .layer(DefaultBodyLimit::max(state.byte_limits.body.get())) 322 .layer(from_fn_with_state( 323 Arc::clone(&state), 324 enforce_pre_auth_limit::<H, C>, 325 )) 326 .merge(lfs::routes::<H, C>()) 327 .merge(receive::routes::<H, C>()) 328 .route(service::HEALTH_ROUTE, get(service::health::<H, C>)) 329 .route(events::EVENTS_ROUTE, get(events::events::<H, C>)) 330 .with_state(state) 331} 332 333pub(crate) async fn enforce_pre_auth_limit<H: HttpTransport, C: Clock>( 334 State(state): State<Arc<XrpcState<H, C>>>, 335 socket: SocketPeer, 336 request: Request, 337 next: Next, 338) -> Response { 339 let peer = state 340 .proxy_trust 341 .client_peer(request.headers(), socket.ip()); 342 match admit_pre_auth(&state, peer) { 343 Ok(guard) => { 344 let response = next.run(request).await; 345 drop(guard); 346 response 347 } 348 Err(error) => error.into_response(), 349 } 350} 351 352pub(crate) fn admit_pre_auth<H: HttpTransport, C: Clock>( 353 state: &XrpcState<H, C>, 354 peer: Option<IpAddr>, 355) -> Result<AdmitGuard, XrpcError> { 356 state 357 .limiter 358 .admit(peer, state.atproto.now()) 359 .map_err(|refusal| match refusal { 360 Refusal::RateLimited => { 361 XrpcError::rate_limited("too many pre-authentication requests, retry shortly") 362 } 363 Refusal::Saturated => { 364 XrpcError::overloaded("knot is shedding pre-authentication load, retry shortly") 365 } 366 }) 367} 368 369pub(crate) const BASIC_CHALLENGE: HeaderValue = HeaderValue::from_static("Basic realm=\"knot\""); 370 371fn strip_bearer(value: &str) -> Option<&str> { 372 let (scheme, rest) = value.split_once(' ')?; 373 scheme.eq_ignore_ascii_case("Bearer").then_some(rest) 374} 375 376fn bearer(headers: &HeaderMap) -> Result<ServiceJwt, XrpcError> { 377 headers 378 .get(AUTHORIZATION) 379 .and_then(|value| value.to_str().ok()) 380 .and_then(strip_bearer) 381 .map(str::trim) 382 .and_then(|token| ServiceJwt::new(token).ok()) 383 .ok_or_else(|| XrpcError::auth_required("missing or malformed Bearer authorization header")) 384} 385 386pub(crate) struct BasicUser(String); 387 388impl BasicUser { 389 pub(crate) fn matches(&self, expected: &str) -> bool { 390 self.0 == expected 391 } 392} 393 394pub(crate) struct BasicPassword(String); 395 396impl BasicPassword { 397 pub(crate) fn as_bytes(&self) -> &[u8] { 398 self.0.as_bytes() 399 } 400} 401 402pub(crate) struct BasicCredentials { 403 pub(crate) user: BasicUser, 404 pub(crate) password: BasicPassword, 405} 406 407pub(crate) fn basic_credentials(value: &str) -> Option<BasicCredentials> { 408 let (scheme, rest) = value.split_once(' ')?; 409 if !scheme.eq_ignore_ascii_case("Basic") { 410 return None; 411 } 412 let decoded = base64::engine::general_purpose::STANDARD 413 .decode(rest.trim()) 414 .ok()?; 415 let text = String::from_utf8(decoded).ok()?; 416 let (user, password) = text.split_once(':')?; 417 Some(BasicCredentials { 418 user: BasicUser(user.to_string()), 419 password: BasicPassword(password.to_string()), 420 }) 421} 422 423fn strip_basic(value: &str) -> Option<String> { 424 basic_credentials(value) 425 .map(|credentials| credentials.password.0) 426 .filter(|password| !password.is_empty()) 427} 428 429fn push_credential(headers: &HeaderMap) -> Result<ServiceJwt, XrpcError> { 430 let value = headers 431 .get(AUTHORIZATION) 432 .and_then(|value| value.to_str().ok()) 433 .ok_or_else(|| XrpcError::auth_required("missing authorization header"))?; 434 strip_bearer(value) 435 .map(str::trim) 436 .map(str::to_string) 437 .or_else(|| strip_basic(value)) 438 .and_then(|token| ServiceJwt::new(token).ok()) 439 .ok_or_else(|| { 440 XrpcError::auth_required("authorization isn't a bearer token or basic credential") 441 }) 442} 443 444pub(crate) fn decode<T: DeserializeOwned>(body: &Bytes) -> Result<T, XrpcError> { 445 serde_json::from_slice(body) 446 .map_err(|error| XrpcError::invalid_request(format!("invalid request body: {error}"))) 447} 448 449pub(crate) fn ok_empty() -> Response { 450 (StatusCode::OK, Json(json!({}))).into_response() 451} 452 453pub(crate) fn current_owner<H: HttpTransport, C: Clock>( 454 state: &XrpcState<H, C>, 455 repo: &RepoDid, 456) -> Option<OwnerDid> { 457 match state.index.owner_of(repo) { 458 Resolved::Ready(owner) => owner, 459 Resolved::Warming => None, 460 } 461} 462 463pub(crate) async fn fold_collaborators<H: HttpTransport, C: Clock>( 464 state: &XrpcState<H, C>, 465 repo: &RepoDid, 466) { 467 let index = Arc::clone(&state.index); 468 let target = repo.clone(); 469 let _ = run_blocking(move || Ok(index.ensure_collaborators(&target))).await; 470} 471 472pub(crate) async fn authorize_push<H: HttpTransport, C: Clock>( 473 state: &XrpcState<H, C>, 474 actor: &AccountDid, 475 repo: &RepoDid, 476 denied: &str, 477) -> Result<(), XrpcError> { 478 fold_collaborators(state, repo).await; 479 let acl = knot_acl::KnotAcl::new(&state.admins, state.admission, &state.index); 480 if knot_acl::can_push(&acl, actor, repo).is_allowed() { 481 Ok(()) 482 } else { 483 Err(XrpcError::forbidden(denied)) 484 } 485} 486 487pub(crate) async fn authenticate_and_authorize_push<H: HttpTransport, C: Clock>( 488 state: &XrpcState<H, C>, 489 socket: SocketPeer, 490 headers: &HeaderMap, 491 repo: &RepoDid, 492 denied: &str, 493) -> Result<AccountDid, XrpcError> { 494 let peer = state.proxy_trust.client_peer(headers, socket.ip()); 495 let guard = admit_pre_auth(state, peer)?; 496 let actor = state.authenticate_push(headers).await?; 497 guard.refund(); 498 authorize_push(state, &actor, repo, denied).await?; 499 Ok(actor) 500} 501 502pub(crate) async fn run_blocking<T, F>(task: F) -> Result<T, XrpcError> 503where 504 F: FnOnce() -> Result<T, XrpcError> + Send + 'static, 505 T: Send + 'static, 506{ 507 match tokio::task::spawn_blocking(task).await { 508 Ok(result) => result, 509 Err(_) => Err(XrpcError::internal("blocking task failed to complete")), 510 } 511} 512 513#[derive(serde::Deserialize)] 514#[serde(transparent)] 515pub(crate) struct OwnerSegment(String); 516 517#[derive(serde::Deserialize)] 518#[serde(transparent)] 519pub(crate) struct RepoNameSegment(String); 520 521impl OwnerSegment { 522 pub(crate) fn as_str(&self) -> &str { 523 &self.0 524 } 525} 526 527impl RepoNameSegment { 528 pub(crate) fn as_str(&self) -> &str { 529 &self.0 530 } 531} 532 533#[derive(serde::Deserialize)] 534#[serde(transparent)] 535pub(crate) struct RepoDidSegment(String); 536 537impl RepoDidSegment { 538 pub(crate) fn as_str(&self) -> &str { 539 &self.0 540 } 541} 542 543#[derive(serde::Deserialize)] 544pub(crate) struct RepoPathParams { 545 pub(crate) did: OwnerSegment, 546 pub(crate) name: RepoNameSegment, 547} 548 549pub(crate) fn resolve_repo_did<H: HttpTransport, C: Clock>( 550 state: &XrpcState<H, C>, 551 segment: &RepoDidSegment, 552) -> Result<RepoDid, XrpcError> { 553 let raw = segment.as_str(); 554 let trimmed = raw.strip_suffix(".git").unwrap_or(raw); 555 let did = RepoDid::new(trimmed).map_err(|_| XrpcError::not_found("repository not found"))?; 556 match state.index.owner_of(&did) { 557 Resolved::Ready(Some(_)) => Ok(did), 558 Resolved::Ready(None) => Err(XrpcError::not_found("repository not found")), 559 Resolved::Warming => Err(XrpcError::warming( 560 "registry projection is still warming, retry shortly", 561 )), 562 } 563} 564 565pub(crate) async fn resolve_repo_named<H: HttpTransport, C: Clock>( 566 state: &XrpcState<H, C>, 567 owner: &OwnerSegment, 568 name: &RepoNameSegment, 569) -> Result<RepoDid, XrpcError> { 570 let owner = resolve_owner_segment(state, owner).await?; 571 let path = ClonePath::parse(name.as_str()) 572 .ok_or_else(|| XrpcError::not_found("repository not found"))?; 573 match state.index.resolve_clone_path(&owner, &path) { 574 Resolved::Ready(Some(did)) => Ok(did), 575 Resolved::Ready(None) => Err(XrpcError::not_found("repository not found")), 576 Resolved::Warming => Err(XrpcError::warming( 577 "registry projection is still warming, retry shortly", 578 )), 579 } 580} 581 582async fn resolve_owner_segment<H: HttpTransport, C: Clock>( 583 state: &XrpcState<H, C>, 584 owner: &OwnerSegment, 585) -> Result<OwnerDid, XrpcError> { 586 let not_found = || XrpcError::not_found("repository not found"); 587 match OwnerRef::parse(owner.as_str()).ok_or_else(not_found)? { 588 OwnerRef::Did(did) => Ok(did), 589 OwnerRef::Handle(handle) => state 590 .atproto 591 .resolve_handle_to_did(&handle) 592 .await 593 .map(OwnerDid::from) 594 .map_err(|_| not_found()), 595 } 596} 597 598#[cfg(test)] 599mod credential_tests { 600 use super::push_credential; 601 use base64::Engine; 602 use http::{HeaderMap, HeaderValue, header::AUTHORIZATION}; 603 604 fn with(value: &str) -> HeaderMap { 605 let mut headers = HeaderMap::new(); 606 headers.insert(AUTHORIZATION, HeaderValue::from_str(value).unwrap()); 607 headers 608 } 609 610 fn basic(user_pass: &str) -> String { 611 format!( 612 "Basic {}", 613 base64::engine::general_purpose::STANDARD.encode(user_pass) 614 ) 615 } 616 617 #[test] 618 fn a_bearer_token_is_taken_verbatim() { 619 assert_eq!( 620 push_credential(&with("Bearer jwt.abc.def")) 621 .unwrap() 622 .as_str(), 623 "jwt.abc.def" 624 ); 625 assert_eq!( 626 push_credential(&with("bearer jwt.abc.def")) 627 .unwrap() 628 .as_str(), 629 "jwt.abc.def" 630 ); 631 } 632 633 #[test] 634 fn a_basic_credential_yields_the_password_after_the_first_colon() { 635 assert_eq!( 636 push_credential(&with(&basic("x-tangled-token:jwt.abc.def"))) 637 .unwrap() 638 .as_str(), 639 "jwt.abc.def", 640 "RFC 7617 puts the token in the password half, so the username stays colon-free" 641 ); 642 } 643 644 #[test] 645 fn malformed_or_empty_credentials_are_rejected() { 646 assert!(push_credential(&HeaderMap::new()).is_err()); 647 assert!(push_credential(&with("Bearer ")).is_err()); 648 assert!(push_credential(&with(&basic("x-tangled-token:"))).is_err()); 649 assert!(push_credential(&with(&basic("no-colon"))).is_err()); 650 assert!(push_credential(&with("Basic !!!not-base64")).is_err()); 651 assert!(push_credential(&with("Digest whatever")).is_err()); 652 } 653}