ap: refactor CachedLoader
This commit is contained in:
parent
78fbbdfedd
commit
bda3a9027d
8 changed files with 191 additions and 248 deletions
|
@ -1,111 +1,35 @@
|
|||
// This file is an adaptation of
|
||||
// This file was originally based on
|
||||
// <https://github.com/timothee-haudebourg/json-ld/blob/0.12.1/core/src/loader/reqwest/content_type.rs>.
|
||||
//
|
||||
// Copyright (C) 2022 Timothée Haudebourg et al.
|
||||
// Licensed under either the Apache License, Version 2.0; or the MIT License.
|
||||
// See <https://github.com/timothee-haudebourg/json-ld/tree/0.12.1> for details.
|
||||
//
|
||||
// Modified by anna <owo@fef.moe> to accept the `application/activity+json`
|
||||
// MIME type as proper JSON-LD.
|
||||
// Heavily modified by anna <owo@fef.moe> to use nyanoblog's own Structured Header
|
||||
// parser and accept the `application/activity+json` MIME type as proper JSON-LD.
|
||||
|
||||
use hashbrown::HashMap;
|
||||
use mime::Mime;
|
||||
use reqwest::header::HeaderValue;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::util::header::{Item, ParseHeader};
|
||||
|
||||
/// Helper structure for parsing the `Content-Type` header for JSON-LD.
|
||||
pub struct ContentType {
|
||||
mime: Mime,
|
||||
params: HashMap<Vec<u8>, Vec<u8>>,
|
||||
profile: Option<String>,
|
||||
}
|
||||
|
||||
impl ContentType {
|
||||
pub fn from_header(value: &HeaderValue) -> Option<ContentType> {
|
||||
enum State {
|
||||
Mime,
|
||||
NextParam,
|
||||
BeginKey,
|
||||
Key,
|
||||
BeginValue,
|
||||
QuotedValue,
|
||||
Value,
|
||||
}
|
||||
|
||||
let mut state = State::Mime;
|
||||
let mut mime = Vec::new();
|
||||
let mut current_key = Vec::new();
|
||||
let mut current_value = Vec::new();
|
||||
let mut params = HashMap::new();
|
||||
|
||||
let mut bytes = value.as_bytes().iter();
|
||||
|
||||
loop {
|
||||
match state {
|
||||
State::Mime => match bytes.next().copied() {
|
||||
Some(b';') => state = State::BeginKey,
|
||||
Some(b) => mime.push(b),
|
||||
None => break,
|
||||
},
|
||||
State::NextParam => match bytes.next().copied() {
|
||||
Some(b';') => state = State::BeginKey,
|
||||
Some(_) => return None,
|
||||
None => break,
|
||||
},
|
||||
State::BeginKey => match bytes.next().copied() {
|
||||
Some(b' ') => {}
|
||||
Some(b) => {
|
||||
current_key.push(b);
|
||||
state = State::Key;
|
||||
}
|
||||
None => return None,
|
||||
},
|
||||
State::Key => match bytes.next().copied() {
|
||||
Some(b'=') => state = State::BeginValue,
|
||||
Some(b) => current_key.push(b),
|
||||
None => return None,
|
||||
},
|
||||
State::BeginValue => match bytes.next().copied() {
|
||||
Some(b'"') => state = State::QuotedValue,
|
||||
Some(b) => {
|
||||
state = State::Value;
|
||||
current_value.push(b);
|
||||
}
|
||||
_ => return None,
|
||||
},
|
||||
State::QuotedValue => match bytes.next().copied() {
|
||||
Some(b'"') => {
|
||||
params.insert(
|
||||
std::mem::take(&mut current_key),
|
||||
std::mem::take(&mut current_value),
|
||||
);
|
||||
state = State::NextParam;
|
||||
}
|
||||
Some(b) => current_value.push(b),
|
||||
None => return None,
|
||||
},
|
||||
State::Value => match bytes.next().copied() {
|
||||
Some(b';') => {
|
||||
params.insert(
|
||||
std::mem::take(&mut current_key),
|
||||
std::mem::take(&mut current_value),
|
||||
);
|
||||
state = State::BeginKey;
|
||||
}
|
||||
Some(b) => current_value.push(b),
|
||||
None => {
|
||||
params.insert(
|
||||
std::mem::take(&mut current_key),
|
||||
std::mem::take(&mut current_value),
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Mime::from_str(std::str::from_utf8(&mime).ok()?)
|
||||
.map(|mime| ContentType { mime, params })
|
||||
.ok()
|
||||
let item = Item::parse_from_header(value, true).ok()?;
|
||||
let mime = Mime::from_str(item.as_token()?).ok()?;
|
||||
let profile = item.param("profile").and_then(|profile| {
|
||||
profile
|
||||
.as_string()
|
||||
.or_else(|| profile.as_token().map(String::from))
|
||||
});
|
||||
Some(ContentType { mime, profile })
|
||||
}
|
||||
|
||||
pub fn is_json_ld(&self) -> bool {
|
||||
|
@ -114,14 +38,11 @@ impl ContentType {
|
|||
"application/ld+json",
|
||||
"application/json",
|
||||
]
|
||||
.iter()
|
||||
.any(|mime| *mime == self.mime)
|
||||
.contains(&self.mime.as_ref())
|
||||
}
|
||||
|
||||
pub fn is_proper_json_ld(&self) -> bool {
|
||||
["application/activity+json", "application/ld+json"]
|
||||
.iter()
|
||||
.any(|mime| *mime == self.mime)
|
||||
["application/activity+json", "application/ld+json"].contains(&self.mime.as_ref())
|
||||
}
|
||||
|
||||
pub fn mime(&self) -> &Mime {
|
||||
|
@ -132,8 +53,8 @@ impl ContentType {
|
|||
self.mime
|
||||
}
|
||||
|
||||
pub fn profile(&self) -> Option<&[u8]> {
|
||||
self.params.get(b"profile".as_slice()).map(Vec::as_slice)
|
||||
pub fn profile(&self) -> Option<&str> {
|
||||
self.profile.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -153,7 +74,7 @@ mod tests {
|
|||
assert_eq!(*content_type.mime(), "application/ld+json");
|
||||
assert_eq!(
|
||||
content_type.profile(),
|
||||
Some(b"http://www.w3.org/ns/json-ld#expanded".as_slice())
|
||||
Some("http://www.w3.org/ns/json-ld#expanded")
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -169,7 +90,7 @@ mod tests {
|
|||
assert_eq!(*content_type.mime(), "application/ld+json");
|
||||
assert_eq!(
|
||||
content_type.profile(),
|
||||
Some(b"http://www.w3.org/ns/json-ld#expanded".as_slice())
|
||||
Some("http://www.w3.org/ns/json-ld#expanded")
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -185,7 +106,7 @@ mod tests {
|
|||
assert_eq!(*content_type.mime(), "application/ld+json");
|
||||
assert_eq!(
|
||||
content_type.profile(),
|
||||
Some(b"http://www.w3.org/ns/json-ld#expanded".as_slice())
|
||||
Some("http://www.w3.org/ns/json-ld#expanded")
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -201,7 +122,7 @@ mod tests {
|
|||
assert_eq!(*content_type.mime(), "application/ld+json");
|
||||
assert_eq!(
|
||||
content_type.profile(),
|
||||
Some(b"http://www.w3.org/ns/json-ld#expanded".as_slice())
|
||||
Some("http://www.w3.org/ns/json-ld#expanded")
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -217,7 +138,7 @@ mod tests {
|
|||
assert_eq!(*content_type.mime(), "application/ld+json");
|
||||
assert_eq!(
|
||||
content_type.profile(),
|
||||
Some(b"http://www.w3.org/ns/json-ld#expanded".as_slice())
|
||||
Some("http://www.w3.org/ns/json-ld#expanded")
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -233,7 +154,7 @@ mod tests {
|
|||
assert_eq!(*content_type.mime(), "application/ld+json");
|
||||
assert_eq!(
|
||||
content_type.profile(),
|
||||
Some(b"http://www.w3.org/ns/json-ld#expanded".as_slice())
|
||||
Some("http://www.w3.org/ns/json-ld#expanded")
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -243,10 +164,7 @@ mod tests {
|
|||
assert_eq!(*content_type.mime(), "application/ld+json");
|
||||
assert_eq!(
|
||||
content_type.profile(),
|
||||
Some(
|
||||
b"http://www.w3.org/ns/json-ld#flattened http://www.w3.org/ns/json-ld#compacted"
|
||||
.as_slice()
|
||||
)
|
||||
Some("http://www.w3.org/ns/json-ld#flattened http://www.w3.org/ns/json-ld#compacted")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,110 +1,49 @@
|
|||
// This file is an adaptation of
|
||||
// This file was originally based on
|
||||
// <https://github.com/timothee-haudebourg/json-ld/blob/0.12.1/core/src/loader/reqwest/link.rs>.
|
||||
//
|
||||
// Copyright (C) 2022 Timothée Haudebourg et al.
|
||||
// Licensed under either the Apache License, Version 2.0; or the MIT License.
|
||||
// See <https://github.com/timothee-haudebourg/json-ld/tree/0.12.1> for details.
|
||||
//
|
||||
// Heavily modified by anna <owo@fef.moe> to use nyanoblog's own Structured Header
|
||||
// parser and accept the `application/activity+json` MIME type as proper JSON-LD.
|
||||
|
||||
use iref::{IriRef, IriRefBuf};
|
||||
use reqwest::header::HeaderValue;
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::util::header::{Item, ParseHeader};
|
||||
|
||||
pub struct Link {
|
||||
href: IriRefBuf,
|
||||
params: HashMap<Vec<u8>, Vec<u8>>,
|
||||
rel: Option<String>,
|
||||
typ: Option<String>,
|
||||
}
|
||||
|
||||
impl Link {
|
||||
pub fn from_header(value: &HeaderValue) -> Option<Self> {
|
||||
enum State {
|
||||
BeginHref,
|
||||
Href,
|
||||
NextParam,
|
||||
BeginKey,
|
||||
Key,
|
||||
BeginValue,
|
||||
Value,
|
||||
}
|
||||
|
||||
let mut state = State::BeginHref;
|
||||
let mut href = Vec::new();
|
||||
let mut current_key = Vec::new();
|
||||
let mut current_value = Vec::new();
|
||||
let mut params = HashMap::new();
|
||||
|
||||
let mut bytes = value.as_bytes().iter();
|
||||
|
||||
loop {
|
||||
match state {
|
||||
State::BeginHref => match bytes.next().copied() {
|
||||
Some(b'<') => state = State::Href,
|
||||
_ => break None,
|
||||
},
|
||||
State::Href => match bytes.next().copied() {
|
||||
Some(b'>') => state = State::NextParam,
|
||||
Some(b) => {
|
||||
href.push(b);
|
||||
}
|
||||
None => break None,
|
||||
},
|
||||
State::NextParam => match bytes.next().copied() {
|
||||
Some(b';') => state = State::BeginKey,
|
||||
Some(_) => break None,
|
||||
None => {
|
||||
break match IriRefBuf::from_vec(href) {
|
||||
Ok(href) => Some(Self { href, params }),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
},
|
||||
State::BeginKey => match bytes.next().copied() {
|
||||
Some(b' ') => {}
|
||||
Some(b) => {
|
||||
current_key.push(b);
|
||||
state = State::Key
|
||||
}
|
||||
None => break None,
|
||||
},
|
||||
State::Key => match bytes.next().copied() {
|
||||
Some(b'=') => state = State::BeginValue,
|
||||
Some(b) => current_key.push(b),
|
||||
None => break None,
|
||||
},
|
||||
State::BeginValue => match bytes.next().copied() {
|
||||
Some(b'"') => state = State::Value,
|
||||
_ => break None,
|
||||
},
|
||||
State::Value => match bytes.next().copied() {
|
||||
Some(b'"') => {
|
||||
params.insert(
|
||||
std::mem::take(&mut current_key),
|
||||
std::mem::take(&mut current_value),
|
||||
);
|
||||
|
||||
state = State::NextParam
|
||||
}
|
||||
Some(b) => current_value.push(b),
|
||||
None => break None,
|
||||
},
|
||||
}
|
||||
}
|
||||
let item = Item::parse_from_header(value, false).unwrap();
|
||||
let href = IriRefBuf::from_str(item.as_url()?).ok()?;
|
||||
let rel = item.param("rel").and_then(|rel| rel.as_string());
|
||||
let typ = item.param("type").and_then(|typ| typ.as_string());
|
||||
Some(Link { href, rel, typ })
|
||||
}
|
||||
|
||||
pub fn href(&self) -> IriRef {
|
||||
self.href.as_iri_ref()
|
||||
}
|
||||
|
||||
pub fn rel(&self) -> Option<&[u8]> {
|
||||
self.params.get(b"rel".as_slice()).map(Vec::as_slice)
|
||||
pub fn rel(&self) -> Option<&str> {
|
||||
self.rel.as_deref()
|
||||
}
|
||||
|
||||
pub fn typ(&self) -> Option<&[u8]> {
|
||||
self.params.get(b"type".as_slice()).map(Vec::as_slice)
|
||||
pub fn typ(&self) -> Option<&str> {
|
||||
self.typ.as_deref()
|
||||
}
|
||||
|
||||
pub fn is_proper_json_ld(&self) -> bool {
|
||||
self.typ()
|
||||
.map(|typ| typ == b"application/activity+json" || typ == b"application/ld+json")
|
||||
.map(|typ| ["application/activity+json", "application/ld+json"].contains(&typ))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
@ -123,16 +62,16 @@ mod tests {
|
|||
)
|
||||
.unwrap();
|
||||
assert_eq!(link.href(), "http://www.example.org/context");
|
||||
assert_eq!(link.rel(), Some(b"context".as_slice()));
|
||||
assert_eq!(link.typ(), Some(b"application/ld+json".as_slice()))
|
||||
assert_eq!(link.rel(), Some("context"));
|
||||
assert_eq!(link.typ(), Some("application/ld+json"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_link_2() {
|
||||
let link = Link::from_header(&HeaderValue::from_str("<http://www.example.org/context>; rel=\"context\"; type=\"application/ld+json\"; foo=\"bar\"").unwrap()).unwrap();
|
||||
assert_eq!(link.href(), "http://www.example.org/context");
|
||||
assert_eq!(link.rel(), Some(b"context".as_slice()));
|
||||
assert_eq!(link.typ(), Some(b"application/ld+json".as_slice()))
|
||||
assert_eq!(link.rel(), Some("context"));
|
||||
assert_eq!(link.typ(), Some("application/ld+json"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -13,18 +13,19 @@
|
|||
use crate::state::AppState;
|
||||
use bytes::Bytes;
|
||||
use futures::future::{BoxFuture, FutureExt};
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use hashbrown::HashSet;
|
||||
use iref::{Iri, IriBuf};
|
||||
use json_ld::{syntax::Parse, Loader, LoadingResult, Profile, RemoteDocument, Value};
|
||||
use json_ld::{
|
||||
syntax::{Parse, Value},
|
||||
Loader, Profile, RemoteDocument,
|
||||
};
|
||||
use locspan::{Meta, Span};
|
||||
use mime::Mime;
|
||||
use rdf_types::vocabulary::Index;
|
||||
use rdf_types::{IriVocabulary, IriVocabularyMut};
|
||||
use reqwest::header::{HeaderValue, ACCEPT, CONTENT_TYPE, LINK, LOCATION};
|
||||
use reqwest::StatusCode;
|
||||
use reqwest::{
|
||||
header::{ACCEPT, CONTENT_TYPE, LINK, LOCATION},
|
||||
StatusCode,
|
||||
};
|
||||
use std::hash::Hash;
|
||||
use std::str::FromStr;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
mod content_type;
|
||||
use content_type::*;
|
||||
|
@ -34,8 +35,9 @@ use link::*;
|
|||
use crate::core::*;
|
||||
use crate::headers;
|
||||
use crate::util::http;
|
||||
use crate::util::http::Response;
|
||||
|
||||
pub struct CachedLoader<I = IriBuf, M = Span, T = json_ld::syntax::Value<M>> {
|
||||
pub struct CachedLoader<I = IriBuf, M = Span, T = Value<M>> {
|
||||
state: AppState,
|
||||
parser: Box<DynParser<I, M, T>>,
|
||||
}
|
||||
|
@ -94,7 +96,7 @@ impl<I: Clone + Eq + Hash + Send + Sync, M: Send, T: Clone + Send> CachedLoader<
|
|||
mut url: I,
|
||||
) -> Result<RemoteDocument<I, M, T>> {
|
||||
const MAX_REDIRECTS: u32 = 8;
|
||||
const ACCEPT_ACTIVITY_PUB: &'static str =
|
||||
const ACCEPT_ACTIVITY_PUB: &str =
|
||||
"application/activity+json, application/ld+json, application/json";
|
||||
|
||||
let mut redirect_count = 0;
|
||||
|
@ -129,18 +131,16 @@ impl<I: Clone + Eq + Hash + Send + Sync, M: Send, T: Clone + Send> CachedLoader<
|
|||
context_url = self.get_context_url(vocabulary, &url, &response)?;
|
||||
}
|
||||
|
||||
let mut profile = HashSet::new();
|
||||
for p in content_type
|
||||
let profile: HashSet<Profile<I>> = content_type
|
||||
.profile()
|
||||
.into_iter()
|
||||
.flat_map(|p| p.split(|b| *b == b' '))
|
||||
{
|
||||
if let Ok(iri) = Iri::new(p) {
|
||||
profile.insert(Profile::new(iri, vocabulary));
|
||||
}
|
||||
}
|
||||
.flat_map(|profile| profile.split(' '))
|
||||
.filter_map(|p| {
|
||||
Iri::new(p).ok().map(|iri| Profile::new(iri, vocabulary))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let bytes = response.bytes().await.map_err(|e| Error::Reqwest(e))?;
|
||||
let bytes = response.bytes().await?;
|
||||
let document = (*self.parser)(vocabulary, &url, bytes)
|
||||
.map_err(|e| Error::MalformedApub(e.to_string()))?;
|
||||
|
||||
|
@ -156,8 +156,7 @@ impl<I: Clone + Eq + Hash + Send + Sync, M: Send, T: Clone + Send> CachedLoader<
|
|||
debug!(target: "ap", "no valid media type found");
|
||||
for link in response.headers().get_all(LINK).into_iter() {
|
||||
if let Some(link) = Link::from_header(link) {
|
||||
if link.rel() == Some(b"alternate") && link.is_proper_json_ld()
|
||||
{
|
||||
if link.rel() == Some("alternate") && link.is_proper_json_ld() {
|
||||
debug!(target: "ap", "link found");
|
||||
let u = link.href().resolved(vocabulary.iri(&url).unwrap());
|
||||
url = vocabulary.insert(u.as_iri());
|
||||
|
@ -204,13 +203,13 @@ impl<I: Clone + Eq + Hash + Send + Sync, M: Send, T: Clone + Send> CachedLoader<
|
|||
&self,
|
||||
vocabulary: &mut (impl Send + Sync + IriVocabularyMut<Iri = I>),
|
||||
url: &I,
|
||||
response: &reqwest::Response,
|
||||
response: &Response,
|
||||
) -> Result<Option<I>> {
|
||||
let mut context_url = None;
|
||||
for link in response.headers().get_all(LINK).into_iter() {
|
||||
if let Some(link) = Link::from_header(link) {
|
||||
if link.rel() == Some(b"alternate")
|
||||
&& link.typ() == Some(b"https://www.w3.org/ns/json-ld#context")
|
||||
if link.rel() == Some("alternate")
|
||||
&& link.typ() == Some("https://www.w3.org/ns/json-ld#context")
|
||||
{
|
||||
if context_url.is_some() {
|
||||
return Err(Error::MalformedApub(String::from(
|
||||
|
|
|
@ -14,6 +14,7 @@ pub type Result<T> = std::result::Result<T, Error>;
|
|||
pub enum Error {
|
||||
BadBearcap,
|
||||
BadCredentials,
|
||||
BadHeader(String),
|
||||
BadRequest,
|
||||
BadSignature,
|
||||
BadToken(jsonwebtoken::errors::Error),
|
||||
|
@ -33,6 +34,7 @@ impl ResponseError for Error {
|
|||
match self {
|
||||
Error::BadBearcap => StatusCode::UNPROCESSABLE_ENTITY,
|
||||
Error::BadCredentials => StatusCode::UNAUTHORIZED,
|
||||
Error::BadHeader(_) => StatusCode::BAD_REQUEST,
|
||||
Error::BadRequest => StatusCode::BAD_REQUEST,
|
||||
Error::BadSignature => StatusCode::UNAUTHORIZED,
|
||||
Error::BadToken(_) => StatusCode::UNAUTHORIZED,
|
||||
|
@ -55,6 +57,7 @@ impl fmt::Display for Error {
|
|||
match self {
|
||||
Error::BadBearcap => write!(f, "Invalid bearcap URL"),
|
||||
Error::BadCredentials => write!(f, "Invalid user name or password"),
|
||||
Error::BadHeader(name) => write!(f, "Value for header \"{name}\" is invalid"),
|
||||
Error::BadRequest => write!(f, "Bad request"),
|
||||
Error::BadSignature => write!(f, "Bad signature"),
|
||||
Error::BadToken(jwt_error) => jwt_error.fmt(f),
|
||||
|
|
|
@ -6,11 +6,15 @@ use crate::core::*;
|
|||
use crate::util::slice::SliceCursor;
|
||||
use crate::util::transcode;
|
||||
|
||||
/// Parse an HTTP Structured Field Value according to
|
||||
/// [RFC 8941](https://www.rfc-editor.org/info/rfc8941).
|
||||
/// Note: This only parses one "line" although the RFC says conforming
|
||||
/// software MUST support values split over several headers.
|
||||
pub trait ParseHeader<'a>: Sized {
|
||||
fn parse_from_ascii(header: &'a [u8]) -> Result<Self>;
|
||||
fn parse_from_ascii(header: &'a [u8], strict: bool) -> Result<Self>;
|
||||
|
||||
fn parse_from_header(header: &'a HeaderValue) -> Result<Self> {
|
||||
Self::parse_from_ascii(header.as_bytes())
|
||||
fn parse_from_header(header: &'a HeaderValue, strict: bool) -> Result<Self> {
|
||||
Self::parse_from_ascii(header.as_bytes(), strict)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,6 +48,7 @@ pub enum BareItem<'a> {
|
|||
Decimal(f32),
|
||||
String(StringItem<'a>),
|
||||
Token(TokenItem<'a>),
|
||||
Url(UrlItem<'a>),
|
||||
ByteSequence(ByteSequenceItem<'a>),
|
||||
Boolean(bool),
|
||||
}
|
||||
|
@ -53,11 +58,13 @@ pub struct StringItem<'a>(&'a str);
|
|||
#[derive(Debug, PartialEq)]
|
||||
pub struct TokenItem<'a>(&'a str);
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct UrlItem<'a>(&'a str);
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ByteSequenceItem<'a>(&'a str);
|
||||
|
||||
impl<'a> ParseHeader<'a> for Dictionary<'a> {
|
||||
fn parse_from_ascii(header: &'a [u8]) -> Result<Self> {
|
||||
Parser::new(header)?.parse_dictionary()
|
||||
fn parse_from_ascii(header: &'a [u8], strict: bool) -> Result<Self> {
|
||||
Parser::new(header, strict)?.parse_dictionary()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,8 +79,8 @@ impl<'a> Dictionary<'a> {
|
|||
}
|
||||
|
||||
impl<'a> ParseHeader<'a> for List<'a> {
|
||||
fn parse_from_ascii(header: &'a [u8]) -> Result<Self> {
|
||||
Parser::new(header)?.parse_list()
|
||||
fn parse_from_ascii(header: &'a [u8], strict: bool) -> Result<Self> {
|
||||
Parser::new(header, strict)?.parse_list()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,12 +99,16 @@ impl<'a> List<'a> {
|
|||
}
|
||||
|
||||
impl<'a> ParseHeader<'a> for Item<'a> {
|
||||
fn parse_from_ascii(header: &'a [u8]) -> Result<Self> {
|
||||
Parser::new(header)?.parse_item()
|
||||
fn parse_from_ascii(header: &'a [u8], strict: bool) -> Result<Self> {
|
||||
Parser::new(header, strict)?.parse_item(!strict)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Item<'a> {
|
||||
pub fn get_params(&self) -> &[(&'a str, BareItem<'a>)] {
|
||||
self.params.as_slice()
|
||||
}
|
||||
|
||||
pub fn param<K>(&self, key: K) -> Option<&BareItem<'a>>
|
||||
where
|
||||
K: Into<&'a str>,
|
||||
|
@ -132,6 +143,10 @@ impl<'a> Item<'a> {
|
|||
self.bare_item.as_token()
|
||||
}
|
||||
|
||||
pub fn as_url(&self) -> Option<&'a str> {
|
||||
self.bare_item.as_url()
|
||||
}
|
||||
|
||||
pub fn as_byte_sequence(&self) -> Option<Bytes> {
|
||||
self.bare_item.as_byte_sequence()
|
||||
}
|
||||
|
@ -170,6 +185,13 @@ impl<'a> BareItem<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn as_url(&self) -> Option<&'a str> {
|
||||
match self {
|
||||
BareItem::Url(u) => Some(u.0),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_byte_sequence(&self) -> Option<Bytes> {
|
||||
match self {
|
||||
BareItem::ByteSequence(bs) => Some(bs.into()),
|
||||
|
@ -186,6 +208,13 @@ impl<'a> BareItem<'a> {
|
|||
}
|
||||
|
||||
impl<'a> Member<'a> {
|
||||
pub fn get_params(&self) -> &[(&'a str, BareItem<'a>)] {
|
||||
match self {
|
||||
Member::Item(i) => i.get_params(),
|
||||
Member::InnerList(l) => l.get_params(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn param<K>(&self, key: K) -> Option<&BareItem<'a>>
|
||||
where
|
||||
K: Into<&'a str>,
|
||||
|
@ -269,6 +298,10 @@ impl<'a> InnerList<'a> {
|
|||
self.items.len()
|
||||
}
|
||||
|
||||
pub fn get_params(&self) -> &[(&'a str, BareItem<'a>)] {
|
||||
self.params.as_slice()
|
||||
}
|
||||
|
||||
pub fn param<K>(&self, key: K) -> Option<&BareItem<'a>>
|
||||
where
|
||||
K: Into<&'a str>,
|
||||
|
@ -290,13 +323,15 @@ impl<'a> InnerList<'a> {
|
|||
|
||||
struct Parser<'a> {
|
||||
cursor: SliceCursor<'a, u8>,
|
||||
strict: bool,
|
||||
}
|
||||
|
||||
impl<'a> Parser<'a> {
|
||||
fn new(data: &'a [u8]) -> Result<Parser> {
|
||||
if data.is_ascii() {
|
||||
fn new(data: &'a [u8], strict: bool) -> Result<Parser> {
|
||||
if data.is_ascii() || (std::str::from_utf8(data).is_ok() && !strict) {
|
||||
Ok(Parser {
|
||||
cursor: SliceCursor::new(data),
|
||||
strict,
|
||||
})
|
||||
} else {
|
||||
Err(Error::BadHeader(String::from(
|
||||
|
@ -335,7 +370,7 @@ impl<'a> Parser<'a> {
|
|||
if self.cursor.peek().copied() == Some(b'(') {
|
||||
self.parse_inner_list().map(Member::InnerList)
|
||||
} else {
|
||||
self.parse_item().map(Member::Item)
|
||||
self.parse_item(false).map(Member::Item)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -377,7 +412,7 @@ impl<'a> Parser<'a> {
|
|||
if self.cursor.peek().copied() == Some(b'(') {
|
||||
Member::InnerList(self.parse_inner_list()?)
|
||||
} else {
|
||||
Member::Item(self.parse_item()?)
|
||||
Member::Item(self.parse_item(false)?)
|
||||
}
|
||||
} else {
|
||||
// parameters
|
||||
|
@ -403,7 +438,7 @@ impl<'a> Parser<'a> {
|
|||
if self.skip_if(|c| c == b')') {
|
||||
break;
|
||||
}
|
||||
items.push(self.parse_item()?);
|
||||
items.push(self.parse_item(false)?);
|
||||
// > Parsers MUST support Inner Lists containing at least 256 members.
|
||||
if items.len() == 256 {
|
||||
break;
|
||||
|
@ -424,8 +459,8 @@ impl<'a> Parser<'a> {
|
|||
/// ```notrust
|
||||
/// sf-item = bare-item parameters
|
||||
/// ```
|
||||
fn parse_item(&mut self) -> Result<Item<'a>> {
|
||||
let bare_item = self.parse_bare_item()?;
|
||||
fn parse_item(&mut self, allow_url: bool) -> Result<Item<'a>> {
|
||||
let bare_item = self.parse_bare_item(allow_url)?;
|
||||
let params = self.parse_parameters()?;
|
||||
Ok(Item { bare_item, params })
|
||||
}
|
||||
|
@ -457,7 +492,7 @@ impl<'a> Parser<'a> {
|
|||
fn parse_parameter(&mut self) -> Result<(&'a str, BareItem<'a>)> {
|
||||
let key = self.parse_key()?;
|
||||
let value = if self.skip_if(|c| c == b'=') {
|
||||
self.parse_bare_item()?
|
||||
self.parse_bare_item(false)?
|
||||
} else {
|
||||
BareItem::Boolean(true)
|
||||
};
|
||||
|
@ -484,7 +519,7 @@ impl<'a> Parser<'a> {
|
|||
/// bare-item = sf-integer / sf-decimal / sf-string
|
||||
/// / sf-token / sf-binary / sf-boolean
|
||||
/// ```
|
||||
fn parse_bare_item(&mut self) -> Result<BareItem<'a>> {
|
||||
fn parse_bare_item(&mut self, allow_url: bool) -> Result<BareItem<'a>> {
|
||||
match self
|
||||
.cursor
|
||||
.peek()
|
||||
|
@ -493,10 +528,11 @@ impl<'a> Parser<'a> {
|
|||
{
|
||||
c if is_numeric_start(c) => self.parse_numeric(),
|
||||
b'"' => self.parse_string(),
|
||||
b'<' if allow_url => self.parse_url(),
|
||||
c if is_token_start(c) => self.parse_token(),
|
||||
b':' => self.parse_byte_sequence(),
|
||||
b'?' => self.parse_boolean(),
|
||||
_ => Err(self.make_error("Unexpected character")),
|
||||
c => Err(self.make_error(format!("Unexpected character {:?}", c as char))),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -550,6 +586,21 @@ impl<'a> Parser<'a> {
|
|||
Ok(BareItem::String(StringItem(slice)))
|
||||
}
|
||||
|
||||
fn parse_url(&mut self) -> Result<BareItem<'a>> {
|
||||
if self.strict {
|
||||
return Err(
|
||||
self.make_error("URLs enclosed in <angle brackets> are forbidden in strict mode")
|
||||
);
|
||||
}
|
||||
|
||||
self.assert_next(|c| c == b'<')?;
|
||||
self.chop();
|
||||
self.skip_while(|c| c != b'>');
|
||||
let slice = self.chop();
|
||||
self.assert_next(|c| c == b'>')?;
|
||||
Ok(BareItem::Url(UrlItem(slice)))
|
||||
}
|
||||
|
||||
/// Parse a Token item (section 3.3.4).
|
||||
///
|
||||
/// ```notrust
|
||||
|
@ -736,15 +787,15 @@ mod tests {
|
|||
use crate::util::transcode::base64_decode;
|
||||
|
||||
fn mklist(header: &'static str) -> Result<List<'static>> {
|
||||
List::parse_from_ascii(header.as_bytes())
|
||||
List::parse_from_ascii(header.as_bytes(), true)
|
||||
}
|
||||
|
||||
fn mkdict(header: &'static str) -> Result<Dictionary<'static>> {
|
||||
Dictionary::parse_from_ascii(header.as_bytes())
|
||||
Dictionary::parse_from_ascii(header.as_bytes(), true)
|
||||
}
|
||||
|
||||
fn mkitem(header: &'static str) -> Result<Item<'static>> {
|
||||
Item::parse_from_ascii(header.as_bytes())
|
||||
Item::parse_from_ascii(header.as_bytes(), true)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
use reqwest::header::{HeaderName, AUTHORIZATION, USER_AGENT};
|
||||
use reqwest::{RequestBuilder, Response};
|
||||
use bytes::Bytes;
|
||||
use reqwest::header::{HeaderMap, HeaderName, AUTHORIZATION, USER_AGENT};
|
||||
use reqwest::{RequestBuilder, StatusCode};
|
||||
|
||||
use crate::core::*;
|
||||
use crate::state::AppState;
|
||||
use crate::util::bear::Bearcap;
|
||||
|
||||
pub enum Response {
|
||||
Reqwest(reqwest::Response),
|
||||
}
|
||||
|
||||
/// Perform an HTTP GET request to the specified URL (supports bearcaps).
|
||||
/// Use the [`crate::headers`] macro for the `headers` parameter.
|
||||
///
|
||||
|
@ -39,7 +44,7 @@ use crate::util::bear::Bearcap;
|
|||
/// let response = get(&state, "bear:?t=b4dc0ffee&u=https://www.example.com", None).await?;
|
||||
/// ```
|
||||
pub async fn get(
|
||||
state: &AppState,
|
||||
_state: &AppState,
|
||||
url: &str,
|
||||
headers: Option<&[(GenericHeaderName<'_>, String)]>,
|
||||
) -> Result<Response> {
|
||||
|
@ -52,15 +57,15 @@ pub async fn get(
|
|||
} else {
|
||||
client.get(url)
|
||||
};
|
||||
perform_request(state, builder, headers.unwrap_or(&[])).await
|
||||
|
||||
let response = perform_request(builder, headers.unwrap_or(&[])).await?;
|
||||
Ok(Response::Reqwest(response))
|
||||
}
|
||||
|
||||
// the state reference will be used for caching
|
||||
async fn perform_request(
|
||||
_state: &AppState,
|
||||
mut builder: RequestBuilder,
|
||||
headers: &[(GenericHeaderName<'_>, String)],
|
||||
) -> Result<Response> {
|
||||
) -> Result<reqwest::Response> {
|
||||
builder = headers.iter().fold(builder, |b, (k, v)| match k {
|
||||
GenericHeaderName::Standard(hdr) => b.header(hdr, v),
|
||||
GenericHeaderName::CustomOwned(hdr) => b.header(hdr, v),
|
||||
|
@ -70,6 +75,26 @@ async fn perform_request(
|
|||
Ok(builder.send().await?)
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn status(&self) -> StatusCode {
|
||||
match self {
|
||||
Response::Reqwest(r) => r.status(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
match self {
|
||||
Response::Reqwest(r) => r.headers(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn bytes(self) -> Result<Bytes> {
|
||||
match self {
|
||||
Response::Reqwest(r) => r.bytes().await.map_err(Error::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a list of HTTP request headers for passing to [`get`].
|
||||
#[macro_export]
|
||||
macro_rules! headers {
|
||||
|
@ -106,3 +131,13 @@ impl<'a> From<HeaderName> for GenericHeaderName<'a> {
|
|||
GenericHeaderName::Standard(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<str> for GenericHeaderName<'_> {
|
||||
fn eq(&self, other: &str) -> bool {
|
||||
match self {
|
||||
GenericHeaderName::CustomOwned(s) => s.eq(other),
|
||||
GenericHeaderName::CustomBorrow(s) => (*s).eq(other),
|
||||
GenericHeaderName::Standard(v) => v.as_str().eq(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
pub mod bear;
|
||||
pub mod crypto;
|
||||
/// Almost complete implementation of [RFC 8941](https://www.rfc-editor.org/rfc/rfc8941).
|
||||
/// Almost complete implementation of [RFC 8941](https://www.rfc-editor.org/info/rfc8941).
|
||||
pub mod header;
|
||||
/// Wrappers for [`reqwest`].
|
||||
pub mod http;
|
||||
|
|
|
@ -25,9 +25,7 @@ impl<'a, T> SliceCursor<'a, T> {
|
|||
chop: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> SliceCursor<'a, T> {
|
||||
/// Return the item at the current position, if any.
|
||||
pub fn current(&self) -> Option<&'a T> {
|
||||
self.pos.index().map(|index| &self.data[index])
|
||||
|
@ -43,7 +41,7 @@ impl<'a, T> SliceCursor<'a, T> {
|
|||
self.pos.next_index().map(|index| &self.data[index])
|
||||
}
|
||||
|
||||
/// Return the current item and reverse the cursor.
|
||||
/// Reverse the cursor to the previous item and return that item.
|
||||
pub fn prev(&mut self) -> Option<&'a T> {
|
||||
self.pos.reverse().map(|index| &self.data[index])
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue