From 89f493cc1a6a8ab89534a3ea7ded2cb56056b86b Mon Sep 17 00:00:00 2001 From: lablans Date: Tue, 11 Apr 2023 06:22:06 +0000 Subject: [PATCH 01/80] Readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 022a0a7..769f088 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ The following command line parameters are required: * `PROXY_URL`: The URL of the local Samply.Proxy which is used to connect to the Samply.Broker * `APP_ID`: The BeamId of the Beam.Connect application * `LOCAL_TARGETS_FILE`: The path to the local service resolution file (see [Routing Section](#Request-Routing)). - * `DISCOVERY_URL`: The URL that is queried to receive the central service discovery this may also be a local file (see [Routing Section](#Request-Routing)). + * `DISCOVERY_URL`: The URL (or local file) to be is queried to receive the central service discovery (see [Routing Section](#Request-Routing)). The following command line parameter is only used in Receiver mode (see [Usage Section](#usage)): * `PROXY_APIKEY`: In Receiver Mode, the API key with which this Beam.Connector is registered for listening at the Samply.Broker From dd0ec2461120da20006b97f48b45f4baee46279e Mon Sep 17 00:00:00 2001 From: lablans Date: Tue, 11 Apr 2023 06:25:19 +0000 Subject: [PATCH 02/80] Rebuild cache --- .github/workflows/rust.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c182d02..3715f33 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -61,14 +61,15 @@ jobs: echo "profilestr=--profile $PROFILE" >> $GITHUB_ENV fi - uses: actions/checkout@v3 - - uses: Swatinem/rust-cache@v2 - with: - key: ${{ matrix.arch }}-${{ env.PROFILE }} - uses: actions-rs/toolchain@v1 with: toolchain: stable override: true target: ${{ env.rustarch }} + - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.arch }}-${{ env.PROFILE }} + prefix-key: "v1-rust" # Increase to invalidate old caches. - name: Build (${{ matrix.arch }}) uses: actions-rs/cargo@v1 with: @@ -85,7 +86,7 @@ jobs: test: name: Run tests needs: [ build-rust ] - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Not implemented yet From aa4d58e562bc201306e6b13fad40ab956598802d Mon Sep 17 00:00:00 2001 From: janskiba Date: Tue, 11 Apr 2023 15:04:22 +0000 Subject: [PATCH 03/80] Add testing to CI --- .github/workflows/rust.yml | 17 +++++++++++++++-- dev/start | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3715f33..e2c28f1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -89,8 +89,21 @@ jobs: runs-on: ubuntu-22.04 steps: - - name: Not implemented yet - run: echo "This will be implemented soonish" + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install testing dependencies + run: | + python -m pip install --upgrade pip + pip install requests + - uses: actions/download-artifact@v3 + with: + name: binaries-amd64 + path: artifacts/binaries-amd64/ + - run: | + ./dev/start bg + python ./dev/test.py docker: needs: [ build-rust, pre-check, test ] diff --git a/dev/start b/dev/start index 4e33223..4502679 100755 --- a/dev/start +++ b/dev/start @@ -108,4 +108,40 @@ function start { docker-compose up --no-build --no-recreate --abort-on-container-exit } -start +function start_bg { + clean + pki/pki devsetup + echo "$VAULT_TOKEN" > ./pki/pki.secret + build + docker-compose up --no-build --no-recreate -d + for ADDR in $P1 $P2; do + TRIES=1 + while [ $TRIES -ne 0 ]; do + set +e + CODE=$(curl -s -o /tmp/body -w '%{response_code}' $ADDR/v1/health) + set -e + if [ "$CODE" == "200" ]; then + TRIES=0 + else + echo "Waiting for $ADDR ... (try ${TRIES}/30, last response was: code=$OUT, body=\"$(cat /tmp/body 2>/dev/null)\")" + sleep 1 + ((TRIES=TRIES+1)) + if [ $TRIES -ge 30 ]; then + echo "ERROR: $ADDR not available after 30 seconds. Giving up and printing docker compose logs." + docker-compose logs + exit 5 + fi + fi + done + done + echo "Services are up!" +} + +case "$1" in + bg) + start_bg + ;; + *) + start + ;; +esac From 099546092511c436d95e258818cef8165491c63b Mon Sep 17 00:00:00 2001 From: janskiba Date: Tue, 11 Apr 2023 15:31:13 +0000 Subject: [PATCH 04/80] wait for beam-connect container --- dev/start | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/start b/dev/start index 4502679..8cc8dc5 100755 --- a/dev/start +++ b/dev/start @@ -134,6 +134,7 @@ function start_bg { fi done done + sleep 4 echo "Services are up!" } From 2c3a5f229ab35f6cbe6bb33021c85dc46be51aa5 Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 12 Apr 2023 07:32:44 +0000 Subject: [PATCH 05/80] start and test in seperate steps --- .github/workflows/rust.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e2c28f1..a1f56f0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -101,9 +101,10 @@ jobs: with: name: binaries-amd64 path: artifacts/binaries-amd64/ - - run: | - ./dev/start bg - python ./dev/test.py + - name: Start containers + run: ./dev/start bg + - name: Run tests + run: python ./dev/test.py docker: needs: [ build-rust, pre-check, test ] From 9a3a654e062f580e33f855e2665c73ef3aa5cbd5 Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 12 Apr 2023 13:39:24 +0000 Subject: [PATCH 06/80] wait for containers --- .github/workflows/rust.yml | 5 +++++ dev/start | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a1f56f0..a12181d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -103,6 +103,11 @@ jobs: path: artifacts/binaries-amd64/ - name: Start containers run: ./dev/start bg + - name: Wait for containers + run: | + echo $(curl -s -o /tmp/body -w '%{response_code}' http://localhost:8081/v1/health) + sleep 10 + echo $(curl -s -o /tmp/body -w '%{response_code}' http://localhost:8081/v1/health) - name: Run tests run: python ./dev/test.py diff --git a/dev/start b/dev/start index 8cc8dc5..4502679 100755 --- a/dev/start +++ b/dev/start @@ -134,7 +134,6 @@ function start_bg { fi done done - sleep 4 echo "Services are up!" } From 6cf4ff3b564f0a78e938135c7d6eaca55a7c36bd Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 12 Apr 2023 13:47:07 +0000 Subject: [PATCH 07/80] ping beam-connect --- .github/workflows/rust.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a12181d..7ba70b1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -105,9 +105,9 @@ jobs: run: ./dev/start bg - name: Wait for containers run: | - echo $(curl -s -o /tmp/body -w '%{response_code}' http://localhost:8081/v1/health) + echo $(curl -s -o /tmp/body -w '%{response_code}' http://localhost:8062/v1/health) sleep 10 - echo $(curl -s -o /tmp/body -w '%{response_code}' http://localhost:8081/v1/health) + echo $(curl -s -o /tmp/body -w '%{response_code}' http://localhost:8063/v1/health) - name: Run tests run: python ./dev/test.py From f4a32d0feb5d1e856caccd431c670bef1a11dc3d Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 12 Apr 2023 14:05:57 +0000 Subject: [PATCH 08/80] show logs --- .github/workflows/rust.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 7ba70b1..df05fde 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -108,6 +108,7 @@ jobs: echo $(curl -s -o /tmp/body -w '%{response_code}' http://localhost:8062/v1/health) sleep 10 echo $(curl -s -o /tmp/body -w '%{response_code}' http://localhost:8063/v1/health) + docker logs dev-connect1-1 - name: Run tests run: python ./dev/test.py From ff1b5334a4f8a8d8a942cd71729240aec2a46b9b Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 12 Apr 2023 14:16:51 +0000 Subject: [PATCH 09/80] show logs --- .github/workflows/rust.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index df05fde..e9c60f0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -103,12 +103,11 @@ jobs: path: artifacts/binaries-amd64/ - name: Start containers run: ./dev/start bg - - name: Wait for containers + - name: Show logs + working-directory: ./dev run: | - echo $(curl -s -o /tmp/body -w '%{response_code}' http://localhost:8062/v1/health) - sleep 10 - echo $(curl -s -o /tmp/body -w '%{response_code}' http://localhost:8063/v1/health) - docker logs dev-connect1-1 + sleep 3 + docker compose logs - name: Run tests run: python ./dev/test.py From 30f465c6237f3318431d570c8a7d4fd68e940680 Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 12 Apr 2023 14:38:55 +0000 Subject: [PATCH 10/80] use distroless in tests --- .github/workflows/rust.yml | 2 +- dev/start | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e9c60f0..6ef64fa 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -102,7 +102,7 @@ jobs: name: binaries-amd64 path: artifacts/binaries-amd64/ - name: Start containers - run: ./dev/start bg + run: ./dev/start ci - name: Show logs working-directory: ./dev run: | diff --git a/dev/start b/dev/start index 4502679..f9355f7 100755 --- a/dev/start +++ b/dev/start @@ -58,7 +58,7 @@ function build() { mkdir -p artifacts/binaries-$ARCH rsync "$CONNECT" artifacts/binaries-$ARCH/ BUILD_DOCKER=1 - elif [ -z "$(docker images -q samply/beam-broker:$TAG)" ] || [ -z "$(docker images -q samply/beam-proxy:$TAG)" ]; then + elif [ -z "$(docker images -q samply/beam-connect:$TAG)" ]; then echo "Will rebuild docker image since it is missing." BUILD_DOCKER=1 elif [ -x ./target ]; then @@ -108,7 +108,8 @@ function start { docker-compose up --no-build --no-recreate --abort-on-container-exit } -function start_bg { +function start_ci { + unset IMGNAME clean pki/pki devsetup echo "$VAULT_TOKEN" > ./pki/pki.secret @@ -138,8 +139,8 @@ function start_bg { } case "$1" in - bg) - start_bg + ci) + start_ci ;; *) start From 25f490a9af89d6010a982e00bda86fa19581cc63 Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 12 Apr 2023 14:44:09 +0000 Subject: [PATCH 11/80] comment out logs --- .github/workflows/rust.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 6ef64fa..34dc017 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -103,11 +103,11 @@ jobs: path: artifacts/binaries-amd64/ - name: Start containers run: ./dev/start ci - - name: Show logs - working-directory: ./dev - run: | - sleep 3 - docker compose logs + # - name: Show logs + # working-directory: ./dev + # run: | + # sleep 3 + # docker compose logs - name: Run tests run: python ./dev/test.py From feba785b77efda20ffe14039c317162dd20afae2 Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 26 Apr 2023 13:01:56 +0000 Subject: [PATCH 12/80] remove check for proxy auth header --- src/logic_ask.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/logic_ask.rs b/src/logic_ask.rs index 5c14941..b309bfc 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -1,5 +1,6 @@ use std::{sync::Arc, str::FromStr}; use std::time::{Duration, SystemTime}; +use hyper::http::HeaderValue; use hyper::{Request, Body, Client, client::HttpConnector, Response, header, StatusCode, body, Uri}; use hyper_proxy::ProxyConnector; use hyper_tls::HttpsConnector; @@ -25,9 +26,9 @@ pub(crate) async fn handler_http( headers.insert(header::VIA, format!("Via: Samply.Beam.Connect/0.1 {}", config.my_app_id).parse().unwrap()); - let auth = - headers.remove(header::PROXY_AUTHORIZATION) - .ok_or(StatusCode::PROXY_AUTHENTICATION_REQUIRED)?; + let auth = headers + .remove(header::PROXY_AUTHORIZATION) + .unwrap_or(config.proxy_auth.parse().expect("Proxy auth header could not be generated.")); // Re-pack Authorization: Not necessary since we're not looking at the Authorization header. // if headers.remove(header::AUTHORIZATION).is_some() { From 92ccca4c4b8ad0632ed1a7ff785908c81c5816bf Mon Sep 17 00:00:00 2001 From: janskiba Date: Thu, 27 Apr 2023 14:02:00 +0000 Subject: [PATCH 13/80] imporve logging --- Cargo.toml | 1 - src/logic_ask.rs | 5 ++++- src/logic_reply.rs | 19 ++++++++++++++----- src/main.rs | 2 +- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 36f17cc..fe1e096 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,6 @@ hyper-proxy = "0.9.1" mz-http-proxy = { version = "0.1.0", features = ["hyper"] } log = "*" -pretty_env_logger = "*" serde = "*" serde_json = "*" diff --git a/src/logic_ask.rs b/src/logic_ask.rs index 5c14941..5872e95 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -50,7 +50,10 @@ pub(crate) async fn handler_http( } let target = &targets.get(uri.authority().unwrap()) //TODO unwrap - .ok_or(StatusCode::UNAUTHORIZED)? + .ok_or_else(|| { + warn!("Failed to lookup virtualhost in central mapping: {}", uri.authority().unwrap()); + StatusCode::UNAUTHORIZED + })? .beamconnect; info!("{method} {uri} via {target}"); diff --git a/src/logic_reply.rs b/src/logic_reply.rs index 54b7222..5564f0e 100644 --- a/src/logic_reply.rs +++ b/src/logic_reply.rs @@ -28,14 +28,18 @@ async fn send_reply(task: &MsgTaskRequest, config: &Config, client: &SamplyHttpC headers: resp.headers().clone(), body: String::from_utf8(body.to_vec())? }; - let http_reply = serde_json::to_string(&http_reply)?; + if !resp.status().is_success() { + warn!("Httptask returned with status {}. Reporting failiure to broker.", resp.status()); + warn!("Response body was: {}", http_reply.body); + } + let http_reply_string = serde_json::to_string(&http_reply)?; let msg = MsgTaskResult { from: config.my_app_id.clone().into(), to: vec![task.from.clone()], task: task.id, - status: WorkStatus::Succeeded, + status: WorkStatus::Succeeded, // TODO: we may want to return something else if we were not successfull metadata: Value::Null, - body: Plain::from(http_reply), + body: Plain::from(http_reply_string), }; let req_to_proxy = Request::builder() .method("PUT") @@ -55,8 +59,13 @@ async fn send_reply(task: &MsgTaskRequest, config: &Config, client: &SamplyHttpC async fn execute_http_task(task: &MsgTaskRequest, config: &Config, client: &SamplyHttpClient) -> Result, BeamConnectError> { let task_req = task.http_request()?; info!("{} | {} {}", task.from, task_req.method, task_req.url); - let target = config.targets_local.get(task_req.url.authority().unwrap()) //TODO unwrap - .ok_or(BeamConnectError::CommunicationWithTargetFailed(String::from("Target not defined")))?; + let target = config + .targets_local + .get(task_req.url.authority().unwrap()) //TODO unwrap + .ok_or_else(|| { + warn!("Lookup of local target {} failed", task_req.url.authority().unwrap()); + BeamConnectError::CommunicationWithTargetFailed(String::from("Target not defined")) + })?; if !target.allowed.contains(&AppId::try_from(&task.from).or(Err(BeamConnectError::IdNotAuthorizedToAccessUrl(task.from.clone(), task_req.url.clone())))?) { return Err(BeamConnectError::IdNotAuthorizedToAccessUrl(task.from.clone(), task_req.url.clone())); } diff --git a/src/main.rs b/src/main.rs index 9f925b2..e9b7270 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,7 @@ mod banner; #[tokio::main] async fn main() -> Result<(), Box>{ - pretty_env_logger::init(); + shared::logger::init_logger()?; banner::print_banner(); let config = Config::load().await?; let config2 = config.clone(); From 932071f23af0454b498f78520aca2c3a318d8373 Mon Sep 17 00:00:00 2001 From: janskiba Date: Thu, 27 Apr 2023 17:48:33 +0000 Subject: [PATCH 14/80] switch to tracing --- Cargo.toml | 2 +- src/banner.rs | 2 +- src/config.rs | 2 +- src/logic_ask.rs | 2 +- src/logic_reply.rs | 2 +- src/main.rs | 2 +- src/structs.rs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fe1e096..b472dcf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ hyper-tls = "0.5.0" hyper-proxy = "0.9.1" mz-http-proxy = { version = "0.1.0", features = ["hyper"] } -log = "*" +tracing = "0.1.35" serde = "*" serde_json = "*" diff --git a/src/banner.rs b/src/banner.rs index c920c1d..9455a60 100644 --- a/src/banner.rs +++ b/src/banner.rs @@ -1,4 +1,4 @@ -use log::info; +use tracing::info; pub fn print_banner() { let commit = match env!("GIT_DIRTY") { diff --git a/src/config.rs b/src/config.rs index 8eb5ef9..f39a34a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,7 +2,7 @@ use std::{error::Error, path::PathBuf, fs::File, str::FromStr}; use clap::Parser; use hyper::{Uri, http::uri::{Authority, Scheme}}; -use log::info; +use tracing::info; use serde::{Serialize, Deserialize}; use shared::{beam_id::{AppId, BeamId, app_to_broker_id, BrokerId}, http_client::{SamplyHttpClient, self}}; diff --git a/src/logic_ask.rs b/src/logic_ask.rs index 5872e95..fe71995 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -3,7 +3,7 @@ use std::time::{Duration, SystemTime}; use hyper::{Request, Body, Client, client::HttpConnector, Response, header, StatusCode, body, Uri}; use hyper_proxy::ProxyConnector; use hyper_tls::HttpsConnector; -use log::{info, debug, warn, error}; +use tracing::{info, debug, warn, error}; use serde_json::Value; use shared::http_client::SamplyHttpClient; use shared::{beam_id::AppId, MsgTaskResult, MsgTaskRequest}; diff --git a/src/logic_reply.rs b/src/logic_reply.rs index 5564f0e..f2899a2 100644 --- a/src/logic_reply.rs +++ b/src/logic_reply.rs @@ -1,7 +1,7 @@ use hyper::{Client, client::HttpConnector, Request, header, StatusCode, body, Response, Body, Uri}; use hyper_proxy::ProxyConnector; use hyper_tls::HttpsConnector; -use log::{info, warn, debug}; +use tracing::{info, warn, debug}; use serde_json::Value; use shared::{MsgTaskRequest, MsgTaskResult, MsgId,beam_id::{BeamId,AppId}, WorkStatus, Plain, http_client::SamplyHttpClient}; diff --git a/src/main.rs b/src/main.rs index e9b7270..5066d30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use config::Config; use hyper::{body, Body, service::{service_fn, make_service_fn}, Request, Response, Server, header::{HeaderName, self, ToStrError}, Uri, http::uri::Authority, server::conn::AddrStream, Client, client::HttpConnector}; use hyper_proxy::ProxyConnector; use hyper_tls::HttpsConnector; -use log::{info, error, debug, warn}; +use tracing::{info, error, debug, warn}; use shared::http_client::SamplyHttpClient; mod msg; diff --git a/src/structs.rs b/src/structs.rs index 414e1b9..a878ba2 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -1,7 +1,7 @@ use std::{string::FromUtf8Error, error::Error, fmt::Display}; use hyper::{StatusCode, header::ToStrError, http::uri::Authority}; -use log::error; +use tracing::error; use shared::errors::SamplyBeamError; #[derive(Debug)] From e5231250e1bf1b24955ef9069621799edfd3e2b5 Mon Sep 17 00:00:00 2001 From: Jan <59206115+Threated@users.noreply.github.com> Date: Fri, 28 Apr 2023 09:25:57 +0200 Subject: [PATCH 15/80] Fix log message --- src/logic_ask.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logic_ask.rs b/src/logic_ask.rs index fe71995..5f91593 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -129,7 +129,7 @@ pub(crate) async fn handler_http( let result = task_results.pop().unwrap(); let response_inner = match result.status { shared::WorkStatus::Succeeded => { - serde_json::from_str::(&result.body.body.ok_or({ + serde_json::from_str::(&result.body.body.ok_or_else(|| { warn!("Recieved one sucessfull result but it has no body"); StatusCode::BAD_GATEWAY })?)? From e0d973e5837604052dd57d017ac42529a21c8747 Mon Sep 17 00:00:00 2001 From: janskiba Date: Fri, 28 Apr 2023 09:30:48 +0000 Subject: [PATCH 16/80] Prevent spam requests from beam connect to beam --- src/errors.rs | 2 ++ src/logic_reply.rs | 40 +++++++++++++++++++++++++--------------- src/main.rs | 8 +++++++- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index dad412c..c45cf02 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -6,6 +6,8 @@ use thiserror::Error; #[derive(Error,Debug)] pub(crate) enum BeamConnectError { + #[error("Regular proxy timout")] + ProxyTimeoutError, #[error("Proxy rejected our authorization")] ProxyRejectedAuthorization, #[error("Unable to communicate with Proxy: {0}")] diff --git a/src/logic_reply.rs b/src/logic_reply.rs index f2899a2..0ce65e8 100644 --- a/src/logic_reply.rs +++ b/src/logic_reply.rs @@ -12,7 +12,8 @@ pub(crate) async fn process_requests(config: Config, client: SamplyHttpClient) - let msgs = fetch_requests(&config, &client).await?; for task in msgs { - let resp = execute_http_task(&task, &config, &client).await?; + // If we fail to execute the http task we should report this as a failure to beam + let resp = execute_http_task(&task, &config, &client).await; send_reply(&task, &config, &client, resp).await?; } @@ -20,26 +21,34 @@ pub(crate) async fn process_requests(config: Config, client: SamplyHttpClient) - Ok(()) } -async fn send_reply(task: &MsgTaskRequest, config: &Config, client: &SamplyHttpClient, mut resp: Response) -> Result<(), BeamConnectError> { - let body = body::to_bytes(resp.body_mut()).await - .map_err(BeamConnectError::FailedToReadTargetsReply)?; - let http_reply = HttpResponse { - status: resp.status(), - headers: resp.headers().clone(), - body: String::from_utf8(body.to_vec())? +async fn send_reply(task: &MsgTaskRequest, config: &Config, client: &SamplyHttpClient, resp: Result, BeamConnectError>) -> Result<(), BeamConnectError> { + let (reply_body, status) = match resp { + Ok(mut resp) => { + let body = body::to_bytes(resp.body_mut()).await + .map_err(BeamConnectError::FailedToReadTargetsReply)?; + let body = String::from_utf8(body.to_vec())?; + if !resp.status().is_success() { + warn!("Httptask returned with status {}. Reporting failiure to broker.", resp.status()); + warn!("Response body was: {}", &body); + }; + (serde_json::to_string(&HttpResponse { + status: resp.status(), + headers: resp.headers().clone(), + body + })?, WorkStatus::Succeeded) + }, + Err(e) => { + warn!("Failed to execute http task. Err: {e}"); + ("Error executing http task. See beam connect logs".to_string(), WorkStatus::PermFailed) + }, }; - if !resp.status().is_success() { - warn!("Httptask returned with status {}. Reporting failiure to broker.", resp.status()); - warn!("Response body was: {}", http_reply.body); - } - let http_reply_string = serde_json::to_string(&http_reply)?; let msg = MsgTaskResult { from: config.my_app_id.clone().into(), to: vec![task.from.clone()], task: task.id, - status: WorkStatus::Succeeded, // TODO: we may want to return something else if we were not successfull + status, metadata: Value::Null, - body: Plain::from(http_reply_string), + body: Plain::from(reply_body), }; let req_to_proxy = Request::builder() .method("PUT") @@ -102,6 +111,7 @@ async fn fetch_requests(config: &Config, client: &SamplyHttpClient) -> Result { info!("Got request: {:?}", resp); }, + StatusCode::GATEWAY_TIMEOUT => return Err(BeamConnectError::ProxyTimeoutError), _ => { return Err(BeamConnectError::ProxyOtherError(format!("Got response code {}", resp.status()))); } diff --git a/src/main.rs b/src/main.rs index 5066d30..200234c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,8 @@ use hyper_tls::HttpsConnector; use tracing::{info, error, debug, warn}; use shared::http_client::SamplyHttpClient; +use crate::errors::BeamConnectError; + mod msg; mod example_targets; mod config; @@ -35,7 +37,11 @@ async fn main() -> Result<(), Box>{ loop { debug!("Waiting for next request ..."); if let Err(e) = logic_reply::process_requests(config2.clone(), client2.clone()).await { - warn!("Error in processing request: {e}. Will continue with the next one."); + if let BeamConnectError::ProxyTimeoutError = e { + debug!("{e}"); + } else { + warn!("Error in processing request: {e}. Will continue with the next one."); + } } } }); From edee273b326c02cb0e8fa2c6ce6930e2e2586050 Mon Sep 17 00:00:00 2001 From: Tobias Kussel Date: Tue, 2 May 2023 14:27:37 +0000 Subject: [PATCH 17/80] Add CONNECT Diagnostics --- src/logic_reply.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/logic_reply.rs b/src/logic_reply.rs index 0ce65e8..f2e2f24 100644 --- a/src/logic_reply.rs +++ b/src/logic_reply.rs @@ -1,4 +1,4 @@ -use hyper::{Client, client::HttpConnector, Request, header, StatusCode, body, Response, Body, Uri}; +use hyper::{Client, client::HttpConnector, Request, header, StatusCode, body, Response, Body, Uri, Method}; use hyper_proxy::ProxyConnector; use hyper_tls::HttpsConnector; use tracing::{info, warn, debug}; @@ -78,6 +78,10 @@ async fn execute_http_task(task: &MsgTaskRequest, config: &Config, client: &Samp if !target.allowed.contains(&AppId::try_from(&task.from).or(Err(BeamConnectError::IdNotAuthorizedToAccessUrl(task.from.clone(), task_req.url.clone())))?) { return Err(BeamConnectError::IdNotAuthorizedToAccessUrl(task.from.clone(), task_req.url.clone())); } + // if task_req.url.sheme().is_none() { + if task_req.method == Method::CONNECT { + debug!("Connect Request URL: {:?}", task_req.url); + } let uri = Uri::builder() .scheme(task_req.url.scheme().unwrap().as_str()) .authority(target.replace.to_owned()) From 3454493fff77bed17d925d2955063f0304880b86 Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 3 May 2023 08:26:49 +0000 Subject: [PATCH 18/80] Build correct uri --- examples/example_central_test.json | 6 ++++++ examples/example_local_test.json | 5 +++++ src/logic_reply.rs | 14 ++++++++------ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/examples/example_central_test.json b/examples/example_central_test.json index ba96edd..cf6fc66 100644 --- a/examples/example_central_test.json +++ b/examples/example_central_test.json @@ -5,6 +5,12 @@ "name": "connect2", "virtualhost": "httpbin.org", "beamconnect": "app2.proxy2.broker" + }, + { + "id": "C2", + "name": "connect2", + "virtualhost": "httpbin.org:443", + "beamconnect": "app2.proxy2.broker" } ] } diff --git a/examples/example_local_test.json b/examples/example_local_test.json index 5440f32..b06c4f9 100644 --- a/examples/example_local_test.json +++ b/examples/example_local_test.json @@ -3,5 +3,10 @@ "external": "httpbin.org", "internal": "httpbin.org", "allowed": ["app1.proxy1.broker"] + }, + { + "external": "httpbin.org:443", + "internal": "httpbin.org:443", + "allowed": ["app1.proxy1.broker"] } ] diff --git a/src/logic_reply.rs b/src/logic_reply.rs index f2e2f24..450facd 100644 --- a/src/logic_reply.rs +++ b/src/logic_reply.rs @@ -1,4 +1,4 @@ -use hyper::{Client, client::HttpConnector, Request, header, StatusCode, body, Response, Body, Uri, Method}; +use hyper::{Client, client::HttpConnector, Request, header, StatusCode, body, Response, Body, Uri, Method, http::uri::Scheme}; use hyper_proxy::ProxyConnector; use hyper_tls::HttpsConnector; use tracing::{info, warn, debug}; @@ -78,15 +78,17 @@ async fn execute_http_task(task: &MsgTaskRequest, config: &Config, client: &Samp if !target.allowed.contains(&AppId::try_from(&task.from).or(Err(BeamConnectError::IdNotAuthorizedToAccessUrl(task.from.clone(), task_req.url.clone())))?) { return Err(BeamConnectError::IdNotAuthorizedToAccessUrl(task.from.clone(), task_req.url.clone())); } - // if task_req.url.sheme().is_none() { if task_req.method == Method::CONNECT { debug!("Connect Request URL: {:?}", task_req.url); } - let uri = Uri::builder() - .scheme(task_req.url.scheme().unwrap().as_str()) + + let mut uri = Uri::builder(); + if let Some(scheme) = task_req.url.scheme_str() { + uri = uri.scheme(scheme).path_and_query(task_req.url.path()) + } + let uri = uri .authority(target.replace.to_owned()) - .path_and_query(task_req.url.path()) - .build().unwrap(); // TODO + .build()?; info!("Rewriten to: {} {}", task_req.method, uri); From 673c54230381c352c15c94bc02f1bf18e6826228 Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 3 May 2023 15:06:55 +0000 Subject: [PATCH 19/80] fix ubuntu certs --- Dockerfile.ci | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile.ci b/Dockerfile.ci index 2dd4017..5424bfe 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -8,6 +8,8 @@ COPY /artifacts/binaries-$TARGETARCH/connect /app/ RUN chmod +x /app/* FROM ${IMGNAME} +RUN apt update +RUN apt install -y ca-certificates #ARG COMPONENT ARG TARGETARCH #COPY /artifacts/binaries-$TARGETARCH/$COMPONENT /usr/local/bin/ From 42ecdcedaadde396a877a3e9b7324db7e1794c5f Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 3 May 2023 15:07:15 +0000 Subject: [PATCH 20/80] wip https support --- Cargo.toml | 1 + src/config.rs | 10 ++++++++-- src/logic_ask.rs | 5 +++++ src/main.rs | 36 +++++++++++++++++++++++++++++++----- src/msg.rs | 2 +- 5 files changed, 46 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b472dcf..d9a3e4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ clap = {version = "4", features = ["derive"]} thiserror = "*" http-serde = "1.1.2" +tokio-native-tls = "0.3.1" [build-dependencies] build-data = "0" diff --git a/src/config.rs b/src/config.rs index f39a34a..9912c40 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,6 +2,7 @@ use std::{error::Error, path::PathBuf, fs::File, str::FromStr}; use clap::Parser; use hyper::{Uri, http::uri::{Authority, Scheme}}; +use tokio_native_tls::{TlsAcceptor, native_tls::{self, Identity}}; use tracing::info; use serde::{Serialize, Deserialize}; use shared::{beam_id::{AppId, BeamId, app_to_broker_id, BrokerId}, http_client::{SamplyHttpClient, self}}; @@ -129,7 +130,8 @@ pub(crate) struct Config { pub(crate) targets_local: LocalMapping, pub(crate) targets_public: CentralMapping, pub(crate) expire: u64, - pub(crate) client: SamplyHttpClient + pub(crate) client: SamplyHttpClient, + pub(crate) tls_acceptor: TlsAcceptor } fn load_local_targets(broker_id: &BrokerId, local_target_path: &Option) -> Result> { @@ -183,6 +185,9 @@ impl Config { let targets_public = load_public_targets(&client, &args.discovery_url).await?; let targets_local = load_local_targets(&broker_id, &args.local_targets_file)?; + let identity = Identity::from_pkcs8(pem, key)?; + let tls_acceptor = native_tls::TlsAcceptor::new(identity)?; + Ok(Config { proxy_url: args.proxy_url, my_app_id: my_app_id.clone(), @@ -191,7 +196,8 @@ impl Config { targets_local, targets_public, expire, - client + client, + tls_acceptor }) } } diff --git a/src/logic_ask.rs b/src/logic_ask.rs index d9047fc..2cd32c1 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -1,6 +1,9 @@ use std::{sync::Arc, str::FromStr}; use std::time::{Duration, SystemTime}; +use hyper::Method; use hyper::http::HeaderValue; +use hyper::server::conn::Http; +use hyper::service::service_fn; use hyper::{Request, Body, Client, client::HttpConnector, Response, header, StatusCode, body, Uri}; use hyper_proxy::ProxyConnector; use hyper_tls::HttpsConnector; @@ -19,6 +22,7 @@ pub(crate) async fn handler_http( config: Arc, client: SamplyHttpClient ) -> Result, MyStatusCode> { + let targets = &config.targets_public; let method = req.method().to_owned(); let uri = req.uri().to_owned(); @@ -180,6 +184,7 @@ async fn http_req_to_struct(req: Request, my_id: &AppId, target_id: &AppId headers, body, }; + dbg!(&http_req); let mut msg = MsgTaskRequest::new( my_id.into(), vec![target_id.into()], diff --git a/src/main.rs b/src/main.rs index 200234c..e53d043 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,11 @@ use std::{net::SocketAddr, str::FromStr, convert::Infallible, string::FromUtf8Error, error::Error, fmt::Display, collections::{hash_map, HashMap}, sync::Arc}; use config::Config; -use hyper::{body, Body, service::{service_fn, make_service_fn}, Request, Response, Server, header::{HeaderName, self, ToStrError}, Uri, http::uri::Authority, server::conn::AddrStream, Client, client::HttpConnector}; +use hyper::{body, Body, service::{service_fn, make_service_fn}, Request, Response, Server, header::{HeaderName, self, ToStrError}, Uri, http::uri::Authority, server::conn::{AddrStream, Http}, Client, client::HttpConnector, Method}; use hyper_proxy::ProxyConnector; use hyper_tls::HttpsConnector; +use logic_ask::handler_http; +use native_tls::Identity; use tracing::{info, error, debug, warn}; use shared::http_client::SamplyHttpClient; @@ -71,15 +73,39 @@ async fn main() -> Result<(), Box>{ Ok(()) } -async fn handler_http_wrapper( +pub(crate) async fn handler_http_wrapper( req: Request, config: Arc, client: SamplyHttpClient ) -> Result, Infallible> { - match logic_ask::handler_http(req, config, client).await { - Ok(e) => Ok(e), - Err(e) => Ok(Response::builder().status(e.code).body(body::Body::empty()).unwrap()), + // On https connections we want to emulate that we successfully connected to get the actual http request + if req.method() == Method::CONNECT { + tokio::spawn(async move { + match hyper::upgrade::on(req).await { + Ok(connection) => { + let tls_connection = config.tls_acceptor.accept(connection).await.unwrap_or_else(|e| todo!()); + Http::new().serve_connection(tls_connection, service_fn(|req| { + let client = client.clone(); + let config = config.clone(); + async move { + match handler_http(req, config, client).await { + Ok(e) => Ok::<_, Infallible>(e), + Err(e) => Ok(Response::builder().status(e.code).body(body::Body::empty()).unwrap()), + } + } + })).await.unwrap_or_else(|e| warn!("Failed to handle upgraded connection: {e}")); + }, + Err(e) => warn!("Failed to upgrade connection: {e}"), + }; + }); + Ok(Response::new(Body::empty())) + } else { + match handler_http(req, config, client).await { + Ok(e) => Ok(e), + Err(e) => Ok(Response::builder().status(e.code).body(body::Body::empty()).unwrap()), + } } + } async fn shutdown_signal() { diff --git a/src/msg.rs b/src/msg.rs index a04b937..5e0e22d 100644 --- a/src/msg.rs +++ b/src/msg.rs @@ -4,7 +4,7 @@ use shared::MsgTaskRequest; use crate::errors::BeamConnectError; -#[derive(Serialize,Deserialize)] +#[derive(Serialize,Deserialize, Debug)] pub(crate) struct HttpRequest { #[serde(with = "hyper_serde")] pub(crate) method: Method, From 4185b0763358be27c5b6dca6a4131593e8c5bb0f Mon Sep 17 00:00:00 2001 From: janskiba Date: Thu, 4 May 2023 07:48:33 +0000 Subject: [PATCH 21/80] working tls --- Dockerfile.ci | 5 ++++- src/config.rs | 10 +++++++--- src/logic_ask.rs | 1 + src/main.rs | 9 +++++++-- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Dockerfile.ci b/Dockerfile.ci index 5424bfe..44e989a 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -7,9 +7,12 @@ ARG TARGETARCH COPY /artifacts/binaries-$TARGETARCH/connect /app/ RUN chmod +x /app/* -FROM ${IMGNAME} +# FROM ${IMGNAME} +FROM ubuntu:latest RUN apt update RUN apt install -y ca-certificates +RUN apt install -y ssl-cert +RUN make-ssl-cert generate-default-snakeoil #ARG COMPONENT ARG TARGETARCH #COPY /artifacts/binaries-$TARGETARCH/$COMPONENT /usr/local/bin/ diff --git a/src/config.rs b/src/config.rs index 9912c40..d4ce8f0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use std::{error::Error, path::PathBuf, fs::File, str::FromStr}; +use std::{error::Error, path::PathBuf, fs::{File, read_to_string}, str::FromStr}; use clap::Parser; use hyper::{Uri, http::uri::{Authority, Scheme}}; @@ -185,8 +185,12 @@ impl Config { let targets_public = load_public_targets(&client, &args.discovery_url).await?; let targets_local = load_local_targets(&broker_id, &args.local_targets_file)?; - let identity = Identity::from_pkcs8(pem, key)?; - let tls_acceptor = native_tls::TlsAcceptor::new(identity)?; + // TODO: Add this to cli options + let identity = Identity::from_pkcs8( + read_to_string("/etc/ssl/certs/ssl-cert-snakeoil.pem")?.as_bytes(), + read_to_string("/etc/ssl/private/ssl-cert-snakeoil.key")?.as_bytes(), + )?; + let tls_acceptor = native_tls::TlsAcceptor::new(identity)?.into(); Ok(Config { proxy_url: args.proxy_url, diff --git a/src/logic_ask.rs b/src/logic_ask.rs index 2cd32c1..c2f441f 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -26,6 +26,7 @@ pub(crate) async fn handler_http( let targets = &config.targets_public; let method = req.method().to_owned(); let uri = req.uri().to_owned(); + println!("{}", uri); let headers = req.headers_mut(); headers.insert(header::VIA, format!("Via: Samply.Beam.Connect/0.1 {}", config.my_app_id).parse().unwrap()); diff --git a/src/main.rs b/src/main.rs index e53d043..2b66a90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,6 @@ use hyper::{body, Body, service::{service_fn, make_service_fn}, Request, Respons use hyper_proxy::ProxyConnector; use hyper_tls::HttpsConnector; use logic_ask::handler_http; -use native_tls::Identity; use tracing::{info, error, debug, warn}; use shared::http_client::SamplyHttpClient; @@ -83,7 +82,13 @@ pub(crate) async fn handler_http_wrapper( tokio::spawn(async move { match hyper::upgrade::on(req).await { Ok(connection) => { - let tls_connection = config.tls_acceptor.accept(connection).await.unwrap_or_else(|e| todo!()); + let tls_connection = match config.tls_acceptor.accept(connection).await { + Err(e) => { + warn!("Error accepting tls connection: {e}"); + return; + }, + Ok(s) => s, + }; Http::new().serve_connection(tls_connection, service_fn(|req| { let client = client.clone(); let config = config.clone(); From de0fa0b8669288aec21679f3c05d9af86dea971f Mon Sep 17 00:00:00 2001 From: janskiba Date: Thu, 4 May 2023 09:05:15 +0000 Subject: [PATCH 22/80] set authority and scheme in https requests --- src/logic_ask.rs | 66 +++++++++++++++++++++++++++++++----------------- src/main.rs | 16 +++--------- 2 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/logic_ask.rs b/src/logic_ask.rs index c2f441f..b2b350f 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -2,6 +2,7 @@ use std::{sync::Arc, str::FromStr}; use std::time::{Duration, SystemTime}; use hyper::Method; use hyper::http::HeaderValue; +use hyper::http::uri::{Authority, Scheme}; use hyper::server::conn::Http; use hyper::service::service_fn; use hyper::{Request, Body, Client, client::HttpConnector, Response, header, StatusCode, body, Uri}; @@ -12,6 +13,7 @@ use serde_json::Value; use shared::http_client::SamplyHttpClient; use shared::{beam_id::AppId, MsgTaskResult, MsgTaskRequest}; +use crate::config::CentralMapping; use crate::{config::Config, structs::MyStatusCode, msg::{HttpRequest, HttpResponse}, errors::BeamConnectError}; /// GET http://some.internal.system?a=b&c=d @@ -20,12 +22,20 @@ use crate::{config::Config, structs::MyStatusCode, msg::{HttpRequest, HttpRespon pub(crate) async fn handler_http( mut req: Request, config: Arc, - client: SamplyHttpClient + authority: Option, ) -> Result, MyStatusCode> { + let client = &config.client; let targets = &config.targets_public; let method = req.method().to_owned(); let uri = req.uri().to_owned(); + let Some(authority) = authority.as_ref().or(uri.authority()) else { + if uri.path() == "/sites" { + // Case 1 for sites request: no authority set and /sites + return respond_with_sites(targets); + } + return Err(StatusCode::BAD_REQUEST.into()) + }; println!("{}", uri); let headers = req.headers_mut(); @@ -40,30 +50,29 @@ pub(crate) async fn handler_http( // return Err(StatusCode::CONFLICT.into()); // } - // If the autority is empty (e.g. if localhost is used) or the authoroty is not in the routing - // table AND the path is /sites, return global routing table - if let Some(path) = uri.path_and_query() { - if (uri.authority().is_none() || targets.get(uri.authority().unwrap()).is_none()) && path == "/sites" { - debug!("Central Site Discovery requested"); - let body = body::Body::from(serde_json::to_string(targets)?); - let response = Response::builder() - .status(200) - .body(body) - .unwrap(); - return Ok(response); - - } - } - - let target = &targets.get(uri.authority().unwrap()) //TODO unwrap - .ok_or_else(|| { - warn!("Failed to lookup virtualhost in central mapping: {}", uri.authority().unwrap()); - StatusCode::UNAUTHORIZED - })? - .beamconnect; + let Some(target) = &targets.get(authority) + .map(|target| &target.beamconnect) else { + return if uri.path() == "/sites" { + // Case 2: target not in sites and /sites + respond_with_sites(targets) + } else { + warn!("Failed to lookup virtualhost in central mapping: {}", authority); + Err(StatusCode::UNAUTHORIZED.into()) + } + }; info!("{method} {uri} via {target}"); + // Set the right authority as it might have been passed by the caller because it was a CONNECT request + *req.uri_mut() = { + let mut parts = req.uri().to_owned().into_parts(); + parts.authority = Some(authority.clone()); + parts.scheme = Some(Scheme::HTTPS); + Uri::from_parts(parts).map_err(|e| { + warn!("Could not transform uri authority: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })? + }; let msg = http_req_to_struct(req, &config.my_app_id, &target, &config.expire).await?; // Send to Proxy @@ -185,7 +194,6 @@ async fn http_req_to_struct(req: Request, my_id: &AppId, target_id: &AppId headers, body, }; - dbg!(&http_req); let mut msg = MsgTaskRequest::new( my_id.into(), vec![target_id.into()], @@ -198,3 +206,15 @@ async fn http_req_to_struct(req: Request, my_id: &AppId, target_id: &AppId Ok(msg) } + +/// If the autority is empty (e.g. if localhost is used) or the authoroty is not in the routing +/// table AND the path is /sites, return global routing table +fn respond_with_sites(targets: &CentralMapping) -> Result, MyStatusCode> { + debug!("Central Site Discovery requested"); + let body = body::Body::from(serde_json::to_string(targets)?); + let response = Response::builder() + .status(200) + .body(body) + .unwrap(); + Ok(response) +} diff --git a/src/main.rs b/src/main.rs index 2b66a90..2bdb1a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -80,6 +80,7 @@ pub(crate) async fn handler_http_wrapper( // On https connections we want to emulate that we successfully connected to get the actual http request if req.method() == Method::CONNECT { tokio::spawn(async move { + let authority = req.uri().authority().cloned(); match hyper::upgrade::on(req).await { Ok(connection) => { let tls_connection = match config.tls_acceptor.accept(connection).await { @@ -90,10 +91,10 @@ pub(crate) async fn handler_http_wrapper( Ok(s) => s, }; Http::new().serve_connection(tls_connection, service_fn(|req| { - let client = client.clone(); let config = config.clone(); + let authority = authority.clone(); async move { - match handler_http(req, config, client).await { + match handler_http(req, config, authority).await { Ok(e) => Ok::<_, Infallible>(e), Err(e) => Ok(Response::builder().status(e.code).body(body::Body::empty()).unwrap()), } @@ -105,19 +106,10 @@ pub(crate) async fn handler_http_wrapper( }); Ok(Response::new(Body::empty())) } else { - match handler_http(req, config, client).await { + match handler_http(req, config, None).await { Ok(e) => Ok(e), Err(e) => Ok(Response::builder().status(e.code).body(body::Body::empty()).unwrap()), } } } - -async fn shutdown_signal() { - // Wait for the CTRL+C signal - info!("Starting ..."); - tokio::signal::ctrl_c() - .await - .expect("failed to install CTRL+C signal handler"); - info!("(1/2) Shutting down gracefully ..."); -} From fb173c8ff7ad5be90ae09546794a4c51612d1887 Mon Sep 17 00:00:00 2001 From: janskiba Date: Thu, 4 May 2023 09:13:03 +0000 Subject: [PATCH 23/80] Clean up --- src/config.rs | 6 +++--- src/logic_ask.rs | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/config.rs b/src/config.rs index d4ce8f0..88bd2bb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use std::{error::Error, path::PathBuf, fs::{File, read_to_string}, str::FromStr}; +use std::{error::Error, path::PathBuf, fs::{File, read_to_string}, str::FromStr, sync::Arc}; use clap::Parser; use hyper::{Uri, http::uri::{Authority, Scheme}}; @@ -131,7 +131,7 @@ pub(crate) struct Config { pub(crate) targets_public: CentralMapping, pub(crate) expire: u64, pub(crate) client: SamplyHttpClient, - pub(crate) tls_acceptor: TlsAcceptor + pub(crate) tls_acceptor: Arc } fn load_local_targets(broker_id: &BrokerId, local_target_path: &Option) -> Result> { @@ -190,7 +190,7 @@ impl Config { read_to_string("/etc/ssl/certs/ssl-cert-snakeoil.pem")?.as_bytes(), read_to_string("/etc/ssl/private/ssl-cert-snakeoil.key")?.as_bytes(), )?; - let tls_acceptor = native_tls::TlsAcceptor::new(identity)?.into(); + let tls_acceptor = Arc::new(native_tls::TlsAcceptor::new(identity)?.into()); Ok(Config { proxy_url: args.proxy_url, diff --git a/src/logic_ask.rs b/src/logic_ask.rs index b2b350f..5e43134 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -36,7 +36,6 @@ pub(crate) async fn handler_http( } return Err(StatusCode::BAD_REQUEST.into()) }; - println!("{}", uri); let headers = req.headers_mut(); headers.insert(header::VIA, format!("Via: Samply.Beam.Connect/0.1 {}", config.my_app_id).parse().unwrap()); @@ -183,7 +182,7 @@ async fn http_req_to_struct(req: Request, my_id: &AppId, target_id: &AppId let headers = req.headers().clone(); let body = body::to_bytes(req).await .map_err(|e| { - println!("{e}"); + warn!("Failed to read body: {e}"); StatusCode::BAD_REQUEST })?; let body = String::from_utf8(body.to_vec())?; From 2b56e75f9334b157f0df6582087b228a81d948c1 Mon Sep 17 00:00:00 2001 From: janskiba Date: Thu, 4 May 2023 09:21:34 +0000 Subject: [PATCH 24/80] refactor --- src/logic_ask.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/logic_ask.rs b/src/logic_ask.rs index 5e43134..cd1df40 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -30,11 +30,12 @@ pub(crate) async fn handler_http( let method = req.method().to_owned(); let uri = req.uri().to_owned(); let Some(authority) = authority.as_ref().or(uri.authority()) else { - if uri.path() == "/sites" { + return if uri.path() == "/sites" { // Case 1 for sites request: no authority set and /sites - return respond_with_sites(targets); + respond_with_sites(targets) + } else { + Err(StatusCode::BAD_REQUEST.into()) } - return Err(StatusCode::BAD_REQUEST.into()) }; let headers = req.headers_mut(); @@ -49,7 +50,8 @@ pub(crate) async fn handler_http( // return Err(StatusCode::CONFLICT.into()); // } - let Some(target) = &targets.get(authority) + let Some(target) = &targets + .get(authority) .map(|target| &target.beamconnect) else { return if uri.path() == "/sites" { // Case 2: target not in sites and /sites From 1ebf860828c50c0c2a4fbdb7b0cc5130705b92d5 Mon Sep 17 00:00:00 2001 From: janskiba Date: Thu, 4 May 2023 09:57:10 +0000 Subject: [PATCH 25/80] Fix container build to link correct libssl --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3715f33..d6099b1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -22,7 +22,7 @@ jobs: build-rust: name: Build (Rust) - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: matrix: From 1a7f3d9c3f0eda50d290cc16964e7c26cee432a9 Mon Sep 17 00:00:00 2001 From: janskiba Date: Thu, 4 May 2023 13:16:41 +0000 Subject: [PATCH 26/80] Exit early when trying to initialize tlc acceptor --- src/config.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index 88bd2bb..d2668ee 100644 --- a/src/config.rs +++ b/src/config.rs @@ -189,8 +189,11 @@ impl Config { let identity = Identity::from_pkcs8( read_to_string("/etc/ssl/certs/ssl-cert-snakeoil.pem")?.as_bytes(), read_to_string("/etc/ssl/private/ssl-cert-snakeoil.key")?.as_bytes(), - )?; - let tls_acceptor = Arc::new(native_tls::TlsAcceptor::new(identity)?.into()); + ).expect("Failed to initialize identity for tls acceptor"); + let tls_acceptor = Arc::new(native_tls::TlsAcceptor::new(identity) + .expect("Failed to initialize tls acceptor") + .into() + ); Ok(Config { proxy_url: args.proxy_url, From 481452bacd07176a8150398bdb5f4f2a0450e2f4 Mon Sep 17 00:00:00 2001 From: janskiba Date: Mon, 8 May 2023 08:18:42 +0000 Subject: [PATCH 27/80] Add ssl files to cli options --- README.md | 9 ++++++++- src/config.rs | 12 ++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 769f088..543ea42 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,14 @@ A mishap in communication will be returned as appropriate HTTP replies. As described in the [command line parameter list](#run-as-an-application), the central cite discovery is fetched from a given URL or local json file. However, to spare the local services from the need to express outward facing connections themselves, Samply.Beam.Connect exports this received information as a local REST endpoint: `GET http://:/sites`. Note, that the information is only fetched at startup and remains static for the program's lifetime. +#### HTTPS support + +Https is supported but requires setting up the following parameters: +* `SSL_CERT_PEM`: Location to the pem file used for incoming SSL connections. +* `SSL_CERT_KEY`: Location to the coresponding key file for the SSL connections. +* `TLS_CA_CERTIFICATES_DIR`: May need to be set if the local target uses a self signed certificate which is not trusted by beam-connect. In this case the certificate of the target must be placed inside `TLS_CA_CERTIFICATES_DIR` as a pem file in order to be trusted. + ## Notes -At the moment Samply.Beam.Connect does not implement streaming and does not support HTTPS connections. In the intended usage scenario, both Samply.Beam.Connect and Samply.Beam.Proxy are positioned right next to each other in the same privileged network and thus speak plain HTTP. Of course, for outgoing traffic, the Samply.Proxy signs and encrypts the payloads on its own. +At the moment Samply.Beam.Connect does not implement streaming. In the intended usage scenario, both Samply.Beam.Connect and Samply.Beam.Proxy are positioned right next to each other in the same privileged network and thus speak plain HTTP or [HTTPS if configured](#https). Of course, for outgoing traffic, the Samply.Proxy signs and encrypts the payloads on its own. In Receiving Mode, Beam.Connect only relays requests to allow-listed resources to mitigate possible misuse. diff --git a/src/config.rs b/src/config.rs index d2668ee..0c25dae 100644 --- a/src/config.rs +++ b/src/config.rs @@ -64,6 +64,14 @@ struct CliArgs { #[clap(long, env, value_parser)] tls_ca_certificates_dir: Option, + /// Pem file used for ssl support. Will use a snakeoil pem if unset. + #[clap(long, env, value_parser, default_value = "/etc/ssl/certs/ssl-cert-snakeoil.pem")] + ssl_cert_pem: PathBuf, + + /// Key file used for ssl support. Will use a snakeoil key if unset. + #[clap(long, env, value_parser, default_value = "/etc/ssl/private/ssl-cert-snakeoil.key")] + ssl_cert_key: PathBuf, + /// Expiry time of the request in seconds #[clap(long, env, value_parser, default_value = "3600")] expire: u64, @@ -187,8 +195,8 @@ impl Config { // TODO: Add this to cli options let identity = Identity::from_pkcs8( - read_to_string("/etc/ssl/certs/ssl-cert-snakeoil.pem")?.as_bytes(), - read_to_string("/etc/ssl/private/ssl-cert-snakeoil.key")?.as_bytes(), + read_to_string(args.ssl_cert_pem)?.as_bytes(), + read_to_string(args.ssl_cert_key)?.as_bytes(), ).expect("Failed to initialize identity for tls acceptor"); let tls_acceptor = Arc::new(native_tls::TlsAcceptor::new(identity) .expect("Failed to initialize tls acceptor") From a1c0bcdf299b446674940698836929281b97a76d Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 24 May 2023 09:43:28 +0000 Subject: [PATCH 28/80] Remove finished todo --- src/config.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 0c25dae..d8ee358 100644 --- a/src/config.rs +++ b/src/config.rs @@ -193,7 +193,6 @@ impl Config { let targets_public = load_public_targets(&client, &args.discovery_url).await?; let targets_local = load_local_targets(&broker_id, &args.local_targets_file)?; - // TODO: Add this to cli options let identity = Identity::from_pkcs8( read_to_string(args.ssl_cert_pem)?.as_bytes(), read_to_string(args.ssl_cert_key)?.as_bytes(), From 0899ec4b01e76264cde7c256afc9e8e73009af96 Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 24 May 2023 11:50:48 +0000 Subject: [PATCH 29/80] Remove unused client in handler --- src/main.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2bdb1a0..4d163f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -51,11 +51,10 @@ async fn main() -> Result<(), Box>{ let make_service = make_service_fn(|_conn: &AddrStream| { // let remote_addr = conn.remote_addr(); - let client = client.clone(); let config = config.clone(); async { Ok::<_, Infallible>(service_fn(move |req| - handler_http_wrapper(req, config.clone(), client.clone()))) + handler_http_wrapper(req, config.clone()))) } }); @@ -75,7 +74,6 @@ async fn main() -> Result<(), Box>{ pub(crate) async fn handler_http_wrapper( req: Request, config: Arc, - client: SamplyHttpClient ) -> Result, Infallible> { // On https connections we want to emulate that we successfully connected to get the actual http request if req.method() == Method::CONNECT { From 1a19a0c9ff4e6ec8f249a28d0502104e8f7dc5a9 Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 10 May 2023 15:18:32 +0000 Subject: [PATCH 30/80] Encode body as b64 encoded Vec --- src/logic_ask.rs | 3 +-- src/logic_reply.rs | 5 ++--- src/msg.rs | 7 ++++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/logic_ask.rs b/src/logic_ask.rs index cd1df40..f2f74f6 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -187,13 +187,12 @@ async fn http_req_to_struct(req: Request, my_id: &AppId, target_id: &AppId warn!("Failed to read body: {e}"); StatusCode::BAD_REQUEST })?; - let body = String::from_utf8(body.to_vec())?; let http_req = HttpRequest { method, url, headers, - body, + body: body.to_vec(), }; let mut msg = MsgTaskRequest::new( my_id.into(), diff --git a/src/logic_reply.rs b/src/logic_reply.rs index 450facd..c6fa1f9 100644 --- a/src/logic_reply.rs +++ b/src/logic_reply.rs @@ -26,15 +26,14 @@ async fn send_reply(task: &MsgTaskRequest, config: &Config, client: &SamplyHttpC Ok(mut resp) => { let body = body::to_bytes(resp.body_mut()).await .map_err(BeamConnectError::FailedToReadTargetsReply)?; - let body = String::from_utf8(body.to_vec())?; if !resp.status().is_success() { warn!("Httptask returned with status {}. Reporting failiure to broker.", resp.status()); - warn!("Response body was: {}", &body); + // warn!("Response body was: {}", &body); }; (serde_json::to_string(&HttpResponse { status: resp.status(), headers: resp.headers().clone(), - body + body: body.to_vec() })?, WorkStatus::Succeeded) }, Err(e) => { diff --git a/src/msg.rs b/src/msg.rs index 5e0e22d..e445038 100644 --- a/src/msg.rs +++ b/src/msg.rs @@ -1,7 +1,6 @@ use hyper::{Method, Uri, HeaderMap, StatusCode}; use serde::{Serialize, Deserialize}; use shared::MsgTaskRequest; - use crate::errors::BeamConnectError; #[derive(Serialize,Deserialize, Debug)] @@ -12,7 +11,8 @@ pub(crate) struct HttpRequest { pub(crate) url: Uri, #[serde(with = "hyper_serde")] pub(crate) headers: HeaderMap, - pub(crate) body: String + #[serde(with = "shared::serde_helpers::serde_base64")] + pub(crate) body: Vec } #[derive(Serialize,Deserialize)] @@ -21,7 +21,8 @@ pub(crate) struct HttpResponse { pub(crate) status: StatusCode, #[serde(with = "hyper_serde")] pub(crate) headers: HeaderMap, - pub(crate) body: String + #[serde(with = "shared::serde_helpers::serde_base64")] + pub(crate) body: Vec } pub(crate) trait IsValidHttpTask { From 10b44e1e911f0ccd04a858414906e4de68b0897e Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 7 Jun 2023 09:18:05 +0000 Subject: [PATCH 31/80] Revert to using auth --- src/logic_ask.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/logic_ask.rs b/src/logic_ask.rs index f2f74f6..778cdba 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -41,9 +41,8 @@ pub(crate) async fn handler_http( headers.insert(header::VIA, format!("Via: Samply.Beam.Connect/0.1 {}", config.my_app_id).parse().unwrap()); - let auth = headers - .remove(header::PROXY_AUTHORIZATION) - .unwrap_or(config.proxy_auth.parse().expect("Proxy auth header could not be generated.")); + let auth = headers.remove(header::PROXY_AUTHORIZATION) + .ok_or(StatusCode::PROXY_AUTHENTICATION_REQUIRED)?; // Re-pack Authorization: Not necessary since we're not looking at the Authorization header. // if headers.remove(header::AUTHORIZATION).is_some() { From 6a5f85637da70836f1d95bcda2270f6277a10659 Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 7 Jun 2023 16:17:11 +0000 Subject: [PATCH 32/80] Polling and http execution --- Cargo.toml | 5 +- src/logic_ask.rs | 8 +- src/main.rs | 5 +- src/sockets.rs | 194 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 src/sockets.rs diff --git a/Cargo.toml b/Cargo.toml index d9a3e4b..cd4d805 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ shared = { git = "https://github.com/samply/beam", branch="develop" } #axum = "0.5.12" tokio = { version = "1", features = ["macros","rt-multi-thread","signal"] } -hyper = { version = "0", features = ["full"] } +hyper = { version = "0.14", features = ["full"] } tower-http = { version = "0", features = ["trace"] } tower = "*" @@ -43,5 +43,8 @@ thiserror = "*" http-serde = "1.1.2" tokio-native-tls = "0.3.1" +[features] +sockets = [] + [build-dependencies] build-data = "0" diff --git a/src/logic_ask.rs b/src/logic_ask.rs index 778cdba..40eba7d 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -73,6 +73,10 @@ pub(crate) async fn handler_http( StatusCode::INTERNAL_SERVER_ERROR })? }; + handle_via_tasks(req, &config, target, auth).await +} + +async fn handle_via_tasks(req: Request, config: &Arc, target: &AppId, auth: HeaderValue) -> Result, MyStatusCode> { let msg = http_req_to_struct(req, &config.my_app_id, &target, &config.expire).await?; // Send to Proxy @@ -83,7 +87,7 @@ pub(crate) async fn handler_http( .body(body::Body::from(serde_json::to_vec(&msg)?)) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; debug!("SENDING request to Proxy: {:?}, {:?}", msg, req_to_proxy); - let resp = client.request(req_to_proxy).await + let resp = config.client.request(req_to_proxy).await .map_err(|_| StatusCode::BAD_GATEWAY)?; if resp.status() != StatusCode::CREATED { return Err(StatusCode::BAD_GATEWAY.into()); @@ -107,7 +111,7 @@ pub(crate) async fn handler_http( .header(header::ACCEPT, "application/json") .uri(results_uri) .body(body::Body::empty()).unwrap(); - let mut resp = client.request(req).await + let mut resp = config.client.request(req).await .map_err(|e| { warn!("Got error from server: {e}"); StatusCode::BAD_GATEWAY diff --git a/src/main.rs b/src/main.rs index 4d163f5..f683de9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,8 @@ mod structs; mod logic_ask; mod logic_reply; mod banner; +#[cfg(feature = "sockets")] +mod sockets; #[tokio::main] async fn main() -> Result<(), Box>{ @@ -46,6 +48,8 @@ async fn main() -> Result<(), Box>{ } } }); + #[cfg(feature = "sockets")] + sockets::spwan_socket_task_poller(config.clone()); let config = Arc::new(config.clone()); @@ -67,7 +71,6 @@ async fn main() -> Result<(), Box>{ } info!("(2/2) Shutting down gracefully ..."); http_executor.abort(); - http_executor.await.unwrap(); Ok(()) } diff --git a/src/sockets.rs b/src/sockets.rs new file mode 100644 index 0000000..e48a71d --- /dev/null +++ b/src/sockets.rs @@ -0,0 +1,194 @@ +use std::{time::Duration, collections::HashSet, io, sync::Arc, convert::Infallible}; + +use hyper::{header, Request, Body, body::{self, HttpBody}, StatusCode, upgrade::{Upgraded, self}, Response, http::{uri::Authority, HeaderValue}, client::conn::Builder, server::conn::Http, service::service_fn, Uri}; +use serde::{Serialize, Deserialize}; +use shared::{MsgId, beam_id::{AppOrProxyId, AppId}}; +use tokio::net::TcpStream; +use tracing::{error, warn, debug}; + +use crate::{config::Config, errors::BeamConnectError, structs::MyStatusCode}; + +#[derive(Debug, Serialize, Deserialize)] +struct SocketTask { + from: AppOrProxyId, + to: Vec, + id: MsgId, + ttl: String, +} + +pub(crate) fn spwan_socket_task_poller(config: Config) { + tokio::spawn(async move { + use BeamConnectError::*; + + loop { + let tasks = match poll_socket_task(&config).await { + Ok(tasks) => tasks, + Err(HyperBuildError(e)) => { + error!("{e}"); + error!("This is most likely caused by wrong configuration"); + break; + }, + Err(ProxyTimeoutError) => continue, + Err(e) => { + warn!("Error during socket task polling: {e}"); + tokio::time::sleep(Duration::from_secs(10)).await; + continue; + } + }; + for task in tasks { + let Ok(client) = AppId::try_from(&task.from) else { + warn!("Invalid app id skipping"); + continue; + }; + match connect_proxy(&task.id, &config).await { + Ok(resp) => tunnel(resp, client, config.clone()), + Err(e) => { + warn!("{e}"); + continue; + }, + }; + + } + + + } + }); +} + +async fn poll_socket_task(config: &Config) -> Result, BeamConnectError> { + let poll_socket_tasks = Request::builder() + .uri(format!("{}v1/sockets", config.proxy_url)) + .header(header::AUTHORIZATION, config.proxy_auth.clone()) + .header(header::ACCEPT, "application/json") + .body(Body::empty())?; + let mut resp = config.client.request(poll_socket_tasks).await.map_err(BeamConnectError::ProxyHyperError)?; + match resp.status() { + StatusCode::OK => {}, + StatusCode::GATEWAY_TIMEOUT => return Err(BeamConnectError::ProxyTimeoutError), + e => return Err(BeamConnectError::ProxyOtherError(format!("Unexpected status code {e}"))) + }; + let body = body::to_bytes(resp.body_mut()).await.map_err(BeamConnectError::ProxyHyperError)?; + Ok(serde_json::from_slice(&body)?) +} + +async fn connect_proxy(task_id: &MsgId, config: &Config) -> Result, BeamConnectError> { + let connect_proxy_req = Request::builder() + .uri(format!("{}v1/sockets/{task_id}", config.proxy_url)) + .header(header::AUTHORIZATION, config.proxy_auth.clone()) + .header(header::ACCEPT, "application/json") + .body(Body::empty()) + .expect("This is a valid request"); + let resp = config.client.request(connect_proxy_req).await.map_err(BeamConnectError::ProxyHyperError)?; + let invalid_status_reason = match resp.status() { + StatusCode::SWITCHING_PROTOCOLS => return Ok(resp), + StatusCode::NOT_FOUND | StatusCode::GONE => { + "Task already expired".to_string() + }, + StatusCode::UNAUTHORIZED => { + "This socket is not for this authorized for this app".to_string() + } + other => other.to_string() + }; + Err(BeamConnectError::ProxyOtherError(invalid_status_reason)) +} + +fn status_to_response(status: StatusCode) -> Response { + let mut res = Response::default(); + *res.status_mut() = status; + res +} + +fn tunnel(proxy: Response, client: AppId, config: Config) { + tokio::spawn(async move { + let proxy = match upgrade::on(proxy).await { + Ok(socket) => socket, + Err(e) => { + warn!("Failed to upgrade connection to proxy: {e}"); + return; + }, + }; + let http_err = Http::new() + .http1_only(true) + .http1_keep_alive(true) + .serve_connection(proxy, service_fn(move |req| { + let client2 = client.clone(); + let config2 = config.clone(); + async move { + Ok::<_, Infallible>(handle_tunnel(req, &client2, &config2).await.unwrap_or_else(status_to_response)) + } + })) + .await; + + if let Err(e) = http_err { + warn!("Error while serving HTTP connection: {e}"); + } + }); +} + +async fn handle_tunnel(mut req: Request, app: &AppId, config: &Config) -> Result, StatusCode> { + // TODO: What exactly happens here if we dont have an authority + let authority = req.uri().authority().unwrap(); + let Some(target) = config.targets_local.get(authority) else { + warn!("Failed to lookup authority {authority}"); + return Err(StatusCode::BAD_REQUEST); + }; + if !target.allowed.contains(&app) { + warn!("App {app} not autherized to access url {}", req.uri()); + return Err(StatusCode::UNAUTHORIZED); + }; + *req.uri_mut() = { + let mut parts = req.uri().to_owned().into_parts(); + parts.authority = Some(authority.clone()); + Uri::from_parts(parts).map_err(|e| { + warn!("Could not transform uri authority: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })? + }; + + let resp = config.client.request(req).await.map_err(|e| { + warn!("Communication with target failed: {e}"); + StatusCode::BAD_GATEWAY + })?; + Ok(resp) +} + +async fn handle_via_sockets(req: Request, config: &Arc, target: &AppId, auth: HeaderValue) -> Result, MyStatusCode> { + let connect_proxy_req = Request::builder() + .uri(format!("{}v1/sockets/{target}", config.proxy_url)) + .header(header::AUTHORIZATION, config.proxy_auth.clone()) + .header(header::ACCEPT, "application/json") + .body(Body::empty()) + .expect("This is a valid request"); + let resp = config.client.request(connect_proxy_req).await.map_err(|e| { + warn!("Failed to reach proxy"); + StatusCode::BAD_GATEWAY + })?; + if resp.status() != StatusCode::SWITCHING_PROTOCOLS { + return Err(resp.status().into()); + } + let proxy_socket = upgrade::on(resp).await.map_err(|e| { + warn!("Failed to upgrade response from proxy to socket: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let (mut sender, conn) = Builder::new() + .http1_preserve_header_case(true) + .http1_title_case_headers(true) + .handshake(proxy_socket) + .await + .map_err(|e| { + warn!("Error doing handshake with proxy"); + StatusCode::BAD_GATEWAY + })?; + tokio::task::spawn(async move { + if let Err(err) = conn.await { + warn!("Connection failed: {:?}", err); + } + }); + + let resp = sender.send_request(req).await.map_err(|e| { + warn!("Failed to send request to proxy"); + StatusCode::BAD_GATEWAY + })?; + Ok(resp) +} From da917cbbd7a8a06045e3410afbc40f70678ba851 Mon Sep 17 00:00:00 2001 From: janskiba Date: Fri, 9 Jun 2023 07:45:44 +0000 Subject: [PATCH 33/80] Update start script --- dev/start | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/dev/start b/dev/start index f9355f7..d1507bf 100755 --- a/dev/start +++ b/dev/start @@ -53,7 +53,7 @@ function build() { CONNECT=./target/debug/connect if [ ! -x ./artifacts/binaries-$ARCH ]; then echo "Binaries missing -- building ..." - BUILD="$(cargo build --message-format=json)" + BUILD="$(cargo build $@ --message-format=json)" echo "Will rebuild docker image since binaries had not been there." mkdir -p artifacts/binaries-$ARCH rsync "$CONNECT" artifacts/binaries-$ARCH/ @@ -63,7 +63,7 @@ function build() { BUILD_DOCKER=1 elif [ -x ./target ]; then echo "Checking for changed Rust source code ..." - BUILD="$(cargo build --message-format=json)" + BUILD="$(cargo build $@ --message-format=json)" if echo $BUILD | jq 'select(.fresh==false)' | grep -q 'fresh'; then echo "Will rebuild docker image due to changes in rust binaries." rsync "$CONNECT" artifacts/binaries-$ARCH/ @@ -104,7 +104,7 @@ function start { clean pki/pki devsetup echo "$VAULT_TOKEN" > ./pki/pki.secret - build + build $@ docker-compose up --no-build --no-recreate --abort-on-container-exit } @@ -113,7 +113,7 @@ function start_ci { clean pki/pki devsetup echo "$VAULT_TOKEN" > ./pki/pki.secret - build + build $@ docker-compose up --no-build --no-recreate -d for ADDR in $P1 $P2; do TRIES=1 @@ -140,9 +140,10 @@ function start_ci { case "$1" in ci) - start_ci + shift + start_ci $@ ;; *) - start + start $@ ;; esac From 9aaadfaaff92c772bddcaa6b71c254b6ce5dee8b Mon Sep 17 00:00:00 2001 From: janskiba Date: Fri, 9 Jun 2023 07:46:17 +0000 Subject: [PATCH 34/80] Working http via sockets --- src/logic_ask.rs | 7 +++++-- src/sockets.rs | 13 +++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/logic_ask.rs b/src/logic_ask.rs index 40eba7d..52e7a31 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -25,7 +25,6 @@ pub(crate) async fn handler_http( authority: Option, ) -> Result, MyStatusCode> { - let client = &config.client; let targets = &config.targets_public; let method = req.method().to_owned(); let uri = req.uri().to_owned(); @@ -73,7 +72,11 @@ pub(crate) async fn handler_http( StatusCode::INTERNAL_SERVER_ERROR })? }; - handle_via_tasks(req, &config, target, auth).await + if cfg!(feature = "sockets") { + crate::sockets::handle_via_sockets(req, &config, target, auth).await + } else { + handle_via_tasks(req, &config, target, auth).await + } } async fn handle_via_tasks(req: Request, config: &Arc, target: &AppId, auth: HeaderValue) -> Result, MyStatusCode> { diff --git a/src/sockets.rs b/src/sockets.rs index e48a71d..074e967 100644 --- a/src/sockets.rs +++ b/src/sockets.rs @@ -1,6 +1,6 @@ use std::{time::Duration, collections::HashSet, io, sync::Arc, convert::Infallible}; -use hyper::{header, Request, Body, body::{self, HttpBody}, StatusCode, upgrade::{Upgraded, self}, Response, http::{uri::Authority, HeaderValue}, client::conn::Builder, server::conn::Http, service::service_fn, Uri}; +use hyper::{header, Request, Body, body::{self, HttpBody}, StatusCode, upgrade::{Upgraded, self}, Response, http::{uri::Authority, HeaderValue}, client::conn::Builder, server::conn::Http, service::service_fn, Uri, Method}; use serde::{Serialize, Deserialize}; use shared::{MsgId, beam_id::{AppOrProxyId, AppId}}; use tokio::net::TcpStream; @@ -44,13 +44,9 @@ pub(crate) fn spwan_socket_task_poller(config: Config) { Ok(resp) => tunnel(resp, client, config.clone()), Err(e) => { warn!("{e}"); - continue; }, }; - } - - } }); } @@ -75,7 +71,7 @@ async fn connect_proxy(task_id: &MsgId, config: &Config) -> Result, app: &AppId, config: &Config) -> Ok(resp) } -async fn handle_via_sockets(req: Request, config: &Arc, target: &AppId, auth: HeaderValue) -> Result, MyStatusCode> { +pub(crate) async fn handle_via_sockets(req: Request, config: &Arc, target: &AppId, auth: HeaderValue) -> Result, MyStatusCode> { let connect_proxy_req = Request::builder() + .method(Method::POST) .uri(format!("{}v1/sockets/{target}", config.proxy_url)) .header(header::AUTHORIZATION, config.proxy_auth.clone()) - .header(header::ACCEPT, "application/json") + .header(header::UPGRADE, "tcp") .body(Body::empty()) .expect("This is a valid request"); let resp = config.client.request(connect_proxy_req).await.map_err(|e| { From 793c1b155cb024d448d658c492586c43f16d6b9f Mon Sep 17 00:00:00 2001 From: janskiba Date: Fri, 9 Jun 2023 11:10:46 +0000 Subject: [PATCH 35/80] Fix conditional compilation --- src/logic_ask.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/logic_ask.rs b/src/logic_ask.rs index 52e7a31..412b0c7 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -72,11 +72,10 @@ pub(crate) async fn handler_http( StatusCode::INTERNAL_SERVER_ERROR })? }; - if cfg!(feature = "sockets") { - crate::sockets::handle_via_sockets(req, &config, target, auth).await - } else { - handle_via_tasks(req, &config, target, auth).await - } + #[cfg(feature = "sockets")] + return crate::sockets::handle_via_sockets(req, &config, target, auth).await; + #[cfg(not(feature = "sockets"))] + return handle_via_tasks(req, &config, target, auth).await; } async fn handle_via_tasks(req: Request, config: &Arc, target: &AppId, auth: HeaderValue) -> Result, MyStatusCode> { From c227216bc0cbd26ea9ca7540743134c7281ea4cb Mon Sep 17 00:00:00 2001 From: janskiba Date: Fri, 9 Jun 2023 11:12:32 +0000 Subject: [PATCH 36/80] spwan tokio task for each socket task --- src/sockets.rs | 61 +++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/src/sockets.rs b/src/sockets.rs index 074e967..9f453b9 100644 --- a/src/sockets.rs +++ b/src/sockets.rs @@ -40,12 +40,15 @@ pub(crate) fn spwan_socket_task_poller(config: Config) { warn!("Invalid app id skipping"); continue; }; - match connect_proxy(&task.id, &config).await { - Ok(resp) => tunnel(resp, client, config.clone()), - Err(e) => { - warn!("{e}"); - }, - }; + let config_clone = config.clone(); + tokio::spawn(async move { + match connect_proxy(&task.id, &config_clone).await { + Ok(resp) => tunnel(resp, client, &config_clone).await, + Err(e) => { + warn!("{e}"); + }, + }; + }); } } }); @@ -94,31 +97,29 @@ fn status_to_response(status: StatusCode) -> Response { res } -fn tunnel(proxy: Response, client: AppId, config: Config) { - tokio::spawn(async move { - let proxy = match upgrade::on(proxy).await { - Ok(socket) => socket, - Err(e) => { - warn!("Failed to upgrade connection to proxy: {e}"); - return; - }, - }; - let http_err = Http::new() - .http1_only(true) - .http1_keep_alive(true) - .serve_connection(proxy, service_fn(move |req| { - let client2 = client.clone(); - let config2 = config.clone(); - async move { - Ok::<_, Infallible>(handle_tunnel(req, &client2, &config2).await.unwrap_or_else(status_to_response)) - } - })) - .await; +async fn tunnel(proxy: Response, client: AppId, config: &Config) { + let proxy = match upgrade::on(proxy).await { + Ok(socket) => socket, + Err(e) => { + warn!("Failed to upgrade connection to proxy: {e}"); + return; + }, + }; + let http_err = Http::new() + .http1_only(true) + .http1_keep_alive(true) + .serve_connection(proxy, service_fn(move |req| { + let client2 = client.clone(); + let config2 = config.clone(); + async move { + Ok::<_, Infallible>(handle_tunnel(req, &client2, &config2).await.unwrap_or_else(status_to_response)) + } + })) + .await; - if let Err(e) = http_err { - warn!("Error while serving HTTP connection: {e}"); - } - }); + if let Err(e) = http_err { + warn!("Error while serving HTTP connection: {e}"); + } } async fn handle_tunnel(mut req: Request, app: &AppId, config: &Config) -> Result, StatusCode> { From 84757d02b64e6ee8ae280cbbb6fa119788af57ee Mon Sep 17 00:00:00 2001 From: janskiba Date: Fri, 9 Jun 2023 11:15:37 +0000 Subject: [PATCH 37/80] Add rust tests --- Cargo.toml | 3 +++ tests/base_tests.rs | 63 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 tests/base_tests.rs diff --git a/Cargo.toml b/Cargo.toml index cd4d805..2f04a20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,3 +48,6 @@ sockets = [] [build-dependencies] build-data = "0" + +[dev-dependencies] +paste = "1.0.12" diff --git a/tests/base_tests.rs b/tests/base_tests.rs new file mode 100644 index 0000000..5eab200 --- /dev/null +++ b/tests/base_tests.rs @@ -0,0 +1,63 @@ +use clap::__derive_refs::once_cell::sync::Lazy; +use hyper::{Response, Body, Request, header, http::HeaderValue, StatusCode, Client, client::HttpConnector}; +use hyper_proxy::{Proxy, Intercept, ProxyConnector}; +use hyper_tls::HttpsConnector; +use serde_json::Value; +use tokio_native_tls::native_tls::TlsConnector; + +const CLIENT: Lazy>>> = Lazy::new(|| { + let tls = TlsConnector::builder() + .danger_accept_invalid_certs(true) + .danger_accept_invalid_hostnames(true) + .build() + .unwrap(); + let connector = HttpsConnector::from((HttpConnector::new(), tls.clone().into())); + let proxy = Proxy::new(Intercept::All, "http://localhost:8062".parse().unwrap()); + let mut proxy_con = ProxyConnector::from_proxy(connector, proxy).unwrap(); + proxy_con.set_tls(Some(tls)); + Client::builder().build(proxy_con) +}); + +pub async fn request(mut req: Request) -> Response { + req.headers_mut().append(header::PROXY_AUTHORIZATION, HeaderValue::from_static("ApiKey app1.proxy1.broker App1Secret")); + CLIENT.request(req).await.unwrap() +} + +pub async fn test_normal(scheme: &str) { + let req = Request::get(format!("{scheme}://httpbin.org/anything")).body(Body::empty()).unwrap(); + let res = request(req).await; + assert_eq!(res.status(), StatusCode::OK, "Could not make normal request via beam-connect"); +} + +pub async fn test_json(scheme: &str) { + let json = serde_json::json!({ + "foo": [1, 2, {}], + "bar": "foo", + "foobar": false, + }); + let req = Request::get(format!("{scheme}://httpbin.org/anything")).body(Body::from(serde_json::to_vec(&json).unwrap())).unwrap(); + let mut res = request(req).await; + assert_eq!(res.status(), StatusCode::OK, "Could not make json request via beam-connect"); + let bytes = hyper::body::to_bytes(res.body_mut()).await.unwrap(); + let recieved: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(recieved.get("json").unwrap(), &json, "Json did not match"); +} + +macro_rules! test_http_and_https { + ($name:ident) => { + paste::paste! { + #[tokio::test] + async fn [<$name _http>]() { + $name("http").await + } + + #[tokio::test] + async fn [<$name _https>]() { + $name("https").await + } + } + }; +} + +test_http_and_https!{test_normal} +test_http_and_https!{test_json} From 503cf6fcc4bc8b62145a6184fc38b74fca823c09 Mon Sep 17 00:00:00 2001 From: janskiba Date: Fri, 9 Jun 2023 14:05:53 +0000 Subject: [PATCH 38/80] Dont do all requests via https --- src/logic_ask.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/logic_ask.rs b/src/logic_ask.rs index 412b0c7..6fe3b31 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -66,7 +66,6 @@ pub(crate) async fn handler_http( *req.uri_mut() = { let mut parts = req.uri().to_owned().into_parts(); parts.authority = Some(authority.clone()); - parts.scheme = Some(Scheme::HTTPS); Uri::from_parts(parts).map_err(|e| { warn!("Could not transform uri authority: {e}"); StatusCode::INTERNAL_SERVER_ERROR From 87bd0201e78e1b1c51835ee79cbf88f035770a54 Mon Sep 17 00:00:00 2001 From: janskiba Date: Fri, 9 Jun 2023 16:08:58 +0000 Subject: [PATCH 39/80] Test ws in rust --- Cargo.toml | 2 ++ tests/base_tests.rs | 70 ++++++++++++++++++++------------------------- tests/common/mod.rs | 41 ++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 39 deletions(-) create mode 100644 tests/common/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 2f04a20..a3f9125 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,4 +50,6 @@ sockets = [] build-data = "0" [dev-dependencies] +futures-util = "0.3.28" paste = "1.0.12" +tokio-tungstenite = "0.19.0" diff --git a/tests/base_tests.rs b/tests/base_tests.rs index 5eab200..e576543 100644 --- a/tests/base_tests.rs +++ b/tests/base_tests.rs @@ -1,30 +1,11 @@ -use clap::__derive_refs::once_cell::sync::Lazy; -use hyper::{Response, Body, Request, header, http::HeaderValue, StatusCode, Client, client::HttpConnector}; -use hyper_proxy::{Proxy, Intercept, ProxyConnector}; -use hyper_tls::HttpsConnector; +use hyper::{Body, Request, StatusCode}; use serde_json::Value; -use tokio_native_tls::native_tls::TlsConnector; -const CLIENT: Lazy>>> = Lazy::new(|| { - let tls = TlsConnector::builder() - .danger_accept_invalid_certs(true) - .danger_accept_invalid_hostnames(true) - .build() - .unwrap(); - let connector = HttpsConnector::from((HttpConnector::new(), tls.clone().into())); - let proxy = Proxy::new(Intercept::All, "http://localhost:8062".parse().unwrap()); - let mut proxy_con = ProxyConnector::from_proxy(connector, proxy).unwrap(); - proxy_con.set_tls(Some(tls)); - Client::builder().build(proxy_con) -}); - -pub async fn request(mut req: Request) -> Response { - req.headers_mut().append(header::PROXY_AUTHORIZATION, HeaderValue::from_static("ApiKey app1.proxy1.broker App1Secret")); - CLIENT.request(req).await.unwrap() -} +mod common; +use common::*; pub async fn test_normal(scheme: &str) { - let req = Request::get(format!("{scheme}://httpbin.org/anything")).body(Body::empty()).unwrap(); + let req = Request::get(format!("{scheme}://httpbin.org")).body(Body::empty()).unwrap(); let res = request(req).await; assert_eq!(res.status(), StatusCode::OK, "Could not make normal request via beam-connect"); } @@ -35,7 +16,7 @@ pub async fn test_json(scheme: &str) { "bar": "foo", "foobar": false, }); - let req = Request::get(format!("{scheme}://httpbin.org/anything")).body(Body::from(serde_json::to_vec(&json).unwrap())).unwrap(); + let req = Request::get(format!("{scheme}://httpbin.org")).body(Body::from(serde_json::to_vec(&json).unwrap())).unwrap(); let mut res = request(req).await; assert_eq!(res.status(), StatusCode::OK, "Could not make json request via beam-connect"); let bytes = hyper::body::to_bytes(res.body_mut()).await.unwrap(); @@ -43,21 +24,32 @@ pub async fn test_json(scheme: &str) { assert_eq!(recieved.get("json").unwrap(), &json, "Json did not match"); } -macro_rules! test_http_and_https { - ($name:ident) => { - paste::paste! { - #[tokio::test] - async fn [<$name _http>]() { - $name("http").await - } - - #[tokio::test] - async fn [<$name _https>]() { - $name("https").await - } - } - }; -} test_http_and_https!{test_normal} test_http_and_https!{test_json} + +#[cfg(feature = "sockets")] +#[cfg(test)] +mod socket_tests { + use super::request; + use futures_util::{SinkExt, StreamExt}; + use hyper::{Request, Body, header, StatusCode}; + use tokio_tungstenite::{tungstenite::{protocol::Role, Message}, WebSocketStream}; + + #[tokio::test] + pub async fn test_ws() { + let resp = request(Request::get(format!("http://echo")) + .header(header::UPGRADE, "websocket") + .header(header::CONNECTION, "upgrade") + .header(header::SEC_WEBSOCKET_VERSION, "13") + .header(header::SEC_WEBSOCKET_KEY, "h/QU7Qscq6DfSTu9aP78HQ==") + .body(Body::empty()).unwrap()).await; + assert_eq!(resp.status(), StatusCode::SWITCHING_PROTOCOLS); + let socket = hyper::upgrade::on(resp).await.unwrap(); + let mut stream = WebSocketStream::from_raw_socket(socket, Role::Client, None).await; + // _ = stream.next().await.unwrap().unwrap(); + stream.send(Message::Text("Hello World".to_string())).await.unwrap(); + let res = stream.next().await.unwrap().unwrap(); + assert_eq!(res, Message::Text("Hello World".to_string())) + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..f9c792d --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,41 @@ + +use clap::__derive_refs::once_cell::sync::Lazy; +use hyper::{Response, Body, Request, header, http::HeaderValue, Client, client::HttpConnector}; +use hyper_proxy::{Proxy, Intercept, ProxyConnector}; +use hyper_tls::HttpsConnector; +use tokio_native_tls::native_tls::TlsConnector; + +const CLIENT: Lazy>>> = Lazy::new(|| { + let tls = TlsConnector::builder() + .danger_accept_invalid_certs(true) + .danger_accept_invalid_hostnames(true) + .build() + .unwrap(); + let connector = HttpsConnector::from((HttpConnector::new(), tls.clone().into())); + let proxy = Proxy::new(Intercept::All, "http://localhost:8062".parse().unwrap()); + let mut proxy_con = ProxyConnector::from_proxy(connector, proxy).unwrap(); + proxy_con.set_tls(Some(tls)); + Client::builder().build(proxy_con) +}); + +pub async fn request(mut req: Request) -> Response { + req.headers_mut().append(header::PROXY_AUTHORIZATION, HeaderValue::from_static("ApiKey app1.proxy1.broker App1Secret")); + CLIENT.request(req).await.unwrap() +} + +#[macro_export] +macro_rules! test_http_and_https { + ($name:ident) => { + paste::paste! { + #[tokio::test] + async fn [<$name _http>]() { + $name("http").await + } + + #[tokio::test] + async fn [<$name _https>]() { + $name("https").await + } + } + }; +} From 4b0c79c24196989faf04ee015ff4b49eeed5436a Mon Sep 17 00:00:00 2001 From: janskiba Date: Fri, 9 Jun 2023 16:09:20 +0000 Subject: [PATCH 40/80] ws echo server --- dev/docker-compose.yml | 4 ++++ examples/example_central_test.json | 6 ++++++ examples/example_local_test.json | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 524edfc..195b270 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -105,6 +105,10 @@ services: secrets: - proxy2.pem - root.crt.pem + echo: + image: jmalloc/echo-server + environment: + PORT: 80 secrets: pki.secret: file: ./pki/pki.secret diff --git a/examples/example_central_test.json b/examples/example_central_test.json index cf6fc66..48049d4 100644 --- a/examples/example_central_test.json +++ b/examples/example_central_test.json @@ -11,6 +11,12 @@ "name": "connect2", "virtualhost": "httpbin.org:443", "beamconnect": "app2.proxy2.broker" + }, + { + "id": "echo", + "name": "echo", + "virtualhost": "echo", + "beamconnect": "app2.proxy2.broker" } ] } diff --git a/examples/example_local_test.json b/examples/example_local_test.json index b06c4f9..b229e77 100644 --- a/examples/example_local_test.json +++ b/examples/example_local_test.json @@ -8,5 +8,10 @@ "external": "httpbin.org:443", "internal": "httpbin.org:443", "allowed": ["app1.proxy1.broker"] + }, + { + "external": "echo", + "internal": "echo:80", + "allowed": ["app1.proxy1.broker"] } ] From feb6024adf5f208e936d054a2f73434cca3df1c9 Mon Sep 17 00:00:00 2001 From: janskiba Date: Mon, 12 Jun 2023 08:37:55 +0000 Subject: [PATCH 41/80] Use postman instead of httpbin --- examples/example_central_test.json | 4 ++-- examples/example_local_test.json | 8 ++++---- tests/base_tests.rs | 9 +++++---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/examples/example_central_test.json b/examples/example_central_test.json index 48049d4..bfc7041 100644 --- a/examples/example_central_test.json +++ b/examples/example_central_test.json @@ -3,13 +3,13 @@ { "id": "C2", "name": "connect2", - "virtualhost": "httpbin.org", + "virtualhost": "postman-echo.com", "beamconnect": "app2.proxy2.broker" }, { "id": "C2", "name": "connect2", - "virtualhost": "httpbin.org:443", + "virtualhost": "postman-echo.com:443", "beamconnect": "app2.proxy2.broker" }, { diff --git a/examples/example_local_test.json b/examples/example_local_test.json index b229e77..feb41bb 100644 --- a/examples/example_local_test.json +++ b/examples/example_local_test.json @@ -1,12 +1,12 @@ [ { - "external": "httpbin.org", - "internal": "httpbin.org", + "external": "postman-echo.com", + "internal": "postman-echo.com", "allowed": ["app1.proxy1.broker"] }, { - "external": "httpbin.org:443", - "internal": "httpbin.org:443", + "external": "postman-echo.com:443", + "internal": "postman-echo.com:443", "allowed": ["app1.proxy1.broker"] }, { diff --git a/tests/base_tests.rs b/tests/base_tests.rs index e576543..9267e23 100644 --- a/tests/base_tests.rs +++ b/tests/base_tests.rs @@ -5,7 +5,7 @@ mod common; use common::*; pub async fn test_normal(scheme: &str) { - let req = Request::get(format!("{scheme}://httpbin.org")).body(Body::empty()).unwrap(); + let req = Request::get(format!("{scheme}://postman-echo.com/get")).body(Body::empty()).unwrap(); let res = request(req).await; assert_eq!(res.status(), StatusCode::OK, "Could not make normal request via beam-connect"); } @@ -16,7 +16,7 @@ pub async fn test_json(scheme: &str) { "bar": "foo", "foobar": false, }); - let req = Request::get(format!("{scheme}://httpbin.org")).body(Body::from(serde_json::to_vec(&json).unwrap())).unwrap(); + let req = Request::post(format!("{scheme}://postman-echo.com/post")).body(Body::from(serde_json::to_vec(&json).unwrap())).unwrap(); let mut res = request(req).await; assert_eq!(res.status(), StatusCode::OK, "Could not make json request via beam-connect"); let bytes = hyper::body::to_bytes(res.body_mut()).await.unwrap(); @@ -47,9 +47,10 @@ mod socket_tests { assert_eq!(resp.status(), StatusCode::SWITCHING_PROTOCOLS); let socket = hyper::upgrade::on(resp).await.unwrap(); let mut stream = WebSocketStream::from_raw_socket(socket, Role::Client, None).await; - // _ = stream.next().await.unwrap().unwrap(); + let _server_hello = stream.next().await.unwrap().unwrap(); stream.send(Message::Text("Hello World".to_string())).await.unwrap(); let res = stream.next().await.unwrap().unwrap(); - assert_eq!(res, Message::Text("Hello World".to_string())) + assert_eq!(res, Message::Text("Hello World".to_string())); + stream.close(None).await.unwrap(); } } From 1b123619cfc9f79fbf6da1455edc7abd031b1a8c Mon Sep 17 00:00:00 2001 From: janskiba Date: Mon, 12 Jun 2023 10:59:52 +0000 Subject: [PATCH 42/80] http upgrades --- src/logic_ask.rs | 9 ++++- src/sockets.rs | 98 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 86 insertions(+), 21 deletions(-) diff --git a/src/logic_ask.rs b/src/logic_ask.rs index 6fe3b31..151123a 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -22,13 +22,13 @@ use crate::{config::Config, structs::MyStatusCode, msg::{HttpRequest, HttpRespon pub(crate) async fn handler_http( mut req: Request, config: Arc, - authority: Option, + https_authority: Option, ) -> Result, MyStatusCode> { let targets = &config.targets_public; let method = req.method().to_owned(); let uri = req.uri().to_owned(); - let Some(authority) = authority.as_ref().or(uri.authority()) else { + let Some(authority) = https_authority.as_ref().or(uri.authority()) else { return if uri.path() == "/sites" { // Case 1 for sites request: no authority set and /sites respond_with_sites(targets) @@ -66,6 +66,11 @@ pub(crate) async fn handler_http( *req.uri_mut() = { let mut parts = req.uri().to_owned().into_parts(); parts.authority = Some(authority.clone()); + if https_authority.is_some() { + parts.scheme = Some(Scheme::HTTPS); + } else { + parts.scheme = Some(Scheme::HTTP); + } Uri::from_parts(parts).map_err(|e| { warn!("Could not transform uri authority: {e}"); StatusCode::INTERNAL_SERVER_ERROR diff --git a/src/sockets.rs b/src/sockets.rs index 9f453b9..0f1cee4 100644 --- a/src/sockets.rs +++ b/src/sockets.rs @@ -1,10 +1,10 @@ -use std::{time::Duration, collections::HashSet, io, sync::Arc, convert::Infallible}; +use std::{time::Duration, collections::HashSet, sync::Arc, convert::Infallible}; -use hyper::{header, Request, Body, body::{self, HttpBody}, StatusCode, upgrade::{Upgraded, self}, Response, http::{uri::Authority, HeaderValue}, client::conn::Builder, server::conn::Http, service::service_fn, Uri, Method}; +use hyper::{header, Request, Body, body, StatusCode, upgrade::{self, OnUpgrade}, Response, http::{HeaderValue}, client::conn::Builder, server::conn::Http, service::service_fn, Uri, Method}; use serde::{Serialize, Deserialize}; use shared::{MsgId, beam_id::{AppOrProxyId, AppId}}; -use tokio::net::TcpStream; -use tracing::{error, warn, debug}; +use tokio::io::AsyncWriteExt; +use tracing::{error, warn, debug, info}; use crate::{config::Config, errors::BeamConnectError, structs::MyStatusCode}; @@ -19,6 +19,7 @@ struct SocketTask { pub(crate) fn spwan_socket_task_poller(config: Config) { tokio::spawn(async move { use BeamConnectError::*; + let mut seen: HashSet = HashSet::new(); loop { let tasks = match poll_socket_task(&config).await { @@ -36,6 +37,10 @@ pub(crate) fn spwan_socket_task_poller(config: Config) { } }; for task in tasks { + if seen.contains(&task.id) { + continue; + } + seen.insert(task.id.clone()); let Ok(client) = AppId::try_from(&task.from) else { warn!("Invalid app id skipping"); continue; @@ -115,6 +120,7 @@ async fn tunnel(proxy: Response, client: AppId, config: &Config) { Ok::<_, Infallible>(handle_tunnel(req, &client2, &config2).await.unwrap_or_else(status_to_response)) } })) + .with_upgrades() .await; if let Err(e) = http_err { @@ -141,24 +147,50 @@ async fn handle_tunnel(mut req: Request, app: &AppId, config: &Config) -> StatusCode::INTERNAL_SERVER_ERROR })? }; - - let resp = config.client.request(req).await.map_err(|e| { + info!("Requesting {} {}", req.method(), req.uri()); + let req_upgrade = if req.headers().contains_key(header::UPGRADE) { + req.extensions_mut().remove::() + } else { + None + }; + let mut resp = config.client.request(req).await.map_err(|e| { warn!("Communication with target failed: {e}"); StatusCode::BAD_GATEWAY })?; + if req_upgrade.is_some() { + tunnel_upgrade(resp.extensions_mut().remove::(), req_upgrade); + } Ok(resp) } -pub(crate) async fn handle_via_sockets(req: Request, config: &Arc, target: &AppId, auth: HeaderValue) -> Result, MyStatusCode> { +fn tunnel_upgrade(client: Option, server: Option) { + if let (Some(client), Some(proxy)) = (client, server) { + tokio::spawn(async move { + let (mut client, mut proxy) = match tokio::try_join!(client, proxy) { + Err(e) => { + warn!("Upgrading connection between client and beam-connect failed: {e}"); + return; + }, + Ok(sockets) => sockets + }; + let result = tokio::io::copy_bidirectional(&mut client, &mut proxy).await; + if let Err(e) = result { + debug!("Relaying socket connection ended: {e}"); + } + }); + } +} + +pub(crate) async fn handle_via_sockets(mut req: Request, config: &Arc, target: &AppId, auth: HeaderValue) -> Result, MyStatusCode> { let connect_proxy_req = Request::builder() .method(Method::POST) .uri(format!("{}v1/sockets/{target}", config.proxy_url)) - .header(header::AUTHORIZATION, config.proxy_auth.clone()) + .header(header::AUTHORIZATION, auth) .header(header::UPGRADE, "tcp") .body(Body::empty()) .expect("This is a valid request"); let resp = config.client.request(connect_proxy_req).await.map_err(|e| { - warn!("Failed to reach proxy"); + warn!("Failed to reach proxy: {e}"); StatusCode::BAD_GATEWAY })?; if resp.status() != StatusCode::SWITCHING_PROTOCOLS { @@ -169,23 +201,51 @@ pub(crate) async fn handle_via_sockets(req: Request, config: &Arc, StatusCode::INTERNAL_SERVER_ERROR })?; - let (mut sender, conn) = Builder::new() + let (mut sender, proxy_conn) = Builder::new() .http1_preserve_header_case(true) .http1_title_case_headers(true) .handshake(proxy_socket) .await .map_err(|e| { - warn!("Error doing handshake with proxy"); + warn!("Error doing handshake with proxy: {e}"); StatusCode::BAD_GATEWAY })?; - tokio::task::spawn(async move { - if let Err(err) = conn.await { - warn!("Connection failed: {:?}", err); - } - }); - - let resp = sender.send_request(req).await.map_err(|e| { - warn!("Failed to send request to proxy"); + let req_upgrade = if req.headers().contains_key(header::UPGRADE) { + req.extensions_mut().remove::() + } else { + None + }; + let resp_future = sender.send_request(req); + let resp = if let Some(upgrade) = req_upgrade { + let (resp, proxy_connection) = tokio::join!(resp_future, proxy_conn.without_shutdown()); + match proxy_connection { + Ok(mut proxy_io) => { + tokio::spawn(async move { + let Ok(mut client) = upgrade.await else { + warn!("Failed to upgrade client connection"); + return; + }; + if !proxy_io.read_buf.is_empty() { + if let Err(e) = client.write_all_buf(&mut proxy_io.read_buf).await { + warn!("Failed to send initial bytes from remote to client: {e}"); + } + } + if let Err(e) = tokio::io::copy_bidirectional(&mut client, &mut proxy_io.io).await { + debug!("Error relaying connection from client to proxy: {e}"); + } + }); + }, + Err(e) => { + warn!("Connection failed: {e}"); + }, + }; + resp + } else { + tokio::spawn(proxy_conn); + resp_future.await + }; + let resp = resp.map_err(|e| { + warn!("Failed to send request to proxy: {e}"); StatusCode::BAD_GATEWAY })?; Ok(resp) From 6b7fbfee862b7d705ae47863be205366aeb8d646 Mon Sep 17 00:00:00 2001 From: janskiba Date: Mon, 12 Jun 2023 11:05:30 +0000 Subject: [PATCH 43/80] Remove old python tests --- .github/workflows/rust.yml | 9 +------- dev/test.py | 43 -------------------------------------- 2 files changed, 1 insertion(+), 51 deletions(-) delete mode 100644 dev/test.py diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a5b3eed..4503171 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -90,13 +90,6 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - name: Install testing dependencies - run: | - python -m pip install --upgrade pip - pip install requests - uses: actions/download-artifact@v3 with: name: binaries-amd64 @@ -109,7 +102,7 @@ jobs: # sleep 3 # docker compose logs - name: Run tests - run: python ./dev/test.py + run: cargo test docker: needs: [ build-rust, pre-check, test ] diff --git a/dev/test.py b/dev/test.py deleted file mode 100644 index eec5010..0000000 --- a/dev/test.py +++ /dev/null @@ -1,43 +0,0 @@ - -try: - import requests -except ImportError: - import os - os.system("python3 -m pip install requests") - import requests - -import unittest - -class TestConnect(unittest.TestCase): - - def test_normal_request(self): - res = request_connect("http://httpbin.org/anything") - self.assertEqual(res.status_code, 200, "Could not make normal request via connect") - - def test_json_body(self): - json = { - "foo": "bar", - "asdf": 3, - "baz": [{}, 2] - } - res = request_connect("http://httpbin.org/anything", json=json) - self.assertEqual(res.status_code, 200, "Could not make normal request via connect") - self.assertEqual(res.json().get("json"), json, "Json did not match") - - -def request_connect(url: str, json = {}, app_id: str = "app1.proxy1.broker", app_secret: str = "App1Secret", proxy_url: str = "http://localhost:8062") -> requests.Response: - proxies = { - "http": proxy_url - } - headers = { - "Proxy-Authorization": f"ApiKey {app_id} {app_secret}", - "Accept": "application/json" - } - return requests.get(url, json=json, proxies=proxies, headers=headers) - - -def main(): - unittest.main() - -if __name__ == "__main__": - main() From e24dbd3b72876838cfe858868eaa52a8542a9180 Mon Sep 17 00:00:00 2001 From: janskiba Date: Mon, 12 Jun 2023 11:26:51 +0000 Subject: [PATCH 44/80] Clean up unwrap and names --- src/sockets.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/sockets.rs b/src/sockets.rs index 0f1cee4..6f78764 100644 --- a/src/sockets.rs +++ b/src/sockets.rs @@ -117,7 +117,7 @@ async fn tunnel(proxy: Response, client: AppId, config: &Config) { let client2 = client.clone(); let config2 = config.clone(); async move { - Ok::<_, Infallible>(handle_tunnel(req, &client2, &config2).await.unwrap_or_else(status_to_response)) + Ok::<_, Infallible>(execute_http_task(req, &client2, &config2).await.unwrap_or_else(status_to_response)) } })) .with_upgrades() @@ -128,9 +128,8 @@ async fn tunnel(proxy: Response, client: AppId, config: &Config) { } } -async fn handle_tunnel(mut req: Request, app: &AppId, config: &Config) -> Result, StatusCode> { - // TODO: What exactly happens here if we dont have an authority - let authority = req.uri().authority().unwrap(); +async fn execute_http_task(mut req: Request, app: &AppId, config: &Config) -> Result, StatusCode> { + let authority = req.uri().authority().expect("Authority is always set by the requesting beam-connect"); let Some(target) = config.targets_local.get(authority) else { warn!("Failed to lookup authority {authority}"); return Err(StatusCode::BAD_REQUEST); From 82f4d2d71376aea7cf8b9e1d887fc4d158e993b4 Mon Sep 17 00:00:00 2001 From: janskiba Date: Mon, 12 Jun 2023 11:31:23 +0000 Subject: [PATCH 45/80] Fix typos --- README.md | 2 +- src/config.rs | 2 +- src/errors.rs | 2 +- src/logic_ask.rs | 4 ++-- src/logic_reply.rs | 4 ++-- src/main.rs | 2 +- src/sockets.rs | 4 ++-- tests/base_tests.rs | 4 ++-- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 543ea42..577298b 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ As described in the [command line parameter list](#run-as-an-application), the c Https is supported but requires setting up the following parameters: * `SSL_CERT_PEM`: Location to the pem file used for incoming SSL connections. -* `SSL_CERT_KEY`: Location to the coresponding key file for the SSL connections. +* `SSL_CERT_KEY`: Location to the corresponding key file for the SSL connections. * `TLS_CA_CERTIFICATES_DIR`: May need to be set if the local target uses a self signed certificate which is not trusted by beam-connect. In this case the certificate of the target must be placed inside `TLS_CA_CERTIFICATES_DIR` as a pem file in order to be trusted. ## Notes diff --git a/src/config.rs b/src/config.rs index d8ee358..6ee7ddf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -163,7 +163,7 @@ async fn load_public_targets(client: &SamplyHttpClient, url: &PathOrUri) -> Resu .try_into() .map_err(|e| BeamConnectError::ConfigurationError(format!("Invalid url for public sites: {e}")))? ).await - .map_err(|e| BeamConnectError::ConfigurationError(format!("Cannot retreive central service discovery configuration: {e}")) + .map_err(|e| BeamConnectError::ConfigurationError(format!("Cannot retrieve central service discovery configuration: {e}")) )?; let body = response.body_mut(); diff --git a/src/errors.rs b/src/errors.rs index c45cf02..91582d4 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -6,7 +6,7 @@ use thiserror::Error; #[derive(Error,Debug)] pub(crate) enum BeamConnectError { - #[error("Regular proxy timout")] + #[error("Regular proxy timeout")] ProxyTimeoutError, #[error("Proxy rejected our authorization")] ProxyRejectedAuthorization, diff --git a/src/logic_ask.rs b/src/logic_ask.rs index 151123a..f964ed7 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -155,7 +155,7 @@ async fn handle_via_tasks(req: Request, config: &Arc, target: &App let response_inner = match result.status { shared::WorkStatus::Succeeded => { serde_json::from_str::(&result.body.body.ok_or_else(|| { - warn!("Recieved one sucessfull result but it has no body"); + warn!("Received one successful result but it has no body"); StatusCode::BAD_GATEWAY })?)? }, @@ -216,7 +216,7 @@ async fn http_req_to_struct(req: Request, my_id: &AppId, target_id: &AppId Ok(msg) } -/// If the autority is empty (e.g. if localhost is used) or the authoroty is not in the routing +/// If the authority is empty (e.g. if localhost is used) or the authoroty is not in the routing /// table AND the path is /sites, return global routing table fn respond_with_sites(targets: &CentralMapping) -> Result, MyStatusCode> { debug!("Central Site Discovery requested"); diff --git a/src/logic_reply.rs b/src/logic_reply.rs index c6fa1f9..eaeaf3d 100644 --- a/src/logic_reply.rs +++ b/src/logic_reply.rs @@ -27,7 +27,7 @@ async fn send_reply(task: &MsgTaskRequest, config: &Config, client: &SamplyHttpC let body = body::to_bytes(resp.body_mut()).await .map_err(BeamConnectError::FailedToReadTargetsReply)?; if !resp.status().is_success() { - warn!("Httptask returned with status {}. Reporting failiure to broker.", resp.status()); + warn!("Httptask returned with status {}. Reporting failure to broker.", resp.status()); // warn!("Response body was: {}", &body); }; (serde_json::to_string(&HttpResponse { @@ -89,7 +89,7 @@ async fn execute_http_task(task: &MsgTaskRequest, config: &Config, client: &Samp .authority(target.replace.to_owned()) .build()?; - info!("Rewriten to: {} {}", task_req.method, uri); + info!("Rewritten to: {} {}", task_req.method, uri); let mut req = Request::builder() .method(task_req.method) diff --git a/src/main.rs b/src/main.rs index f683de9..f02a2e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,7 +49,7 @@ async fn main() -> Result<(), Box>{ } }); #[cfg(feature = "sockets")] - sockets::spwan_socket_task_poller(config.clone()); + sockets::spawn_socket_task_poller(config.clone()); let config = Arc::new(config.clone()); diff --git a/src/sockets.rs b/src/sockets.rs index 6f78764..230be9a 100644 --- a/src/sockets.rs +++ b/src/sockets.rs @@ -16,7 +16,7 @@ struct SocketTask { ttl: String, } -pub(crate) fn spwan_socket_task_poller(config: Config) { +pub(crate) fn spawn_socket_task_poller(config: Config) { tokio::spawn(async move { use BeamConnectError::*; let mut seen: HashSet = HashSet::new(); @@ -135,7 +135,7 @@ async fn execute_http_task(mut req: Request, app: &AppId, config: &Config) return Err(StatusCode::BAD_REQUEST); }; if !target.allowed.contains(&app) { - warn!("App {app} not autherized to access url {}", req.uri()); + warn!("App {app} not authorized to access url {}", req.uri()); return Err(StatusCode::UNAUTHORIZED); }; *req.uri_mut() = { diff --git a/tests/base_tests.rs b/tests/base_tests.rs index 9267e23..81e92f6 100644 --- a/tests/base_tests.rs +++ b/tests/base_tests.rs @@ -20,8 +20,8 @@ pub async fn test_json(scheme: &str) { let mut res = request(req).await; assert_eq!(res.status(), StatusCode::OK, "Could not make json request via beam-connect"); let bytes = hyper::body::to_bytes(res.body_mut()).await.unwrap(); - let recieved: Value = serde_json::from_slice(&bytes).unwrap(); - assert_eq!(recieved.get("json").unwrap(), &json, "Json did not match"); + let received: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(received.get("json").unwrap(), &json, "Json did not match"); } From a2395e2207e31631255cd1c85c8aa08f4b76df00 Mon Sep 17 00:00:00 2001 From: janskiba Date: Mon, 12 Jun 2023 11:44:33 +0000 Subject: [PATCH 46/80] Build feature in CI --- .github/workflows/rust.yml | 32 ++++++++++++++++++++++++-------- Dockerfile.ci | 3 ++- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 4503171..c3b6df8 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -29,6 +29,9 @@ jobs: arch: - amd64 - arm64 + features: + - sockets + - "" steps: - name: Set arch ${{ matrix.arch }} @@ -69,17 +72,17 @@ jobs: - uses: Swatinem/rust-cache@v2 with: key: ${{ matrix.arch }}-${{ env.PROFILE }} - prefix-key: "v1-rust" # Increase to invalidate old caches. + prefix-key: v1-rust-${{ matrix.features && format('features_{0}', matrix.features) || 'nofeatures' }} # Increase to invalidate old caches. - name: Build (${{ matrix.arch }}) uses: actions-rs/cargo@v1 with: use-cross: ${{ env.is_cross }} command: build - args: --target ${{ env.rustarch }} ${{ env.profilestr }} + args: --target ${{ env.rustarch }} ${{ matrix.features && format('--features {0}', matrix.features) }} ${{ env.profilestr }} - name: Upload Artifact uses: actions/upload-artifact@v3 with: - name: binaries-${{ matrix.arch }} + name: binaries-${{ matrix.arch }}-${{ matrix.features }} path: | target/${{ env.rustarch }}/${{ env.PROFILE }}/connect @@ -88,11 +91,17 @@ jobs: needs: [ build-rust ] runs-on: ubuntu-22.04 + strategy: + matrix: + features: + - "" + - "sockets" + steps: - uses: actions/checkout@v3 - uses: actions/download-artifact@v3 with: - name: binaries-amd64 + name: binaries-amd64-${{ matrix.features }} path: artifacts/binaries-amd64/ - name: Start containers run: ./dev/start ci @@ -102,16 +111,24 @@ jobs: # sleep 3 # docker compose logs - name: Run tests - run: cargo test + run: cargo test ${{ format('--features "{0}"', matrix.features) }} docker: needs: [ build-rust, pre-check, test ] + + strategy: + matrix: + features: + - "" + - "sockets" + # This workflow defines how a maven package is built, tested and published. # Visit: https://github.com/samply/github-workflows/blob/develop/.github/workflows/docker-ci.yml, for more information uses: samply/github-workflows/.github/workflows/docker-ci.yml@main with: # The Docker Hub Repository you want eventually push to, e.g samply/share-client image-name: "samply/beam-connect" + image-tag-suffix: ${{ matrix.features && format('-{0}', matrix.features) }} # Define special prefixes for docker tags. They will prefix each images tag. # image-tag-prefix: "foo" # Define the build context of your image, typically default '.' will be enough @@ -120,9 +137,8 @@ jobs: build-file: './Dockerfile.ci' # NOTE: This doesn't work currently # A list of build arguments, passed to the docker build -# build-args: | -# PROFILE=${{ env.PROFILE }} -# COMPONENT=broker + build-args: | + FEATURE=-${{ matrix.features }} # Define the target platforms of the docker build (default "linux/amd64,linux/arm64/v8") # build-platforms: "linux/amd64,linux/arm64" # If your actions generate an artifact in a previous build step, you can tell this workflow to download it diff --git a/Dockerfile.ci b/Dockerfile.ci index 44e989a..dd462af 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -3,8 +3,9 @@ ARG IMGNAME=gcr.io/distroless/cc FROM alpine AS chmodder +ARG FEATURE ARG TARGETARCH -COPY /artifacts/binaries-$TARGETARCH/connect /app/ +COPY /artifacts/binaries-$TARGETARCH$FEATURE/connect /app/ RUN chmod +x /app/* # FROM ${IMGNAME} From 83475b9e658ba21a0cf9c8fe303a0a331dca79ee Mon Sep 17 00:00:00 2001 From: janskiba Date: Mon, 12 Jun 2023 12:05:14 +0000 Subject: [PATCH 47/80] Update beam tag based on feature for tests --- .github/workflows/rust.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c3b6df8..01feb25 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -103,6 +103,11 @@ jobs: with: name: binaries-amd64-${{ matrix.features }} path: artifacts/binaries-amd64/ + - name: Set diffrent image tag + run: | + if [[ ${{ matrix.features }} == 'sockets' ]]; then + echo "TAG=feature-sockets-sockets" >> $GITHUB_ENV + fi - name: Start containers run: ./dev/start ci # - name: Show logs From 8c6c283c4cda4bee20832e83433c6dbeb352dfc3 Mon Sep 17 00:00:00 2001 From: janskiba Date: Mon, 12 Jun 2023 12:27:43 +0000 Subject: [PATCH 48/80] Default to empty string --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 01feb25..90db701 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -105,7 +105,7 @@ jobs: path: artifacts/binaries-amd64/ - name: Set diffrent image tag run: | - if [[ ${{ matrix.features }} == 'sockets' ]]; then + if [[ ${{ matrix.features || '' }} == 'sockets' ]]; then echo "TAG=feature-sockets-sockets" >> $GITHUB_ENV fi - name: Start containers From 2d3f639fb0c4f80d8a797c7c95ece99b06784a63 Mon Sep 17 00:00:00 2001 From: janskiba Date: Mon, 12 Jun 2023 12:59:21 +0000 Subject: [PATCH 49/80] Fix empty string --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 90db701..56ed034 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -105,7 +105,7 @@ jobs: path: artifacts/binaries-amd64/ - name: Set diffrent image tag run: | - if [[ ${{ matrix.features || '' }} == 'sockets' ]]; then + if [[ ${{ format('"{0}"', matrix.features) }} == 'sockets' ]]; then echo "TAG=feature-sockets-sockets" >> $GITHUB_ENV fi - name: Start containers From 8cdc88a3c17f503015a3fa614f4e5200970e1110 Mon Sep 17 00:00:00 2001 From: janskiba Date: Fri, 23 Jun 2023 12:43:14 +0000 Subject: [PATCH 50/80] Update sockets branch as it is merged now --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 56ed034..587c45b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -106,7 +106,7 @@ jobs: - name: Set diffrent image tag run: | if [[ ${{ format('"{0}"', matrix.features) }} == 'sockets' ]]; then - echo "TAG=feature-sockets-sockets" >> $GITHUB_ENV + echo "TAG=develop-sockets" >> $GITHUB_ENV fi - name: Start containers run: ./dev/start ci From a24c508dcb836beb7b8b72f0015a3ada77f58b81 Mon Sep 17 00:00:00 2001 From: janskiba Date: Mon, 17 Jul 2023 14:57:56 +0000 Subject: [PATCH 51/80] Start replacing beam-shared with beam-lib --- Cargo.toml | 2 +- src/config.rs | 2 +- src/errors.rs | 2 +- src/example_targets.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a3f9125..084a8de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ inherits = "release" strip = false [dependencies] -shared = { git = "https://github.com/samply/beam", branch="develop" } +beam-lib = { git = "https://github.com/samply/beam", branch="develop" } #axum = "0.5.12" tokio = { version = "1", features = ["macros","rt-multi-thread","signal"] } diff --git a/src/config.rs b/src/config.rs index 6ee7ddf..f45bd19 100644 --- a/src/config.rs +++ b/src/config.rs @@ -221,7 +221,7 @@ mod tests { use super::CentralMapping; use super::LocalMapping; use crate::example_targets::example_local; - use shared::beam_id::{BrokerId,BeamId,app_to_broker_id}; + use beam-lib::{BrokerId,BeamId,app_to_broker_id}; #[test] fn serde_authority() { diff --git a/src/errors.rs b/src/errors.rs index 91582d4..e4199c6 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,7 +1,7 @@ use std::{str::Utf8Error, string::FromUtf8Error}; use hyper::Uri; -use shared::beam_id::AppOrProxyId; +use beam-lib::AppOrProxyId; use thiserror::Error; #[derive(Error,Debug)] diff --git a/src/example_targets.rs b/src/example_targets.rs index e609f4a..b59bdda 100644 --- a/src/example_targets.rs +++ b/src/example_targets.rs @@ -1,5 +1,5 @@ use hyper::http::uri::Authority; -use shared::beam_id::{AppId, BeamId, BrokerId, ProxyId}; +use beam-lib::{AppId, BeamId, BrokerId, ProxyId}; use crate::config::{CentralMapping, LocalMapping, LocalMappingEntry}; From 52c4316e5d7aca4fe8b9fd6480c16bd88ae47659 Mon Sep 17 00:00:00 2001 From: janskiba Date: Tue, 18 Jul 2023 13:35:17 +0000 Subject: [PATCH 52/80] Migrate most stuff to beam-lib --- Cargo.toml | 5 ++-- dev/docker-compose.yml | 8 +++--- src/config.rs | 51 ++++++++++++++++++++------------------ src/errors.rs | 4 +-- src/example_targets.rs | 6 ++--- src/logic_ask.rs | 35 ++++++++++++-------------- src/logic_reply.rs | 56 ++++++++++++++++++++++++------------------ src/msg.rs | 2 +- src/sockets.rs | 12 ++------- 9 files changed, 89 insertions(+), 90 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 084a8de..90f7077 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,8 @@ inherits = "release" strip = false [dependencies] -beam-lib = { git = "https://github.com/samply/beam", branch="develop" } +beam-lib = { git = "https://github.com/samply/beam", branch="develop", features = ["strict-ids"] } +shared = { git = "https://github.com/samply/beam", branch="develop" } #axum = "0.5.12" tokio = { version = "1", features = ["macros","rt-multi-thread","signal"] } @@ -37,7 +38,7 @@ serde = "*" serde_json = "*" hyper_serde = "0.13" -clap = {version = "4", features = ["derive"]} +clap = { version = "4", features = ["derive", "env"] } thiserror = "*" http-serde = "1.1.2" diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 195b270..9ff2f3a 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.7" services: vault: - image: vault + image: hashicorp/vault ports: - 127.0.0.1:8200:8200 environment: @@ -37,8 +37,7 @@ services: environment: BROKER_URL: ${BROKER_URL} PROXY_ID: ${PROXY1_ID} - APP_0_ID: ${APP1_ID_SHORT} - APP_0_KEY: ${APP_KEY} + APP_app1_KEY: ${APP_KEY} PRIVKEY_FILE: /run/secrets/proxy1.pem BIND_ADDR: 0.0.0.0:8081 RUST_LOG: ${RUST_LOG} @@ -95,8 +94,7 @@ services: environment: BROKER_URL: ${BROKER_URL} PROXY_ID: ${PROXY2_ID} - APP_0_ID: ${APP2_ID_SHORT} - APP_0_KEY: ${APP_KEY} + APP_app2_KEY: ${APP_KEY} PRIVKEY_FILE: /run/secrets/proxy2.pem BIND_ADDR: 0.0.0.0:8082 RUST_LOG: ${RUST_LOG} diff --git a/src/config.rs b/src/config.rs index f45bd19..6a8f369 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,11 +1,11 @@ -use std::{error::Error, path::PathBuf, fs::{File, read_to_string}, str::FromStr, sync::Arc}; +use std::{error::Error, path::PathBuf, fs::read_to_string, str::FromStr, sync::Arc}; use clap::Parser; -use hyper::{Uri, http::uri::{Authority, Scheme}}; +use hyper::{Uri, http::uri::Authority}; +use shared::http_client::{SamplyHttpClient, self}; use tokio_native_tls::{TlsAcceptor, native_tls::{self, Identity}}; -use tracing::info; use serde::{Serialize, Deserialize}; -use shared::{beam_id::{AppId, BeamId, app_to_broker_id, BrokerId}, http_client::{SamplyHttpClient, self}}; +use beam_lib::{AppId, set_broker_id}; use crate::{example_targets, errors::BeamConnectError}; @@ -40,7 +40,7 @@ struct CliArgs { #[clap(long, env, value_parser)] proxy_url: Uri, - /// Your short App ID (e.g. connect1) + /// Your App ID (e.g. connect1.proxy1.broker) #[clap(long, env, value_parser)] app_id: String, @@ -103,7 +103,7 @@ pub(crate) struct Site { pub(crate) beamconnect: AppId, } -#[derive(Clone,Deserialize,Debug)] +#[derive(Clone, Deserialize, Debug)] pub(crate) struct LocalMapping { pub(crate) entries: Vec } @@ -119,7 +119,7 @@ impl LocalMapping { } /// Maps an external authority to some internal authority if the requesting App is allowed to -#[derive(Clone,Deserialize,Debug)] +#[derive(Clone, Deserialize, Debug)] pub(crate) struct LocalMappingEntry { #[serde(with = "http_serde::authority", rename="external")] pub(crate) needle: Authority, // Host part of URL @@ -142,7 +142,7 @@ pub(crate) struct Config { pub(crate) tls_acceptor: Arc } -fn load_local_targets(broker_id: &BrokerId, local_target_path: &Option) -> Result> { +fn load_local_targets(broker_id: &str, local_target_path: &Option) -> Result> { if let Some(json_file) = local_target_path { if json_file.exists() { let json_string = std::fs::read_to_string(json_file)?; @@ -178,13 +178,15 @@ async fn load_public_targets(client: &SamplyHttpClient, url: &PathOrUri) -> Resu } impl Config { - pub(crate) async fn load() -> Result> { + pub(crate) async fn load() -> Result> { let args = CliArgs::parse(); - let broker_id = app_to_broker_id(&args.app_id)?; - AppId::set_broker_id(broker_id.clone()); - let my_app_id = AppId::new(&args.app_id)?; - let broker_id = BrokerId::new(&broker_id)?; - + let broker_id = args.app_id + .splitn(3, '.') + .last() + .ok_or_else(|| BeamConnectError::ConfigurationError(format!("Invalid beam id: {}", args.app_id)))?; + set_broker_id(broker_id.to_owned()); + let app_id = AppId::new(&args.app_id)?; + let expire = args.expire; let tls_ca_certificates = shared::crypto::load_certificates_from_dir(args.tls_ca_certificates_dir)?; @@ -204,8 +206,8 @@ impl Config { Ok(Config { proxy_url: args.proxy_url, - my_app_id: my_app_id.clone(), - proxy_auth: format!("ApiKey {} {}", my_app_id, args.proxy_apikey), + my_app_id: app_id.clone(), + proxy_auth: format!("ApiKey {} {}", app_id, args.proxy_apikey), bind_addr: args.bind_addr, targets_local, targets_public, @@ -218,10 +220,11 @@ impl Config { #[cfg(test)] mod tests { + use beam_lib::set_broker_id; + use super::CentralMapping; use super::LocalMapping; use crate::example_targets::example_local; - use beam-lib::{BrokerId,BeamId,app_to_broker_id}; #[test] fn serde_authority() { @@ -253,6 +256,7 @@ mod tests { } ] }"#; + set_broker_id("broker.ccp-it.dktk.dkfz.de".to_owned()); let obj: CentralMapping = serde_json::from_str(serialized).unwrap(); assert_eq!(obj.sites.len(), 4); let mut routes = obj.sites.iter(); @@ -268,14 +272,13 @@ mod tests { #[test] fn local_target_configuration() { - let broker_id = app_to_broker_id("foo.bar.broker.example").unwrap(); - BrokerId::set_broker_id(broker_id.clone()); - let broker_id = BrokerId::new(&broker_id).unwrap(); + let broker_id = "broker.ccp-it.dktk.dkfz.de"; + set_broker_id(broker_id.to_owned()); let serialized = r#"[ - {"external": "ifconfig.me","internal":"ifconfig.me","allowed":["connect1.proxy23.broker.example","connect2.proxy23.broker.example"]}, - {"external": "ip-api.com","internal":"ip-api.com","allowed":["connect1.proxy23.broker.example","connect2.proxy23.broker.example"]}, - {"external": "wttr.in","internal":"wttr.in","allowed":["connect1.proxy23.broker.example","connect2.proxy23.broker.example"]}, - {"external": "node23.uk12.network","internal":"host23.internal.network","allowed":["connect1.proxy23.broker.example","connect2.proxy23.broker.example"]} + {"external": "ifconfig.me","internal":"ifconfig.me","allowed":["connect1.proxy23.broker.ccp-it.dktk.dkfz.de","connect2.proxy23.broker.ccp-it.dktk.dkfz.de"]}, + {"external": "ip-api.com","internal":"ip-api.com","allowed":["connect1.proxy23.broker.ccp-it.dktk.dkfz.de","connect2.proxy23.broker.ccp-it.dktk.dkfz.de"]}, + {"external": "wttr.in","internal":"wttr.in","allowed":["connect1.proxy23.broker.ccp-it.dktk.dkfz.de","connect2.proxy23.broker.ccp-it.dktk.dkfz.de"]}, + {"external": "node23.uk12.network","internal":"host23.internal.network","allowed":["connect1.proxy23.broker.ccp-it.dktk.dkfz.de","connect2.proxy23.broker.ccp-it.dktk.dkfz.de"]} ]"#; let obj: LocalMapping = LocalMapping{entries:serde_json::from_str(serialized).unwrap()}; let expect = example_local(&broker_id); diff --git a/src/errors.rs b/src/errors.rs index e4199c6..4419928 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,7 +1,7 @@ -use std::{str::Utf8Error, string::FromUtf8Error}; +use std::string::FromUtf8Error; use hyper::Uri; -use beam-lib::AppOrProxyId; +use beam_lib::AppOrProxyId; use thiserror::Error; #[derive(Error,Debug)] diff --git a/src/example_targets.rs b/src/example_targets.rs index b59bdda..916590e 100644 --- a/src/example_targets.rs +++ b/src/example_targets.rs @@ -1,9 +1,9 @@ use hyper::http::uri::Authority; -use beam-lib::{AppId, BeamId, BrokerId, ProxyId}; +use beam_lib::{AppId, ProxyId}; -use crate::config::{CentralMapping, LocalMapping, LocalMappingEntry}; +use crate::config::{LocalMapping, LocalMappingEntry}; -pub(crate) fn example_local(broker_id: &BrokerId) -> LocalMapping { +pub(crate) fn example_local(broker_id: &str) -> LocalMapping { let proxy23 = ProxyId::new(&format!("proxy23.{}", broker_id)).unwrap(); let app1_id = AppId::new(&format!("connect1.{}",proxy23)).unwrap(); let app2_id = AppId::new(&format!("connect2.{}",proxy23)).unwrap(); diff --git a/src/logic_ask.rs b/src/logic_ask.rs index f964ed7..c2a58ae 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -11,7 +11,7 @@ use hyper_tls::HttpsConnector; use tracing::{info, debug, warn, error}; use serde_json::Value; use shared::http_client::SamplyHttpClient; -use shared::{beam_id::AppId, MsgTaskResult, MsgTaskRequest}; +use beam_lib::{AppId, TaskResult, TaskRequest, WorkStatus, FailureStrategy, MsgId}; use crate::config::CentralMapping; use crate::{config::Config, structs::MyStatusCode, msg::{HttpRequest, HttpResponse}, errors::BeamConnectError}; @@ -83,7 +83,7 @@ pub(crate) async fn handler_http( } async fn handle_via_tasks(req: Request, config: &Arc, target: &AppId, auth: HeaderValue) -> Result, MyStatusCode> { - let msg = http_req_to_struct(req, &config.my_app_id, &target, &config.expire).await?; + let msg = http_req_to_struct(req, &config.my_app_id, &target, config.expire).await?; // Send to Proxy let req_to_proxy = Request::builder() @@ -140,7 +140,7 @@ async fn handle_via_tasks(req: Request, config: &Arc, target: &App let bytes = body::to_bytes(resp.body_mut()).await .map_err(|_| StatusCode::BAD_GATEWAY)?; - let mut task_results = serde_json::from_slice::>(&bytes) + let mut task_results = serde_json::from_slice::>>(&bytes) .map_err(|e| { warn!("Unable to parse HTTP result: {}", e); StatusCode::BAD_GATEWAY @@ -153,14 +153,11 @@ async fn handle_via_tasks(req: Request, config: &Arc, target: &App } let result = task_results.pop().unwrap(); let response_inner = match result.status { - shared::WorkStatus::Succeeded => { - serde_json::from_str::(&result.body.body.ok_or_else(|| { - warn!("Received one successful result but it has no body"); - StatusCode::BAD_GATEWAY - })?)? + WorkStatus::Succeeded => { + result.body }, e => { - warn!("Reply had unexpected workresult code: {}", e); + warn!("Reply had unexpected workresult code: {e:?}"); return Err(StatusCode::BAD_GATEWAY)?; } }; @@ -187,7 +184,7 @@ async fn handle_via_tasks(req: Request, config: &Arc, target: &App Ok(resp) } -async fn http_req_to_struct(req: Request, my_id: &AppId, target_id: &AppId, expire: &u64) -> Result { +async fn http_req_to_struct(req: Request, my_id: &AppId, target_id: &AppId, expire: u64) -> Result, MyStatusCode> { let method = req.method().clone(); let url = req.uri().clone(); let headers = req.headers().clone(); @@ -203,15 +200,15 @@ async fn http_req_to_struct(req: Request, my_id: &AppId, target_id: &AppId headers, body: body.to_vec(), }; - let mut msg = MsgTaskRequest::new( - my_id.into(), - vec![target_id.into()], - serde_json::to_string(&http_req)?, - shared::FailureStrategy::Discard, - Value::Null - ); - - msg.expire = SystemTime::now() + Duration::from_secs(*expire); + let msg = TaskRequest { + from: my_id.clone().into(), + to: vec![target_id.clone().into()], + body: http_req, + failure_strategy: FailureStrategy::Discard, + metadata: Value::Null, + ttl: format!("{expire}s"), + id: MsgId::new() + }; Ok(msg) } diff --git a/src/logic_reply.rs b/src/logic_reply.rs index eaeaf3d..02dc28c 100644 --- a/src/logic_reply.rs +++ b/src/logic_reply.rs @@ -1,11 +1,12 @@ +use beam_lib::{TaskRequest, TaskResult, WorkStatus, AppId, AppOrProxyId}; use hyper::{Client, client::HttpConnector, Request, header, StatusCode, body, Response, Body, Uri, Method, http::uri::Scheme}; use hyper_proxy::ProxyConnector; use hyper_tls::HttpsConnector; use tracing::{info, warn, debug}; use serde_json::Value; -use shared::{MsgTaskRequest, MsgTaskResult, MsgId,beam_id::{BeamId,AppId}, WorkStatus, Plain, http_client::SamplyHttpClient}; +use shared::http_client::SamplyHttpClient; -use crate::{config::Config, errors::BeamConnectError, msg::{IsValidHttpTask, HttpResponse}}; +use crate::{config::Config, errors::BeamConnectError, msg::{HttpResponse, HttpRequest}}; pub(crate) async fn process_requests(config: Config, client: SamplyHttpClient) -> Result<(), BeamConnectError> { // Fetch tasks from Proxy @@ -21,7 +22,7 @@ pub(crate) async fn process_requests(config: Config, client: SamplyHttpClient) - Ok(()) } -async fn send_reply(task: &MsgTaskRequest, config: &Config, client: &SamplyHttpClient, resp: Result, BeamConnectError>) -> Result<(), BeamConnectError> { +async fn send_reply(task: &TaskRequest, config: &Config, client: &SamplyHttpClient, resp: Result, BeamConnectError>) -> Result<(), BeamConnectError> { let (reply_body, status) = match resp { Ok(mut resp) => { let body = body::to_bytes(resp.body_mut()).await @@ -30,28 +31,32 @@ async fn send_reply(task: &MsgTaskRequest, config: &Config, client: &SamplyHttpC warn!("Httptask returned with status {}. Reporting failure to broker.", resp.status()); // warn!("Response body was: {}", &body); }; - (serde_json::to_string(&HttpResponse { + (HttpResponse { status: resp.status(), headers: resp.headers().clone(), body: body.to_vec() - })?, WorkStatus::Succeeded) + }, WorkStatus::Succeeded) }, Err(e) => { warn!("Failed to execute http task. Err: {e}"); - ("Error executing http task. See beam connect logs".to_string(), WorkStatus::PermFailed) + (HttpResponse { + body: b"Error executing http task. See beam connect logs".to_vec(), + status: StatusCode::INTERNAL_SERVER_ERROR, + headers: Default::default(), + }, WorkStatus::PermFailed) }, }; - let msg = MsgTaskResult { + let msg = TaskResult { from: config.my_app_id.clone().into(), to: vec![task.from.clone()], task: task.id, status, metadata: Value::Null, - body: Plain::from(reply_body), + body: reply_body, }; let req_to_proxy = Request::builder() .method("PUT") - .uri(format!("{}v1/tasks/{}/results/{}", config.proxy_url, task.id,config.my_app_id.clone())) + .uri(format!("{}v1/tasks/{}/results/{}", config.proxy_url, task.id, config.my_app_id.clone())) .header(header::AUTHORIZATION, config.proxy_auth.clone()) .body(body::Body::from(serde_json::to_vec(&msg)?)) .map_err( BeamConnectError::HyperBuildError)?; @@ -64,8 +69,9 @@ async fn send_reply(task: &MsgTaskRequest, config: &Config, client: &SamplyHttpC Ok(()) } -async fn execute_http_task(task: &MsgTaskRequest, config: &Config, client: &SamplyHttpClient) -> Result, BeamConnectError> { - let task_req = task.http_request()?; +// TODO: Take ownership of `task` to save clones +async fn execute_http_task(task: &TaskRequest, config: &Config, client: &SamplyHttpClient) -> Result, BeamConnectError> { + let task_req = &task.body; info!("{} | {} {}", task.from, task_req.method, task_req.url); let target = config .targets_local @@ -74,9 +80,10 @@ async fn execute_http_task(task: &MsgTaskRequest, config: &Config, client: &Samp warn!("Lookup of local target {} failed", task_req.url.authority().unwrap()); BeamConnectError::CommunicationWithTargetFailed(String::from("Target not defined")) })?; - if !target.allowed.contains(&AppId::try_from(&task.from).or(Err(BeamConnectError::IdNotAuthorizedToAccessUrl(task.from.clone(), task_req.url.clone())))?) { - return Err(BeamConnectError::IdNotAuthorizedToAccessUrl(task.from.clone(), task_req.url.clone())); - } + match &task.from { + AppOrProxyId::App(app) if target.allowed.contains(app) => {}, + id => return Err(BeamConnectError::IdNotAuthorizedToAccessUrl(id.clone(), task_req.url.clone())), + }; if task_req.method == Method::CONNECT { debug!("Connect Request URL: {:?}", task_req.url); } @@ -92,10 +99,10 @@ async fn execute_http_task(task: &MsgTaskRequest, config: &Config, client: &Samp info!("Rewritten to: {} {}", task_req.method, uri); let mut req = Request::builder() - .method(task_req.method) + .method(task_req.method.clone()) .uri(uri); - *req.headers_mut().unwrap() = task_req.headers; - let body = body::Body::from(task_req.body); + *req.headers_mut().unwrap() = task_req.headers.clone(); + let body = body::Body::from(task_req.body.clone()); let req = req.body(body)?; debug!("Issuing request: {:?}", req); let resp = client.request(req).await @@ -103,7 +110,7 @@ async fn execute_http_task(task: &MsgTaskRequest, config: &Config, client: &Samp Ok(resp) } -async fn fetch_requests(config: &Config, client: &SamplyHttpClient) -> Result, BeamConnectError> { +async fn fetch_requests(config: &Config, client: &SamplyHttpClient) -> Result>, BeamConnectError> { let req_to_proxy = Request::builder() .uri(format!("{}v1/tasks?to={}&wait_count=1&filter=todo", config.proxy_url, config.my_app_id)) .header(header::AUTHORIZATION, config.proxy_auth.clone()) @@ -123,12 +130,13 @@ async fn fetch_requests(config: &Config, client: &SamplyHttpClient) -> Result>(&bytes); - if let Err(e) = msgs { - warn!("Unable to decode MsgTaskRequest; error: {e}. Content: {}", String::from_utf8_lossy(&bytes)); - return Err(e.into()); - } - let msgs = msgs.unwrap(); + let msgs = match serde_json::from_slice::>>(&bytes) { + Err(e) => { + warn!("Unable to decode TaskRequest; error: {e}. Content: {}", String::from_utf8_lossy(&bytes)); + return Err(e.into()); + }, + Ok(msgs) => msgs + }; debug!("Broker gave us {} tasks: {:?}", msgs.len(), msgs.first()); Ok(msgs) } diff --git a/src/msg.rs b/src/msg.rs index e445038..017312b 100644 --- a/src/msg.rs +++ b/src/msg.rs @@ -15,7 +15,7 @@ pub(crate) struct HttpRequest { pub(crate) body: Vec } -#[derive(Serialize,Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub(crate) struct HttpResponse { #[serde(with = "hyper_serde")] pub(crate) status: StatusCode, diff --git a/src/sockets.rs b/src/sockets.rs index 230be9a..bc18abf 100644 --- a/src/sockets.rs +++ b/src/sockets.rs @@ -1,20 +1,12 @@ use std::{time::Duration, collections::HashSet, sync::Arc, convert::Infallible}; use hyper::{header, Request, Body, body, StatusCode, upgrade::{self, OnUpgrade}, Response, http::{HeaderValue}, client::conn::Builder, server::conn::Http, service::service_fn, Uri, Method}; -use serde::{Serialize, Deserialize}; -use shared::{MsgId, beam_id::{AppOrProxyId, AppId}}; use tokio::io::AsyncWriteExt; use tracing::{error, warn, debug, info}; +use beam_lib::{SocketTask, MsgId, AppId, AppOrProxyId}; use crate::{config::Config, errors::BeamConnectError, structs::MyStatusCode}; -#[derive(Debug, Serialize, Deserialize)] -struct SocketTask { - from: AppOrProxyId, - to: Vec, - id: MsgId, - ttl: String, -} pub(crate) fn spawn_socket_task_poller(config: Config) { tokio::spawn(async move { @@ -41,7 +33,7 @@ pub(crate) fn spawn_socket_task_poller(config: Config) { continue; } seen.insert(task.id.clone()); - let Ok(client) = AppId::try_from(&task.from) else { + let AppOrProxyId::App(client) = task.from else { warn!("Invalid app id skipping"); continue; }; From 34c33660023ad9cbbb31efab69860551711e035c Mon Sep 17 00:00:00 2001 From: janskiba Date: Tue, 18 Jul 2023 14:45:40 +0000 Subject: [PATCH 53/80] Implement path redirects via config --- src/config.rs | 34 ++++++++++++++++++++++++++++++++-- src/example_targets.rs | 4 ++-- src/logic_reply.rs | 10 ++++++++-- src/sockets.rs | 10 ++++++++-- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/config.rs b/src/config.rs index 6a8f369..3e1e4ac 100644 --- a/src/config.rs +++ b/src/config.rs @@ -123,11 +123,41 @@ impl LocalMapping { pub(crate) struct LocalMappingEntry { #[serde(with = "http_serde::authority", rename="external")] pub(crate) needle: Authority, // Host part of URL - #[serde(with = "http_serde::authority", rename="internal")] - pub(crate) replace: Authority, + #[serde(rename="internal")] + pub(crate) replace: AuthorityReplacement, pub(crate) allowed: Vec } +#[derive(Debug, Clone, PartialEq)] +pub struct AuthorityReplacement { + pub authority: Authority, + pub path: Option +} + +impl From for AuthorityReplacement { + fn from(authority: Authority) -> Self { + Self { authority, path: None } + } +} + +impl<'de> Deserialize<'de> for AuthorityReplacement { + fn deserialize(deserializer: D) -> Result + where D: serde::Deserializer<'de> + { + let string = String::deserialize(deserializer)?; + match string.split_once('/') { + Some((auth, path)) => Ok(Self { + authority: auth.parse().map_err(serde::de::Error::custom)?, + path: Some(path.to_owned()), + }), + None => Ok(Self { + authority: string.parse().map_err(serde::de::Error::custom)?, + path: None, + }) + } + } +} + #[derive(Clone)] #[allow(dead_code)] pub(crate) struct Config { diff --git a/src/example_targets.rs b/src/example_targets.rs index 916590e..9d886f8 100644 --- a/src/example_targets.rs +++ b/src/example_targets.rs @@ -13,8 +13,8 @@ pub(crate) fn example_local(broker_id: &str) -> LocalMapping { ("wttr.in", "wttr.in", vec![app1_id.clone(), app2_id.clone()]), ("node23.uk12.network", "host23.internal.network", vec![app1_id, app2_id]) ].map(|(needle,replace,allowed)| LocalMappingEntry { - needle: Authority::from_static(needle), - replace: Authority::from_static(replace), + needle: Authority::from_static(needle).into(), + replace: Authority::from_static(replace).into(), allowed }) .into_iter() diff --git a/src/logic_reply.rs b/src/logic_reply.rs index 02dc28c..05d873a 100644 --- a/src/logic_reply.rs +++ b/src/logic_reply.rs @@ -89,11 +89,17 @@ async fn execute_http_task(task: &TaskRequest, config: &Config, cli } let mut uri = Uri::builder(); + // Normal non CONNECT http request replacement if let Some(scheme) = task_req.url.scheme_str() { - uri = uri.scheme(scheme).path_and_query(task_req.url.path()) + uri = uri.scheme(scheme); + uri = if let Some(path) = target.replace.path { + uri.path_and_query(&format!("/{path}{}", task_req.url.path())) + } else { + uri.path_and_query(task_req.url.path()) + }; } let uri = uri - .authority(target.replace.to_owned()) + .authority(target.replace.authority.to_owned()) .build()?; info!("Rewritten to: {} {}", task_req.method, uri); diff --git a/src/sockets.rs b/src/sockets.rs index bc18abf..bf21a5b 100644 --- a/src/sockets.rs +++ b/src/sockets.rs @@ -1,6 +1,6 @@ use std::{time::Duration, collections::HashSet, sync::Arc, convert::Infallible}; -use hyper::{header, Request, Body, body, StatusCode, upgrade::{self, OnUpgrade}, Response, http::{HeaderValue}, client::conn::Builder, server::conn::Http, service::service_fn, Uri, Method}; +use hyper::{header, Request, Body, body, StatusCode, upgrade::{self, OnUpgrade}, Response, http::{HeaderValue, uri::PathAndQuery}, client::conn::Builder, server::conn::Http, service::service_fn, Uri, Method}; use tokio::io::AsyncWriteExt; use tracing::{error, warn, debug, info}; use beam_lib::{SocketTask, MsgId, AppId, AppOrProxyId}; @@ -132,7 +132,13 @@ async fn execute_http_task(mut req: Request, app: &AppId, config: &Config) }; *req.uri_mut() = { let mut parts = req.uri().to_owned().into_parts(); - parts.authority = Some(authority.clone()); + parts.authority = Some(target.replace.authority.clone()); + if let Some(path) = target.replace.path { + parts.path_and_query = Some(PathAndQuery::try_from(&format!("/{path}{}", parts.path_and_query.as_ref().map(PathAndQuery::as_str).unwrap_or(""))).map_err(|e| { + warn!("Failed to set redirect path: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + })?); + } Uri::from_parts(parts).map_err(|e| { warn!("Could not transform uri authority: {e}"); StatusCode::INTERNAL_SERVER_ERROR From 37ce82e08b5b703d0affc131e797dd97853a8aa7 Mon Sep 17 00:00:00 2001 From: janskiba Date: Tue, 18 Jul 2023 14:51:02 +0000 Subject: [PATCH 54/80] Add parsing tests --- src/config.rs | 2 +- src/example_targets.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/config.rs b/src/config.rs index 3e1e4ac..5f05dec 100644 --- a/src/config.rs +++ b/src/config.rs @@ -305,7 +305,7 @@ mod tests { let broker_id = "broker.ccp-it.dktk.dkfz.de"; set_broker_id(broker_id.to_owned()); let serialized = r#"[ - {"external": "ifconfig.me","internal":"ifconfig.me","allowed":["connect1.proxy23.broker.ccp-it.dktk.dkfz.de","connect2.proxy23.broker.ccp-it.dktk.dkfz.de"]}, + {"external": "ifconfig.me","internal":"ifconfig.me/asdf","allowed":["connect1.proxy23.broker.ccp-it.dktk.dkfz.de","connect2.proxy23.broker.ccp-it.dktk.dkfz.de"]}, {"external": "ip-api.com","internal":"ip-api.com","allowed":["connect1.proxy23.broker.ccp-it.dktk.dkfz.de","connect2.proxy23.broker.ccp-it.dktk.dkfz.de"]}, {"external": "wttr.in","internal":"wttr.in","allowed":["connect1.proxy23.broker.ccp-it.dktk.dkfz.de","connect2.proxy23.broker.ccp-it.dktk.dkfz.de"]}, {"external": "node23.uk12.network","internal":"host23.internal.network","allowed":["connect1.proxy23.broker.ccp-it.dktk.dkfz.de","connect2.proxy23.broker.ccp-it.dktk.dkfz.de"]} diff --git a/src/example_targets.rs b/src/example_targets.rs index 9d886f8..49a2666 100644 --- a/src/example_targets.rs +++ b/src/example_targets.rs @@ -8,13 +8,13 @@ pub(crate) fn example_local(broker_id: &str) -> LocalMapping { let app1_id = AppId::new(&format!("connect1.{}",proxy23)).unwrap(); let app2_id = AppId::new(&format!("connect2.{}",proxy23)).unwrap(); let map = LocalMapping {entries: [ - ("ifconfig.me", "ifconfig.me", vec![app1_id.clone(), app2_id.clone()]), + ("ifconfig.me", "ifconfig.me/asdf", vec![app1_id.clone(), app2_id.clone()]), ("ip-api.com", "ip-api.com", vec![app1_id.clone(), app2_id.clone()]), ("wttr.in", "wttr.in", vec![app1_id.clone(), app2_id.clone()]), ("node23.uk12.network", "host23.internal.network", vec![app1_id, app2_id]) ].map(|(needle,replace,allowed)| LocalMappingEntry { - needle: Authority::from_static(needle).into(), - replace: Authority::from_static(replace).into(), + needle: Authority::from_static(needle), + replace: serde_json::from_value(serde_json::Value::String(replace.to_owned())).unwrap(), allowed }) .into_iter() From ebdbe639e11f2ba1a0eb0b74bffbd325f1ee5b1a Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 19 Jul 2023 08:13:24 +0000 Subject: [PATCH 55/80] Add integration tests --- examples/example_central_test.json | 16 ++++++++++++++-- examples/example_local_test.json | 18 ++++++++++++++---- tests/base_tests.rs | 14 ++++++++++---- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/examples/example_central_test.json b/examples/example_central_test.json index bfc7041..786e3f9 100644 --- a/examples/example_central_test.json +++ b/examples/example_central_test.json @@ -3,13 +3,25 @@ { "id": "C2", "name": "connect2", - "virtualhost": "postman-echo.com", + "virtualhost": "postman-get", "beamconnect": "app2.proxy2.broker" }, { "id": "C2", "name": "connect2", - "virtualhost": "postman-echo.com:443", + "virtualhost": "postman-get:443", + "beamconnect": "app2.proxy2.broker" + }, + { + "id": "C2", + "name": "connect2", + "virtualhost": "postman-post", + "beamconnect": "app2.proxy2.broker" + }, + { + "id": "C2", + "name": "connect2", + "virtualhost": "postman-post:443", "beamconnect": "app2.proxy2.broker" }, { diff --git a/examples/example_local_test.json b/examples/example_local_test.json index feb41bb..5205ead 100644 --- a/examples/example_local_test.json +++ b/examples/example_local_test.json @@ -1,12 +1,22 @@ [ { - "external": "postman-echo.com", - "internal": "postman-echo.com", + "external": "postman-get", + "internal": "postman-echo.com/get", "allowed": ["app1.proxy1.broker"] }, { - "external": "postman-echo.com:443", - "internal": "postman-echo.com:443", + "external": "postman-post", + "internal": "postman-echo.com/post", + "allowed": ["app1.proxy1.broker"] + }, + { + "external": "postman-get:443", + "internal": "postman-echo.com:443/get", + "allowed": ["app1.proxy1.broker"] + }, + { + "external": "postman-post:443", + "internal": "postman-echo.com:443/post", "allowed": ["app1.proxy1.broker"] }, { diff --git a/tests/base_tests.rs b/tests/base_tests.rs index 81e92f6..623988f 100644 --- a/tests/base_tests.rs +++ b/tests/base_tests.rs @@ -1,13 +1,19 @@ use hyper::{Body, Request, StatusCode}; -use serde_json::Value; +use serde_json::{Value, json}; mod common; use common::*; pub async fn test_normal(scheme: &str) { - let req = Request::get(format!("{scheme}://postman-echo.com/get")).body(Body::empty()).unwrap(); - let res = request(req).await; + let req = Request::get(format!("{scheme}://postman-get?foo1=bar1&foo2=bar2")).body(Body::empty()).unwrap(); + let mut res = request(req).await; assert_eq!(res.status(), StatusCode::OK, "Could not make normal request via beam-connect"); + let bytes = hyper::body::to_bytes(res.body_mut()).await.unwrap(); + let received: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(received.get("args").unwrap(), &json!({ + "foo1": "bar1", + "foo2": "bar2" + }), "Json did not match"); } pub async fn test_json(scheme: &str) { @@ -16,7 +22,7 @@ pub async fn test_json(scheme: &str) { "bar": "foo", "foobar": false, }); - let req = Request::post(format!("{scheme}://postman-echo.com/post")).body(Body::from(serde_json::to_vec(&json).unwrap())).unwrap(); + let req = Request::post(format!("{scheme}://postman-post")).body(Body::from(serde_json::to_vec(&json).unwrap())).unwrap(); let mut res = request(req).await; assert_eq!(res.status(), StatusCode::OK, "Could not make json request via beam-connect"); let bytes = hyper::body::to_bytes(res.body_mut()).await.unwrap(); From 5be6d4db43de199792b95f87d7dc4e32abcdf864 Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 19 Jul 2023 08:13:35 +0000 Subject: [PATCH 56/80] Fix query params --- src/logic_reply.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/logic_reply.rs b/src/logic_reply.rs index 05d873a..810224f 100644 --- a/src/logic_reply.rs +++ b/src/logic_reply.rs @@ -1,5 +1,5 @@ use beam_lib::{TaskRequest, TaskResult, WorkStatus, AppId, AppOrProxyId}; -use hyper::{Client, client::HttpConnector, Request, header, StatusCode, body, Response, Body, Uri, Method, http::uri::Scheme}; +use hyper::{Client, client::HttpConnector, Request, header, StatusCode, body, Response, Body, Uri, Method, http::uri::{Scheme, PathAndQuery}}; use hyper_proxy::ProxyConnector; use hyper_tls::HttpsConnector; use tracing::{info, warn, debug}; @@ -93,9 +93,9 @@ async fn execute_http_task(task: &TaskRequest, config: &Config, cli if let Some(scheme) = task_req.url.scheme_str() { uri = uri.scheme(scheme); uri = if let Some(path) = target.replace.path { - uri.path_and_query(&format!("/{path}{}", task_req.url.path())) + uri.path_and_query(&format!("/{path}{}", task_req.url.path_and_query().unwrap_or(&PathAndQuery::from_static("")))) } else { - uri.path_and_query(task_req.url.path()) + uri.path_and_query(task_req.url.path_and_query().unwrap_or(&PathAndQuery::from_static("")).as_str()) }; } let uri = uri From 6f7bd7807d38a118a53fec4acde9e9d2b05cc330 Mon Sep 17 00:00:00 2001 From: Tobias Kussel Date: Thu, 20 Jul 2023 08:22:29 +0200 Subject: [PATCH 57/80] Add internal path redirection to example local targets --- examples/example_local_targets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/example_local_targets.json b/examples/example_local_targets.json index f7be15c..1389bdb 100644 --- a/examples/example_local_targets.json +++ b/examples/example_local_targets.json @@ -1,5 +1,5 @@ [ {"external": "uk1.virtual","internal":"ifconfig.me","allowed":["connect.uk1.broker.example","connect.uk2.broker.example"]}, {"external": "uk2.virtual","internal":"ip-api.com","allowed":["connect.uk2.broker.example","connect.uk1.broker.example"]}, - {"external": "node23.uk12.network","internal":"host23.internal.network","allowed":["connect.uk2.broker.example","connect.uk12.broker.example"]} + {"external": "node23.uk12.network","internal":"host23.internal.network/node23","allowed":["connect.uk2.broker.example","connect.uk12.broker.example"]} ] From 711b24c9239ffa5e43792fd311cb6afdc2ef0168 Mon Sep 17 00:00:00 2001 From: lablans Date: Thu, 20 Jul 2023 13:21:28 +0000 Subject: [PATCH 58/80] Don't depend on cross image --- Cross.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cross.toml b/Cross.toml index 8139e11..e253e5b 100644 --- a/Cross.toml +++ b/Cross.toml @@ -1,5 +1,5 @@ [target.aarch64-unknown-linux-gnu] -image = "ghcr.io/lablans/cross-test:aarch64-unknown-linux-gnu" +pre-build = ["dpkg --add-architecture arm64 && apt-get update && apt-get install --assume-yes libssl-dev:arm64 && rm -rf /var/lib/apt/lists/*"] [target.x86_64-unknown-linux-gnu] -image = "ghcr.io/lablans/cross-test:x86_64-unknown-linux-gnu" +pre-build = ["dpkg --add-architecture amd64 && apt-get update && apt-get install --assume-yes libssl-dev:amd64 && rm -rf /var/lib/apt/lists/*"] From f3daa49e7a7b1b0e421e2715e6c5dbec6a5e47c4 Mon Sep 17 00:00:00 2001 From: janskiba Date: Mon, 21 Aug 2023 13:08:00 +0000 Subject: [PATCH 59/80] Fix no-auth --- src/logic_ask.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/logic_ask.rs b/src/logic_ask.rs index c2a58ae..283d2ac 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -40,8 +40,9 @@ pub(crate) async fn handler_http( headers.insert(header::VIA, format!("Via: Samply.Beam.Connect/0.1 {}", config.my_app_id).parse().unwrap()); - let auth = headers.remove(header::PROXY_AUTHORIZATION) - .ok_or(StatusCode::PROXY_AUTHENTICATION_REQUIRED)?; + let auth = headers + .remove(header::PROXY_AUTHORIZATION) + .unwrap_or(config.proxy_auth.parse().expect("Proxy auth header could not be generated.")); // Re-pack Authorization: Not necessary since we're not looking at the Authorization header. // if headers.remove(header::AUTHORIZATION).is_some() { From cfb74b62b6e9057cffc4b9571eb60dfb82226193 Mon Sep 17 00:00:00 2001 From: janskiba Date: Mon, 21 Aug 2023 15:45:30 +0000 Subject: [PATCH 60/80] Use local http(s) echo service for tests --- dev/docker-compose.yml | 17 ++++++++++++++++- examples/example_central_test.json | 8 ++++---- examples/example_local_test.json | 20 ++++++++++---------- tests/base_tests.rs | 12 +++++++----- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 9ff2f3a..511df83 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -76,6 +76,7 @@ services: - 8063:8063 volumes: - ../examples/:/map + - ssl-cert:/custom-cert environment: PROXY_URL: "http://proxy2:8082" BIND_ADDR: 0.0.0.0:8063 @@ -86,6 +87,7 @@ services: RUST_LOG: ${RUST_LOG} NO_PROXY: proxy2 no_proxy: proxy2 + TLS_CA_CERTIFICATES_DIR: /custom-cert proxy2: depends_on: [broker] image: samply/beam-proxy:${TAG} @@ -103,10 +105,20 @@ services: secrets: - proxy2.pem - root.crt.pem - echo: + ws-echo: image: jmalloc/echo-server environment: PORT: 80 + echo: + image: mendhak/http-https-echo + container_name: my.example.com # We set this so that we can connect via this common name so that the ssl cert CN matches + environment: + - HTTP_PORT=80 + - HTTPS_PORT=443 + volumes: + - ssl-cert:/app/custom-cert + entrypoint: ["sh", "-c", "cp ./fullchain.pem ./custom-cert/cert.pem && node ./index.js"] + user: "0:0" secrets: pki.secret: file: ./pki/pki.secret @@ -118,3 +130,6 @@ secrets: file: ./pki/dummy.priv.pem root.crt.pem: file: ./pki/root.crt.pem + +volumes: + ssl-cert: diff --git a/examples/example_central_test.json b/examples/example_central_test.json index 786e3f9..42e16ec 100644 --- a/examples/example_central_test.json +++ b/examples/example_central_test.json @@ -3,25 +3,25 @@ { "id": "C2", "name": "connect2", - "virtualhost": "postman-get", + "virtualhost": "echo-get", "beamconnect": "app2.proxy2.broker" }, { "id": "C2", "name": "connect2", - "virtualhost": "postman-get:443", + "virtualhost": "echo-get:443", "beamconnect": "app2.proxy2.broker" }, { "id": "C2", "name": "connect2", - "virtualhost": "postman-post", + "virtualhost": "echo-post", "beamconnect": "app2.proxy2.broker" }, { "id": "C2", "name": "connect2", - "virtualhost": "postman-post:443", + "virtualhost": "echo-post:443", "beamconnect": "app2.proxy2.broker" }, { diff --git a/examples/example_local_test.json b/examples/example_local_test.json index 5205ead..f9e8fac 100644 --- a/examples/example_local_test.json +++ b/examples/example_local_test.json @@ -1,27 +1,27 @@ [ { - "external": "postman-get", - "internal": "postman-echo.com/get", + "external": "echo-get", + "internal": "my.example.com/get", "allowed": ["app1.proxy1.broker"] }, { - "external": "postman-post", - "internal": "postman-echo.com/post", + "external": "echo-post", + "internal": "my.example.com/post", "allowed": ["app1.proxy1.broker"] }, { - "external": "postman-get:443", - "internal": "postman-echo.com:443/get", + "external": "echo-get:443", + "internal": "my.example.com:443/get", "allowed": ["app1.proxy1.broker"] }, { - "external": "postman-post:443", - "internal": "postman-echo.com:443/post", + "external": "echo-post:443", + "internal": "my.example.com:443/post", "allowed": ["app1.proxy1.broker"] }, { - "external": "echo", - "internal": "echo:80", + "external": "ws-echo", + "internal": "ws-echo:80", "allowed": ["app1.proxy1.broker"] } ] diff --git a/tests/base_tests.rs b/tests/base_tests.rs index 623988f..ac6ca0d 100644 --- a/tests/base_tests.rs +++ b/tests/base_tests.rs @@ -5,15 +5,16 @@ mod common; use common::*; pub async fn test_normal(scheme: &str) { - let req = Request::get(format!("{scheme}://postman-get?foo1=bar1&foo2=bar2")).body(Body::empty()).unwrap(); + let req = Request::get(format!("{scheme}://echo-get?foo1=bar1&foo2=bar2")).body(Body::empty()).unwrap(); let mut res = request(req).await; assert_eq!(res.status(), StatusCode::OK, "Could not make normal request via beam-connect"); let bytes = hyper::body::to_bytes(res.body_mut()).await.unwrap(); let received: Value = serde_json::from_slice(&bytes).unwrap(); - assert_eq!(received.get("args").unwrap(), &json!({ + assert_eq!(received.get("query").unwrap(), &json!({ "foo1": "bar1", "foo2": "bar2" }), "Json did not match"); + assert_eq!(received.get("path"), Some(&json!("/get/"))) } pub async fn test_json(scheme: &str) { @@ -22,12 +23,13 @@ pub async fn test_json(scheme: &str) { "bar": "foo", "foobar": false, }); - let req = Request::post(format!("{scheme}://postman-post")).body(Body::from(serde_json::to_vec(&json).unwrap())).unwrap(); + let req = Request::post(format!("{scheme}://echo-post")).body(Body::from(serde_json::to_vec(&json).unwrap())).unwrap(); let mut res = request(req).await; assert_eq!(res.status(), StatusCode::OK, "Could not make json request via beam-connect"); let bytes = hyper::body::to_bytes(res.body_mut()).await.unwrap(); let received: Value = serde_json::from_slice(&bytes).unwrap(); - assert_eq!(received.get("json").unwrap(), &json, "Json did not match"); + assert_eq!(received.get("body").and_then(Value::as_str).and_then(|s| serde_json::from_str::(s).ok()).unwrap(), json, "Json did not match"); + assert_eq!(received.get("path"), Some(&json!("/post/"))) } @@ -44,7 +46,7 @@ mod socket_tests { #[tokio::test] pub async fn test_ws() { - let resp = request(Request::get(format!("http://echo")) + let resp = request(Request::get(format!("http://ws-echo")) .header(header::UPGRADE, "websocket") .header(header::CONNECTION, "upgrade") .header(header::SEC_WEBSOCKET_VERSION, "13") From 35b188617fdc58a50380464f59ba72c4904a3cc3 Mon Sep 17 00:00:00 2001 From: janskiba Date: Tue, 22 Aug 2023 07:16:21 +0000 Subject: [PATCH 61/80] Fix ws tests --- examples/example_central_test.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/example_central_test.json b/examples/example_central_test.json index 42e16ec..fd5f552 100644 --- a/examples/example_central_test.json +++ b/examples/example_central_test.json @@ -25,9 +25,9 @@ "beamconnect": "app2.proxy2.broker" }, { - "id": "echo", - "name": "echo", - "virtualhost": "echo", + "id": "ws-echo", + "name": "ws-echo", + "virtualhost": "ws-echo", "beamconnect": "app2.proxy2.broker" } ] From e77db374595bd6c1baa38b90b769356b0c893398 Mon Sep 17 00:00:00 2001 From: janskiba Date: Tue, 22 Aug 2023 08:05:21 +0000 Subject: [PATCH 62/80] Make no-auth a config option --- src/config.rs | 9 ++++++++- src/logic_ask.rs | 10 +++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/config.rs b/src/config.rs index 5f05dec..29af462 100644 --- a/src/config.rs +++ b/src/config.rs @@ -75,6 +75,11 @@ struct CliArgs { /// Expiry time of the request in seconds #[clap(long, env, value_parser, default_value = "3600")] expire: u64, + + /// If set will enable any local apps to authenticate without the `Proxy-Authorization` header. + /// Security note: This allows any app with network access to beam-connect to send requests to any other beam-connect service in the beam network. + #[clap(long, env, action)] + no_auth: bool, } #[derive(Serialize, Deserialize,Clone,Debug)] @@ -169,7 +174,8 @@ pub(crate) struct Config { pub(crate) targets_public: CentralMapping, pub(crate) expire: u64, pub(crate) client: SamplyHttpClient, - pub(crate) tls_acceptor: Arc + pub(crate) tls_acceptor: Arc, + pub(crate) no_auth: bool, } fn load_local_targets(broker_id: &str, local_target_path: &Option) -> Result> { @@ -239,6 +245,7 @@ impl Config { my_app_id: app_id.clone(), proxy_auth: format!("ApiKey {} {}", app_id, args.proxy_apikey), bind_addr: args.bind_addr, + no_auth: args.no_auth, targets_local, targets_public, expire, diff --git a/src/logic_ask.rs b/src/logic_ask.rs index 283d2ac..23546e8 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -40,9 +40,13 @@ pub(crate) async fn handler_http( headers.insert(header::VIA, format!("Via: Samply.Beam.Connect/0.1 {}", config.my_app_id).parse().unwrap()); - let auth = headers - .remove(header::PROXY_AUTHORIZATION) - .unwrap_or(config.proxy_auth.parse().expect("Proxy auth header could not be generated.")); + let auth = if config.no_auth { + headers + .remove(header::PROXY_AUTHORIZATION) + .unwrap_or(config.proxy_auth.parse().expect("Proxy auth header could not be generated.")) + } else { + headers.remove(header::PROXY_AUTHORIZATION).ok_or(StatusCode::PROXY_AUTHENTICATION_REQUIRED)? + }; // Re-pack Authorization: Not necessary since we're not looking at the Authorization header. // if headers.remove(header::AUTHORIZATION).is_some() { From dfbf69b54d89bf5e71f930862bdec2e8155201f1 Mon Sep 17 00:00:00 2001 From: Tobias Kussel Date: Tue, 22 Aug 2023 10:41:24 +0200 Subject: [PATCH 63/80] Add `NO_AUTH` to readme --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 577298b..910c1f7 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,9 @@ The following command line parameter is only used in Receiver mode (see [Usage S The following command line parameter is optional, as it uses a default value: * `BIND_ADDR`: The interface and port Beam.Connect is listening on. Defaults to `0.0.0.0:8062`. +If the following flag is optional. + * `NO_AUTH`: Samply.Beam.Connect does not require a `Proxy Authorization` header, i.e. it forwards requests without (client) authentication + All parameters can be given as environment variables instead. ### Run using Docker @@ -58,9 +61,10 @@ docker run -e PROXY_URL='' \ -e DISCOVERY_URL='' \ -e PROXY_APIKEY='' \ -e BIND_ADDR='' \ + -e NO_AUTH='true' \ samply/beam-connect ``` -Again, the last environment variable `PROXY_APIKEY` is only required for usage in Receiver Mode and `BIND_ADDR` is optional. +Again, the environment variable `PROXY_APIKEY` is only required for usage in Receiver Mode. `BIND_ADDR` and `NO_AUTH` are optional. ### Use Beam.Connect to forward a HTTP request We give an example [cURL](https://curl.se/) request showing the usage of Beam.Connect to access an internal service within University Hospital #23 (`uk23`): From decc7a7664517e8fe1e8cd8fbe8221565ada80cd Mon Sep 17 00:00:00 2001 From: janskiba Date: Tue, 22 Aug 2023 15:04:20 +0000 Subject: [PATCH 64/80] Remove beam shared and use reqwest --- Cargo.toml | 6 ++-- src/config.rs | 57 +++++++++++++++-------------- src/errors.rs | 4 +-- src/logic_ask.rs | 35 +++++++----------- src/logic_reply.rs | 88 ++++++++++++++++++++------------------------- src/main.rs | 30 +++++++++------- src/msg.rs | 31 ++++++++-------- src/shutdown.rs | 25 +++++++++++++ src/sockets.rs | 89 +++++++++++++++++++++++++++++----------------- src/structs.rs | 17 +-------- 10 files changed, 205 insertions(+), 177 deletions(-) create mode 100644 src/shutdown.rs diff --git a/Cargo.toml b/Cargo.toml index 90f7077..d24d1db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,9 +19,7 @@ strip = false [dependencies] beam-lib = { git = "https://github.com/samply/beam", branch="develop", features = ["strict-ids"] } -shared = { git = "https://github.com/samply/beam", branch="develop" } -#axum = "0.5.12" tokio = { version = "1", features = ["macros","rt-multi-thread","signal"] } hyper = { version = "0.14", features = ["full"] } tower-http = { version = "0", features = ["trace"] } @@ -43,6 +41,10 @@ clap = { version = "4", features = ["derive", "env"] } thiserror = "*" http-serde = "1.1.2" tokio-native-tls = "0.3.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +reqwest = { version = "0.11.19", features = ["json"] } +anyhow = "1" +openssl = "*" # Already used by native_tls which does not reexport it. This is used for b64 en/decode [features] sockets = [] diff --git a/src/config.rs b/src/config.rs index 29af462..c216acb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,9 @@ -use std::{error::Error, path::PathBuf, fs::read_to_string, str::FromStr, sync::Arc}; +use std::{path::PathBuf, fs::{read_to_string, self}, str::FromStr, sync::Arc}; +use anyhow::Result; use clap::Parser; use hyper::{Uri, http::uri::Authority}; -use shared::http_client::{SamplyHttpClient, self}; +use reqwest::{Certificate, Client}; use tokio_native_tls::{TlsAcceptor, native_tls::{self, Identity}}; use serde::{Serialize, Deserialize}; use beam_lib::{AppId, set_broker_id}; @@ -21,7 +22,7 @@ impl FromStr for PathOrUri { fn from_str(s: &str) -> Result { match Uri::try_from(s) { Ok(uri) => Ok(Self::Uri(uri)), - Err(e_uri) => { + Err(..) => { let p = PathBuf::from(s); if p.is_file() { Ok(Self::Path(p)) @@ -173,12 +174,12 @@ pub(crate) struct Config { pub(crate) targets_local: LocalMapping, pub(crate) targets_public: CentralMapping, pub(crate) expire: u64, - pub(crate) client: SamplyHttpClient, + pub(crate) client: Client, pub(crate) tls_acceptor: Arc, pub(crate) no_auth: bool, } -fn load_local_targets(broker_id: &str, local_target_path: &Option) -> Result> { +fn load_local_targets(broker_id: &str, local_target_path: &Option) -> Result { if let Some(json_file) = local_target_path { if json_file.exists() { let json_string = std::fs::read_to_string(json_file)?; @@ -188,33 +189,37 @@ fn load_local_targets(broker_id: &str, local_target_path: &Option) -> R Ok(example_targets::example_local(broker_id)) } -async fn load_public_targets(client: &SamplyHttpClient, url: &PathOrUri) -> Result { - let bytes = match url { +async fn load_public_targets(client: &Client, url: &PathOrUri) -> Result { + match url { PathOrUri::Path(path) => { - std::fs::read_to_string(path).map_err(|e| BeamConnectError::ConfigurationError(format!("Failed to open central config file: {e}")))?.into() + serde_json::from_slice(&std::fs::read(path).map_err(|e| BeamConnectError::ConfigurationError(format!("Failed to open central config file: {e}")))?) }, PathOrUri::Uri(url) => { - let mut response = client.get(url - .to_string() - .try_into() - .map_err(|e| BeamConnectError::ConfigurationError(format!("Invalid url for public sites: {e}")))? - ).await - .map_err(|e| BeamConnectError::ConfigurationError(format!("Cannot retrieve central service discovery configuration: {e}")) - )?; - - let body = response.body_mut(); - hyper::body::to_bytes(body).await.map_err(|e| BeamConnectError::ConfigurationError(format!("Invalid central site discovery response: {e}")))? + Ok(client.get(url.to_string()) + .send().await + .map_err(|e| BeamConnectError::ConfigurationError(format!("Cannot retrieve central service discovery configuration: {e}")))? + .json() + .await + .map_err(|e| BeamConnectError::ConfigurationError(format!("Invalid central site discovery response: {e}")))? + ) }, - }; - - let deserialized = serde_json::from_slice::(&bytes) - .map_err(|e| BeamConnectError::ConfigurationError(format!("Cannot parse central service discovery configuration: {e}")))?; + }.map_err(|e| BeamConnectError::ConfigurationError(format!("Cannot parse central service discovery configuration: {e}"))) +} - Ok(deserialized) +fn build_client(tls_cert_dir: Option<&PathBuf>) -> Result { + let mut client_builder = Client::builder(); + if let Some(tls_ca_dir) = tls_cert_dir { + for path_res in tls_ca_dir.read_dir()? { + if let Ok(path_buf) = path_res { + client_builder = client_builder.add_root_certificate(Certificate::from_pem(&fs::read(path_buf.path())?)?); + } + } + } + Ok(client_builder.build()?) } impl Config { - pub(crate) async fn load() -> Result> { + pub(crate) async fn load() -> Result { let args = CliArgs::parse(); let broker_id = args.app_id .splitn(3, '.') @@ -224,9 +229,7 @@ impl Config { let app_id = AppId::new(&args.app_id)?; let expire = args.expire; - - let tls_ca_certificates = shared::crypto::load_certificates_from_dir(args.tls_ca_certificates_dir)?; - let client = http_client::build(&tls_ca_certificates, None, None)?; + let client = build_client(args.tls_ca_certificates_dir.as_ref())?; let targets_public = load_public_targets(&client, &args.discovery_url).await?; let targets_local = load_local_targets(&broker_id, &args.local_targets_file)?; diff --git a/src/errors.rs b/src/errors.rs index 4419928..e2569f0 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -11,7 +11,7 @@ pub(crate) enum BeamConnectError { #[error("Proxy rejected our authorization")] ProxyRejectedAuthorization, #[error("Unable to communicate with Proxy: {0}")] - ProxyHyperError(hyper::Error), + ProxyReqwestError(reqwest::Error), #[error("Unable to communicate with Proxy: {0}")] ProxyOtherError(String), #[error("Constructing HTTP request failed: {0}")] @@ -23,7 +23,7 @@ pub(crate) enum BeamConnectError { #[error("Unable to communicate with target host: {0}")] CommunicationWithTargetFailed(String), #[error("Unable to fetch reply from target host: {0}")] - FailedToReadTargetsReply(hyper::Error), + FailedToReadTargetsReply(reqwest::Error), #[error("Response was not valid UTF-8: {0}")] ResponseNotValidUtf8String(#[from] FromUtf8Error), #[error("Reply invalid: {0}")] diff --git a/src/logic_ask.rs b/src/logic_ask.rs index 23546e8..5b643cd 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -1,20 +1,13 @@ use std::{sync::Arc, str::FromStr}; -use std::time::{Duration, SystemTime}; -use hyper::Method; use hyper::http::HeaderValue; use hyper::http::uri::{Authority, Scheme}; -use hyper::server::conn::Http; -use hyper::service::service_fn; -use hyper::{Request, Body, Client, client::HttpConnector, Response, header, StatusCode, body, Uri}; -use hyper_proxy::ProxyConnector; -use hyper_tls::HttpsConnector; +use hyper::{Request, Body, Response, header, StatusCode, body, Uri}; use tracing::{info, debug, warn, error}; use serde_json::Value; -use shared::http_client::SamplyHttpClient; use beam_lib::{AppId, TaskResult, TaskRequest, WorkStatus, FailureStrategy, MsgId}; use crate::config::CentralMapping; -use crate::{config::Config, structs::MyStatusCode, msg::{HttpRequest, HttpResponse}, errors::BeamConnectError}; +use crate::{config::Config, structs::MyStatusCode, msg::{HttpRequest, HttpResponse}}; /// GET http://some.internal.system?a=b&c=d /// Host: @@ -91,14 +84,12 @@ async fn handle_via_tasks(req: Request, config: &Arc, target: &App let msg = http_req_to_struct(req, &config.my_app_id, &target, config.expire).await?; // Send to Proxy - let req_to_proxy = Request::builder() - .method("POST") - .uri(format!("{}v1/tasks", config.proxy_url)) + debug!("SENDING request to Proxy: {msg:?}"); + let resp = config.client.post(format!("{}v1/tasks", config.proxy_url)) .header(header::AUTHORIZATION, auth.clone()) - .body(body::Body::from(serde_json::to_vec(&msg)?)) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - debug!("SENDING request to Proxy: {:?}, {:?}", msg, req_to_proxy); - let resp = config.client.request(req_to_proxy).await + .json(&msg) + .send() + .await .map_err(|_| StatusCode::BAD_GATEWAY)?; if resp.status() != StatusCode::CREATED { return Err(StatusCode::BAD_GATEWAY.into()); @@ -117,12 +108,12 @@ async fn handle_via_tasks(req: Request, config: &Arc, target: &App .path_and_query(format!("{}/results?wait_count=1&wait_timeout=10000", location.path())) .build().unwrap(); // TODO debug!("Fetching reply from Proxy: {results_uri}"); - let req = Request::builder() + let resp = config.client + .get(results_uri.to_string()) .header(header::AUTHORIZATION, auth) .header(header::ACCEPT, "application/json") - .uri(results_uri) - .body(body::Body::empty()).unwrap(); - let mut resp = config.client.request(req).await + .send() + .await .map_err(|e| { warn!("Got error from server: {e}"); StatusCode::BAD_GATEWAY @@ -143,9 +134,7 @@ async fn handle_via_tasks(req: Request, config: &Arc, target: &App } } - let bytes = body::to_bytes(resp.body_mut()).await - .map_err(|_| StatusCode::BAD_GATEWAY)?; - let mut task_results = serde_json::from_slice::>>(&bytes) + let mut task_results = resp.json::>>().await .map_err(|e| { warn!("Unable to parse HTTP result: {}", e); StatusCode::BAD_GATEWAY diff --git a/src/logic_reply.rs b/src/logic_reply.rs index 810224f..275f1b3 100644 --- a/src/logic_reply.rs +++ b/src/logic_reply.rs @@ -1,14 +1,12 @@ -use beam_lib::{TaskRequest, TaskResult, WorkStatus, AppId, AppOrProxyId}; -use hyper::{Client, client::HttpConnector, Request, header, StatusCode, body, Response, Body, Uri, Method, http::uri::{Scheme, PathAndQuery}}; -use hyper_proxy::ProxyConnector; -use hyper_tls::HttpsConnector; +use beam_lib::{TaskRequest, TaskResult, WorkStatus, AppOrProxyId}; +use hyper::{header, StatusCode, body, Uri, Method, http::uri::PathAndQuery}; use tracing::{info, warn, debug}; use serde_json::Value; -use shared::http_client::SamplyHttpClient; +use reqwest::{Client, Response}; use crate::{config::Config, errors::BeamConnectError, msg::{HttpResponse, HttpRequest}}; -pub(crate) async fn process_requests(config: Config, client: SamplyHttpClient) -> Result<(), BeamConnectError> { +pub(crate) async fn process_requests(config: Config, client: Client) -> Result<(), BeamConnectError> { // Fetch tasks from Proxy let msgs = fetch_requests(&config, &client).await?; @@ -22,18 +20,20 @@ pub(crate) async fn process_requests(config: Config, client: SamplyHttpClient) - Ok(()) } -async fn send_reply(task: &TaskRequest, config: &Config, client: &SamplyHttpClient, resp: Result, BeamConnectError>) -> Result<(), BeamConnectError> { +async fn send_reply(task: &TaskRequest, config: &Config, client: &Client, resp: Result) -> Result<(), BeamConnectError> { let (reply_body, status) = match resp { - Ok(mut resp) => { - let body = body::to_bytes(resp.body_mut()).await - .map_err(BeamConnectError::FailedToReadTargetsReply)?; - if !resp.status().is_success() { + Ok(resp) => { + let status = resp.status(); + let headers = resp.headers().clone(); + if !status.is_success() { warn!("Httptask returned with status {}. Reporting failure to broker.", resp.status()); // warn!("Response body was: {}", &body); }; + let body = resp.bytes().await + .map_err(BeamConnectError::FailedToReadTargetsReply)?; (HttpResponse { - status: resp.status(), - headers: resp.headers().clone(), + status, + headers, body: body.to_vec() }, WorkStatus::Succeeded) }, @@ -54,15 +54,15 @@ async fn send_reply(task: &TaskRequest, config: &Config, client: &S metadata: Value::Null, body: reply_body, }; - let req_to_proxy = Request::builder() - .method("PUT") - .uri(format!("{}v1/tasks/{}/results/{}", config.proxy_url, task.id, config.my_app_id.clone())) + debug!("Delivering response to Proxy: {msg:?}"); + let resp = client + .put(format!("{}v1/tasks/{}/results/{}", config.proxy_url, task.id, config.my_app_id.clone())) .header(header::AUTHORIZATION, config.proxy_auth.clone()) - .body(body::Body::from(serde_json::to_vec(&msg)?)) - .map_err( BeamConnectError::HyperBuildError)?; - debug!("Delivering response to Proxy: {:?}, {:?}", msg, req_to_proxy); - let resp = client.request(req_to_proxy).await - .map_err(BeamConnectError::ProxyHyperError)?; + .json(&msg) + .send() + .await + .map_err(BeamConnectError::ProxyReqwestError)?; + if resp.status() != StatusCode::CREATED { return Err(BeamConnectError::ProxyOtherError(format!("Got error code {} trying to submit our result.", resp.status()))); } @@ -70,7 +70,7 @@ async fn send_reply(task: &TaskRequest, config: &Config, client: &S } // TODO: Take ownership of `task` to save clones -async fn execute_http_task(task: &TaskRequest, config: &Config, client: &SamplyHttpClient) -> Result, BeamConnectError> { +async fn execute_http_task(task: &TaskRequest, config: &Config, client: &Client) -> Result { let task_req = &task.body; info!("{} | {} {}", task.from, task_req.method, task_req.url); let target = config @@ -103,28 +103,25 @@ async fn execute_http_task(task: &TaskRequest, config: &Config, cli .build()?; info!("Rewritten to: {} {}", task_req.method, uri); - - let mut req = Request::builder() - .method(task_req.method.clone()) - .uri(uri); - *req.headers_mut().unwrap() = task_req.headers.clone(); - let body = body::Body::from(task_req.body.clone()); - let req = req.body(body)?; - debug!("Issuing request: {:?}", req); - let resp = client.request(req).await + let resp = client + .request(task_req.method.clone(), task_req.url.to_string()) + .headers(task_req.headers.clone()) + .body(body::Body::from(task_req.body.clone())) + .send() + .await .map_err(|e| BeamConnectError::CommunicationWithTargetFailed(e.to_string()))?; Ok(resp) } -async fn fetch_requests(config: &Config, client: &SamplyHttpClient) -> Result>, BeamConnectError> { - let req_to_proxy = Request::builder() - .uri(format!("{}v1/tasks?to={}&wait_count=1&filter=todo", config.proxy_url, config.my_app_id)) +async fn fetch_requests(config: &Config, client: &Client) -> Result>, BeamConnectError> { + info!("fetching requests from proxy"); + let resp = client + .get(format!("{}v1/tasks?to={}&wait_count=1&filter=todo", config.proxy_url, config.my_app_id)) .header(header::AUTHORIZATION, config.proxy_auth.clone()) .header(header::ACCEPT, "application/json") - .body(body::Body::empty())?; - info!("Requesting {req_to_proxy:?}"); - let mut resp = client.request(req_to_proxy).await - .map_err(BeamConnectError::ProxyHyperError)?; + .send() + .await + .map_err(BeamConnectError::ProxyReqwestError)?; match resp.status() { StatusCode::OK => { info!("Got request: {:?}", resp); @@ -134,15 +131,8 @@ async fn fetch_requests(config: &Config, client: &SamplyHttpClient) -> Result>>(&bytes) { - Err(e) => { - warn!("Unable to decode TaskRequest; error: {e}. Content: {}", String::from_utf8_lossy(&bytes)); - return Err(e.into()); - }, - Ok(msgs) => msgs - }; - debug!("Broker gave us {} tasks: {:?}", msgs.len(), msgs.first()); - Ok(msgs) + resp.json().await.map_err(|e| { + warn!("Unable to decode TaskRequest; error: {e}."); + BeamConnectError::ProxyOtherError(e.to_string()) + }) } diff --git a/src/main.rs b/src/main.rs index f02a2e7..14dc62a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,14 @@ -use std::{net::SocketAddr, str::FromStr, convert::Infallible, string::FromUtf8Error, error::Error, fmt::Display, collections::{hash_map, HashMap}, sync::Arc}; +use std::{net::SocketAddr, str::FromStr, convert::Infallible, error::Error, sync::Arc, time::Duration}; use config::Config; -use hyper::{body, Body, service::{service_fn, make_service_fn}, Request, Response, Server, header::{HeaderName, self, ToStrError}, Uri, http::uri::Authority, server::conn::{AddrStream, Http}, Client, client::HttpConnector, Method}; -use hyper_proxy::ProxyConnector; -use hyper_tls::HttpsConnector; +use hyper::{body, Body, service::{service_fn, make_service_fn}, Request, Response, Server, server::conn::{AddrStream, Http}, Method}; use logic_ask::handler_http; -use tracing::{info, error, debug, warn}; -use shared::http_client::SamplyHttpClient; +use tracing::{info, debug, warn}; +use tracing_subscriber::{EnvFilter, filter::LevelFilter}; use crate::errors::BeamConnectError; +mod shutdown; mod msg; mod example_targets; mod config; @@ -23,7 +22,7 @@ mod sockets; #[tokio::main] async fn main() -> Result<(), Box>{ - shared::logger::init_logger()?; + tracing::subscriber::set_global_default(tracing_subscriber::fmt().with_env_filter(EnvFilter::builder().with_default_directive(LevelFilter::INFO.into()).from_env_lossy()).finish())?; banner::print_banner(); let config = Config::load().await?; let config2 = config.clone(); @@ -40,10 +39,17 @@ async fn main() -> Result<(), Box>{ loop { debug!("Waiting for next request ..."); if let Err(e) = logic_reply::process_requests(config2.clone(), client2.clone()).await { - if let BeamConnectError::ProxyTimeoutError = e { - debug!("{e}"); - } else { - warn!("Error in processing request: {e}. Will continue with the next one."); + match e { + BeamConnectError::ProxyTimeoutError => { + debug!("{e}"); + }, + BeamConnectError::ProxyReqwestError(e) => { + warn!("Error reaching beam proxy: {e}"); + tokio::time::sleep(Duration::from_secs(10)).await; + } + _ => { + warn!("Error in processing request: {e}. Will continue with the next one."); + } } } } @@ -64,7 +70,7 @@ async fn main() -> Result<(), Box>{ let server = Server::bind(&listen) .serve(make_service) - .with_graceful_shutdown(shared::graceful_shutdown::wait_for_signal()); + .with_graceful_shutdown(crate::shutdown::wait_for_signal()); if let Err(e) = server.await { eprintln!("server error: {}", e); diff --git a/src/msg.rs b/src/msg.rs index 017312b..d73fb4b 100644 --- a/src/msg.rs +++ b/src/msg.rs @@ -1,7 +1,5 @@ use hyper::{Method, Uri, HeaderMap, StatusCode}; use serde::{Serialize, Deserialize}; -use shared::MsgTaskRequest; -use crate::errors::BeamConnectError; #[derive(Serialize,Deserialize, Debug)] pub(crate) struct HttpRequest { @@ -11,7 +9,7 @@ pub(crate) struct HttpRequest { pub(crate) url: Uri, #[serde(with = "hyper_serde")] pub(crate) headers: HeaderMap, - #[serde(with = "shared::serde_helpers::serde_base64")] + #[serde(with = "serde_base64")] pub(crate) body: Vec } @@ -21,20 +19,25 @@ pub(crate) struct HttpResponse { pub(crate) status: StatusCode, #[serde(with = "hyper_serde")] pub(crate) headers: HeaderMap, - #[serde(with = "shared::serde_helpers::serde_base64")] + #[serde(with = "serde_base64")] pub(crate) body: Vec } -pub(crate) trait IsValidHttpTask { - fn http_request(&self) -> Result; -} -impl IsValidHttpTask for MsgTaskRequest { - fn http_request(&self) -> Result { - let req_struct: HttpRequest = serde_json::from_str(self.body.body.as_ref().ok_or(BeamConnectError::ReplyInvalid("MsgTaskRequest had no content.".to_string()))?)?; - if false { // TODO - return Err(BeamConnectError::IdNotAuthorizedToAccessUrl(self.from.clone(), req_struct.url)); - } - Ok(req_struct) +// https://github.com/serde-rs/json/issues/360#issuecomment-330095360 +pub mod serde_base64 { + use serde::{Serializer, de, Deserialize, Deserializer}; + use openssl::base64; + + pub fn serialize(bytes: &[u8], serializer: S) -> Result + where S: Serializer + { + serializer.serialize_str(&base64::encode_block(bytes)) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where D: Deserializer<'de> + { + base64::decode_block(<&str>::deserialize(deserializer)?).map_err(de::Error::custom) } } diff --git a/src/shutdown.rs b/src/shutdown.rs new file mode 100644 index 0000000..d9780be --- /dev/null +++ b/src/shutdown.rs @@ -0,0 +1,25 @@ +use tracing::info; + +#[cfg(unix)] +pub async fn wait_for_signal() { + use tokio::signal::unix::{signal, SignalKind}; + let mut sigterm = signal(SignalKind::terminate()) + .expect("Unable to register shutdown handler; are you running a Unix-based OS?"); + let mut sigint = signal(SignalKind::interrupt()) + .expect("Unable to register shutdown handler; are you running a Unix-based OS?"); + let signal = tokio::select! { + _ = sigterm.recv() => "SIGTERM", + _ = sigint.recv() => "SIGINT" + }; + // The following does not print in docker-compose setups but it does when run individually. + // Probably a docker-compose error. + info!("Received signal ({signal}) - shutting down gracefully."); +} + +#[cfg(windows)] +pub async fn wait_for_signal() { + if let Err(e) = tokio::signal::ctrl_c().await { + panic!("Unable to register shutdown handler: {e}."); + } + info!("Received shutdown signal - shutting down gracefully."); +} diff --git a/src/sockets.rs b/src/sockets.rs index bf21a5b..7e1d5ed 100644 --- a/src/sockets.rs +++ b/src/sockets.rs @@ -1,9 +1,10 @@ use std::{time::Duration, collections::HashSet, sync::Arc, convert::Infallible}; -use hyper::{header, Request, Body, body, StatusCode, upgrade::{self, OnUpgrade}, Response, http::{HeaderValue, uri::PathAndQuery}, client::conn::Builder, server::conn::Http, service::service_fn, Uri, Method}; -use tokio::io::AsyncWriteExt; +use hyper::{header, Request, Body, StatusCode, upgrade::OnUpgrade, http::{HeaderValue, uri::PathAndQuery}, client::conn::Builder, server::conn::Http, service::service_fn, Uri}; +use tokio::{io::AsyncWriteExt, net::TcpStream}; use tracing::{error, warn, debug, info}; use beam_lib::{SocketTask, MsgId, AppId, AppOrProxyId}; +use reqwest::Response; use crate::{config::Config, errors::BeamConnectError, structs::MyStatusCode}; @@ -52,29 +53,29 @@ pub(crate) fn spawn_socket_task_poller(config: Config) { } async fn poll_socket_task(config: &Config) -> Result, BeamConnectError> { - let poll_socket_tasks = Request::builder() - .uri(format!("{}v1/sockets", config.proxy_url)) + let resp = config.client + .get(format!("{}v1/sockets", config.proxy_url)) .header(header::AUTHORIZATION, config.proxy_auth.clone()) .header(header::ACCEPT, "application/json") - .body(Body::empty())?; - let mut resp = config.client.request(poll_socket_tasks).await.map_err(BeamConnectError::ProxyHyperError)?; + .send() + .await + .map_err(BeamConnectError::ProxyReqwestError)?; match resp.status() { StatusCode::OK => {}, StatusCode::GATEWAY_TIMEOUT => return Err(BeamConnectError::ProxyTimeoutError), e => return Err(BeamConnectError::ProxyOtherError(format!("Unexpected status code {e}"))) }; - let body = body::to_bytes(resp.body_mut()).await.map_err(BeamConnectError::ProxyHyperError)?; - Ok(serde_json::from_slice(&body)?) + resp.json().await.map_err(BeamConnectError::ProxyReqwestError) } -async fn connect_proxy(task_id: &MsgId, config: &Config) -> Result, BeamConnectError> { - let connect_proxy_req = Request::builder() - .uri(format!("{}v1/sockets/{task_id}", config.proxy_url)) +async fn connect_proxy(task_id: &MsgId, config: &Config) -> Result { + let resp = config.client + .get(format!("{}v1/sockets/{task_id}", config.proxy_url)) .header(header::AUTHORIZATION, config.proxy_auth.clone()) .header(header::UPGRADE, "tcp") - .body(Body::empty()) - .expect("This is a valid request"); - let resp = config.client.request(connect_proxy_req).await.map_err(BeamConnectError::ProxyHyperError)?; + .send() + .await + .map_err(BeamConnectError::ProxyReqwestError)?; let invalid_status_reason = match resp.status() { StatusCode::SWITCHING_PROTOCOLS => return Ok(resp), StatusCode::NOT_FOUND | StatusCode::GONE => { @@ -88,14 +89,14 @@ async fn connect_proxy(task_id: &MsgId, config: &Config) -> Result Response { - let mut res = Response::default(); +fn status_to_response(status: StatusCode) -> hyper::Response { + let mut res = hyper::Response::default(); *res.status_mut() = status; res } -async fn tunnel(proxy: Response, client: AppId, config: &Config) { - let proxy = match upgrade::on(proxy).await { +async fn tunnel(proxy: Response, client: AppId, config: &Config) { + let proxy = match proxy.upgrade().await { Ok(socket) => socket, Err(e) => { warn!("Failed to upgrade connection to proxy: {e}"); @@ -120,7 +121,7 @@ async fn tunnel(proxy: Response, client: AppId, config: &Config) { } } -async fn execute_http_task(mut req: Request, app: &AppId, config: &Config) -> Result, StatusCode> { +async fn execute_http_task(mut req: Request, app: &AppId, config: &Config) -> Result, StatusCode> { let authority = req.uri().authority().expect("Authority is always set by the requesting beam-connect"); let Some(target) = config.targets_local.get(authority) else { warn!("Failed to lookup authority {authority}"); @@ -150,10 +151,34 @@ async fn execute_http_task(mut req: Request, app: &AppId, config: &Config) } else { None }; - let mut resp = config.client.request(req).await.map_err(|e| { - warn!("Communication with target failed: {e}"); + + // See https://github.com/hyperium/hyper/blob/master/examples/client.rs#L42 + let host = req.uri().host().expect("uri has no host"); + let port = req.uri().port_u16().unwrap_or(80); + let addr = format!("{}:{}", host, port); + let client_socket = TcpStream::connect(&addr).await.map_err(|e| { + warn!("Connection to {addr} failed: {e}"); StatusCode::BAD_GATEWAY })?; + let (mut sender, proxy_conn) = Builder::new() + .http1_preserve_header_case(true) + .http1_title_case_headers(true) + .handshake(client_socket) + .await + .map_err(|e| { + warn!("Error executuing http task. Failed handshake with server({addr}): {e}"); + StatusCode::BAD_GATEWAY + })?; + tokio::task::spawn(async move { + if let Err(err) = proxy_conn.await { + println!("Connection failed: {:?}", err); + } + }); + let mut resp = sender.send_request(req).await.map_err(|e| { + warn!("Error sending request to destination server({addr}): {e}"); + StatusCode::BAD_GATEWAY + })?; + if req_upgrade.is_some() { tunnel_upgrade(resp.extensions_mut().remove::(), req_upgrade); } @@ -178,22 +203,22 @@ fn tunnel_upgrade(client: Option, server: Option) { } } -pub(crate) async fn handle_via_sockets(mut req: Request, config: &Arc, target: &AppId, auth: HeaderValue) -> Result, MyStatusCode> { - let connect_proxy_req = Request::builder() - .method(Method::POST) - .uri(format!("{}v1/sockets/{target}", config.proxy_url)) +pub(crate) async fn handle_via_sockets(mut req: Request, config: &Arc, target: &AppId, auth: HeaderValue) -> Result, MyStatusCode> { + let resp = config.client + .post(format!("{}v1/sockets/{target}", config.proxy_url)) .header(header::AUTHORIZATION, auth) .header(header::UPGRADE, "tcp") - .body(Body::empty()) - .expect("This is a valid request"); - let resp = config.client.request(connect_proxy_req).await.map_err(|e| { - warn!("Failed to reach proxy: {e}"); - StatusCode::BAD_GATEWAY - })?; + .send() + .await + .map_err(|e| { + warn!("Failed to reach proxy: {e}"); + StatusCode::BAD_GATEWAY + } + )?; if resp.status() != StatusCode::SWITCHING_PROTOCOLS { return Err(resp.status().into()); } - let proxy_socket = upgrade::on(resp).await.map_err(|e| { + let proxy_socket = resp.upgrade().await.map_err(|e| { warn!("Failed to upgrade response from proxy to socket: {e}"); StatusCode::INTERNAL_SERVER_ERROR })?; diff --git a/src/structs.rs b/src/structs.rs index a878ba2..21bc797 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -1,8 +1,6 @@ use std::{string::FromUtf8Error, error::Error, fmt::Display}; -use hyper::{StatusCode, header::ToStrError, http::uri::Authority}; -use tracing::error; -use shared::errors::SamplyBeamError; +use hyper::{StatusCode, header::ToStrError}; #[derive(Debug)] pub(crate) struct MyStatusCode { @@ -27,19 +25,6 @@ impl From for StatusCode { } } -impl From for MyStatusCode { - fn from(e: SamplyBeamError) -> Self { - let code = match e { - SamplyBeamError::InvalidBeamId(e) => { - error!("{e}"); - StatusCode::BAD_REQUEST - }, - _ => StatusCode::NOT_IMPLEMENTED, - }; - Self { code } - } -} - impl From for MyStatusCode { fn from(_: FromUtf8Error) -> Self { Self { code: StatusCode::UNPROCESSABLE_ENTITY } From f12b2048d9f94427b8f36332fb85c2aed9889c65 Mon Sep 17 00:00:00 2001 From: janskiba Date: Tue, 22 Aug 2023 15:22:48 +0000 Subject: [PATCH 65/80] Remove useless client libs --- Cargo.toml | 5 +---- tests/base_tests.rs | 27 ++++++++++++--------------- tests/common/mod.rs | 29 +++++++++++------------------ 3 files changed, 24 insertions(+), 37 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d24d1db..95db952 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,9 +26,7 @@ tower-http = { version = "0", features = ["trace"] } tower = "*" # HTTP client with proxy support -hyper-tls = "0.5.0" -hyper-proxy = "0.9.1" -mz-http-proxy = { version = "0.1.0", features = ["hyper"] } +reqwest = { version = "0.11.19", features = ["json"] } tracing = "0.1.35" @@ -42,7 +40,6 @@ thiserror = "*" http-serde = "1.1.2" tokio-native-tls = "0.3.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -reqwest = { version = "0.11.19", features = ["json"] } anyhow = "1" openssl = "*" # Already used by native_tls which does not reexport it. This is used for b64 en/decode diff --git a/tests/base_tests.rs b/tests/base_tests.rs index ac6ca0d..6e59787 100644 --- a/tests/base_tests.rs +++ b/tests/base_tests.rs @@ -1,15 +1,13 @@ -use hyper::{Body, Request, StatusCode}; +use hyper::StatusCode; use serde_json::{Value, json}; mod common; -use common::*; +use common::TEST_CLIENT; pub async fn test_normal(scheme: &str) { - let req = Request::get(format!("{scheme}://echo-get?foo1=bar1&foo2=bar2")).body(Body::empty()).unwrap(); - let mut res = request(req).await; + let res = TEST_CLIENT.get(format!("{scheme}://echo-get?foo1=bar1&foo2=bar2")).send().await.unwrap(); assert_eq!(res.status(), StatusCode::OK, "Could not make normal request via beam-connect"); - let bytes = hyper::body::to_bytes(res.body_mut()).await.unwrap(); - let received: Value = serde_json::from_slice(&bytes).unwrap(); + let received: Value = res.json().await.unwrap(); assert_eq!(received.get("query").unwrap(), &json!({ "foo1": "bar1", "foo2": "bar2" @@ -23,11 +21,9 @@ pub async fn test_json(scheme: &str) { "bar": "foo", "foobar": false, }); - let req = Request::post(format!("{scheme}://echo-post")).body(Body::from(serde_json::to_vec(&json).unwrap())).unwrap(); - let mut res = request(req).await; + let res = TEST_CLIENT.post(format!("{scheme}://echo-post")).json(&json).send().await.unwrap(); assert_eq!(res.status(), StatusCode::OK, "Could not make json request via beam-connect"); - let bytes = hyper::body::to_bytes(res.body_mut()).await.unwrap(); - let received: Value = serde_json::from_slice(&bytes).unwrap(); + let received: Value = res.json().await.unwrap(); assert_eq!(received.get("body").and_then(Value::as_str).and_then(|s| serde_json::from_str::(s).ok()).unwrap(), json, "Json did not match"); assert_eq!(received.get("path"), Some(&json!("/post/"))) } @@ -39,21 +35,22 @@ test_http_and_https!{test_json} #[cfg(feature = "sockets")] #[cfg(test)] mod socket_tests { - use super::request; use futures_util::{SinkExt, StreamExt}; - use hyper::{Request, Body, header, StatusCode}; + use hyper::{header, StatusCode}; use tokio_tungstenite::{tungstenite::{protocol::Role, Message}, WebSocketStream}; + + use crate::common::TEST_CLIENT; #[tokio::test] pub async fn test_ws() { - let resp = request(Request::get(format!("http://ws-echo")) + let resp = TEST_CLIENT.get(format!("http://ws-echo")) .header(header::UPGRADE, "websocket") .header(header::CONNECTION, "upgrade") .header(header::SEC_WEBSOCKET_VERSION, "13") .header(header::SEC_WEBSOCKET_KEY, "h/QU7Qscq6DfSTu9aP78HQ==") - .body(Body::empty()).unwrap()).await; + .send().await.unwrap(); assert_eq!(resp.status(), StatusCode::SWITCHING_PROTOCOLS); - let socket = hyper::upgrade::on(resp).await.unwrap(); + let socket = resp.upgrade().await.unwrap(); let mut stream = WebSocketStream::from_raw_socket(socket, Role::Client, None).await; let _server_hello = stream.next().await.unwrap().unwrap(); stream.send(Message::Text("Hello World".to_string())).await.unwrap(); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f9c792d..dcb590c 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,28 +1,21 @@ use clap::__derive_refs::once_cell::sync::Lazy; -use hyper::{Response, Body, Request, header, http::HeaderValue, Client, client::HttpConnector}; -use hyper_proxy::{Proxy, Intercept, ProxyConnector}; -use hyper_tls::HttpsConnector; -use tokio_native_tls::native_tls::TlsConnector; +use hyper::{header, http::HeaderValue, HeaderMap}; +use reqwest::{Client, Proxy}; -const CLIENT: Lazy>>> = Lazy::new(|| { - let tls = TlsConnector::builder() +pub static TEST_CLIENT: Lazy = Lazy::new(|| { + Client::builder() .danger_accept_invalid_certs(true) - .danger_accept_invalid_hostnames(true) + .proxy(Proxy::all("http://localhost:8062").unwrap()) + .default_headers({ + let mut headers = HeaderMap::new(); + headers.append(header::PROXY_AUTHORIZATION, HeaderValue::from_static("ApiKey app1.proxy1.broker App1Secret")); + headers + }) .build() - .unwrap(); - let connector = HttpsConnector::from((HttpConnector::new(), tls.clone().into())); - let proxy = Proxy::new(Intercept::All, "http://localhost:8062".parse().unwrap()); - let mut proxy_con = ProxyConnector::from_proxy(connector, proxy).unwrap(); - proxy_con.set_tls(Some(tls)); - Client::builder().build(proxy_con) + .unwrap() }); -pub async fn request(mut req: Request) -> Response { - req.headers_mut().append(header::PROXY_AUTHORIZATION, HeaderValue::from_static("ApiKey app1.proxy1.broker App1Secret")); - CLIENT.request(req).await.unwrap() -} - #[macro_export] macro_rules! test_http_and_https { ($name:ident) => { From 91cdd0fa12c9df335286aa924ff78c0cc919fbb1 Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 23 Aug 2023 09:25:14 +0000 Subject: [PATCH 66/80] Remove remove redundant images in docker-compose --- dev/docker-compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 511df83..a24705d 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -52,7 +52,6 @@ services: build: context: ../ dockerfile: Dockerfile.ci - image: samply/beam-connect:${TAG} ports: - 8062:8062 volumes: @@ -71,7 +70,6 @@ services: build: context: ../ dockerfile: Dockerfile.ci - image: samply/beam-connect:${TAG} ports: - 8063:8063 volumes: From 2ad55d9df4b63eeaf99375257e236388f9fa8c6d Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 23 Aug 2023 09:26:10 +0000 Subject: [PATCH 67/80] Replace hyper_serde with http_serde --- Cargo.toml | 7 +++---- src/msg.rs | 10 +++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 95db952..86055c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,18 +26,17 @@ tower-http = { version = "0", features = ["trace"] } tower = "*" # HTTP client with proxy support -reqwest = { version = "0.11.19", features = ["json"] } +reqwest = { version = "0.11.19", features = ["json", "stream"] } tracing = "0.1.35" serde = "*" serde_json = "*" -hyper_serde = "0.13" clap = { version = "4", features = ["derive", "env"] } thiserror = "*" -http-serde = "1.1.2" +http-serde = "1.1" tokio-native-tls = "0.3.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } anyhow = "1" @@ -52,4 +51,4 @@ build-data = "0" [dev-dependencies] futures-util = "0.3.28" paste = "1.0.12" -tokio-tungstenite = "0.19.0" +tokio-tungstenite = "0.20.0" diff --git a/src/msg.rs b/src/msg.rs index d73fb4b..87117c1 100644 --- a/src/msg.rs +++ b/src/msg.rs @@ -3,11 +3,11 @@ use serde::{Serialize, Deserialize}; #[derive(Serialize,Deserialize, Debug)] pub(crate) struct HttpRequest { - #[serde(with = "hyper_serde")] + #[serde(with = "http_serde::method")] pub(crate) method: Method, - #[serde(with = "hyper_serde")] + #[serde(with = "http_serde::uri")] pub(crate) url: Uri, - #[serde(with = "hyper_serde")] + #[serde(with = "http_serde::header_map")] pub(crate) headers: HeaderMap, #[serde(with = "serde_base64")] pub(crate) body: Vec @@ -15,9 +15,9 @@ pub(crate) struct HttpRequest { #[derive(Debug, Serialize, Deserialize)] pub(crate) struct HttpResponse { - #[serde(with = "hyper_serde")] + #[serde(with = "http_serde::status_code")] pub(crate) status: StatusCode, - #[serde(with = "hyper_serde")] + #[serde(with = "http_serde::header_map")] pub(crate) headers: HeaderMap, #[serde(with = "serde_base64")] pub(crate) body: Vec From b6abf13c91bcc1341250fdb6890e80f85b3d5248 Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 23 Aug 2023 09:26:41 +0000 Subject: [PATCH 68/80] Fix socket https connections --- src/sockets.rs | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/src/sockets.rs b/src/sockets.rs index 7e1d5ed..50d1d05 100644 --- a/src/sockets.rs +++ b/src/sockets.rs @@ -152,37 +152,27 @@ async fn execute_http_task(mut req: Request, app: &AppId, config: &Config) None }; - // See https://github.com/hyperium/hyper/blob/master/examples/client.rs#L42 - let host = req.uri().host().expect("uri has no host"); - let port = req.uri().port_u16().unwrap_or(80); - let addr = format!("{}:{}", host, port); - let client_socket = TcpStream::connect(&addr).await.map_err(|e| { - warn!("Connection to {addr} failed: {e}"); - StatusCode::BAD_GATEWAY - })?; - let (mut sender, proxy_conn) = Builder::new() - .http1_preserve_header_case(true) - .http1_title_case_headers(true) - .handshake(client_socket) + let mut resp = config.client + .execute(req.try_into().expect("This should always convert")) .await .map_err(|e| { - warn!("Error executuing http task. Failed handshake with server({addr}): {e}"); + warn!("Error executuing http task. Failed handshake with server: {e}"); StatusCode::BAD_GATEWAY })?; - tokio::task::spawn(async move { - if let Err(err) = proxy_conn.await { - println!("Connection failed: {:?}", err); - } - }); - let mut resp = sender.send_request(req).await.map_err(|e| { - warn!("Error sending request to destination server({addr}): {e}"); - StatusCode::BAD_GATEWAY - })?; if req_upgrade.is_some() { tunnel_upgrade(resp.extensions_mut().remove::(), req_upgrade); } - Ok(resp) + Ok(convert_to_hyper_response(resp)) +} + +// TODO: Make a PR to add into_parts for reqwest::Response or even a conversion trait impl to avoid clones +fn convert_to_hyper_response(resp: Response) -> hyper::Response { + let mut builder = hyper::http::response::Builder::new() + .status(resp.status()) + .version(resp.version()); + builder.headers_mut().map(|headers| *headers = resp.headers().clone()); + builder.body(hyper::Body::wrap_stream(resp.bytes_stream())).expect("This should always convert") } fn tunnel_upgrade(client: Option, server: Option) { From d521f0994d53f210233cad9e09530afcf412ea19 Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 23 Aug 2023 09:48:04 +0000 Subject: [PATCH 69/80] Remove tower deps --- Cargo.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 86055c0..64eefa2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,13 +22,11 @@ beam-lib = { git = "https://github.com/samply/beam", branch="develop", features tokio = { version = "1", features = ["macros","rt-multi-thread","signal"] } hyper = { version = "0.14", features = ["full"] } -tower-http = { version = "0", features = ["trace"] } -tower = "*" # HTTP client with proxy support reqwest = { version = "0.11.19", features = ["json", "stream"] } -tracing = "0.1.35" +tracing = "0.1" serde = "*" serde_json = "*" From 17d04c3772efc2ef1530e97ef3c0bfaaf634a75d Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 23 Aug 2023 10:06:55 +0000 Subject: [PATCH 70/80] fix url override --- src/logic_reply.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logic_reply.rs b/src/logic_reply.rs index 275f1b3..c48e2a1 100644 --- a/src/logic_reply.rs +++ b/src/logic_reply.rs @@ -104,7 +104,7 @@ async fn execute_http_task(task: &TaskRequest, config: &Config, cli info!("Rewritten to: {} {}", task_req.method, uri); let resp = client - .request(task_req.method.clone(), task_req.url.to_string()) + .request(task_req.method.clone(), uri.to_string()) .headers(task_req.headers.clone()) .body(body::Body::from(task_req.body.clone())) .send() From 7247d8c32b3075892c348aefb0e4e8065c23abfc Mon Sep 17 00:00:00 2001 From: Tobias Kussel Date: Wed, 23 Aug 2023 11:25:57 +0000 Subject: [PATCH 71/80] Add http echo test to no_proxy envs --- dev/docker-compose.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index a24705d..163cede 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -62,8 +62,8 @@ services: PROXY_APIKEY: ${APP_KEY} DISCOVERY_URL: "./map/example_central_test.json" RUST_LOG: ${RUST_LOG} - NO_PROXY: proxy1 - no_proxy: proxy1 + NO_PROXY: proxy1,my.example.com + no_proxy: proxy1,my.example.com connect2: depends_on: - proxy2 @@ -83,8 +83,8 @@ services: DISCOVERY_URL: "./map/example_central_test.json" LOCAL_TARGETS_FILE: "./map/example_local_test.json" RUST_LOG: ${RUST_LOG} - NO_PROXY: proxy2 - no_proxy: proxy2 + NO_PROXY: proxy2,my.example.com + no_proxy: proxy2,my.example.com TLS_CA_CERTIFICATES_DIR: /custom-cert proxy2: depends_on: [broker] From 9ac7c229b8747e1256faa0032b2a6969fd589b43 Mon Sep 17 00:00:00 2001 From: Tobias Kussel Date: Wed, 23 Aug 2023 11:32:42 +0000 Subject: [PATCH 72/80] Remove duplicate NO_PROXY env from docker-compose --- dev/docker-compose.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 163cede..20323c8 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -21,7 +21,6 @@ services: BROKER_URL: ${BROKER_URL} PKI_ADDRESS: http://vault:8200 no_proxy: vault - NO_PROXY: vault PRIVKEY_FILE: /run/secrets/dummy.pem BIND_ADDR: 0.0.0.0:8080 RUST_LOG: ${RUST_LOG} @@ -41,7 +40,6 @@ services: PRIVKEY_FILE: /run/secrets/proxy1.pem BIND_ADDR: 0.0.0.0:8081 RUST_LOG: ${RUST_LOG} - NO_PROXY: broker no_proxy: broker secrets: - proxy1.pem @@ -62,7 +60,6 @@ services: PROXY_APIKEY: ${APP_KEY} DISCOVERY_URL: "./map/example_central_test.json" RUST_LOG: ${RUST_LOG} - NO_PROXY: proxy1,my.example.com no_proxy: proxy1,my.example.com connect2: depends_on: @@ -83,7 +80,6 @@ services: DISCOVERY_URL: "./map/example_central_test.json" LOCAL_TARGETS_FILE: "./map/example_local_test.json" RUST_LOG: ${RUST_LOG} - NO_PROXY: proxy2,my.example.com no_proxy: proxy2,my.example.com TLS_CA_CERTIFICATES_DIR: /custom-cert proxy2: @@ -98,7 +94,6 @@ services: PRIVKEY_FILE: /run/secrets/proxy2.pem BIND_ADDR: 0.0.0.0:8082 RUST_LOG: ${RUST_LOG} - NO_PROXY: broker no_proxy: broker secrets: - proxy2.pem From 88ac2bbb60a1f4557ad5bd87179de5860bebabdd Mon Sep 17 00:00:00 2001 From: Tobias Kussel Date: Fri, 1 Sep 2023 12:55:26 +0000 Subject: [PATCH 73/80] If authority is empty, use 'host' header --- src/logic_ask.rs | 8 +++++++- tests/base_tests.rs | 25 ++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/logic_ask.rs b/src/logic_ask.rs index 5b643cd..235f919 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -21,7 +21,13 @@ pub(crate) async fn handler_http( let targets = &config.targets_public; let method = req.method().to_owned(); let uri = req.uri().to_owned(); - let Some(authority) = https_authority.as_ref().or(uri.authority()) else { + + let host_header_auth = req.headers().get(header::HOST).and_then(|v| v.to_str().ok()).and_then(|v| Authority::from_str(v).ok()); + let authority = https_authority + .as_ref() + .or(uri.authority()) + .or(host_header_auth.as_ref()); + let Some(authority) = authority else { return if uri.path() == "/sites" { // Case 1 for sites request: no authority set and /sites respond_with_sites(targets) diff --git a/tests/base_tests.rs b/tests/base_tests.rs index 6e59787..f1243e4 100644 --- a/tests/base_tests.rs +++ b/tests/base_tests.rs @@ -1,4 +1,5 @@ -use hyper::StatusCode; +use hyper::{StatusCode, header}; +use reqwest::Client; use serde_json::{Value, json}; mod common; @@ -28,6 +29,28 @@ pub async fn test_json(scheme: &str) { assert_eq!(received.get("path"), Some(&json!("/post/"))) } +#[tokio::test] +pub async fn test_empty_authority() { // Test only works with http, so no macro usage + let client = Client::builder() + .danger_accept_invalid_certs(true) + .build() + .unwrap(); + let res = client.get("http://localhost:8062") + .query(&[("foo1","bar1"),("foo2","bar2")]) + .header(header::HOST, "echo-get") + .header(header::PROXY_AUTHORIZATION, "ApiKey app1.proxy1.broker App1Secret") + .send().await + .unwrap(); + assert_eq!(res.status(), StatusCode::OK, "Could not make normal request via beam-connect"); + let received: Value = res.json().await.unwrap(); + assert_eq!(received.get("query").unwrap(), &json!({ + "foo1": "bar1", + "foo2": "bar2" + }), "Json did not match"); + assert_eq!(received.get("path"), Some(&json!("/get/"))) +} + + test_http_and_https!{test_normal} test_http_and_https!{test_json} From 8c141f41daf32a8fc4f421028eab173e15d24dab Mon Sep 17 00:00:00 2001 From: Tobias Kussel Date: Fri, 1 Sep 2023 13:00:23 +0000 Subject: [PATCH 74/80] Fix /site endpoint behaviour --- src/logic_ask.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/logic_ask.rs b/src/logic_ask.rs index 235f919..ecf01d2 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -25,15 +25,15 @@ pub(crate) async fn handler_http( let host_header_auth = req.headers().get(header::HOST).and_then(|v| v.to_str().ok()).and_then(|v| Authority::from_str(v).ok()); let authority = https_authority .as_ref() - .or(uri.authority()) + .or(uri.authority()); + if authority.is_none() && uri.path() == "/site" { + // Case 1 for sites request: no authority set and /sites + return respond_with_sites(targets) + } + let authority = authority .or(host_header_auth.as_ref()); let Some(authority) = authority else { - return if uri.path() == "/sites" { - // Case 1 for sites request: no authority set and /sites - respond_with_sites(targets) - } else { - Err(StatusCode::BAD_REQUEST.into()) - } + return Err(StatusCode::BAD_REQUEST.into()) }; let headers = req.headers_mut(); From c6a240bc217a9fcfd1bf7336f93042a050e03c06 Mon Sep 17 00:00:00 2001 From: Tobias Kussel Date: Tue, 5 Sep 2023 13:42:51 +0000 Subject: [PATCH 75/80] fix sites endpoint --- src/logic_ask.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logic_ask.rs b/src/logic_ask.rs index ecf01d2..7f34d12 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -26,7 +26,7 @@ pub(crate) async fn handler_http( let authority = https_authority .as_ref() .or(uri.authority()); - if authority.is_none() && uri.path() == "/site" { + if authority.is_none() && uri.path() == "/sites" { // Case 1 for sites request: no authority set and /sites return respond_with_sites(targets) } From c2bee08c719e039480d4396ab9c3e08803a463e1 Mon Sep 17 00:00:00 2001 From: Tobias Kussel Date: Tue, 5 Sep 2023 14:38:14 +0000 Subject: [PATCH 76/80] Add sockets features to beamlib --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 64eefa2..46edc42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ anyhow = "1" openssl = "*" # Already used by native_tls which does not reexport it. This is used for b64 en/decode [features] -sockets = [] +sockets = ["beam-lib/sockets"] [build-dependencies] build-data = "0" From fb2c77eed75cc49f99a3d4f1d0f518864b41f6eb Mon Sep 17 00:00:00 2001 From: janskiba Date: Tue, 5 Sep 2023 16:20:54 +0000 Subject: [PATCH 77/80] Dont depend on claps `once_cell` --- Cargo.toml | 1 + tests/common/mod.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 46edc42..b5cb48b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ sockets = ["beam-lib/sockets"] build-data = "0" [dev-dependencies] +once_cell = "1" futures-util = "0.3.28" paste = "1.0.12" tokio-tungstenite = "0.20.0" diff --git a/tests/common/mod.rs b/tests/common/mod.rs index dcb590c..8b13e1c 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,5 +1,5 @@ -use clap::__derive_refs::once_cell::sync::Lazy; +use once_cell::sync::Lazy; use hyper::{header, http::HeaderValue, HeaderMap}; use reqwest::{Client, Proxy}; From ad32718647d3fb2bccb530e1567797b9a44dbcce Mon Sep 17 00:00:00 2001 From: janskiba Date: Tue, 19 Dec 2023 08:31:17 +0000 Subject: [PATCH 78/80] feat: Support upgrading a request to https --- src/config.rs | 4 +++- src/example_targets.rs | 3 ++- src/logic_reply.rs | 6 +++++- src/sockets.rs | 3 +++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/config.rs b/src/config.rs index c216acb..ebb594a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -131,7 +131,9 @@ pub(crate) struct LocalMappingEntry { pub(crate) needle: Authority, // Host part of URL #[serde(rename="internal")] pub(crate) replace: AuthorityReplacement, - pub(crate) allowed: Vec + pub(crate) allowed: Vec, + #[serde(default, rename = "forceHttps")] + pub(crate) force_https: bool, } #[derive(Debug, Clone, PartialEq)] diff --git a/src/example_targets.rs b/src/example_targets.rs index 49a2666..b6251a8 100644 --- a/src/example_targets.rs +++ b/src/example_targets.rs @@ -15,7 +15,8 @@ pub(crate) fn example_local(broker_id: &str) -> LocalMapping { ].map(|(needle,replace,allowed)| LocalMappingEntry { needle: Authority::from_static(needle), replace: serde_json::from_value(serde_json::Value::String(replace.to_owned())).unwrap(), - allowed + allowed, + force_https: false }) .into_iter() .collect()}; diff --git a/src/logic_reply.rs b/src/logic_reply.rs index c48e2a1..ca18c15 100644 --- a/src/logic_reply.rs +++ b/src/logic_reply.rs @@ -91,7 +91,11 @@ async fn execute_http_task(task: &TaskRequest, config: &Config, cli let mut uri = Uri::builder(); // Normal non CONNECT http request replacement if let Some(scheme) = task_req.url.scheme_str() { - uri = uri.scheme(scheme); + if target.force_https { + uri = uri.scheme(hyper::http::uri::Scheme::HTTPS); + } else { + uri = uri.scheme(scheme); + } uri = if let Some(path) = target.replace.path { uri.path_and_query(&format!("/{path}{}", task_req.url.path_and_query().unwrap_or(&PathAndQuery::from_static("")))) } else { diff --git a/src/sockets.rs b/src/sockets.rs index 50d1d05..d331834 100644 --- a/src/sockets.rs +++ b/src/sockets.rs @@ -133,6 +133,9 @@ async fn execute_http_task(mut req: Request, app: &AppId, config: &Config) }; *req.uri_mut() = { let mut parts = req.uri().to_owned().into_parts(); + if target.force_https { + parts.scheme = Some(hyper::http::uri::Scheme::HTTPS) + } parts.authority = Some(target.replace.authority.clone()); if let Some(path) = target.replace.path { parts.path_and_query = Some(PathAndQuery::try_from(&format!("/{path}{}", parts.path_and_query.as_ref().map(PathAndQuery::as_str).unwrap_or(""))).map_err(|e| { From 22dfea26f782395aaa49047dca79e20d7b1e5d7a Mon Sep 17 00:00:00 2001 From: janskiba Date: Tue, 19 Dec 2023 09:14:44 +0000 Subject: [PATCH 79/80] Trigger CI --- src/logic_ask.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/logic_ask.rs b/src/logic_ask.rs index 7f34d12..16bd50b 100644 --- a/src/logic_ask.rs +++ b/src/logic_ask.rs @@ -22,7 +22,10 @@ pub(crate) async fn handler_http( let method = req.method().to_owned(); let uri = req.uri().to_owned(); - let host_header_auth = req.headers().get(header::HOST).and_then(|v| v.to_str().ok()).and_then(|v| Authority::from_str(v).ok()); + let host_header_auth = req.headers() + .get(header::HOST) + .and_then(|v| v.to_str().ok()) + .and_then(|v| dbg!(Authority::from_str(v)).ok()); let authority = https_authority .as_ref() .or(uri.authority()); From 0457c321b40c43cb8499641ced32b226837b0345 Mon Sep 17 00:00:00 2001 From: Tobias Kussel Date: Tue, 19 Dec 2023 10:19:00 +0000 Subject: [PATCH 80/80] Bump version number --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b5cb48b..7e59782 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "connect" -version = "0.1.0" +version = "0.1.1" edition = "2021" license = "Apache-2.0"