diff --git a/Cargo.lock b/Cargo.lock index 4c9eee8..d1206f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1098,9 +1098,11 @@ dependencies = [ "futures-util", "grok", "hex", - "http 1.1.0", + "http", + "http-body-util", "human-panic", - "hyper 0.14.27", + "hyper", + "hyper-util", "indicatif", "lazy_static", "memchr", @@ -1337,6 +1339,25 @@ dependencies = [ "onig", ] +[[package]] +name = "h2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.1.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.11.2" @@ -1388,17 +1409,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "http" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.1.0" @@ -1410,17 +1420,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" -dependencies = [ - "bytes", - "http 0.2.11", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.0" @@ -1428,7 +1427,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", - "http 1.1.0", + "http", ] [[package]] @@ -1439,8 +1438,8 @@ checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" dependencies = [ "bytes", "futures-core", - "http 1.1.0", - "http-body 1.0.0", + "http", + "http-body", "pin-project-lite", ] @@ -1472,29 +1471,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "hyper" -version = "0.14.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http 0.2.11", - "http-body 0.4.5", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.4.10", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.2.0" @@ -1504,9 +1480,11 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.1.0", - "http-body 1.0.0", + "h2", + "http", + "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1521,8 +1499,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", - "http 1.1.0", - "hyper 1.2.0", + "http", + "hyper", "hyper-util", "rustls", "rustls-pki-types", @@ -1540,11 +1518,11 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.1.0", - "http-body 1.0.0", - "hyper 1.2.0", + "http", + "http-body", + "hyper", "pin-project-lite", - "socket2 0.5.5", + "socket2", "tokio", "tower", "tower-service", @@ -2720,10 +2698,10 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.1.0", - "http-body 1.0.0", + "http", + "http-body", "http-body-util", - "hyper 1.2.0", + "hyper", "hyper-rustls", "hyper-util", "ipnet", @@ -3188,16 +3166,6 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.5" @@ -3521,7 +3489,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.5", + "socket2", "tokio-macros", "windows-sys 0.48.0", ] diff --git a/Cargo.toml b/Cargo.toml index 64803e7..15bca66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,13 +33,13 @@ futures-util = "0.3.21" grok = "2.0.0" hex = "0.4.3" http = "1.1" +http-body-util = "0.1.1" human-panic = "1.1.3" -hyper = { version = "0.14.13", features = [ +hyper = { version = "1.2.0", features = [ "http1", "server", - "tcp", - "runtime", ] } +hyper-util = { version = "0.1.3", features = ["full"] } indicatif = "0.17.0" lazy_static = "1.4.0" memchr = "2.5.0" diff --git a/src/auth.rs b/src/auth.rs index eb49848..9db59f6 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,10 +1,16 @@ use crate::config::{api_client_configuration_from_token, Config}; use crate::Arguments; use anyhow::Error; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Response, Server, StatusCode}; +use bytes::Bytes; +use http_body_util::Full; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper::{Response, StatusCode}; +use hyper_util::rt::TokioIo; use qstring::QString; use std::convert::Infallible; +use tokio::net::TcpListener; +use tokio::spawn; use tokio::sync::broadcast; use tracing::{debug, error, info, warn}; @@ -20,50 +26,71 @@ pub async fn handle_login_command(args: Arguments) -> Result<(), Error> { let (tx, mut rx) = broadcast::channel(1); // Bind to a random local port - let redirect_server_addr = ([127, 0, 0, 1], 0).into(); - let make_service = make_service_fn(move |_| { - let tx = tx.clone(); - - async move { - Ok::<_, Infallible>(service_fn(move |req| { - let token = req - .uri() - .query() - .and_then(|query| QString::from(query).get("token").map(String::from)); - let tx = tx.clone(); - - async move { - match token { - Some(token) => { - tx.send(token).expect("error sending token via channel"); - Ok::<_, Error>( - Response::builder() - .status(StatusCode::OK) - .body(Body::from( - "You have been logged in to the CLI. You can now close this tab.", - )) - .unwrap(), - ) - } - None => Ok::<_, Error>( - Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::from("Expected token query string parameter")) - .unwrap(), - ), - } - } - })) - } - }); - let server = Server::bind(&redirect_server_addr).serve(make_service); + let tcp_listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); // Include the port of the local HTTP server so the // API can redirect the browser back to us after the login // flow is completed - let port: u16 = server.local_addr().port(); + let port: u16 = tcp_listener.local_addr().unwrap().port(); let login_url = format!("{}signin?cli_redirect_port={}", args.base_url, port); + // Spawn web server which will handle a redirect from the login page. + spawn(async move { + loop { + let (stream, _) = tcp_listener + .accept() + .await + .expect("unable to accept connection"); + + let tx = tx.clone(); + + // Use an adapter to access something implementing `tokio::io` traits as if they implement + // `hyper::rt` IO traits. + let io = TokioIo::new(stream); + + // Spawn a tokio task to serve multiple connections concurrently + tokio::task::spawn(async move { + // Finally, we bind the incoming connection to our `hello` service + if let Err(err) = http1::Builder::new() + // `service_fn` converts our function in a `Service` + .serve_connection( + io, + service_fn(move |req| { + let token = req + .uri() + .query() + .and_then(|query| QString::from(query).get("token").map(String::from)); + let tx = tx.clone(); + + async move { + match token { + Some(token) => { + tx.send(token).expect("error sending token via channel"); + Ok::>, Infallible>( + Response::builder() + .status(StatusCode::OK) + .body(Full::new(Bytes::from("You have been logged in to the CLI. You can now close this tab."))) + .unwrap(), + ) + } + None => Ok::>, Infallible>( + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Full::new(Bytes::from("Expected token query string parameter."))) + .unwrap(), + ), + } + } + }), + ) + .await + { + println!("Error serving connection: {:?}", err); + } + }); + } + }); + debug!("listening for the login redirect on port {port} (redirect url: {login_url})"); // Open the user's web browser to start the login flow @@ -71,33 +98,27 @@ pub async fn handle_login_command(args: Arguments) -> Result<(), Error> { info!("Please go to this URL to login: {}", login_url); } - let mut config = Config::load(args.config).await?; - - // Shut down the web server once the token is received - server - .with_graceful_shutdown(async move { - // Wait for the token to be received - match rx.recv().await { - Ok(token) => { - debug!("api token: {}", token); - - // Save the token to the config file - config.api_token = Some(token); - match config.save().await { - Ok(_) => { - info!("You are logged in to Fiberplane"); - } - Err(e) => error!( - "Error saving API token to config file {}: {:?}", - config.path.display(), - e - ), - }; + // Wait on the channel. Once we receive something it means that the user has + // logged in. + match rx.recv().await { + Ok(token) => { + let mut config = Config::load(args.config).await?; + + // Save the token to the config file + config.api_token = Some(token); + match config.save().await { + Ok(_) => { + info!("You are logged in to Fiberplane"); } - Err(_) => error!("login error"), - } - }) - .await?; + Err(e) => error!( + "Error saving API token to config file {}: {:?}", + config.path.display(), + e + ), + }; + } + Err(_) => error!("login error"), + }; Ok(()) } diff --git a/src/experiments.rs b/src/experiments.rs index d27aaa7..49fbf20 100644 --- a/src/experiments.rs +++ b/src/experiments.rs @@ -1,5 +1,4 @@ use crate::config::api_client_configuration; -use crate::fp_urls::NotebookUrlBuilder; use crate::interactive; use crate::output::{output_details, output_json, GenericKeyValue}; use crate::templates::NOTEBOOK_ID_REGEX; @@ -9,21 +8,17 @@ use directories::ProjectDirs; use fiberplane::base64uuid::Base64Uuid; use fiberplane::markdown::notebook_to_markdown; use fiberplane::models::formatting::{Annotation, AnnotationWithOffset, Mention}; -use fiberplane::models::notebooks::{Cell, ProviderCell, TextCell}; +use fiberplane::models::notebooks::{Cell, TextCell}; use fiberplane::models::timestamps::Timestamp; use fiberplane::models::utils::char_count; use fiberplane::models::{formatting, notebooks}; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Error, Response, Server, StatusCode}; use lazy_static::lazy_static; -use qstring::QString; use regex::{Regex, Replacer}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, VecDeque}; -use std::{convert::Infallible, sync::Arc}; -use std::{fmt::Write, io::ErrorKind, net::IpAddr, path::PathBuf, str::FromStr}; +use std::{fmt::Write, io::ErrorKind, path::PathBuf, str::FromStr}; use tokio::fs; -use tracing::{debug, error, info, warn}; +use tracing::{debug, info, warn}; use url::Url; lazy_static! { @@ -47,9 +42,6 @@ enum SubCommand { /// and save them to the given directory as Markdown Crawl(CrawlArgs), - /// Open Prometheus graphs in a given notebook - PrometheusGraphToNotebook(PrometheusGraphToNotebookArgs), - /// Panics the CLI in order to test out `human-panic` #[clap(hide = true)] #[doc(hidden)] @@ -100,32 +92,6 @@ struct CrawlArgs { token: Option, } -#[derive(Parser)] -struct PrometheusGraphToNotebookArgs { - #[clap(long, short, env)] - notebook_id: Option, - - /// Server port number - #[clap(long, short, env, default_value = "9090")] - port: u16, - - /// Hostname to listen on - #[clap(long, short = 'H', env, default_value = "127.0.0.1")] - listen_host: IpAddr, - - #[clap(from_global)] - workspace_id: Option, - - #[clap(from_global)] - base_url: Url, - - #[clap(from_global)] - config: Option, - - #[clap(from_global)] - token: Option, -} - #[derive(ValueEnum, Clone)] enum MessageOutput { /// Output the result as a table @@ -139,9 +105,6 @@ pub async fn handle_command(args: Arguments) -> Result<()> { match args.sub_command { SubCommand::Message(args) => handle_message_command(args).await, SubCommand::Crawl(args) => handle_crawl_command(args).await, - SubCommand::PrometheusGraphToNotebook(args) => { - handle_prometheus_redirect_command(args).await - } SubCommand::Panic => panic!("manually created panic called by `fpx experiments panic`"), } } @@ -394,103 +357,3 @@ fn cache_file_path() -> PathBuf { .cache_dir() .join("cache.toml") } - -async fn handle_prometheus_redirect_command(args: PrometheusGraphToNotebookArgs) -> Result<()> { - let client = Arc::new(api_client_configuration(args.token, args.config, args.base_url).await?); - let workspace_id = interactive::workspace_picker(&client, args.workspace_id).await?; - let notebook_id = - interactive::notebook_picker(&client, args.notebook_id, Some(workspace_id)).await?; - let notebook_url = NotebookUrlBuilder::new(workspace_id, notebook_id) - .base_url(client.server.clone()) - .url() - .expect("Error building URL"); - - let listen_addr = (args.listen_host, args.port).into(); - let make_service = make_service_fn(move |_| { - let client = client.clone(); - async move { - Ok::<_, Infallible>(service_fn(move |req| { - let client = client.clone(); - async move { - if !req.uri().path().starts_with("/graph") { - return Ok::<_, Error>( - Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::from( - "Prometheus-to-notebook can only be used for graph URLs", - )) - .expect("Error creating response"), - ); - } - let query = req - .uri() - .query() - .and_then(|query| QString::from(query).get("g0.expr").map(String::from)); - - match query { - Some(query) => { - // Append cell to notebook and return the URL - let id = Base64Uuid::new().to_string(); - if let Err(err) = client - .notebook_cells_append( - notebook_id, - None, - None, - vec![Cell::Provider( - ProviderCell::builder() - .id(id.clone()) - .intent("prometheus,timeseries") - .query_data(format!( - "application/x-www-form-urlencoded,query={query}" - )) - .build(), - )], - ) - .await - { - error!("Error appending cell to notebook: {:?}", err); - return Ok::<_, Error>( - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::from("Error appending cell to notebook")) - .unwrap(), - ); - }; - - let url = NotebookUrlBuilder::new(workspace_id, notebook_id) - .base_url(client.server.clone()) - .cell_id(id) - .url() - .expect("Error building URL"); - - debug!("Redirecting to: {}", url.as_str()); - - Ok::<_, Error>( - Response::builder() - .status(StatusCode::TEMPORARY_REDIRECT) - .header("Location", url.as_str()) - .body(Body::empty()) - .unwrap(), - ) - } - None => Ok::<_, Error>( - Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::from("Expected `g0.expr` query string parameter")) - .unwrap(), - ), - } - } - })) - } - }); - let server = Server::bind(&listen_addr).serve(make_service); - - info!( - "Opening Prometheus graph URLs that start with: http://{listen_addr}/graph will now add them to the notebook: {notebook_url} ", - ); - - server.await?; - - Ok(()) -}