Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions Cargo-minimal.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2768,13 +2768,13 @@ dependencies = [
"http",
"once_cell",
"payjoin-test-utils",
"percent-encoding-rfc3986",
"reqwest",
"rustls 0.23.37",
"serde",
"serde_json",
"tokio",
"tracing",
"url",
"web-time",
]

Expand All @@ -2794,6 +2794,7 @@ dependencies = [
"nix 0.30.1",
"payjoin",
"payjoin-test-utils",
"percent-encoding-rfc3986",
"r2d2",
"r2d2_sqlite",
"reqwest",
Expand All @@ -2805,7 +2806,6 @@ dependencies = [
"tokio-rustls 0.26.4",
"tracing",
"tracing-subscriber",
"url",
]

[[package]]
Expand Down Expand Up @@ -4980,7 +4980,6 @@ dependencies = [
"idna",
"percent-encoding",
"serde",
"serde_derive",
]

[[package]]
Expand Down
4 changes: 2 additions & 2 deletions Cargo-recent.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2736,13 +2736,13 @@ dependencies = [
"http",
"once_cell",
"payjoin-test-utils",
"percent-encoding-rfc3986",
"reqwest",
"rustls 0.23.31",
"serde",
"serde_json",
"tokio",
"tracing",
"url",
"web-time",
]

Expand All @@ -2762,6 +2762,7 @@ dependencies = [
"nix 0.30.1",
"payjoin",
"payjoin-test-utils",
"percent-encoding-rfc3986",
"r2d2",
"r2d2_sqlite",
"reqwest",
Expand All @@ -2773,7 +2774,6 @@ dependencies = [
"tokio-rustls 0.26.2",
"tracing",
"tracing-subscriber",
"url",
]

[[package]]
Expand Down
6 changes: 6 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,9 @@ name = "uri_deserialize_pjuri"
path = "fuzz_targets/uri/deserialize_pjuri.rs"
doc = false
bench = false

[[bin]]
name = "url_decode_url"
path = "fuzz_targets/url/decode_url.rs"
doc = false
bench = false
4 changes: 3 additions & 1 deletion fuzz/cycle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

# Continuously cycle over fuzz targets running each for 1 hour.
# It uses chrt SCHED_IDLE so that other process takes priority.
# A number of concurrent forks can be applied for parallelization. Be sure to leave one or two available CPUs open for the OS.
#
# For cargo-fuzz usage see https://github.com/rust-fuzz/cargo-fuzz?tab=readme-ov-file#usage

set -euo pipefail

FORKS=${1:-1}
REPO_DIR=$(git rev-parse --show-toplevel)
# can't find the file because of the ENV var
# shellcheck source=/dev/null
Expand All @@ -17,7 +19,7 @@ while :; do
targetName=$(targetFileToName "$targetFile")
echo "Fuzzing target $targetName ($targetFile)"
# fuzz for one hour
cargo +nightly fuzz run "$targetName" -- -max_total_time=3600
cargo +nightly fuzz run "$targetName" -- -max_total_time=3600 -fork="$FORKS"
# minimize the corpus
cargo +nightly fuzz cmin "$targetName"
done
Expand Down
9 changes: 8 additions & 1 deletion fuzz/fuzz.sh
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
#!/usr/bin/env bash

# This script is used to briefly fuzz every target when no target is provided. Otherwise, it will briefly fuzz the provided target
# When fuzzing with a specific target a number of concurrent forks can be applied. Be sure to leave one or two available CPUs open for the OS.

set -euo pipefail

TARGET=""
FORKS=1

if [[ $# -gt 0 ]]; then
TARGET="$1"
shift
fi

if [[ $# -gt 0 ]]; then
FORKS="$1"
shift
fi

REPO_DIR=$(git rev-parse --show-toplevel)

# can't find the file because of the ENV var
Expand All @@ -26,5 +33,5 @@ fi
for targetFile in $targetFiles; do
targetName=$(targetFileToName "$targetFile")
echo "Fuzzing target $targetName ($targetFile)"
cargo fuzz run "$targetName" -- -max_total_time=30
cargo fuzz run "$targetName" -- -max_total_time=30 -fork="$FORKS"
done
76 changes: 76 additions & 0 deletions fuzz/fuzz_targets/url/decode_url.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#![no_main]

use std::str;

use libfuzzer_sys::fuzz_target;
// Adjust this path to wherever your Url module lives in your crate.
use payjoin::Url;

fn do_test(data: &[u8]) {
let Ok(s) = str::from_utf8(data) else { return };

let Ok(mut url) = Url::parse(s) else { return };

let _ = url.scheme();
let _ = url.has_host();
let _ = url.domain();
let _ = url.host_str();
let _ = url.port();
let _ = url.path();
let _ = url.query();
let _ = url.fragment();
let _ = url.as_str();
let _ = url.to_string();
if let Some(segs) = url.path_segments() {
let _ = segs.collect::<Vec<_>>();
}

// domain() and host_str() must be consistent: if host_str() is None, domain() must also be None
if url.host_str().is_none() {
assert!(url.domain().is_none(), "domain() must be None when host_str() is None");
}

// Cross-check IPv4/IPv6 parsing against std::net
if let Some(host_str) = url.host_str() {
if let Ok(std_addr) = host_str.parse::<std::net::Ipv4Addr>() {
assert!(url.domain().is_none(), "domain() must be None for IPv4 host");
let _ = std_addr.octets(); // ensure std agrees it's a valid IPv4
}
let bracketed = host_str.trim_start_matches('[').trim_end_matches(']');
if let Ok(std_addr) = bracketed.parse::<std::net::Ipv6Addr>() {
assert!(url.domain().is_none(), "domain() must be None for IPv6 host");
let _ = std_addr.segments(); // ensure std agrees it's a valid IPv6
}
}

let raw = url.as_str().to_owned();
if let Ok(reparsed) = Url::parse(&raw) {
assert_eq!(
reparsed.as_str(),
raw,
"round-trip mismatch: first={raw:?} second={:?}",
reparsed.as_str()
);
}

url.set_port(Some(8080));
url.set_port(None);
url.set_fragment(Some("fuzz"));
url.set_fragment(None);
url.set_query(Some("k=v"));
url.set_query(None);
url.query_pairs_mut().append_pair("fuzz_key", "fuzz_val");

if let Some(mut segs) = url.path_segments_mut() {
segs.push("fuzz_segment");
}

let _ = url.join("relative/path");
let _ = url.join("/absolute/path");
let _ = url.join("../dotdot");
let _ = url.join("https://other.example.com/new");
}

fuzz_target!(|data| {
do_test(data);
});
2 changes: 1 addition & 1 deletion payjoin-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ http-body-util = { version = "0.1.3", optional = true }
hyper = { version = "1.6.0", features = ["http1", "server"], optional = true }
hyper-util = { version = "0.1.16", optional = true }
payjoin = { version = "0.25.0", default-features = false }
percent-encoding-rfc3986 = { version = "0.1.3" }
r2d2 = "0.8.10"
r2d2_sqlite = "0.22.0"
reqwest = { version = "0.12.23", default-features = false, features = [
Expand All @@ -51,7 +52,6 @@ tokio-rustls = { version = "0.26.2", features = [
], default-features = false, optional = true }
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
url = { version = "2.5.4", features = ["serde"] }

[dev-dependencies]
nix = { version = "0.30.1", features = ["aio", "process", "signal"] }
Expand Down
13 changes: 9 additions & 4 deletions payjoin-cli/src/app/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ use anyhow::Result;
use config::builder::DefaultState;
use config::{ConfigError, File, FileFormat};
use payjoin::bitcoin::FeeRate;
use payjoin::Version;
use payjoin::{Url, Version};
use serde::Deserialize;
use url::Url;

use crate::cli::{Cli, Commands};
use crate::db;
Expand Down Expand Up @@ -317,10 +316,16 @@ fn handle_subcommands(config: Builder, cli: &Cli) -> Result<Builder, ConfigError
#[cfg(feature = "v1")]
let config = config
.set_override_option("v1.port", port.map(|p| p.to_string()))?
.set_override_option("v1.pj_endpoint", pj_endpoint.as_ref().map(|s| s.as_str()))?;
.set_override_option(
"v1.pj_endpoint",
pj_endpoint.clone().map(|s| s.to_string()),
)?;
#[cfg(feature = "v2")]
let config = config
.set_override_option("v2.pj_directory", pj_directory.as_ref().map(|s| s.as_str()))?
.set_override_option(
"v2.pj_directory",
pj_directory.clone().map(|s| s.to_string()),
)?
.set_override_option(
"v2.ohttp_keys",
ohttp_keys.as_ref().map(|s| s.to_string_lossy().into_owned()),
Expand Down
24 changes: 19 additions & 5 deletions payjoin-cli/src/app/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,7 @@ impl App {
// If --port 0 is specified, a free port is chosen, so we need to set it
// on the endpoint which must not have a port.
if port == 0 {
endpoint
.set_port(Some(listener.local_addr()?.port()))
.expect("setting port must succeed");
endpoint.set_port(Some(listener.local_addr()?.port()));
}

let pj_uri_string = self.construct_payjoin_uri(amount, endpoint.as_str())?;
Expand Down Expand Up @@ -234,8 +232,24 @@ impl App {
(&Method::GET, "/bip21") => {
let query_string = req.uri().query().unwrap_or("");
tracing::trace!("{:?}, {query_string:?}", req.method());
let query_params: HashMap<_, _> =
url::form_urlencoded::parse(query_string.as_bytes()).into_owned().collect();
let query_params: HashMap<String, String> = query_string
.split('&')
.filter(|s| !s.is_empty())
.filter_map(|pair| {
let (k, v) = pair.split_once('=')?;
let key = percent_encoding_rfc3986::percent_decode_str(k)
.ok()?
.decode_utf8()
.ok()?
.into_owned();
let val = percent_encoding_rfc3986::percent_decode_str(v)
.ok()?
.decode_utf8()
.ok()?
.into_owned();
Some((key, val))
})
.collect();
let amount = query_params.get("amount").map(|amt| {
Amount::from_btc(amt.parse().expect("Failed to parse amount")).unwrap()
});
Expand Down
2 changes: 1 addition & 1 deletion payjoin-cli/src/app/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,7 @@ impl App {
async fn unwrap_relay_or_else_fetch(
&self,
directory: Option<impl payjoin::IntoUrl>,
) -> Result<url::Url> {
) -> Result<payjoin::Url> {
let directory = directory.map(|url| url.into_url()).transpose()?;
let selected_relay =
self.relay_manager.lock().expect("Lock should not be poisoned").get_selected_relay();
Expand Down
19 changes: 10 additions & 9 deletions payjoin-cli/src/app/v2/ohttp.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
use std::sync::{Arc, Mutex};

use anyhow::{anyhow, Result};
use payjoin::Url;

use super::Config;

#[derive(Debug, Clone)]
pub struct RelayManager {
selected_relay: Option<url::Url>,
failed_relays: Vec<url::Url>,
selected_relay: Option<Url>,
failed_relays: Vec<Url>,
}

impl RelayManager {
pub fn new() -> Self { RelayManager { selected_relay: None, failed_relays: Vec::new() } }

pub fn set_selected_relay(&mut self, relay: url::Url) { self.selected_relay = Some(relay); }
pub fn set_selected_relay(&mut self, relay: Url) { self.selected_relay = Some(relay); }

pub fn get_selected_relay(&self) -> Option<url::Url> { self.selected_relay.clone() }
pub fn get_selected_relay(&self) -> Option<Url> { self.selected_relay.clone() }

pub fn add_failed_relay(&mut self, relay: url::Url) { self.failed_relays.push(relay); }
pub fn add_failed_relay(&mut self, relay: Url) { self.failed_relays.push(relay); }

pub fn get_failed_relays(&self) -> Vec<url::Url> { self.failed_relays.clone() }
pub fn get_failed_relays(&self) -> Vec<Url> { self.failed_relays.clone() }
}

pub(crate) struct ValidatedOhttpKeys {
pub(crate) ohttp_keys: payjoin::OhttpKeys,
pub(crate) relay_url: url::Url,
pub(crate) relay_url: Url,
}

pub(crate) async fn unwrap_ohttp_keys_or_else_fetch(
config: &Config,
directory: Option<url::Url>,
directory: Option<Url>,
relay_manager: Arc<Mutex<RelayManager>>,
) -> Result<ValidatedOhttpKeys> {
if let Some(ohttp_keys) = config.v2()?.ohttp_keys.clone() {
Expand All @@ -46,7 +47,7 @@ pub(crate) async fn unwrap_ohttp_keys_or_else_fetch(

async fn fetch_ohttp_keys(
config: &Config,
directory: Option<url::Url>,
directory: Option<Url>,
relay_manager: Arc<Mutex<RelayManager>>,
) -> Result<ValidatedOhttpKeys> {
use payjoin::bitcoin::secp256k1::rand::prelude::SliceRandom;
Expand Down
14 changes: 9 additions & 5 deletions payjoin-cli/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use std::path::PathBuf;
use clap::{value_parser, Parser, Subcommand};
use payjoin::bitcoin::amount::ParseAmountError;
use payjoin::bitcoin::{Amount, FeeRate};
use payjoin::Url;
use serde::Deserialize;
use url::Url;

#[derive(Debug, Clone, Deserialize, Parser)]
pub struct Flags {
Expand Down Expand Up @@ -114,13 +114,13 @@ pub enum Commands {

#[cfg(feature = "v1")]
/// The `pj=` endpoint to receive the payjoin request
#[arg(long = "pj-endpoint", value_parser = value_parser!(Url))]
pj_endpoint: Option<Url>,
#[arg(long = "pj-endpoint", value_parser = parse_boxed_url)]
pj_endpoint: Option<Box<Url>>,

#[cfg(feature = "v2")]
/// The directory to store payjoin requests
#[arg(long = "pj-directory", value_parser = value_parser!(Url))]
pj_directory: Option<Url>,
#[arg(long = "pj-directory", value_parser = parse_boxed_url)]
pj_directory: Option<Box<Url>>,

#[cfg(feature = "v2")]
/// The path to the ohttp keys file
Expand All @@ -144,3 +144,7 @@ pub fn parse_fee_rate_in_sat_per_vb(s: &str) -> Result<FeeRate, std::num::ParseF
let fee_rate_sat_per_kwu = fee_rate_sat_per_vb * 250.0_f32;
Ok(FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu.ceil() as u64))
}

fn parse_boxed_url(s: &str) -> Result<Box<Url>, String> {
s.parse::<Url>().map(Box::new).map_err(|e| e.to_string())
}
Loading
Loading