|
5 | 5 | //
|
6 | 6 | //
|
7 | 7 |
|
8 |
| -use std::net::Ipv4Addr; |
9 |
| -use std::net::SocketAddr; |
10 |
| - |
11 |
| -use http::*; |
12 |
| -use hyper::client::conn::handshake; |
13 |
| -use hyper::server::conn::Http; |
14 |
| -use hyper::service::service_fn; |
15 |
| -use hyper::Body; |
| 8 | +use std::net::TcpListener; |
| 9 | + |
| 10 | +use actix_web::get; |
| 11 | +use actix_web::web; |
| 12 | +use actix_web::App; |
| 13 | +use actix_web::HttpResponse; |
| 14 | +use actix_web::HttpServer; |
| 15 | +use rust_embed::RustEmbed; |
16 | 16 | use stdext::spawn;
|
17 |
| -use tokio::net::TcpListener; |
18 |
| -use tokio::net::TcpStream; |
| 17 | +use url::Url; |
19 | 18 |
|
20 | 19 | use crate::browser;
|
21 | 20 |
|
22 |
| -async fn handle_request(request: Request<Body>, port: i32) -> anyhow::Result<Response<Body>> { |
23 |
| - // connect to R help server |
24 |
| - let addr = format!("localhost:{}", port); |
25 |
| - let stream = TcpStream::connect(addr.as_str()).await?; |
26 |
| - let (mut sender, conn) = handshake(stream).await?; |
| 21 | +// Embed src/resources which is where replacement resources can be found. |
| 22 | +#[derive(RustEmbed)] |
| 23 | +#[folder = "src/resources/"] |
| 24 | +struct Asset; |
27 | 25 |
|
28 |
| - // spawn a task to poll the connection and drive the HTTP state |
29 |
| - tokio::spawn(async move { |
30 |
| - if let Err(error) = conn.await { |
31 |
| - log::error!("HELP PROXY ERROR: {}", error); |
| 26 | +// Starts the help proxy. |
| 27 | +pub fn start(target_port: u16) { |
| 28 | + spawn!("ark-help-proxy", move || { |
| 29 | + match task(target_port) { |
| 30 | + Ok(value) => log::info!("Help proxy server exited with value: {:?}", value), |
| 31 | + Err(error) => log::error!("Help proxy server exited unexpectedly: {}", error), |
32 | 32 | }
|
33 | 33 | });
|
| 34 | +} |
34 | 35 |
|
35 |
| - // send the request |
36 |
| - let response = sender.send_request(request).await?; |
| 36 | +// The help proxy main entry point. |
| 37 | +#[tokio::main] |
| 38 | +async fn task(target_port: u16) -> anyhow::Result<()> { |
| 39 | + // Create the help proxy. |
| 40 | + let help_proxy = HelpProxy::new(target_port)?; |
37 | 41 |
|
38 |
| - // forward the response |
39 |
| - Ok(response) |
| 42 | + // Set the help proxy port. |
| 43 | + unsafe { browser::PORT = help_proxy.source_port }; |
| 44 | + |
| 45 | + // Run the help proxy. |
| 46 | + Ok(help_proxy.run().await?) |
40 | 47 | }
|
41 | 48 |
|
42 |
| -#[tokio::main] |
43 |
| -async fn task(port: i32) -> anyhow::Result<()> { |
44 |
| - let addr = SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); |
45 |
| - let listener = TcpListener::bind(addr).await?; |
46 |
| - |
47 |
| - if let Ok(addr) = listener.local_addr() { |
48 |
| - let port = addr.port(); |
49 |
| - log::info!("Help proxy listening on port {}", port); |
50 |
| - unsafe { browser::PORT = port }; |
51 |
| - } |
| 49 | +// AppState struct. |
| 50 | +#[derive(Clone)] |
| 51 | +struct AppState { |
| 52 | + pub target_port: u16, |
| 53 | +} |
52 | 54 |
|
53 |
| - loop { |
54 |
| - let (stream, _) = listener.accept().await?; |
55 |
| - tokio::spawn(async move { |
56 |
| - let http = Http::new(); |
57 |
| - let status = http.serve_connection( |
58 |
| - stream, |
59 |
| - service_fn(|request| async move { handle_request(request, port).await }), |
60 |
| - ); |
| 55 | +// HelpProxy struct. |
| 56 | +struct HelpProxy { |
| 57 | + pub source_port: u16, |
| 58 | + pub target_port: u16, |
| 59 | +} |
61 | 60 |
|
62 |
| - if let Err(error) = status.await { |
63 |
| - log::error!("HELP PROXY ERROR: {}", error); |
64 |
| - } |
| 61 | +// HelpProxy implementation. |
| 62 | +impl HelpProxy { |
| 63 | + // Creates a new HelpProxy. |
| 64 | + pub fn new(target_port: u16) -> anyhow::Result<Self> { |
| 65 | + Ok(HelpProxy { |
| 66 | + source_port: TcpListener::bind("127.0.0.1:0")?.local_addr()?.port(), |
| 67 | + target_port, |
| 68 | + }) |
| 69 | + } |
| 70 | + |
| 71 | + // Runs the HelpProxy. |
| 72 | + pub async fn run(&self) -> anyhow::Result<()> { |
| 73 | + // Create the app state. |
| 74 | + let app_state = web::Data::new(AppState { |
| 75 | + target_port: self.target_port, |
65 | 76 | });
|
| 77 | + |
| 78 | + // Create the server. |
| 79 | + let server = HttpServer::new(move || { |
| 80 | + App::new() |
| 81 | + .app_data(app_state.clone()) |
| 82 | + .service(proxy_request) |
| 83 | + }) |
| 84 | + .bind(("127.0.0.1", self.source_port))?; |
| 85 | + |
| 86 | + // Run the server. |
| 87 | + Ok(server.run().await?) |
66 | 88 | }
|
67 | 89 | }
|
68 | 90 |
|
69 |
| -pub fn start(port: i32) { |
70 |
| - spawn!("ark-help-proxy", move || { |
71 |
| - match task(port) { |
72 |
| - Ok(value) => log::info!("Help proxy server exited with value {:?}", value), |
73 |
| - Err(error) => log::error!("Help proxy server exited unexpectedly: {}", error), |
74 |
| - } |
75 |
| - }); |
| 91 | +// Proxies a request. |
| 92 | +#[get("/{url:.*}")] |
| 93 | +async fn proxy_request(path: web::Path<(String,)>, app_state: web::Data<AppState>) -> HttpResponse { |
| 94 | + // Get the URL path. |
| 95 | + let (path,) = path.into_inner(); |
| 96 | + |
| 97 | + // Construct the target URL string. |
| 98 | + let target_url_string = format!("http://localhost:{}/{path}", app_state.target_port); |
| 99 | + |
| 100 | + // Parse the target URL string into the target URL. |
| 101 | + let target_url = match Url::parse(&target_url_string) { |
| 102 | + Ok(url) => url, |
| 103 | + Err(error) => { |
| 104 | + log::error!("Error proxying {}: {}", target_url_string, error); |
| 105 | + return HttpResponse::BadGateway().finish(); |
| 106 | + }, |
| 107 | + }; |
| 108 | + |
| 109 | + // Get the target URL. |
| 110 | + match reqwest::get(target_url.clone()).await { |
| 111 | + // OK. |
| 112 | + Ok(response) => { |
| 113 | + // We only handle OK. Everything else is unexpected. |
| 114 | + if response.status() != reqwest::StatusCode::OK { |
| 115 | + return HttpResponse::BadGateway().finish(); |
| 116 | + } |
| 117 | + |
| 118 | + // Get the headers we need. |
| 119 | + let headers = response.headers().clone(); |
| 120 | + let content_type = headers.get("content-type"); |
| 121 | + |
| 122 | + // Log. |
| 123 | + log::info!( |
| 124 | + "Proxing URL '{:?}' path '{}' content-type is '{:?}'", |
| 125 | + target_url.to_string(), |
| 126 | + target_url.path(), |
| 127 | + content_type, |
| 128 | + ); |
| 129 | + |
| 130 | + // Build and return the response. |
| 131 | + let mut http_response_builder = HttpResponse::Ok(); |
| 132 | + if content_type.is_some() { |
| 133 | + http_response_builder.content_type(content_type.unwrap()); |
| 134 | + } |
| 135 | + |
| 136 | + // Certain resources are replaced. |
| 137 | + let replacement_embedded_file = match target_url.path().to_lowercase() { |
| 138 | + path if path.ends_with("r.css") => Asset::get("R.css"), |
| 139 | + path if path.ends_with("prism.css") => Asset::get("prism.css"), |
| 140 | + _ => None, |
| 141 | + }; |
| 142 | + |
| 143 | + // Return the replacement resource or the real resource. |
| 144 | + match replacement_embedded_file { |
| 145 | + Some(replacement_embedded_file) => { |
| 146 | + http_response_builder.body(replacement_embedded_file.data) |
| 147 | + }, |
| 148 | + None => http_response_builder.body(match response.bytes().await { |
| 149 | + Ok(body) => body, |
| 150 | + Err(error) => { |
| 151 | + log::error!("Error proxying {}: {}", target_url_string, error); |
| 152 | + return HttpResponse::BadGateway().finish(); |
| 153 | + }, |
| 154 | + }), |
| 155 | + } |
| 156 | + }, |
| 157 | + // Error. |
| 158 | + Err(error) => { |
| 159 | + log::error!("Error proxying {}: {}", target_url, error); |
| 160 | + HttpResponse::BadGateway().finish() |
| 161 | + }, |
| 162 | + } |
76 | 163 | }
|
0 commit comments