Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-httpclient

Named HTTP clients for Pudu: a client factory, delegating handlers, pooled keep-alive connections, handler lifetimes, logging, and resilience

0.1.1Apache-2.01

InstallClose

Connect.pudu

Pudu116 lines4.9 KB

GitHub ↗
1/** @Transport.Connect.Module — connections opened directly or through a proxy */2module PuduLangHttpClient.Transport.Connect34import Std.Bytes as ByteSeq5import Std.Http.Message as Message6import PuduLangHttpClient.Constants.Defaults as Defaults7import PuduLangHttpClient.Constants.Messages as Messages8import PuduLangHttpClient.Domain.Expiry as Expiry9import PuduLangHttpClient.Domain.Uri as Uri10import PuduLangHttpClient as HttpClient11import PuduLangHttpClient.Proxy as Proxy12import PuduLangHttpClient.Transport.Wire as Wire13import PuduLangHttpClient.Utils.Template as Template1415/** @Transport.Connect.Route — where a request's connection goes and how it is shared */16export type Route = { key: Str, target: Uri.Endpoint, via: Option[Proxy.Proxy] }1718/// The route to an endpoint: directly, or through the proxy unless it bypasses the host.19export fn routeTo(target: &Uri.Endpoint, proxy: &Option[Proxy.Proxy]) -> Route {20  match proxy {21    case Some(found) => {22      if Proxy.bypasses(&found, target.host) {23        Route{key: "direct " + Uri.origin(target), target: *target, via: None}24      } else if target.secure {25        Route{key: "tunnel " + found.address + " " + Uri.origin(target), target: *target, via: Some(found)}26      } else {27        Route{key: "proxy " + found.address, target: *target, via: Some(found)}28      }29    }30    case None => Route{key: "direct " + Uri.origin(target), target: *target, via: None}31  }32}3334/// Whether requests on the route name their target in absolute form, as a plain proxy needs.35export fn isProxied(route: &Route) -> Bool {36  match route.via {37    case Some(_) => !route.target.secure38    case None => false39  }40}4142/// A new connection along the route, opened within `connectTimeout` milliseconds as well as the43/// request's deadline; a secured target is verified against its own host name.44export fn open(route: &Route, connectTimeout: Int, deadline: Option[Int]) -> Result[Wire.Link, Wire.Fault] {45  let connecting = if connectTimeout == Defaults.INFINITE { deadline } else { Expiry.earliest(deadline, Some(clock() + connectTimeout)) }46  let outcome = match route.via {47    case None => dialSecured(&route.target, connecting)48    case Some(proxy) => through(&proxy, &route.target, connecting)49  }50  match outcome {51    case Err(Wire.Stalled) => {52      if Wire.budget(deadline) == None { Err(Wire.Stalled) } else { Err(Wire.Broken(HttpClient.Connection(Template.fill(Messages.STALLED, &["connecting"])))) }53    }54    case other => other55  }56}5758/// A connection to the endpoint, secured when the endpoint is.59fn dialSecured(target: &Uri.Endpoint, deadline: Option[Int]) -> Result[Wire.Link, Wire.Fault] {60  let link = Wire.dial(target.host, target.port, deadline) ?61  if target.secure { Wire.secure(link, target.host, deadline) } else { Ok(link) }62}6364/// A connection through the proxy: to the proxy itself for a plain target, or a tunnel the proxy65/// opens to a secured target, secured end to end.66fn through(proxy: &Proxy.Proxy, target: &Uri.Endpoint, deadline: Option[Int]) -> Result[Wire.Link, Wire.Fault] {67  let place = match Uri.endpointOf(proxy.address) {68    case Some(found) => found69    case None => { return Err(Wire.Broken(HttpClient.ProxyTunnel(proxy.address))) }70  }71  let link = Wire.dial(place.host, place.port, deadline) ?72  if !target.secure { return Ok(link) }73  let authority = target.host + ":" + show(target.port)74  var head = "CONNECT " + authority + " HTTP/1.1\r\nhost: " + authority + "\r\n"75  if let Some(credentials) = Proxy.authorization(proxy) { head = head + "proxy-authorization: " + credentials + "\r\n" }76  if let Err(fault) = Wire.write(&link, &(head + "\r\n").toBytes(), deadline) {77    Wire.close(&link)78    return Err(fault)79  }80  match tunnelAnswer(&link, deadline) {81    case Ok(code) => {82      if code >= 200 && code < 300 { Wire.secure(link, target.host, deadline) } else {83        Wire.close(&link)84        Err(Wire.Broken(HttpClient.ProxyTunnel("status " + show(code))))85      }86    }87    case Err(fault) => {88      Wire.close(&link)89      Err(fault)90    }91  }92}9394/// The status a proxy answered a tunnel request with.95fn tunnelAnswer(link: &Wire.Link, deadline: Option[Int]) -> Result[Int, Wire.Fault] {96  var pending = ByteSeq.empty()97  loop {98    let end = pending.indexOf("\r\n\r\n".toBytes())99    if end >= 0 {100      let text = match ByteSeq.toText(&pending.take(end + 4)) {101        case Ok(found) => found102        case Err(_) => { return Err(Wire.Broken(HttpClient.ProxyTunnel("the answer is not text"))) }103      }104      return match Message.parseResponse(text) {105        case Ok(parsed) => Ok(parsed.status.code)106        case Err(problem) => Err(Wire.Broken(HttpClient.ProxyTunnel(Message.explain(&problem))))107      }108    }109    if pending.length() > Defaults.MAX_RESPONSE_HEADERS { return Err(Wire.Broken(HttpClient.ProxyTunnel("the answer is too long"))) }110    match Wire.read(link, deadline) ? {111      case Some(piece) => { pending = pending.concat(piece) }112      case None => { return Err(Wire.Broken(HttpClient.ProxyTunnel("the proxy closed the connection"))) }113    }114  }115}116