blob: db3c49a44008a419e87ff09eedc88f1204a41420 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::bss_cache::{Bss, BssId},
async_trait::async_trait,
fidl::{Error as FidlError, Socket as FidlSocket},
fidl_fuchsia_location_position::{Position, PositionExtras},
fidl_fuchsia_mem::Buffer as MemBuffer,
fidl_fuchsia_net_http::{
Body as HttpBody, LoaderProxyInterface, Request as HttpRequest, Response as HttpResponse,
},
fuchsia_async::futures::io::AsyncReadExt,
fuchsia_async::futures::Future,
fuchsia_zircon as zx,
itertools::Itertools,
serde_json::{json, map::Map as JsonMap, value::Value as JsonValue},
static_assertions::assert_eq_size_val,
std::{borrow::Borrow, convert::TryFrom},
};
// Geographic constants.
const LATITUDE_RANGE: std::ops::RangeInclusive<f64> = -90.0..=90.0;
const LONGITUDE_RANGE: std::ops::RangeInclusive<f64> = -180.0..=180.0;
// Google Maps constants.
const SERVICE_URL: &'static str = "https://www.googleapis.com/geolocation/v1/geolocate";
const MAC_ADDR_KEY: &'static str = "macAddress";
const AP_LIST_KEY: &'static str = "wifiAccessPoints";
const RSSI_KEY: &'static str = "signalStrength";
const LOCATION_KEY: &'static str = "location";
const LATITUDE_KEY: &'static str = "lat";
const LONGITUDE_KEY: &'static str = "lng";
const ACCURACY_KEY: &'static str = "accuracy";
// HTTP constants.
const HTTP_METHOD_POST: &'static str = "POST";
#[async_trait(?Send)]
pub trait BssResolver {
/// Resolves WLAN BSS (base-station) meta-data to a `Position`.
async fn resolve<'a, I, T, U>(&self, bsses: I) -> Result<Position, ResolverError>
where
I: IntoIterator,
I::Item: Borrow<(T, U)>,
T: Borrow<BssId>,
U: Borrow<Bss>;
}
/// A service for resolving WLAN BSS (base-station) meta-data to `Position`s.
pub struct RealBssResolver<L: LoaderProxyInterface> {
http_loader: L,
api_key: String,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ResolverError {
NoBsses,
Internal,
Lookup,
}
impl<L: LoaderProxyInterface> RealBssResolver<L> {
pub fn new(http_loader: L, api_key: impl Into<String>) -> Self {
Self { http_loader, api_key: api_key.into() }
}
}
#[async_trait(?Send)]
impl<L: LoaderProxyInterface> BssResolver for RealBssResolver<L> {
async fn resolve<'a, I, T, U>(&self, bsses: I) -> Result<Position, ResolverError>
where
I: IntoIterator,
I::Item: Borrow<(T, U)>,
T: Borrow<BssId>,
U: Borrow<Bss>,
{
let mut bsses = bsses.into_iter().peekable();
if bsses.peek().is_none() {
return Err(ResolverError::NoBsses);
}
parse_response(send_query(bsses, &self.api_key, &self.http_loader)?.await).await
}
}
fn send_query<'a, I, T, U>(
bsses: I,
api_key: impl AsRef<str>,
http_loader: &impl LoaderProxyInterface,
) -> Result<impl Future<Output = Result<HttpResponse, FidlError>>, ResolverError>
where
I: Iterator,
I::Item: Borrow<(T, U)>,
T: Borrow<BssId>,
U: Borrow<Bss>,
{
let api_key = api_key.as_ref();
let request_body = serialize_bsses(bsses);
let request_size = {
let body_size = request_body.len();
assert_eq_size_val!(body_size, 0u64);
u64::try_from(body_size).expect("failed to convert usize to u64")
};
let request_vmo = zx::Vmo::create(request_size).map_err(|_| ResolverError::Internal)?;
let _ = request_vmo.write(&request_body.as_bytes(), 0).map_err(|_| ResolverError::Internal)?;
Ok(http_loader.fetch(HttpRequest {
method: Some(HTTP_METHOD_POST.to_owned()),
url: Some(format!("{}?key={}", SERVICE_URL, api_key)),
headers: None,
body: Some(HttpBody::Buffer(MemBuffer { vmo: request_vmo, size: request_size })),
deadline: None,
..HttpRequest::EMPTY
}))
}
async fn parse_response(
response: Result<HttpResponse, FidlError>,
) -> Result<Position, ResolverError> {
let response: HttpResponse = response.map_err(|_fidl_error| ResolverError::Internal)?;
let response: FidlSocket = response.body.ok_or(ResolverError::Lookup)?;
let response: String = read_socket(response).await.ok_or(ResolverError::Lookup)?;
json_to_position(serde_json::from_str(&response).map_err(|_| ResolverError::Lookup)?)
}
fn serialize_bsses<'a, I, T, U>(bsses: I) -> String
where
I: Iterator,
I::Item: Borrow<(T, U)>,
T: Borrow<BssId>,
U: Borrow<Bss>,
{
let bsses = bsses
.map(|item| bss_to_json(item.borrow().0.borrow(), item.borrow().1.borrow()))
.collect::<Vec<_>>();
json!({ AP_LIST_KEY: bsses }).to_string()
}
fn bss_to_json(bss_id: impl Borrow<BssId>, bss: impl Borrow<Bss>) -> JsonValue {
let mut json = JsonMap::new();
json.insert(
MAC_ADDR_KEY.to_owned(),
json!(bss_id.borrow().iter().map(|bss_byte| format!("{:02x}", bss_byte)).join(":")),
);
if let Some(rssi) = bss.borrow().rssi {
json.insert(RSSI_KEY.to_owned(), json!(rssi));
};
JsonValue::from(json)
}
async fn read_socket(socket: zx::Socket) -> Option<String> {
let mut buf = Vec::new();
match fuchsia_async::Socket::from_socket(socket).ok() {
Some(mut socket) => match socket.read_to_end(&mut buf).await {
Ok(_num_bytes_read) => String::from_utf8(buf).ok(),
Err(_) => None,
},
None => None,
}
}
fn json_to_position(response: JsonValue) -> Result<Position, ResolverError> {
let latitude = response[LOCATION_KEY][LATITUDE_KEY].as_f64().ok_or(ResolverError::Lookup)?;
if !LATITUDE_RANGE.contains(&latitude) {
return Err(ResolverError::Lookup);
}
let longitude = response[LOCATION_KEY][LONGITUDE_KEY].as_f64().ok_or(ResolverError::Lookup)?;
if !LONGITUDE_RANGE.contains(&longitude) {
return Err(ResolverError::Lookup);
}
let extras = match response[ACCURACY_KEY].as_f64() {
Some(accuracy) => PositionExtras {
accuracy_meters: Some(accuracy),
altitude_meters: None,
..PositionExtras::EMPTY
},
None => {
PositionExtras { accuracy_meters: None, altitude_meters: None, ..PositionExtras::EMPTY }
}
};
Ok(Position { latitude, longitude, extras })
}
#[cfg(test)]
mod tests {
mod request_generation {
use {
super::super::{test_doubles::HttpRequestValidator, *},
fuchsia_async as fasync,
};
#[fasync::run_until_stalled(test)]
async fn request_uses_post_method() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: Some(-30), frequency: Some(2412) })];
let mut request_method = None;
let http_loader = HttpRequestValidator::new(|request| {
request_method = Some(request.method.expect("request had no method"));
});
let _ = RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await;
assert_eq!(request_method.expect("request was never issued"), "POST".to_owned());
}
#[fasync::run_until_stalled(test)]
async fn request_url_is_correct() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: Some(-30), frequency: Some(2412) })];
let mut request_url = None;
let http_loader = HttpRequestValidator::new(|request| {
request_url = Some(request.url.expect("request had no url"));
});
let _ = RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await;
assert_eq!(
request_url.expect("request was never issued"),
"https://www.googleapis.com/geolocation/v1/geolocate?key=fake_key".to_owned()
);
}
#[fasync::run_until_stalled(test)]
async fn request_body_contains_all_bsses() {
let bsses = vec![
([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None }),
([1, 1, 1, 1, 1, 1], Bss { rssi: None, frequency: None }),
];
let mut request_body = None;
let http_loader = HttpRequestValidator::new(|request| {
request_body = Some(request.body.expect("request had no body"));
});
let _ = RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await;
assert_eq!(
strip_whitespace(read_request_body(
request_body.expect("request was never issued")
)),
strip_whitespace(
r#"{
"wifiAccessPoints":[
{"macAddress":"00:00:00:00:00:00"},
{"macAddress":"01:01:01:01:01:01"}
]
}"#
)
);
}
#[fasync::run_until_stalled(test)]
async fn request_body_includes_rssi() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: Some(-30), frequency: None })];
let mut request_body = None;
let http_loader = HttpRequestValidator::new(|request| {
request_body = Some(request.body.expect("request had no body"));
});
let _ = RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await;
assert_eq!(
strip_whitespace(read_request_body(
request_body.expect("request was never issued")
)),
strip_whitespace(
r#"{
"wifiAccessPoints":[
{"macAddress":"00:00:00:00:00:00",
"signalStrength": -30}
]
}"#
)
)
}
#[fasync::run_until_stalled(test)]
async fn skips_api_query_when_bsses_is_empty() {
let http_loader =
HttpRequestValidator::new(|_request| panic!("should not issue API query"));
let _ = RealBssResolver::new(http_loader, "fake_key")
.resolve(std::iter::empty::<(BssId, Bss)>())
.await;
}
#[fasync::run_until_stalled(test)]
async fn returns_no_bsses_error_when_bsses_is_empty() {
let http_loader = HttpRequestValidator::new(|_| ());
assert_eq!(
RealBssResolver::new(http_loader, "fake_key")
.resolve(std::iter::empty::<(BssId, Bss)>())
.await,
Err(ResolverError::NoBsses)
);
}
fn read_request_body(body: HttpBody) -> String {
match body {
HttpBody::Buffer(shared_buf) => {
let mut local_buf = vec![
0_u8;
usize::try_from(shared_buf.size).expect(
"internal error: failed to convert u64 to usize"
)
];
shared_buf
.vmo
.read(&mut local_buf, 0)
.expect("internal error: failed to read from VMO");
String::from_utf8(local_buf)
.expect("internal error: failed to convert buffer to UTF-8")
}
HttpBody::Stream(_) => panic!("internal error: stream bodies not supported"),
}
}
fn strip_whitespace<S: AsRef<str>>(input: S) -> String {
input.as_ref().replace("\n", "").replace(" ", "")
}
}
mod response_error_handling {
use {
super::super::{
test_doubles::{HttpByteResponder, HttpFidlResponder},
*,
},
fuchsia_async as fasync,
};
#[fasync::run_until_stalled(test)]
async fn returns_internal_error_on_fidl_error() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
let http_loader = HttpFidlResponder::new(|| Err(FidlError::InvalidHeader));
assert_eq!(
RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
Err(ResolverError::Internal)
);
}
#[fasync::run_until_stalled(test)]
async fn returns_lookup_error_when_response_has_no_body() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
let http_loader = HttpFidlResponder::new(|| {
Ok(HttpResponse {
error: None,
body: None,
final_url: None,
status_code: None,
status_line: None,
headers: None,
redirect: None,
..HttpResponse::EMPTY
})
});
assert_eq!(
RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
Err(ResolverError::Lookup)
);
}
#[fasync::run_until_stalled(test)]
async fn returns_lookup_error_when_body_is_unreadable() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
let http_loader = HttpFidlResponder::new(|| {
Ok(HttpResponse {
error: None,
body: Some(zx::Socket::from(zx::Handle::invalid())),
final_url: None,
status_code: None,
status_line: None,
headers: None,
redirect: None,
..HttpResponse::EMPTY
})
});
assert_eq!(
RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
Err(ResolverError::Lookup)
);
}
#[fasync::run_until_stalled(test)]
async fn returns_lookup_error_when_body_is_not_valid_json() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
let http_loader = HttpByteResponder::new(b"hello world".to_vec());
assert_eq!(
RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
Err(ResolverError::Lookup)
);
}
#[fasync::run_until_stalled(test)]
async fn returns_lookup_error_when_body_is_not_a_dictionary() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
let http_loader = HttpByteResponder::new(b"42".to_vec());
assert_eq!(
RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
Err(ResolverError::Lookup)
);
}
#[fasync::run_until_stalled(test)]
async fn returns_lookup_error_when_body_is_missing_location() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
let http_loader = HttpByteResponder::new(b"{}".to_vec());
assert_eq!(
RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
Err(ResolverError::Lookup)
);
}
#[fasync::run_until_stalled(test)]
async fn returns_lookup_error_when_body_is_missing_latitude() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
let http_loader = HttpByteResponder::new(
br#"{
"location": {
"lng": -0.1
},
"accuracy": 1200.4
}"#
.to_vec(),
);
assert_eq!(
RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
Err(ResolverError::Lookup)
);
}
#[fasync::run_until_stalled(test)]
async fn returns_lookup_error_when_body_is_missing_longitude() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
let http_loader = HttpByteResponder::new(
br#"{
"location": {
"lat": 51.0,
},
"accuracy": 1200.4
}"#
.to_vec(),
);
assert_eq!(
RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
Err(ResolverError::Lookup)
);
}
#[fasync::run_until_stalled(test)]
async fn returns_lookup_error_when_latitude_is_too_high() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
let http_loader = HttpByteResponder::new(
br#"{
"location": {
"lat": 90.1,
"lng": 0.0
}
}"#
.to_vec(),
);
assert_eq!(
RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
Err(ResolverError::Lookup)
);
}
#[fasync::run_until_stalled(test)]
async fn returns_lookup_error_when_latitude_is_too_low() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
let http_loader = HttpByteResponder::new(
br#"{
"location": {
"lat": -90.1,
"lng": 0.0
}
}"#
.to_vec(),
);
assert_eq!(
RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
Err(ResolverError::Lookup)
);
}
#[fasync::run_until_stalled(test)]
async fn returns_lookup_error_when_longitude_is_too_high() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
let http_loader = HttpByteResponder::new(
br#"{
"location": {
"lat": 0.0,
"lng": 180.1
}
}"#
.to_vec(),
);
assert_eq!(
RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
Err(ResolverError::Lookup)
);
}
#[fasync::run_until_stalled(test)]
async fn returns_lookup_error_when_longitude_is_too_low() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
let http_loader = HttpByteResponder::new(
br#"{
"location": {
"lat": 0.0,
"lng": -180.1
}
}"#
.to_vec(),
);
assert_eq!(
RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
Err(ResolverError::Lookup)
);
}
}
mod response_success_reporting {
use {
super::super::{test_doubles::HttpByteResponder, *},
fuchsia_async as fasync,
matches::assert_matches,
};
#[fasync::run_until_stalled(test)]
async fn returns_success_when_all_fields_are_present() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
let http_loader = HttpByteResponder::new(
br#"{
"location": {
"lat": 51.0,
"lng": -0.1
},
"accuracy": 1200.4
}"#
.to_vec(),
);
assert_matches!(
RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
Ok(_)
);
}
#[fasync::run_until_stalled(test)]
async fn returns_success_when_all_fields_except_accuracy_are_present() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
let http_loader = HttpByteResponder::new(
br#"{
"location": {
"lat": 51.0,
"lng": -0.1
}
}"#
.to_vec(),
);
assert_matches!(
RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
Ok(_)
);
}
}
mod response_success_contents {
use {
super::super::{test_doubles::HttpByteResponder, *},
fuchsia_async as fasync,
matches::assert_matches,
};
#[fasync::run_until_stalled(test)]
async fn provides_precise_latitude() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
let http_loader = HttpByteResponder::new(
br#"{
"location": {
"lat": 89.0,
"lng": 0
}
}"#
.to_vec(),
);
assert_matches!(
RealBssResolver::new(http_loader, "fake_key")
.resolve(bsses)
.await.expect("position is none").latitude,
// One degree of latitude is approximately 111 kilometers. By limiting rounding
// error to 1/10,000,000 of a degree, we limit rounding error to
// 111/10000 = 0.011 meters, or 1.1 cm.
latitude if (88.999_999_9..89.000_000_1).contains(&latitude)
);
}
#[fasync::run_until_stalled(test)]
async fn provides_precise_longitude() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
let http_loader = HttpByteResponder::new(
br#"{
"location": {
"lat": 0,
"lng": 179.0
}
}"#
.to_vec(),
);
assert_matches!(
RealBssResolver::new(http_loader, "fake_key")
.resolve(bsses)
.await.expect("no position").longitude,
// At the equator, one degree of longitude is approximately 111 kilometers.
// By limiting rounding error to 1/10,000,000 of a degree, we limit rounding error
// to 111/10000 = 0.011 meters, or 1.1 cm.
longitude if (178.999_999_9..179.099_999_9).contains(&longitude)
);
}
#[fasync::run_until_stalled(test)]
async fn provides_precise_accuracy_when_present() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
let http_loader = HttpByteResponder::new(
br#"{
"location": {
"lat": 51.0,
"lng": -0.1
},
"accuracy": 1200.4
}"#
.to_vec(),
);
assert_matches!(
RealBssResolver::new(http_loader, "fake_key")
.resolve(bsses)
.await.expect("no position").extras.accuracy_meters.expect("accuracy is none"),
accuracy if (1200.39..1200.41).contains(&accuracy)
);
}
#[fasync::run_until_stalled(test)]
async fn does_not_fabricate_accuracy() {
let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
let http_loader = HttpByteResponder::new(
br#"{
"location": {
"lat": 51.0,
"lng": -0.1
}
}"#
.to_vec(),
);
assert_eq!(
RealBssResolver::new(http_loader, "fake_key")
.resolve(bsses)
.await
.expect("no position")
.extras
.accuracy_meters,
None
);
}
}
}
#[cfg(test)]
mod test_doubles {
use {
super::*,
fidl::endpoints::ClientEnd,
fidl_fuchsia_net_http::LoaderClientMarker,
fuchsia_async::futures::future::{ready, Ready},
std::sync::RwLock,
};
const HTTP_OK: u32 = 200;
type FetchResponse = Result<HttpResponse, FidlError>;
// Test double that
// 1) invokes `validate` to run test assertions, and
// 2) returns HTTP_OK, assuming `validate` did not abort
pub(super) struct HttpRequestValidator<F>
where
F: FnMut(HttpRequest) + Send + Sync,
{
// RwLock is needed because
// a) we need interior mutability for F to be FnMut, and
// b) we need to implement Send and Sync.
validate: RwLock<F>,
}
// Test double that invokes a function which yields `FetchResponse`,
// and returns that as a FIDL response. Useful for testing error
// handling.
pub(super) struct HttpFidlResponder<F>
where
F: Fn() -> FetchResponse + Send + Sync,
{
fetch: F,
}
// Test double that invokes a function which yields a `Vec<u8>`,
// and returns the Vec as the HTTP response. Useful for testing
// response parsing.
pub(super) struct HttpByteResponder {
response: Vec<u8>,
}
impl<F> HttpRequestValidator<F>
where
F: FnMut(HttpRequest) + Send + Sync,
{
pub(super) fn new(validate: F) -> Self {
Self { validate: RwLock::new(validate) }
}
}
impl<F> LoaderProxyInterface for HttpRequestValidator<F>
where
F: FnMut(HttpRequest) + Send + Sync,
{
type FetchResponseFut = Ready<Result<HttpResponse, FidlError>>;
fn fetch(&self, request: HttpRequest) -> Self::FetchResponseFut {
// Note: the `&mut *` here is due to https://github.com/rust-lang/rust/issues/65489
let validate = &mut *self.validate.write().expect("internal error");
let final_url = request.url.clone();
validate(request);
ready(Ok(HttpResponse {
error: None,
body: None,
final_url,
status_code: Some(HTTP_OK),
status_line: None,
headers: None,
redirect: None,
..HttpResponse::EMPTY
}))
}
fn start(
&self,
_request: HttpRequest,
_client: ClientEnd<LoaderClientMarker>,
) -> Result<(), FidlError> {
panic!("internal error: this fake does not implement `start()`");
}
}
impl<F> HttpFidlResponder<F>
where
F: Fn() -> FetchResponse + Send + Sync,
{
pub(super) fn new(fetch: F) -> Self {
Self { fetch }
}
}
impl<F> LoaderProxyInterface for HttpFidlResponder<F>
where
F: Fn() -> FetchResponse + Send + Sync,
{
type FetchResponseFut = Ready<FetchResponse>;
fn fetch(&self, _request: HttpRequest) -> Self::FetchResponseFut {
ready((self.fetch)())
}
fn start(
&self,
_request: HttpRequest,
_client: ClientEnd<LoaderClientMarker>,
) -> Result<(), FidlError> {
panic!("internal error: this stub does not implement `start()`");
}
}
impl HttpByteResponder {
pub(super) fn new(response: Vec<u8>) -> Self {
Self { response }
}
}
impl LoaderProxyInterface for HttpByteResponder {
type FetchResponseFut = Ready<FetchResponse>;
fn fetch(&self, _request: HttpRequest) -> Self::FetchResponseFut {
let (local_socket, remote_socket) =
zx::Socket::create(zx::SocketOpts::STREAM).expect("internal error");
local_socket.write(&self.response).expect("internal error");
ready(Ok(HttpResponse {
error: None,
body: Some(remote_socket),
final_url: None,
status_code: Some(HTTP_OK),
status_line: None,
headers: None,
redirect: None,
..HttpResponse::EMPTY
}))
}
fn start(
&self,
_request: HttpRequest,
_client: ClientEnd<LoaderClientMarker>,
) -> Result<(), FidlError> {
panic!("internal error: this stub does not implement `start()`");
}
}
}