pub fn new(cfgloc: &ConfigLocation) -> Result<Self, ClientError> {
Ok(Client {
auth: AuthConfig::load(cfgloc)?,
- client: reqwest::blocking::Client::new(),
+ client: Self::build_client()?,
accounts: HashMap::new(),
statuses: HashMap::new(),
notifications: HashMap::new(),
})
}
+ fn build_client() -> Result<reqwest::blocking::Client, ClientError>
+ {
+ // We turn off cross-site HTTP redirections in our client
+ // objects. That's because in general it doesn't do any good
+ // for a Mastodon API endpoint to redirect to another host:
+ // most requests have to be authenticated, and if you're
+ // redirected to another host, that host (by normal reqwest
+ // policy for cross-site redirections) won't receive your auth
+ // header. So we might as well reject the redirection in the
+ // first place.
+ //
+ // An exception is when setting up streaming connections,
+ // because those _can_ redirect to another host - but in that
+ // case, the target host can be known in advance (via the
+ // streaming URL in the instance configuration), and also, we
+ // still can't let _reqwest_ quietly follow the redirection,
+ // because it will strip off the auth. So we follow it
+ // ourselves manually, vet the target URL, and put the auth
+ // back on.
+ let no_xsite = reqwest::redirect::Policy::custom(|attempt| {
+ if attempt.previous().len() > 10 {
+ attempt.error("too many redirects")
+ } else if let Some(prev_url) = attempt.previous().last() {
+ let next_url = attempt.url();
+ if (prev_url.host(), prev_url.port()) !=
+ (next_url.host(), next_url.port())
+ {
+ // Stop and pass the 3xx response back to the
+ // caller, rather than throwing a fatal error.
+ // That way, the streaming setup can implement its
+ // special case.
+ attempt.stop()
+ } else {
+ attempt.follow()
+ }
+ } else {
+ panic!("confusing redirect with no previous URLs!");
+ }
+ });
+
+ let client = reqwest::blocking::Client::builder()
+ .redirect(no_xsite)
+ .build()?;
+ Ok(client)
+ }
+
pub fn set_writable(&mut self, permit: bool) {
self.permit_write = permit;
}
}
pub fn start_streaming_thread<Recv: Fn(StreamUpdate) + Send + 'static>(
- &self, id: &StreamId, receiver: Box<Recv>) -> Result<(), ClientError>
+ &mut self, id: &StreamId, receiver: Box<Recv>) ->
+ Result<(), ClientError>
{
let req = match id {
StreamId::User => Req::get("v1/streaming/user"),
};
-
- let client = reqwest::blocking::Client::new();
- let (url, req) = self.api_request_cl(&client, req)?;
+ let method = req.method.clone(); // to reuse for redirects below
+
+ let client = Self::build_client()?;
+ let (url, mut req) = self.api_request_cl(&client, req)?;
let mut rsp = req.send()?;
+ if rsp.status().is_redirection() {
+ // We follow one redirection here, and we permit it to be
+ // to precisely the (host, port) pair that was specified
+ // in the instance's configuration as the streaming API
+ // endpoint. And when we follow that redirection, we do it
+ // manually, and send the same bearer auth token that we'd
+ // send for the normal API.
+ //
+ // An example server that needs this is the Mastodon
+ // development test instance (the one you can set up in a
+ // VM by running 'vagrant up' in the source checkout).
+ // That runs on http://mastodon.local (via Vagrant
+ // inserting that hostname in your /etc/hosts
+ // temporarily), but its streaming APIs run on a separate
+ // HTTP server, so you get redirected to
+ // http://mastodon.local:4000.
+
+ match rsp.headers().get(reqwest::header::LOCATION) {
+ None => {
+ return Err(ClientError::UrlError(
+ url.clone(),
+ "received redirection without a Location header"
+ .to_owned()));
+ }
+ Some(hval) => {
+ let bval = hval.as_bytes();
+ let sval = match std::str::from_utf8(bval) {
+ Ok(s) => s,
+ Err(_) => return Err(ClientError::UrlError(
+ url, "HTTP redirect URL was invalid UTF-8"
+ .to_owned())),
+ };
+ let newurl = match rsp.url().join(sval) {
+ Ok(u) => u,
+ Err(e) => return Err(ClientError::UrlError(
+ url, format!("processing redirection: {}", e))),
+ };
+
+ let instance = self.instance()?;
+ let ok = match &instance.configuration.urls.streaming {
+ None => false,
+ Some(s) => if let Ok(goodurl) = Url::parse(s) {
+ (goodurl.host(), goodurl.port()) ==
+ (newurl.host(), newurl.port())
+ } else {
+ false
+ }
+ };
+ if !ok {
+ return Err(ClientError::UrlError(
+ url.clone(),
+ format!("redirection to suspicious URL {}",
+ sval)));
+ }
+ req = client.request(method, newurl)
+ .bearer_auth(&self.auth.user_token);
+ rsp = req.send()?;
+ }
+ }
+ };
+
let rspstatus = rsp.status();
if !rspstatus.is_success() {
return Err(ClientError::UrlError(