mirror of
https://github.com/servo/servo.git
synced 2025-07-24 15:50:21 +01:00
convert net crate to use hyper
This commit is contained in:
parent
92a8c7a80c
commit
12727d4dd0
10 changed files with 164 additions and 93 deletions
|
@ -13,8 +13,8 @@ path = "../util"
|
|||
[dependencies.geom]
|
||||
git = "https://github.com/servo/rust-geom"
|
||||
|
||||
[dependencies.http]
|
||||
git = "https://github.com/servo/rust-http"
|
||||
[dependencies.hyper]
|
||||
git = "https://github.com/hyperium/hyper"
|
||||
branch = "servo"
|
||||
|
||||
[dependencies.png]
|
||||
|
|
|
@ -7,7 +7,7 @@ use file_loader;
|
|||
|
||||
use std::io::fs::PathExtensions;
|
||||
use url::Url;
|
||||
use http::status::Ok as StatusOk;
|
||||
use hyper::http::RawStatus;
|
||||
use servo_util::resource_files::resources_dir_path;
|
||||
|
||||
|
||||
|
@ -23,7 +23,7 @@ pub fn factory(mut load_data: LoadData, start_chan: Sender<TargetedLoadResponse>
|
|||
content_type: Some(("text".to_string(), "html".to_string())),
|
||||
charset: Some("utf-8".to_string()),
|
||||
headers: None,
|
||||
status: Some(StatusOk),
|
||||
status: Some(RawStatus(200, "OK".into_string()))
|
||||
});
|
||||
chan.send(Done(Ok(())));
|
||||
return
|
||||
|
|
|
@ -6,8 +6,7 @@ use resource_task::{Done, Payload, Metadata, LoadData, TargetedLoadResponse, sta
|
|||
|
||||
use serialize::base64::FromBase64;
|
||||
|
||||
use http::headers::test_utils::from_stream_with_str;
|
||||
use http::headers::content_type::MediaType;
|
||||
use hyper::mime::Mime;
|
||||
use url::{percent_decode, NonRelativeSchemeData};
|
||||
|
||||
|
||||
|
@ -59,8 +58,8 @@ fn load(load_data: LoadData, start_chan: Sender<TargetedLoadResponse>) {
|
|||
|
||||
// Parse the content type using rust-http.
|
||||
// FIXME: this can go into an infinite loop! (rust-http #25)
|
||||
let content_type: Option<MediaType> = from_stream_with_str(ct_str);
|
||||
metadata.set_content_type(&content_type);
|
||||
let content_type: Option<Mime> = from_str(ct_str);
|
||||
metadata.set_content_type(content_type.as_ref());
|
||||
|
||||
let progress_chan = start_sending(senders, metadata);
|
||||
let bytes = percent_decode(parts[1].as_bytes());
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
//! This library will eventually become the core of the Fetch crate
|
||||
//! with CORSRequest being expanded into FetchRequest (etc)
|
||||
|
||||
use http::method::Method;
|
||||
use hyper::method::Method;
|
||||
use std::ascii::AsciiExt;
|
||||
use std::comm::{Sender, Receiver, channel};
|
||||
use time;
|
||||
|
|
|
@ -3,8 +3,10 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use url::Url;
|
||||
use http::method::{Get, Method};
|
||||
use http::headers::request::HeaderCollection;
|
||||
use hyper::method::{Get, Method};
|
||||
use hyper::mime::{Mime, Text, Html, Charset, Utf8};
|
||||
use hyper::header::Headers;
|
||||
use hyper::header::common::ContentType;
|
||||
use fetch::cors_cache::CORSCache;
|
||||
use fetch::response::Response;
|
||||
|
||||
|
@ -58,7 +60,7 @@ pub enum ResponseTainting {
|
|||
pub struct Request {
|
||||
pub method: Method,
|
||||
pub url: Url,
|
||||
pub headers: HeaderCollection,
|
||||
pub headers: Headers,
|
||||
pub unsafe_request: bool,
|
||||
pub body: Option<Vec<u8>>,
|
||||
pub preserve_content_codings: bool,
|
||||
|
@ -87,7 +89,7 @@ impl Request {
|
|||
Request {
|
||||
method: Get,
|
||||
url: url,
|
||||
headers: HeaderCollection::new(),
|
||||
headers: Headers::new(),
|
||||
unsafe_request: false,
|
||||
body: None,
|
||||
preserve_content_codings: false,
|
||||
|
@ -116,7 +118,7 @@ impl Request {
|
|||
"about" => match self.url.non_relative_scheme_data() {
|
||||
Some(s) if s.as_slice() == "blank" => {
|
||||
let mut response = Response::new();
|
||||
let _ = response.headers.insert_raw("Content-Type".to_string(), b"text/html;charset=utf-8");
|
||||
response.headers.set(ContentType(Mime(Text, Html, vec![(Charset, Utf8)])));
|
||||
response
|
||||
},
|
||||
_ => Response::network_error()
|
||||
|
|
|
@ -3,11 +3,10 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use url::Url;
|
||||
use http::status::{Status, UnregisteredStatus};
|
||||
use http::status::Ok as StatusOk;
|
||||
use http::headers::HeaderEnum;
|
||||
use http::headers::response::HeaderCollection;
|
||||
use std::ascii::OwnedAsciiExt;
|
||||
use hyper::status::StatusCode;
|
||||
use hyper::status::Ok as StatusOk;
|
||||
use hyper::header::Headers;
|
||||
use std::ascii::AsciiExt;
|
||||
use std::comm::Receiver;
|
||||
|
||||
/// [Response type](http://fetch.spec.whatwg.org/#concept-response-type)
|
||||
|
@ -57,8 +56,9 @@ pub struct Response {
|
|||
pub response_type: ResponseType,
|
||||
pub termination_reason: Option<TerminationReason>,
|
||||
pub url: Option<Url>,
|
||||
pub status: Status,
|
||||
pub headers: HeaderCollection,
|
||||
/// `None` can be considered a StatusCode of `0`.
|
||||
pub status: Option<StatusCode>,
|
||||
pub headers: Headers,
|
||||
pub body: ResponseBody,
|
||||
/// [Internal response](http://fetch.spec.whatwg.org/#concept-internal-response), only used if the Response is a filtered response
|
||||
pub internal_response: Option<Box<Response>>,
|
||||
|
@ -70,8 +70,8 @@ impl Response {
|
|||
response_type: Default,
|
||||
termination_reason: None,
|
||||
url: None,
|
||||
status: StatusOk,
|
||||
headers: HeaderCollection::new(),
|
||||
status: Some(StatusOk),
|
||||
headers: Headers::new(),
|
||||
body: Empty,
|
||||
internal_response: None
|
||||
}
|
||||
|
@ -82,8 +82,8 @@ impl Response {
|
|||
response_type: Error,
|
||||
termination_reason: None,
|
||||
url: None,
|
||||
status: UnregisteredStatus(0, "".to_string()),
|
||||
headers: HeaderCollection::new(),
|
||||
status: None,
|
||||
headers: Headers::new(),
|
||||
body: Empty,
|
||||
internal_response: None
|
||||
}
|
||||
|
@ -110,32 +110,30 @@ impl Response {
|
|||
match filter_type {
|
||||
Default | Error => unreachable!(),
|
||||
Basic => {
|
||||
let mut headers = HeaderCollection::new();
|
||||
for h in old_headers.iter() {
|
||||
match h.header_name().into_ascii_lower().as_slice() {
|
||||
"set-cookie" | "set-cookie2" => {},
|
||||
_ => headers.insert(h)
|
||||
let headers = old_headers.iter().filter(|header| {
|
||||
match header.name().to_ascii_lower().as_slice() {
|
||||
"set-cookie" | "set-cookie2" => false,
|
||||
_ => true
|
||||
}
|
||||
}
|
||||
}).collect();
|
||||
response.headers = headers;
|
||||
response.response_type = filter_type;
|
||||
},
|
||||
CORS => {
|
||||
let mut headers = HeaderCollection::new();
|
||||
for h in old_headers.iter() {
|
||||
match h.header_name().into_ascii_lower().as_slice() {
|
||||
let headers = old_headers.iter().filter(|header| {
|
||||
match header.name().to_ascii_lower().as_slice() {
|
||||
"cache-control" | "content-language" |
|
||||
"content-type" | "expires" | "last-modified" | "Pragma" => {},
|
||||
"content-type" | "expires" | "last-modified" | "Pragma" => false,
|
||||
// XXXManishearth handle Access-Control-Expose-Headers
|
||||
_ => headers.insert(h)
|
||||
_ => true
|
||||
}
|
||||
}
|
||||
}).collect();
|
||||
response.headers = headers;
|
||||
response.response_type = filter_type;
|
||||
},
|
||||
Opaque => {
|
||||
response.headers = HeaderCollection::new();
|
||||
response.status = UnregisteredStatus(0, "".to_string());
|
||||
response.headers = Headers::new();
|
||||
response.status = None;
|
||||
response.body = Empty;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,11 +6,13 @@ use resource_task::{Metadata, Payload, Done, TargetedLoadResponse, LoadData, sta
|
|||
|
||||
use log;
|
||||
use std::collections::HashSet;
|
||||
use http::client::{RequestWriter, NetworkStream};
|
||||
use http::headers::HeaderEnum;
|
||||
use hyper::client::Request;
|
||||
use hyper::header::common::{ContentLength, ContentType, Host, Location};
|
||||
use hyper::method::{Get, Head};
|
||||
use hyper::status::Redirection;
|
||||
use std::io::Reader;
|
||||
use servo_util::task::spawn_named;
|
||||
use url::Url;
|
||||
use url::{Url, UrlParser};
|
||||
|
||||
pub fn factory(load_data: LoadData, start_chan: Sender<TargetedLoadResponse>) {
|
||||
spawn_named("http_loader", proc() load(load_data, start_chan))
|
||||
|
@ -67,55 +69,75 @@ fn load(load_data: LoadData, start_chan: Sender<TargetedLoadResponse>) {
|
|||
|
||||
info!("requesting {:s}", url.serialize());
|
||||
|
||||
let request = RequestWriter::<NetworkStream>::new(load_data.method.clone(), url.clone());
|
||||
let mut writer = match request {
|
||||
Ok(w) => box w,
|
||||
let mut req = match Request::new(load_data.method.clone(), url.clone()) {
|
||||
Ok(req) => req,
|
||||
Err(e) => {
|
||||
send_error(url, e.desc.to_string(), senders);
|
||||
send_error(url, e.to_string(), senders);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Preserve the `host` header set automatically by RequestWriter.
|
||||
let host = writer.headers.host.clone();
|
||||
writer.headers = load_data.headers.clone();
|
||||
writer.headers.host = host;
|
||||
if writer.headers.accept_encoding.is_none() {
|
||||
// Preserve the `host` header set automatically by Request.
|
||||
let host = req.headers().get::<Host>().unwrap().clone();
|
||||
*req.headers_mut() = load_data.headers.clone();
|
||||
req.headers_mut().set(host);
|
||||
// FIXME(seanmonstar): use AcceptEncoding from Hyper once available
|
||||
//if !req.headers.has::<AcceptEncoding>() {
|
||||
// We currently don't support HTTP Compression (FIXME #2587)
|
||||
writer.headers.accept_encoding = Some(String::from_str("identity".as_slice()))
|
||||
}
|
||||
match load_data.data {
|
||||
req.headers_mut().set_raw("Accept-Encoding", vec![b"identity".to_vec()]);
|
||||
//}
|
||||
let writer = match load_data.data {
|
||||
Some(ref data) => {
|
||||
writer.headers.content_length = Some(data.len());
|
||||
req.headers_mut().set(ContentLength(data.len()));
|
||||
let mut writer = match req.start() {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
send_error(url, e.to_string(), senders);
|
||||
return;
|
||||
}
|
||||
};
|
||||
match writer.write(data.as_slice()) {
|
||||
Err(e) => {
|
||||
send_error(url, e.desc.to_string(), senders);
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
writer
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
let mut response = match writer.read_response() {
|
||||
None => {
|
||||
match load_data.method {
|
||||
Get | Head => (),
|
||||
_ => req.headers_mut().set(ContentLength(0))
|
||||
}
|
||||
match req.start() {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
send_error(url, e.to_string(), senders);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut response = match writer.send() {
|
||||
Ok(r) => r,
|
||||
Err((_, e)) => {
|
||||
send_error(url, e.desc.to_string(), senders);
|
||||
Err(e) => {
|
||||
send_error(url, e.to_string(), senders);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Dump headers, but only do the iteration if info!() is enabled.
|
||||
info!("got HTTP response {:s}, headers:", response.status.to_string());
|
||||
info!("got HTTP response {}, headers:", response.status);
|
||||
if log_enabled!(log::INFO) {
|
||||
for header in response.headers.iter() {
|
||||
info!(" - {:s}: {:s}", header.header_name(), header.header_value());
|
||||
info!(" - {}", header);
|
||||
}
|
||||
}
|
||||
|
||||
if 3 == (response.status.code() / 100) {
|
||||
match response.headers.location {
|
||||
Some(new_url) => {
|
||||
if response.status.class() == Redirection {
|
||||
match response.headers.get::<Location>() {
|
||||
Some(&Location(ref new_url)) => {
|
||||
// CORS (http://fetch.spec.whatwg.org/#http-fetch, status section, point 9, 10)
|
||||
match load_data.cors {
|
||||
Some(ref c) => {
|
||||
|
@ -130,7 +152,14 @@ fn load(load_data: LoadData, start_chan: Sender<TargetedLoadResponse>) {
|
|||
}
|
||||
_ => {}
|
||||
}
|
||||
info!("redirecting to {:s}", new_url.serialize());
|
||||
let new_url = match UrlParser::new().base_url(&url).parse(new_url.as_slice()) {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
send_error(url, e.to_string(), senders);
|
||||
return;
|
||||
}
|
||||
};
|
||||
info!("redirecting to {}", new_url);
|
||||
url = new_url;
|
||||
continue;
|
||||
}
|
||||
|
@ -139,9 +168,12 @@ fn load(load_data: LoadData, start_chan: Sender<TargetedLoadResponse>) {
|
|||
}
|
||||
|
||||
let mut metadata = Metadata::default(url);
|
||||
metadata.set_content_type(&response.headers.content_type);
|
||||
metadata.set_content_type(match response.headers.get() {
|
||||
Some(&ContentType(ref mime)) => Some(mime),
|
||||
None => None
|
||||
});
|
||||
metadata.headers = Some(response.headers.clone());
|
||||
metadata.status = Some(response.status.clone());
|
||||
metadata.status = Some(response.status_raw().clone());
|
||||
|
||||
let progress_chan = match start_sending_opt(senders, metadata) {
|
||||
Ok(p) => p,
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
extern crate collections;
|
||||
extern crate geom;
|
||||
extern crate http;
|
||||
extern crate hyper;
|
||||
extern crate png;
|
||||
#[phase(plugin, link)]
|
||||
extern crate log;
|
||||
|
|
|
@ -12,14 +12,13 @@ use sniffer_task;
|
|||
use sniffer_task::SnifferTask;
|
||||
|
||||
use std::comm::{channel, Receiver, Sender};
|
||||
use http::headers::content_type::MediaType;
|
||||
use http::headers::response::HeaderCollection as ResponseHeaderCollection;
|
||||
use http::headers::request::HeaderCollection as RequestHeaderCollection;
|
||||
use http::method::{Method, Get};
|
||||
use hyper::mime::{Mime, Charset};
|
||||
use hyper::header::Headers;
|
||||
use hyper::header::common::UserAgent;
|
||||
use hyper::method::{Method, Get};
|
||||
use url::Url;
|
||||
|
||||
use http::status::Ok as StatusOk;
|
||||
use http::status::Status;
|
||||
use hyper::http::RawStatus;
|
||||
|
||||
use servo_util::task::spawn_named;
|
||||
|
||||
|
@ -33,7 +32,7 @@ pub enum ControlMsg {
|
|||
pub struct LoadData {
|
||||
pub url: Url,
|
||||
pub method: Method,
|
||||
pub headers: RequestHeaderCollection,
|
||||
pub headers: Headers,
|
||||
pub data: Option<Vec<u8>>,
|
||||
pub cors: Option<ResourceCORSData>,
|
||||
pub consumer: Sender<LoadResponse>,
|
||||
|
@ -44,7 +43,7 @@ impl LoadData {
|
|||
LoadData {
|
||||
url: url,
|
||||
method: Get,
|
||||
headers: RequestHeaderCollection::new(),
|
||||
headers: Headers::new(),
|
||||
data: None,
|
||||
cors: None,
|
||||
consumer: consumer,
|
||||
|
@ -72,10 +71,10 @@ pub struct Metadata {
|
|||
pub charset: Option<String>,
|
||||
|
||||
/// Headers
|
||||
pub headers: Option<ResponseHeaderCollection>,
|
||||
pub headers: Option<Headers>,
|
||||
|
||||
/// HTTP Status
|
||||
pub status: Option<Status>
|
||||
pub status: Option<RawStatus>
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
|
@ -86,21 +85,19 @@ impl Metadata {
|
|||
content_type: None,
|
||||
charset: None,
|
||||
headers: None,
|
||||
status: Some(StatusOk) // http://fetch.spec.whatwg.org/#concept-response-status-message
|
||||
status: Some(RawStatus(200, "OK".into_string())) // http://fetch.spec.whatwg.org/#concept-response-status-message
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the parts of a MediaType that we care about.
|
||||
pub fn set_content_type(&mut self, content_type: &Option<MediaType>) {
|
||||
match *content_type {
|
||||
/// Extract the parts of a Mime that we care about.
|
||||
pub fn set_content_type(&mut self, content_type: Option<&Mime>) {
|
||||
match content_type {
|
||||
None => (),
|
||||
Some(MediaType { ref type_,
|
||||
ref subtype,
|
||||
ref parameters }) => {
|
||||
self.content_type = Some((type_.clone(), subtype.clone()));
|
||||
Some(&Mime(ref type_, ref subtype, ref parameters)) => {
|
||||
self.content_type = Some((type_.to_string(), subtype.to_string()));
|
||||
for &(ref k, ref v) in parameters.iter() {
|
||||
if "charset" == k.as_slice() {
|
||||
self.charset = Some(v.clone());
|
||||
if &Charset == k {
|
||||
self.charset = Some(v.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -224,7 +221,7 @@ impl ResourceManager {
|
|||
|
||||
fn load(&self, load_data: LoadData) {
|
||||
let mut load_data = load_data;
|
||||
load_data.headers.user_agent = self.user_agent.clone();
|
||||
self.user_agent.map(|ref ua| load_data.headers.set(UserAgent(ua.clone())));
|
||||
let senders = ResponseSenders {
|
||||
immediate_consumer: self.sniffer_task.clone(),
|
||||
eventual_consumer: load_data.consumer.clone(),
|
||||
|
|
47
components/servo/Cargo.lock
generated
47
components/servo/Cargo.lock
generated
|
@ -87,6 +87,15 @@ dependencies = [
|
|||
"util 0.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/alexcrichton/cookie-rs#9b579dd9b8cf0624eee1d013e9b48577acd3c40e"
|
||||
dependencies = [
|
||||
"openssl 0.0.0 (git+https://github.com/sfackler/rust-openssl.git)",
|
||||
"url 0.1.0 (git+https://github.com/servo/rust-url)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core_foundation"
|
||||
version = "0.1.0"
|
||||
|
@ -353,6 +362,20 @@ dependencies = [
|
|||
"url 0.1.0 (git+https://github.com/servo/rust-url)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/hyperium/hyper?ref=servo#414a302f6333abd4b2ae38ea328bc41f8ca8fdbe"
|
||||
dependencies = [
|
||||
"cookie 0.0.1 (git+https://github.com/alexcrichton/cookie-rs)",
|
||||
"mime 0.0.1 (git+https://github.com/hyperium/mime.rs)",
|
||||
"move-acceptor 0.0.1 (git+https://github.com/reem/rust-move-acceptor)",
|
||||
"openssl 0.0.0 (git+https://github.com/sfackler/rust-openssl.git)",
|
||||
"typeable 0.0.1 (git+https://github.com/reem/rust-typeable)",
|
||||
"unsafe-any 0.1.0 (git+https://github.com/reem/rust-unsafe-any)",
|
||||
"url 0.1.0 (git+https://github.com/servo/rust-url)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io_surface"
|
||||
version = "0.1.0"
|
||||
|
@ -427,6 +450,16 @@ name = "lazy_static"
|
|||
version = "0.1.0"
|
||||
source = "git+https://github.com/Kimundi/lazy-static.rs#62976cb611c5396e11315ae64c9c389576240eb7"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/hyperium/mime.rs#467c271814d51659f12de88f87dcd3dc3280ee9b"
|
||||
|
||||
[[package]]
|
||||
name = "move-acceptor"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/reem/rust-move-acceptor#25c5c33a83f605fdd0f3d37d2589e2b0b4e6cbd1"
|
||||
|
||||
[[package]]
|
||||
name = "mozjs-sys"
|
||||
version = "0.0.0"
|
||||
|
@ -451,7 +484,7 @@ name = "net"
|
|||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"geom 0.1.0 (git+https://github.com/servo/rust-geom)",
|
||||
"http 0.1.0-pre (git+https://github.com/servo/rust-http?ref=servo)",
|
||||
"hyper 0.0.1 (git+https://github.com/hyperium/hyper?ref=servo)",
|
||||
"png 0.1.0 (git+https://github.com/servo/rust-png)",
|
||||
"stb_image 0.1.0 (git+https://github.com/servo/rust-stb-image)",
|
||||
"url 0.1.0 (git+https://github.com/servo/rust-url)",
|
||||
|
@ -512,7 +545,7 @@ dependencies = [
|
|||
"geom 0.1.0 (git+https://github.com/servo/rust-geom)",
|
||||
"gfx 0.0.1",
|
||||
"html5ever 0.0.0 (git+https://github.com/servo/html5ever?ref=servo)",
|
||||
"http 0.1.0-pre (git+https://github.com/servo/rust-http?ref=servo)",
|
||||
"hyper 0.0.1 (git+https://github.com/hyperium/hyper?ref=servo)",
|
||||
"js 0.1.0 (git+https://github.com/servo/rust-mozjs)",
|
||||
"msg 0.0.1",
|
||||
"net 0.0.1",
|
||||
|
@ -596,6 +629,16 @@ dependencies = [
|
|||
name = "task_info"
|
||||
version = "0.0.1"
|
||||
|
||||
|
||||
name = "typeable"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/reem/rust-typeable#55154e1809db8ceec8f8519bdbb638c2fbd712f5"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-any"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/reem/rust-unsafe-any#2863af363bbd83079b6773920bba5b736408db33"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "0.1.0"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue