Resolve AP through proxy as well

This commit is contained in:
Johan Anderholm 2018-03-23 16:52:24 +01:00
parent 3bdc5e0073
commit 1a04e3b899
6 changed files with 63 additions and 37 deletions

View file

@ -16,6 +16,7 @@ extprim = "1.5.1"
futures = "0.1.8" futures = "0.1.8"
httparse = "1.2.4" httparse = "1.2.4"
hyper = "0.11.2" hyper = "0.11.2"
hyper-proxy = { version = "0.4.1", default_features = false }
lazy_static = "0.2.0" lazy_static = "0.2.0"
log = "0.3.5" log = "0.3.5"
num-bigint = "0.1.35" num-bigint = "0.1.35"

View file

@ -1,8 +1,10 @@
const AP_FALLBACK: &'static str = "ap.spotify.com:443"; const AP_FALLBACK: &'static str = "ap.spotify.com:443";
const APRESOLVE_ENDPOINT: &'static str = "http://apresolve.spotify.com/"; const APRESOLVE_ENDPOINT: &'static str = "http://apresolve.spotify.com/";
use futures::{future, Future, Stream}; use futures::{Future, Stream};
use hyper::{self, Client, Uri}; use hyper::client::HttpConnector;
use hyper::{self, Client, Method, Request, Uri};
use hyper_proxy::{Intercept, Proxy, ProxyConnector};
use serde_json; use serde_json;
use std::str::FromStr; use std::str::FromStr;
use tokio_core::reactor::Handle; use tokio_core::reactor::Handle;
@ -14,11 +16,29 @@ pub struct APResolveData {
ap_list: Vec<String>, ap_list: Vec<String>,
} }
fn apresolve(handle: &Handle) -> Box<Future<Item = String, Error = Error>> { fn apresolve(handle: &Handle, proxy: &Option<String>) -> Box<Future<Item = String, Error = Error>> {
let url = Uri::from_str(APRESOLVE_ENDPOINT).expect("invalid AP resolve URL"); let url = Uri::from_str(APRESOLVE_ENDPOINT).expect("invalid AP resolve URL");
let use_proxy = proxy.is_some();
let client = Client::new(handle); let mut req = Request::new(Method::Get, url.clone());
let response = client.get(url); let response = match proxy {
&Some(ref val) => {
let proxy_url = Uri::from_str(&val).expect("invalid http proxy");
let proxy = Proxy::new(Intercept::All, proxy_url);
let connector = HttpConnector::new(4, handle);
let proxy_connector = ProxyConnector::from_proxy_unsecured(connector, proxy);
if let Some(headers) = proxy_connector.http_headers(&url) {
req.headers_mut().extend(headers.iter());
req.set_proxy(true);
}
let client = Client::configure().connector(proxy_connector).build(handle);
client.request(req)
}
_ => {
let client = Client::new(handle);
client.request(req)
}
};
let body = response.and_then(|response| { let body = response.and_then(|response| {
response.body().fold(Vec::new(), |mut acc, chunk| { response.body().fold(Vec::new(), |mut acc, chunk| {
@ -32,8 +52,19 @@ fn apresolve(handle: &Handle) -> Box<Future<Item = String, Error = Error>> {
let data = let data =
body.and_then(|body| serde_json::from_str::<APResolveData>(&body).chain_err(|| "invalid JSON")); body.and_then(|body| serde_json::from_str::<APResolveData>(&body).chain_err(|| "invalid JSON"));
let ap = data.and_then(|data| { let ap = data.and_then(move |data| {
let ap = data.ap_list.first().ok_or("empty AP List")?; let mut aps = data.ap_list.iter().filter(|ap| {
if use_proxy {
// It is unlikely that the proxy will accept CONNECT on anything other than 443.
Uri::from_str(ap)
.ok()
.map_or(false, |uri| uri.port().map_or(false, |port| port == 443))
} else {
true
}
});
let ap = aps.next().ok_or("empty AP List")?;
Ok(ap.clone()) Ok(ap.clone())
}); });
@ -47,17 +78,11 @@ pub(crate) fn apresolve_or_fallback<E>(
where where
E: 'static, E: 'static,
{ {
if proxy.is_some() { let ap = apresolve(handle, proxy).or_else(|e| {
// TODO: Use a proper proxy library and filter out a 443 proxy instead of relying on fallback. warn!("Failed to resolve Access Point: {}", e.description());
// The problem with current libraries (hyper-proxy, reqwest) is that they depend on TLS warn!("Using fallback \"{}\"", AP_FALLBACK);
// and this is a dependency we might not want. Ok(AP_FALLBACK.into())
Box::new(future::result(Ok(AP_FALLBACK.into()))) });
} else {
let ap = apresolve(handle).or_else(|e| { Box::new(ap)
warn!("Failed to resolve Access Point: {}", e.description());
warn!("Using fallback \"{}\"", AP_FALLBACK);
Ok(AP_FALLBACK.into())
});
Box::new(ap)
}
} }

View file

@ -21,8 +21,8 @@ use proxytunnel;
pub type Transport = Framed<TcpStream, APCodec>; pub type Transport = Framed<TcpStream, APCodec>;
pub fn connect<A: ToSocketAddrs>( pub fn connect(
addr: A, addr: String,
handle: &Handle, handle: &Handle,
proxy: &Option<String>, proxy: &Option<String>,
) -> Box<Future<Item = Transport, Error = io::Error>> { ) -> Box<Future<Item = Transport, Error = io::Error>> {
@ -38,7 +38,7 @@ pub fn connect<A: ToSocketAddrs>(
.unwrap() .unwrap()
.next() .next()
.unwrap(), .unwrap(),
Some(addr.to_socket_addrs().unwrap().next().unwrap()), Some(addr.clone()),
) )
} }
None => (addr.to_socket_addrs().unwrap().next().unwrap(), None), None => (addr.to_socket_addrs().unwrap().next().unwrap(), None),
@ -46,8 +46,8 @@ pub fn connect<A: ToSocketAddrs>(
let socket = TcpStream::connect(&addr, handle); let socket = TcpStream::connect(&addr, handle);
if let Some(connect_url) = connect_url { if let Some(connect_url) = connect_url {
let connection = let connection = socket
socket.and_then(move |socket| proxytunnel::connect(socket, connect_url).and_then(handshake)); .and_then(move |socket| proxytunnel::connect(socket, &connect_url).and_then(handshake));
Box::new(connection) Box::new(connection)
} else { } else {
let connection = socket.and_then(handshake); let connection = socket.and_then(handshake);

View file

@ -18,6 +18,7 @@ extern crate crypto;
extern crate extprim; extern crate extprim;
extern crate httparse; extern crate httparse;
extern crate hyper; extern crate hyper;
extern crate hyper_proxy;
extern crate num_bigint; extern crate num_bigint;
extern crate num_integer; extern crate num_integer;
extern crate num_traits; extern crate num_traits;

View file

@ -1,13 +1,13 @@
use std::error::Error;
use std::io;
use std::str::FromStr;
use futures::{Async, Future, Poll}; use futures::{Async, Future, Poll};
use httparse; use httparse;
use hyper::Uri;
use std::io;
use std::net::SocketAddr;
use tokio_io::io::{read, write_all, Read, Window, WriteAll}; use tokio_io::io::{read, write_all, Read, Window, WriteAll};
use tokio_io::{AsyncRead, AsyncWrite}; use tokio_io::{AsyncRead, AsyncWrite};
use std::error::Error;
pub struct ProxyTunnel<T> { pub struct ProxyTunnel<T> {
state: ProxyState<T>, state: ProxyState<T>,
} }
@ -17,7 +17,7 @@ enum ProxyState<T> {
ProxyResponse(Read<T, Window<Vec<u8>>>), ProxyResponse(Read<T, Window<Vec<u8>>>),
} }
pub fn connect<T: AsyncRead + AsyncWrite>(connection: T, connect_url: SocketAddr) -> ProxyTunnel<T> { pub fn connect<T: AsyncRead + AsyncWrite>(connection: T, connect_url: &str) -> ProxyTunnel<T> {
let proxy = proxy_connect(connection, connect_url); let proxy = proxy_connect(connection, connect_url);
ProxyTunnel { ProxyTunnel {
state: ProxyState::ProxyConnect(proxy), state: ProxyState::ProxyConnect(proxy),
@ -95,14 +95,13 @@ impl<T: AsyncRead + AsyncWrite> Future for ProxyTunnel<T> {
} }
} }
fn proxy_connect<T: AsyncWrite>(connection: T, connect_url: SocketAddr) -> WriteAll<T, Vec<u8>> { fn proxy_connect<T: AsyncWrite>(connection: T, connect_url: &str) -> WriteAll<T, Vec<u8>> {
// TODO: It would be better to use a non-resolved url here. This usually works, let uri = Uri::from_str(&connect_url).unwrap();
// but it may fail in some environments and it will leak DNS requests.
let buffer = format!( let buffer = format!(
"CONNECT {0}:{1} HTTP/1.1\r\n\ "CONNECT {0}:{1} HTTP/1.1\r\n\
\r\n", \r\n",
connect_url.ip(), uri.host().expect(&format!("No host in {}", uri)),
connect_url.port() uri.port().expect(&format!("No port in {}", uri))
).into_bytes(); ).into_bytes();
write_all(connection, buffer) write_all(connection, buffer)

View file

@ -56,7 +56,7 @@ impl Session {
let proxy = config.proxy.clone(); let proxy = config.proxy.clone();
let connection = access_point.and_then(move |addr| { let connection = access_point.and_then(move |addr| {
info!("Connecting to AP \"{}\"", addr); info!("Connecting to AP \"{}\"", addr);
connection::connect::<&str>(&addr, &handle_, &proxy) connection::connect(addr, &handle_, &proxy)
}); });
let device_id = config.device_id.clone(); let device_id = config.device_id.clone();