diff --git a/.github/workflows/ci-build-image-docker-build.yml b/.github/workflows/ci-build-image-docker-build.yml index 59c34de..c7421a9 100644 --- a/.github/workflows/ci-build-image-docker-build.yml +++ b/.github/workflows/ci-build-image-docker-build.yml @@ -2,11 +2,9 @@ name: Build and Push CI Docker Build Image on: workflow_dispatch: - workflow_call: - outputs: - image-tag: - description: 'The Docker image tag to use' - value: ${{ jobs.build-and-push.outputs.image-tag }} + push: + paths: + - 'ci/Dockerfile' env: REGISTRY: ghcr.io @@ -35,9 +33,12 @@ jobs: - name: Determine image tag id: determine-tag run: | - IMAGE_TAG="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.dockerfile-sha.outputs.sha }}" + BRANCH_NAME="${{ github.ref_name }}" + SAFE_BRANCH=$(echo "$BRANCH_NAME" | sed 's/\//-/g') + IMAGE_NAME_WITH_BRANCH="${{ env.IMAGE_NAME }}-${SAFE_BRANCH}" + IMAGE_TAG="${{ env.REGISTRY }}/${IMAGE_NAME_WITH_BRANCH}:${{ steps.dockerfile-sha.outputs.sha }}" echo "image-tag=${IMAGE_TAG}" >> $GITHUB_OUTPUT - echo "Image tag: ${IMAGE_TAG}" + echo "image-name-with-branch=${IMAGE_NAME_WITH_BRANCH}" >> $GITHUB_OUTPUT - name: Log in to GitHub Container Registry uses: docker/login-action@v3 @@ -60,10 +61,9 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: ${{ env.REGISTRY }}/${{ steps.determine-tag.outputs.image-name-with-branch }} tags: | type=raw,value=${{ steps.dockerfile-sha.outputs.sha }} - type=raw,value=latest,enable={{is_default_branch}} - name: Build and push Docker image if: steps.check-image.outputs.exists == 'false' diff --git a/.github/workflows/rust-tests.yml b/.github/workflows/rust-tests.yml index cc7a863..3137b7a 100644 --- a/.github/workflows/rust-tests.yml +++ b/.github/workflows/rust-tests.yml @@ -3,7 +3,11 @@ name: Rust Format, Clippy and Tests on: pull_request: branches: [ main ] + paths-ignore: + - 'ci/Dockerfile' push: + paths-ignore: + - 'ci/Dockerfile' workflow_dispatch: env: @@ -11,18 +15,53 @@ env: RUST_BACKTRACE: 1 jobs: - build-image: - uses: ./.github/workflows/ci-build-image-docker-build.yml + verify-image: + runs-on: ubuntu-latest permissions: contents: read - packages: write - secrets: inherit + packages: read + outputs: + image-tag: ${{ steps.determine-tag.outputs.image-tag }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Calculate Dockerfile SHA256 + id: dockerfile-sha + run: | + DOCKERFILE_SHA=$(git hash-object ci/Dockerfile) + echo "sha=${DOCKERFILE_SHA}" >> $GITHUB_OUTPUT + echo "Dockerfile SHA: ${DOCKERFILE_SHA}" + + - name: Determine image tag + id: determine-tag + run: | + # Sanitize branch name for use in Docker image name (replace / with -) + BRANCH_NAME="${{ github.ref_name }}" + SAFE_BRANCH=$(echo "$BRANCH_NAME" | sed 's/\//-/g') + IMAGE_TAG="ghcr.io/${{ github.repository }}/ci-rust-${SAFE_BRANCH}:${{ steps.dockerfile-sha.outputs.sha }}" + echo "image-tag=${IMAGE_TAG}" >> $GITHUB_OUTPUT + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Check if image exists + run: | + if ! docker manifest inspect ${{ steps.determine-tag.outputs.image-tag }} > /dev/null 2>&1; then + echo "Error: Required CI Docker image not found: ${{ steps.determine-tag.outputs.image-tag }}" + exit 1 + fi check: - needs: build-image + needs: verify-image runs-on: ubuntu-latest container: - image: ${{ needs.build-image.outputs.image-tag }} + image: ${{ needs.verify-image.outputs.image-tag }} options: -v /var/run/docker.sock:/var/run/docker.sock volumes: - /var/run/docker.sock:/var/run/docker.sock diff --git a/Cargo.lock b/Cargo.lock index a9e2759..d8d8e66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,17 +8,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.16", - "once_cell", - "version_check", -] - [[package]] name = "ahash" version = "0.8.12" @@ -151,45 +140,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "asn1-rs" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" -dependencies = [ - "asn1-rs-derive", - "asn1-rs-impl", - "displaydoc", - "nom", - "num-traits", - "rusticata-macros", - "thiserror 1.0.69", - "time", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", - "synstructure", -] - -[[package]] -name = "asn1-rs-impl" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "async-stream" version = "0.3.6" @@ -540,18 +490,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -561,29 +499,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "borsh" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" -dependencies = [ - "borsh-derive", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "brotli" version = "3.5.0" @@ -611,28 +526,6 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" -[[package]] -name = "bytecheck" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "bytemuck" version = "1.24.0" @@ -659,9 +552,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "bytesize" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f4369ba008f82b968b1acbe31715ec37bd45236fa0726605a36cc3060ea256" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" [[package]] name = "cc" @@ -681,12 +574,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "chrono" version = "0.4.42" @@ -832,9 +719,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -857,9 +744,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -959,12 +846,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - [[package]] name = "der" version = "0.7.10" @@ -976,20 +857,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "der-parser" -version = "9.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" -dependencies = [ - "asn1-rs", - "displaydoc", - "nom", - "num-bigint", - "num-traits", - "rusticata-macros", -] - [[package]] name = "deranged" version = "0.5.5" @@ -1392,12 +1259,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures" version = "0.3.31" @@ -1544,10 +1405,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] @@ -1557,11 +1416,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi", "wasip2", - "wasm-bindgen", ] [[package]] @@ -1605,9 +1462,6 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.8", -] [[package]] name = "hashbrown" @@ -1688,9 +1542,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hex-conservative" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" dependencies = [ "arrayvec", ] @@ -1730,12 +1584,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -1807,11 +1660,11 @@ dependencies = [ "hyper", "hyper-util", "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.4", ] [[package]] @@ -2288,12 +2141,6 @@ dependencies = [ "hashbrown 0.15.5", ] -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "matchers" version = "0.2.0" @@ -2340,12 +2187,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2395,28 +2236,12 @@ dependencies = [ "memoffset", ] -[[package]] -name = "no_debug" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f23a60c850e1144fc1dd9435152e0cfdc7dd18725350b4243584118013a52a4" - [[package]] name = "nohash-hasher" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nonempty" version = "0.7.0" @@ -2556,15 +2381,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "oid-registry" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" -dependencies = [ - "asn1-rs", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -2757,7 +2573,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76f63d3f67d99c95a1f85623fc43242fd644dd12ccbaa18c38a54e1580c6846a" dependencies = [ - "ahash 0.8.12", + "ahash", "async-trait", "brotli", "bytes", @@ -2776,14 +2592,12 @@ dependencies = [ "nix", "once_cell", "openssl-probe", - "ouroboros", "parking_lot", "percent-encoding", "pingora-error", "pingora-http", "pingora-pool", "pingora-runtime", - "pingora-rustls", "pingora-timeout", "prometheus", "rand 0.8.5", @@ -2798,7 +2612,6 @@ dependencies = [ "tokio-test", "unicase", "windows-sys 0.59.0", - "x509-parser", "zstd", ] @@ -2877,23 +2690,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "pingora-rustls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d581533d775229a95cfd1833059d149bb613d620d16a06e468e5f197563714" -dependencies = [ - "log", - "no_debug", - "pingora-error", - "ring", - "rustls", - "rustls-native-certs", - "rustls-pemfile", - "rustls-pki-types", - "tokio-rustls", -] - [[package]] name = "pingora-timeout" version = "0.6.0" @@ -3015,15 +2811,6 @@ dependencies = [ "elliptic-curve", ] -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit", -] - [[package]] name = "proc-macro-error" version = "1.0.4" @@ -3167,26 +2954,6 @@ version = "2.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "pulldown-cmark" version = "0.13.0" @@ -3225,61 +2992,6 @@ dependencies = [ "image", ] -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.17", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.17", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - [[package]] name = "quote" version = "1.0.42" @@ -3295,12 +3007,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.8.5" @@ -3366,8 +3072,8 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fae430c6b28f1ad601274e78b7dffa0546de0b73b4cd32f46723c0c2a16f7a5" dependencies = [ + "aws-lc-rs", "pem", - "ring", "rustls-pki-types", "time", "yasna", @@ -3422,15 +3128,6 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" -[[package]] -name = "rend" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" -dependencies = [ - "bytecheck", -] - [[package]] name = "reqwest" version = "0.12.24" @@ -3450,8 +3147,8 @@ dependencies = [ "log", "percent-encoding", "pin-project-lite", - "quinn", "rustls", + "rustls-native-certs", "rustls-pki-types", "serde", "serde_json", @@ -3466,7 +3163,6 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.4", ] [[package]] @@ -3502,35 +3198,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rkyv" -version = "0.7.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" -dependencies = [ - "bitvec", - "bytecheck", - "bytes", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "rqrr" version = "0.10.0" @@ -3584,28 +3251,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" dependencies = [ "arrayvec", - "borsh", - "bytes", "num-traits", - "rand 0.8.5", - "rkyv", "serde", - "serde_json", -] - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rusticata-macros" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" -dependencies = [ - "nom", ] [[package]] @@ -3630,7 +3277,6 @@ dependencies = [ "aws-lc-rs", "log", "once_cell", - "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -3639,12 +3285,11 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.7.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ "openssl-probe", - "rustls-pemfile", "rustls-pki-types", "schannel", "security-framework", @@ -3665,7 +3310,6 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ - "web-time", "zeroize", ] @@ -3774,7 +3418,6 @@ dependencies = [ "regex", "sea-schema", "sqlx", - "tokio", "tracing", "tracing-subscriber", "url", @@ -3816,14 +3459,11 @@ version = "0.32.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a5d1c518eaf5eda38e5773f902b26ab6d5e9e9e2bb2349ca6c64cf96f80448c" dependencies = [ - "bigdecimal", "chrono", "inherent", "ordered-float", - "rust_decimal", "sea-query-derive", "serde_json", - "time", "uuid", ] @@ -3833,13 +3473,10 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" dependencies = [ - "bigdecimal", "chrono", - "rust_decimal", "sea-query", "serde_json", "sqlx", - "time", "uuid", ] @@ -3882,12 +3519,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - [[package]] name = "sec1" version = "0.7.3" @@ -3945,9 +3576,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.1" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ "bitflags 2.10.0", "core-foundation", @@ -3978,11 +3609,11 @@ dependencies = [ [[package]] name = "serde-saphyr" -version = "0.0.8" +version = "0.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0916ccf524f1ccec1b3c02193c9e3d2e167aee9b6b294829dce6f4411332155" +checksum = "9b9e06cddad47cc6214c0c456cf209b99a58b54223e7af2f6d4b88a5a9968499" dependencies = [ - "ahash 0.8.12", + "ahash", "base64", "encoding_rs_io", "nohash-hasher", @@ -4175,12 +3806,6 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - [[package]] name = "simple_asn1" version = "0.6.3" @@ -4258,8 +3883,7 @@ dependencies = [ [[package]] name = "sqlx" version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +source = "git+https://github.com/bitshock-src/sqlx.git?branch=issues-960-v0.8.6#56ec38aefda52ff14435984318046968aa322c85" dependencies = [ "sqlx-core", "sqlx-macros", @@ -4271,11 +3895,9 @@ dependencies = [ [[package]] name = "sqlx-core" version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +source = "git+https://github.com/bitshock-src/sqlx.git?branch=issues-960-v0.8.6#56ec38aefda52ff14435984318046968aa322c85" dependencies = [ "base64", - "bigdecimal", "bytes", "chrono", "crc", @@ -4293,27 +3915,24 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rust_decimal", "rustls", + "rustls-native-certs", "serde", "serde_json", "sha2", "smallvec 1.15.1", "thiserror 2.0.17", - "time", "tokio", "tokio-stream", "tracing", "url", "uuid", - "webpki-roots 0.26.11", ] [[package]] name = "sqlx-macros" version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +source = "git+https://github.com/bitshock-src/sqlx.git?branch=issues-960-v0.8.6#56ec38aefda52ff14435984318046968aa322c85" dependencies = [ "proc-macro2", "quote", @@ -4325,8 +3944,7 @@ dependencies = [ [[package]] name = "sqlx-macros-core" version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +source = "git+https://github.com/bitshock-src/sqlx.git?branch=issues-960-v0.8.6#56ec38aefda52ff14435984318046968aa322c85" dependencies = [ "dotenvy", "either", @@ -4350,12 +3968,10 @@ dependencies = [ [[package]] name = "sqlx-mysql" version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +source = "git+https://github.com/bitshock-src/sqlx.git?branch=issues-960-v0.8.6#56ec38aefda52ff14435984318046968aa322c85" dependencies = [ "atoi", "base64", - "bigdecimal", "bitflags 2.10.0", "byteorder", "bytes", @@ -4380,7 +3996,6 @@ dependencies = [ "percent-encoding", "rand 0.8.5", "rsa", - "rust_decimal", "serde", "sha1", "sha2", @@ -4388,7 +4003,6 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror 2.0.17", - "time", "tracing", "uuid", "whoami", @@ -4397,12 +4011,10 @@ dependencies = [ [[package]] name = "sqlx-postgres" version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +source = "git+https://github.com/bitshock-src/sqlx.git?branch=issues-960-v0.8.6#56ec38aefda52ff14435984318046968aa322c85" dependencies = [ "atoi", "base64", - "bigdecimal", "bitflags 2.10.0", "byteorder", "chrono", @@ -4420,10 +4032,8 @@ dependencies = [ "log", "md-5", "memchr", - "num-bigint", "once_cell", "rand 0.8.5", - "rust_decimal", "serde", "serde_json", "sha2", @@ -4431,7 +4041,6 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror 2.0.17", - "time", "tracing", "uuid", "whoami", @@ -4440,8 +4049,7 @@ dependencies = [ [[package]] name = "sqlx-sqlite" version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +source = "git+https://github.com/bitshock-src/sqlx.git?branch=issues-960-v0.8.6#56ec38aefda52ff14435984318046968aa322c85" dependencies = [ "atoi", "chrono", @@ -4458,7 +4066,6 @@ dependencies = [ "serde_urlencoded", "sqlx-core", "thiserror 2.0.17", - "time", "tracing", "url", "uuid", @@ -4594,6 +4201,7 @@ dependencies = [ "tempfile", "tokio", "toml", + "url", "uuid", ] @@ -4663,8 +4271,10 @@ dependencies = [ "hex", "indexmap 2.12.1", "secp256k1 0.31.1", + "sqlx", "tar", "tempfile", + "tokio", "ureq", ] @@ -4724,12 +4334,6 @@ dependencies = [ "windows", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "tar" version = "0.4.44" @@ -4975,18 +4579,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "toml_edit" -version = "0.23.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" -dependencies = [ - "indexmap 2.12.1", - "toml_datetime", - "toml_parser", - "winnow", -] - [[package]] name = "toml_parser" version = "1.0.4" @@ -5009,7 +4601,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" dependencies = [ "async-trait", - "axum", "base64", "bytes", "h2", @@ -5021,6 +4612,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", + "rustls-native-certs", "socket2", "sync_wrapper", "tokio", @@ -5092,9 +4684,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" dependencies = [ "bitflags 2.10.0", "bytes", @@ -5134,9 +4726,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -5145,9 +4737,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", ] @@ -5267,14 +4859,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" dependencies = [ "base64", - "flate2", "log", "percent-encoding", - "rustls", - "rustls-pki-types", "ureq-proto", "utf-8", - "webpki-roots 1.0.4", ] [[package]] @@ -5441,34 +5029,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.4", -] - -[[package]] -name = "webpki-roots" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "whoami" version = "1.6.1" @@ -5891,12 +5451,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" [[package]] name = "wit-bindgen" @@ -5910,32 +5467,6 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "x509-parser" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" -dependencies = [ - "asn1-rs", - "data-encoding", - "der-parser", - "lazy_static", - "nom", - "oid-registry", - "rusticata-macros", - "thiserror 1.0.69", - "time", -] - [[package]] name = "xattr" version = "1.6.1" @@ -5995,18 +5526,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.28" +version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" +checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.28" +version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" +checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 0d33ee8..3fbf592 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,13 @@ lto = true codegen-units = 1 panic = 'abort' strip = true + + +[patch.crates-io] +sqlx = { git = "https://github.com/bitshock-src/sqlx.git", branch = "issues-960-v0.8.6" } +sqlx-core ={ git = "https://github.com/bitshock-src/sqlx.git", branch = "issues-960-v0.8.6" } +sqlx-macros = { git = "https://github.com/bitshock-src/sqlx.git", branch = "issues-960-v0.8.6" } +sqlx-macros-core = { git = "https://github.com/bitshock-src/sqlx.git", branch = "issues-960-v0.8.6" } +sqlx-mysql = { git = "https://github.com/bitshock-src/sqlx.git", branch = "issues-960-v0.8.6" } +sqlx-postgres = { git = "https://github.com/bitshock-src/sqlx.git", branch = "issues-960-v0.8.6" } +sqlx-sqlite = { git = "https://github.com/bitshock-src/sqlx.git", branch = "issues-960-v0.8.6" } diff --git a/Dockerfile b/Dockerfile index 29f0c34..ea86aa6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ ARG BUILDPLATFORM +ARG WEBPKI_ROOTS=false FROM --platform=$BUILDPLATFORM bitshock/linux-musl-rust:1.91.1 AS builder @@ -43,17 +44,15 @@ RUN . /app/build.env && \ --target ${RUST_TARGET} && \ cp /app/target/${RUST_TARGET}/release/swgr /app/swgr +FROM scratch AS webpki-roots-true +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +FROM scratch AS webpki-roots-false -FROM scratch AS final - -# Copy the binary from the consistent location +FROM webpki-roots-$WEBPKI_ROOTS AS final COPY --from=builder /app/swgr /usr/sbin/swgr - -COPY server/config/sqlite-persistent.yaml /etc/swgr/config.yaml - +COPY server/config/persistence.yaml /etc/swgr/config.yaml ENV RUST_LOG=info - CMD ["service", "--config", "/etc/swgr/config.yaml"] - ENTRYPOINT ["/usr/sbin/swgr"] + diff --git a/README.md b/README.md index 404a83b..a030113 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,18 @@ docker run bitshock/switchgear The image is configured with a default configuration file path of `/etc/swgr/config.yaml` . Mount a volume on top of `/etc/swgr` to provide your own configuration file. +To build the Docker image: + +```shell +docker buildx build --platform linux/arm64,linux/amd64 -t swgr . +``` + +Set the build arg `WEBPKI_ROOTS=true` if you need Mozilla's web PKI roots bundle installed on the image: + +```shell +docker buildx build --platform linux/arm64,linux/amd64 --build-arg WEBPKI_ROOTS=true -t swgr . +``` + ## Administration Switchgear can be configured by both the REST API and the CLI. @@ -372,6 +384,9 @@ lnurl-service: # Timeout in seconds for Lightning node client connections (float) ln-client-timeout-secs: 2.0 + + # Optional trusted roots pem bundle for all LN clients + ln-trusted-roots: "/etc/ssl/certs/ln-ca.pem" # List of allowed host headers for incoming requests # Used for safely generating callback/invoice URLs. @@ -562,8 +577,8 @@ store: connect-timeout-secs: 2.0 # Total timeout in seconds for complete request/response total-timeout-secs: 5.0 - # List of trusted CA certificate paths for TLS verification - trusted-roots: ["/etc/ssl/certs/ca.pem"] + # Optional pem bundle of trusted CA certificate paths for TLS verification + trusted-roots: "/etc/ssl/certs/ca.pem" # Path to bearer token file for authentication authorization: "/etc/ssl/certs/auth.token" ``` @@ -622,7 +637,7 @@ store: base-url: "https://discovery.internal:8081" connect-timeout-secs: 2.0 total-timeout-secs: 5.0 - trusted-roots: ["/etc/ssl/certs/internal-ca.pem"] + trusted-roots: "/etc/ssl/certs/internal-ca.pem" authorization: "/etc/ssl/certs/discovery.token" # Local database for Offers diff --git a/ci/Dockerfile b/ci/Dockerfile index cb65b5f..4bac90c 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -1,3 +1,4 @@ +# CI Rust build image FROM rust:1.91.1 RUN apt-get update && \ diff --git a/migration/Cargo.toml b/migration/Cargo.toml index 0d440df..44d9e6c 100644 --- a/migration/Cargo.toml +++ b/migration/Cargo.toml @@ -12,10 +12,4 @@ publish = true [dependencies] tokio = { version = "1", features = ["macros", "rt-multi-thread"] } - -[dependencies.sea-orm-migration] -version = "1.1" -features = [ - "runtime-tokio-rustls", - "sqlx-postgres", "sqlx-mysql", "sqlx-sqlite" -] +sea-orm-migration = { version = "1.1", default-features = false, features = ["cli", "sqlx-postgres", "sqlx-mysql", "sqlx-sqlite"] } diff --git a/migration/src/m20220101_000001_create_table.rs b/migration/src/m20220101_000001_create_table.rs index 47c655e..2e15505 100644 --- a/migration/src/m20220101_000001_create_table.rs +++ b/migration/src/m20220101_000001_create_table.rs @@ -21,7 +21,11 @@ impl MigrationTrait for DiscoveryBackendMigration { .col(string_null(DiscoveryBackend::Name)) .col(integer(DiscoveryBackend::Weight).not_null()) .col(boolean(DiscoveryBackend::Enabled).not_null()) - .col(json(DiscoveryBackend::Implementation).not_null()) + .col( + ColumnDef::new(DiscoveryBackend::Implementation) + .json_binary() + .not_null(), + ) .col(timestamp_with_time_zone(DiscoveryBackend::CreatedAt).not_null()) .col(timestamp_with_time_zone(DiscoveryBackend::UpdatedAt).not_null()) .primary_key(Index::create().col(DiscoveryBackend::Address)) diff --git a/migration/src/m20250724_182058_create_table.rs b/migration/src/m20250724_182058_create_table.rs index 14d63ed..4f93f44 100644 --- a/migration/src/m20250724_182058_create_table.rs +++ b/migration/src/m20250724_182058_create_table.rs @@ -19,7 +19,7 @@ impl MigrationTrait for OfferMigration { ) .col( ColumnDef::new(OfferMetadataTable::Metadata) - .json() + .json_binary() .not_null(), ) .col( diff --git a/pingora/Cargo.toml b/pingora/Cargo.toml index 74f0034..e176fe1 100644 --- a/pingora/Cargo.toml +++ b/pingora/Cargo.toml @@ -20,15 +20,15 @@ backoff = { version = "0.4", features = ["tokio"] } chrono = { version = "0.4", features = ["serde"] } switchgear-service = { version = "0.1.7", path = "../service" } log = "0.4" -pingora-core = { version = "0.6", features = ["rustls"] } -pingora-error = { version = "0.6" } -pingora-load-balancing = { version = "0.6" } +pingora-core = { version = "0.6", default-features = false } +pingora-error = { version = "0.6", default-features = false } +pingora-load-balancing = { version = "0.6", default-features = false } thiserror = "2.0" tokio = { version = "1", features = ["full"] } -uuid = { version = "1.17", features = ["v4", "serde"] } +uuid = { version = "1.18", features = ["v4", "serde"] } url = "2.5" [dev-dependencies] chrono = "0.4" tokio = { version = "1", features = ["full"] } -uuid = "1.17" +uuid = "1.18" diff --git a/server/Cargo.toml b/server/Cargo.toml index a3f71fb..ae8326c 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -15,7 +15,7 @@ publish = true [dependencies] anyhow = "1.0" async-trait = "0.1" -axum-server = { version = "0.7", features = ["tokio-rustls", "tls-rustls"] } +axum-server = { version = "0.7", default-features = false, features = ["tokio-rustls", "tls-rustls"] } backoff = "0.4" chrono = { version = "0.4", features = ["serde"] } clap = { version = "4.5", features = ["derive"] } @@ -25,15 +25,14 @@ jemallocator = "0.5" jsonwebtoken = "10.2" log = "0.4" p256 = { version = "0.13", features = ["ecdsa"] } -pingora-load-balancing = { version = "0.6" } +pingora-load-balancing = { version = "0.6", default-features = false } pkcs8 = { version = "0.10", features = ["pem"] } rand = "0.8" -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } -rustls = { version = "0.23", features = ["ring"] } +rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs"] } rustls-pemfile = "2" serde = { version = "1", features = ["derive"] } +serde-saphyr = "0.0.10" serde_json = "1.0" -serde-saphyr = "0.0.8" shellexpand = "3.1" signal-hook = "0.3" signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } @@ -41,19 +40,20 @@ simplelog = {version = "0.12", features = ["paris"] } switchgear-pingora = { version = "0.1.7",path = "../pingora" } switchgear-service = { version = "0.1.7",path = "../service" } tokio = { version = "1", features = ["full"] } -uuid = { version = "1.17", features = ["v4", "serde"] } +url = { version = "2.5", features = ["serde"] } +uuid = { version = "1.18", features = ["v4", "serde"] } [dev-dependencies] chrono = { version = "0.4", features = ["serde"] } lightning-invoice = { version = "0.33", features = ["serde", "std"] } switchgear-testing = { version = "0.1.7", path = "../testing" } -rcgen = { version = "0.14", features = ["crypto"] } -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } -serde-saphyr = "0.0.8" +rcgen = { version = "0.14", default-features = false, features = ["crypto", "aws_lc_rs", "pem"] } +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-native-roots-no-provider"] } +serde-saphyr = "0.0.10" sysinfo = "0.37" -tempfile = "3.0" +tempfile = "3.23" toml = "0.9" -uuid = { version = "1.17", features = ["v4", "serde"] } +uuid = { version = "1.18", features = ["v4", "serde"] } jemallocator = "0.5" [lib] diff --git a/server/config/lnurl-standalone.yaml b/server/config/lnurl-standalone.yaml index 3d1ed61..c139ad8 100644 --- a/server/config/lnurl-standalone.yaml +++ b/server/config/lnurl-standalone.yaml @@ -12,6 +12,7 @@ lnurl-service: backend-update-frequency-secs: 1.0 invoice-expiry-secs: 180 ln-client-timeout-secs: 2.0 + ln-trusted-roots: ${LN_TRUSTED_ROOTS:-} allowed-hosts: [ "${LNURL_SERVICE_ALLOWED_HOSTS:-}" ] backoff: type: "exponential" @@ -33,7 +34,7 @@ store: base-url: "${OFFER_STORE_HTTP_BASE_URL:-https://127.0.0.1:8082}" # Update to your offers service URL connect-timeout-secs: 2.0 total-timeout-secs: 2.0 - trusted-roots: ["${OFFER_STORE_HTTP_TRUSTED_ROOTS:-/etc/ssl/certs/roots.pem}"] + trusted-roots: ${OFFER_STORE_HTTP_TRUSTED_ROOTS:-} authorization: "${OFFER_STORE_HTTP_AUTHORIZATION:-/etc/ssl/certs/offer-authorization.token}" discover: @@ -41,6 +42,6 @@ store: base-url: "${DISCOVERY_STORE_HTTP_BASE_URL:-https://127.0.0.1:8081}" # Update to your discovery service URL connect-timeout-secs: 2.0 total-timeout-secs: 2.0 - trusted-roots: ["${DISCOVERY_STORE_HTTP_TRUSTED_ROOTS:-/etc/ssl/certs/roots.pem}"] + trusted-roots: ${DISCOVERY_STORE_HTTP_TRUSTED_ROOTS:-} authorization: "${DISCOVERY_STORE_HTTP_AUTHORIZATION:-/etc/ssl/certs/discovery-authorization.token}" diff --git a/server/config/memory-basic-no-tls.yaml b/server/config/memory-basic-no-tls.yaml index 6841275..aa16ec7 100644 --- a/server/config/memory-basic-no-tls.yaml +++ b/server/config/memory-basic-no-tls.yaml @@ -13,6 +13,7 @@ lnurl-service: backend-update-frequency-secs: 1.0 invoice-expiry-secs: 180 ln-client-timeout-secs: 2.0 + ln-trusted-roots: ${LN_TRUSTED_ROOTS:-} allowed-hosts: [ "${LNURL_SERVICE_ALLOWED_HOSTS:-}" ] backoff: type: "exponential" diff --git a/server/config/memory-basic.yaml b/server/config/memory-basic.yaml index a1c47ab..13e9b7e 100644 --- a/server/config/memory-basic.yaml +++ b/server/config/memory-basic.yaml @@ -12,6 +12,7 @@ lnurl-service: backend-update-frequency-secs: 1.0 invoice-expiry-secs: 180 ln-client-timeout-secs: 2.0 + ln-trusted-roots: ${LN_TRUSTED_ROOTS:-} allowed-hosts: [ "${LNURL_SERVICE_ALLOWED_HOSTS:-}" ] backoff: type: "exponential" diff --git a/server/config/mixed-persistence.yaml b/server/config/mixed-persistence.yaml deleted file mode 100644 index 42e270c..0000000 --- a/server/config/mixed-persistence.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Mixed persistence configuration for testing different storage combinations -# Discovery uses file storage, offers use SQLite -# Tests heterogeneous storage backend scenarios - -lnurl-service: - partitions: [ "default" ] - address: "${LNURL_SERVICE_ADDRESS:-127.0.0.1:8080}" - health-check-frequency-secs: 1.0 - parallel-health-check: true - health-check-consecutive-success-to-healthy: 1 - health-check-consecutive-failure-to-unhealthy: 1 - backend-update-frequency-secs: 1.0 - invoice-expiry-secs: 180 - ln-client-timeout-secs: 2.0 - allowed-hosts: [ "${LNURL_SERVICE_ALLOWED_HOSTS:-}" ] - backoff: - type: "exponential" - max-elapsed-time-secs: 2.5 - backend-selection: "round-robin" - selection-capacity-bias: -0.2 - tls: - cert-path: "${LNURL_SERVICE_TLS_CERT_PATH:-/etc/ssl/certs/lnurl-cert.pem}" - key-path: "${LNURL_SERVICE_TLS_KEY_PATH:-/etc/ssl/certs/lnurl-key.pem}" - bech32-qr-scale: 8 - bech32-qr-light: 255 - bech32-qr-dark: 0 - -discovery-service: - address: "${DISCOVERY_SERVICE_ADDRESS:-127.0.0.1:8081}" - auth-authority: "${DISCOVERY_SERVICE_AUTH_AUTHORITY_PATH:-/etc/ssl/certs/discovery-auth-authority.pem}" - tls: - cert-path: "${DISCOVERY_SERVICE_TLS_CERT_PATH:-/etc/ssl/certs/discovery-cert.pem}" - key-path: "${DISCOVERY_SERVICE_TLS_KEY_PATH:-/etc/ssl/certs/discovery-key.pem}" - -offer-service: - address: "${OFFER_SERVICE_ADDRESS:-127.0.0.1:8082}" - auth-authority: "${OFFER_SERVICE_AUTH_AUTHORITY_PATH:-/etc/ssl/certs/offer-auth-authority.pem}" - tls: - cert-path: "${OFFER_SERVICE_TLS_CERT_PATH:-/etc/ssl/certs/offer-cert.pem}" - key-path: "${OFFER_SERVICE_TLS_KEY_PATH:-/etc/ssl/certs/offer-key.pem}" - max-page-size: 100 - -store: - offer: - type: "database" - database-url: "${OFFER_STORE_DATABASE_URL:-sqlite:///data/offer_store.db?mode=rwc}" - max-connections: 5 - discover: - type: "memory" - \ No newline at end of file diff --git a/server/config/sqlite-persistent.yaml b/server/config/persistence.yaml similarity index 97% rename from server/config/sqlite-persistent.yaml rename to server/config/persistence.yaml index 3e4fbb0..2326eef 100644 --- a/server/config/sqlite-persistent.yaml +++ b/server/config/persistence.yaml @@ -12,6 +12,7 @@ lnurl-service: backend-update-frequency-secs: 1.0 invoice-expiry-secs: 180 ln-client-timeout-secs: 2.0 + ln-trusted-roots: ${LN_TRUSTED_ROOTS:-} allowed-hosts: [ "${LNURL_SERVICE_ALLOWED_HOSTS:-}" ] backoff: type: "exponential" diff --git a/server/src/commands/discovery/backend.rs b/server/src/commands/discovery/backend.rs index 89dfb1e..b233c96 100644 --- a/server/src/commands/discovery/backend.rs +++ b/server/src/commands/discovery/backend.rs @@ -2,7 +2,8 @@ use crate::commands::{cli_read_to_string, cli_write_all}; use anyhow::{anyhow, bail, Context}; use clap::{Parser, ValueEnum}; use log::info; -use reqwest::{Certificate, Url}; +use rustls::pki_types::pem::PemObject; +use rustls::pki_types::CertificateDer; use std::fmt::{Display, Formatter}; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -20,6 +21,7 @@ use switchgear_service::components::pool::cln::grpc::config::{ use switchgear_service::components::pool::lnd::grpc::config::{ LndGrpcClientAuth, LndGrpcClientAuthPath, LndGrpcDiscoveryBackendImplementation, }; +use url::Url; #[derive(Parser, Debug)] pub enum DiscoveryBackendManagementCommands { @@ -169,7 +171,7 @@ pub fn new_backend( url: Url::parse("https://127.0.0.1:9736")?, domain: Some("localhost".to_string()), auth: ClnGrpcClientAuth::Path(ClnGrpcClientAuthPath { - ca_cert_path: PathBuf::from("/path/to/ca.pem"), + ca_cert_path: PathBuf::from("/path/to/ca.pem").into(), client_cert_path: PathBuf::from("/path/to/client.pem"), client_key_path: PathBuf::from("/path/to/client-key.pem"), }), @@ -180,7 +182,7 @@ pub fn new_backend( url: Url::parse("https://127.0.0.1:10009")?, domain: Some("localhost".to_string()), auth: LndGrpcClientAuth::Path(LndGrpcClientAuthPath { - tls_cert_path: PathBuf::from("/path/to/tls.cert"), + tls_cert_path: PathBuf::from("/path/to/tls.cert").into(), macaroon_path: PathBuf::from("/path/to/admin.macaroon"), }), amp_invoice: false, @@ -458,19 +460,12 @@ fn create_backend_client( }; let trusted_roots = if let Some(trusted_roots_path) = trusted_roots_path { - let trusted_roots = fs::read(&trusted_roots_path).with_context(|| { - format!( - "reading trusted roots file: {}", - trusted_roots_path.to_string_lossy() - ) - })?; - - vec![Certificate::from_pem(&trusted_roots).with_context(|| { - format!( - "parsing trusted roots file: {}", - trusted_roots_path.to_string_lossy() - ) - })?] + CertificateDer::pem_file_iter(&trusted_roots_path) + .with_context(|| format!("parsing root certificate: {}", trusted_roots_path.display()))? + .collect::, _>>() + .with_context(|| { + format!("parsing root certificate: {}", trusted_roots_path.display()) + })? } else { vec![] }; @@ -479,7 +474,7 @@ fn create_backend_client( base_url, Duration::from_secs(1), Duration::from_secs(1), - trusted_roots, + &trusted_roots, authorization, )?) } diff --git a/server/src/commands/offer/mod.rs b/server/src/commands/offer/mod.rs index 75882a6..539e42a 100644 --- a/server/src/commands/offer/mod.rs +++ b/server/src/commands/offer/mod.rs @@ -3,11 +3,13 @@ use crate::commands::offer::record::OfferRecordManagementCommands; use crate::commands::token::TokenCommands; use anyhow::{anyhow, Context}; use clap::{Parser, Subcommand}; -use reqwest::{Certificate, Url}; +use rustls::pki_types::pem::PemObject; +use rustls::pki_types::CertificateDer; use std::path::PathBuf; use std::time::Duration; use std::{env, fs}; use switchgear_service::components::offer::http::HttpOfferStore; +use url::Url; pub mod metadata; pub mod record; @@ -75,19 +77,12 @@ pub fn create_offer_client( }; let trusted_roots = if let Some(trusted_roots_path) = trusted_roots_path { - let trusted_roots = fs::read(&trusted_roots_path).with_context(|| { - format!( - "reading trusted roots file: {}", - trusted_roots_path.to_string_lossy() - ) - })?; - - vec![Certificate::from_pem(&trusted_roots).with_context(|| { - format!( - "parsing trusted roots file: {}", - trusted_roots_path.to_string_lossy() - ) - })?] + CertificateDer::pem_file_iter(&trusted_roots_path) + .with_context(|| format!("parsing root certificate: {}", trusted_roots_path.display()))? + .collect::, _>>() + .with_context(|| { + format!("parsing root certificate: {}", trusted_roots_path.display()) + })? } else { vec![] }; @@ -96,7 +91,7 @@ pub fn create_offer_client( base_url, Duration::from_secs(1), Duration::from_secs(1), - trusted_roots, + &trusted_roots, authorization, )?) } diff --git a/server/src/commands/services.rs b/server/src/commands/services.rs index f6a82fe..c8b43a5 100644 --- a/server/src/commands/services.rs +++ b/server/src/commands/services.rs @@ -30,10 +30,6 @@ pub async fn execute( ) -> anyhow::Result<()> { info!("starting services"); - rustls::crypto::ring::default_provider() - .install_default() - .map_err(|_| anyhow!("failed to stand up rustls encryption platform"))?; - let (signals_fut, signals_handle) = get_signals_fut()?; let config_injector = ServerConfigInjector::new(config_path)?; diff --git a/server/src/config.rs b/server/src/config.rs index 057bb4c..56fe9c3 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -28,6 +28,7 @@ pub struct LnUrlBalancerServiceConfig { pub backend_selection: BackendSelectionConfig, pub tls: Option, pub ln_client_timeout_secs: f64, + pub ln_trusted_roots: Option, pub selection_capacity_bias: Option, pub comment_allowed: Option, pub bech32_qr_scale: usize, @@ -95,7 +96,7 @@ pub enum OfferStoreConfig { base_url: String, connect_timeout_secs: f64, total_timeout_secs: f64, - trusted_roots: Vec, + trusted_roots: Option, authorization: PathBuf, }, } @@ -114,7 +115,7 @@ pub enum DiscoveryStoreConfig { base_url: String, connect_timeout_secs: f64, total_timeout_secs: f64, - trusted_roots: Vec, + trusted_roots: Option, authorization: PathBuf, }, } diff --git a/server/src/di/inject/injectors/balance.rs b/server/src/di/inject/injectors/balance.rs index 1214901..a847817 100644 --- a/server/src/di/inject/injectors/balance.rs +++ b/server/src/di/inject/injectors/balance.rs @@ -2,10 +2,12 @@ use crate::config::{BackendSelectionConfig, BackoffConfig, LnUrlBalancerServiceC use crate::di::delegates::{BackoffProviderDelegate, LnBalancerDelegate}; use crate::di::inject::injectors::config::{ServerConfigInjector, ServiceEnablementInjector}; use crate::di::inject::injectors::store::discovery::DiscoveryStoreInjector; -use anyhow::anyhow; +use anyhow::{anyhow, Context}; use pingora_load_balancing::discovery::ServiceDiscovery; use pingora_load_balancing::health_check::HealthCheck; use pingora_load_balancing::{Backends, LoadBalancer}; +use rustls::pki_types::pem::PemObject; +use rustls::pki_types::CertificateDer; use std::cell::RefCell; use std::rc::Rc; use std::sync::Arc; @@ -94,8 +96,19 @@ impl BalancerInjector { } }; - let pool = - DefaultLnClientPool::new(Duration::from_secs_f64(lnurl_config.ln_client_timeout_secs)); + let trusted_roots = if let Some(trusted_roots) = &lnurl_config.ln_trusted_roots { + CertificateDer::pem_file_iter(trusted_roots) + .with_context(|| format!("parsing root certificate: {}", trusted_roots.display()))? + .collect::, _>>() + .with_context(|| format!("parsing root certificate: {}", trusted_roots.display()))? + } else { + vec![] + }; + + let pool = DefaultLnClientPool::new( + Duration::from_secs_f64(lnurl_config.ln_client_timeout_secs), + trusted_roots, + ); let discovery = DefaultPingoraLnDiscovery::new( discovery, diff --git a/server/src/di/inject/injectors/service/tls.rs b/server/src/di/inject/injectors/service/tls.rs index 27b8794..07896b0 100644 --- a/server/src/di/inject/injectors/service/tls.rs +++ b/server/src/di/inject/injectors/service/tls.rs @@ -1,13 +1,27 @@ use crate::config::TlsConfig; +use anyhow::Context; use axum_server::tls_rustls::RustlsConfig; -use rustls_pemfile::{certs, private_key}; +use rustls::pki_types::pem::PemObject; +use rustls::pki_types::CertificateDer; +use rustls_pemfile::private_key; use std::fs::File; use std::io::BufReader; pub fn load_server_x509_credentials(tls_config: &TlsConfig) -> anyhow::Result { - let cert_chain = certs(&mut BufReader::new(File::open(&tls_config.cert_path)?)) - .filter_map(Result::ok) - .collect(); + let cert_chain = CertificateDer::pem_file_iter(&tls_config.cert_path) + .with_context(|| { + format!( + "parsing root certificate: {}", + tls_config.cert_path.display() + ) + })? + .collect::, _>>() + .with_context(|| { + format!( + "parsing root certificate: {}", + tls_config.cert_path.display() + ) + })?; let key_der = private_key(&mut BufReader::new(File::open(&tls_config.key_path)?))? .ok_or_else(|| anyhow::anyhow!("no private key found in key file"))?; diff --git a/server/src/di/inject/injectors/store/discovery.rs b/server/src/di/inject/injectors/store/discovery.rs index 2233d96..dcd4ccc 100644 --- a/server/src/di/inject/injectors/store/discovery.rs +++ b/server/src/di/inject/injectors/store/discovery.rs @@ -64,7 +64,7 @@ impl DiscoveryStoreInjector { trusted_roots, authorization, } => { - let trusted_roots = load_server_certificate(trusted_roots) + let trusted_roots = load_server_certificate(trusted_roots.as_deref()) .with_context(|| "loading server certificate for http discovery store")?; let authorization_token = std::fs::read(authorization.as_path()).with_context(|| { @@ -81,12 +81,10 @@ impl DiscoveryStoreInjector { })?; DiscoveryBackendStoreDelegate::Http( HttpDiscoveryBackendStore::create( - base_url - .parse() - .with_context(|| format!("invalid base url: {base_url}"))?, + base_url, Duration::from_secs_f64(*total_timeout), Duration::from_secs_f64(*connect_timeout), - trusted_roots, + &trusted_roots, authorization_token.to_string(), ) .with_context(|| "creating http client for discovery store")?, diff --git a/server/src/di/inject/injectors/store/offer.rs b/server/src/di/inject/injectors/store/offer.rs index 19ab844..310e9d2 100644 --- a/server/src/di/inject/injectors/store/offer.rs +++ b/server/src/di/inject/injectors/store/offer.rs @@ -61,7 +61,7 @@ impl OfferStoreInjector { trusted_roots, authorization, } => { - let trusted_roots = load_server_certificate(trusted_roots) + let trusted_roots = load_server_certificate(trusted_roots.as_deref()) .with_context(|| "loading server certificates for http offer store")?; let authorization_token = std::fs::read(authorization.as_path()).with_context(|| { @@ -78,12 +78,10 @@ impl OfferStoreInjector { })?; OfferStoreDelegate::Http( HttpOfferStore::create( - base_url - .parse() - .with_context(|| format!("parsing offer url {base_url}"))?, + base_url, Duration::from_secs_f64(*total_timeout), Duration::from_secs_f64(*connect_timeout), - trusted_roots, + &trusted_roots, authorization_token.to_string(), ) .with_context(|| "creating http client for offer store")?, diff --git a/server/src/di/inject/injectors/store/tls.rs b/server/src/di/inject/injectors/store/tls.rs index 0ee468c..0718652 100644 --- a/server/src/di/inject/injectors/store/tls.rs +++ b/server/src/di/inject/injectors/store/tls.rs @@ -1,33 +1,29 @@ -use anyhow::{anyhow, Context}; -use reqwest::Certificate; -use rustls_pemfile::certs; -use std::fs::File; -use std::io::BufReader; -use std::path::PathBuf; +use anyhow::Context; +use rustls::pki_types::pem::PemObject; +use rustls::pki_types::CertificateDer; +use std::path::Path; pub fn load_server_certificate( - server_certificate_paths: &[PathBuf], -) -> anyhow::Result> { - let mut server_certificates = Vec::new(); - for server_certificate_path in server_certificate_paths { - let server_certificate = certs(&mut BufReader::new(File::open(server_certificate_path)?)) - .filter_map(Result::ok) - .next() - .ok_or_else(|| { - anyhow!(format!( - "no certificate found in {}", - server_certificate_path.display() - )) - })?; - - let server_certificate = - reqwest::Certificate::from_der(&server_certificate).with_context(|| { + server_certificate_paths: Option<&Path>, +) -> anyhow::Result>> { + let certificates = if let Some(server_certificate_paths) = server_certificate_paths { + CertificateDer::pem_file_iter(server_certificate_paths) + .with_context(|| { + format!( + "parsing root certificate: {}", + server_certificate_paths.display() + ) + })? + .collect::, _>>() + .with_context(|| { format!( - "loading certificate from {}", - server_certificate_path.display() + "parsing root certificate: {}", + server_certificate_paths.display() ) - })?; - server_certificates.push(server_certificate); - } - Ok(server_certificates) + })? + } else { + vec![] + }; + + Ok(certificates) } diff --git a/server/src/main.rs b/server/src/main.rs index 77f39a7..4d47386 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -5,6 +5,7 @@ mod signals; use crate::commands::offer::metadata::OfferMetadataManagementCommands; use crate::commands::offer::record::OfferRecordManagementCommands; +use anyhow::anyhow; use clap::{Parser, Subcommand}; use commands::discovery::backend::DiscoveryBackendManagementCommands; use commands::discovery::DiscoveryCommands; @@ -95,6 +96,10 @@ async fn main() -> ExitCode { } async fn _main(args: CliArgs) -> anyhow::Result<()> { + rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .map_err(|_| anyhow!("failed to stand up rustls encryption platform"))?; + match args.command { RootCommands::Service { config, enablement } => { commands::services::execute(config, enablement).await diff --git a/server/tests/features/backend_create_delete.rs b/server/tests/features/backend_create_delete.rs index 6ffaa41..82beb28 100644 --- a/server/tests/features/backend_create_delete.rs +++ b/server/tests/features/backend_create_delete.rs @@ -57,10 +57,10 @@ async fn test_complete_backend_lifecycle_management() { .expect("assert"); // Register both backends individually - step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "lnd") + step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "lnd", true) .await .expect("assert"); - step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "cln") + step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "cln", true) .await .expect("assert"); @@ -90,7 +90,7 @@ async fn test_complete_backend_lifecycle_management() { .expect("assert"); // 6. When the admin creates the LND backend again - step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "lnd") + step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "lnd", true) .await .expect("assert"); diff --git a/server/tests/features/backend_enable_disable.rs b/server/tests/features/backend_enable_disable.rs index 6cc59ec..de8a4ba 100644 --- a/server/tests/features/backend_enable_disable.rs +++ b/server/tests/features/backend_enable_disable.rs @@ -52,10 +52,10 @@ async fn test_complete_backend_lifecycle_management() { .await .expect("assert"); // Register both backends individually - step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "lnd") + step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "lnd", true) .await .expect("assert"); - step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "cln") + step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "cln", true) .await .expect("assert"); diff --git a/server/tests/features/cli-discovery-manage.feature b/server/tests/features/cli-discovery-manage.feature index c2bf0ea..db96333 100644 --- a/server/tests/features/cli-discovery-manage.feature +++ b/server/tests/features/cli-discovery-manage.feature @@ -61,17 +61,23 @@ Feature: Discovery CLI management And backend list should be output @discovery-get - Scenario: Get a backend + Scenario Outline: Get a backend Given the lnurl server is ready to start When I start the lnurl server with the configuration Then the server should start successfully Given a valid backend JSON exists When I run "swgr discovery post" with backend JSON Then the command should succeed - When I run "swgr discovery get" for backend address + When I run "swgr discovery get" for backend address with Then the command should succeed And backend details should be output + Examples: + | certificate-location | + | --trusted-roots | + | env var DISCOVERY_STORE_HTTP_TRUSTED_ROOTS | + | env var SSL_CERT_FILE | + @discovery-get-all Scenario: Get all backends Given the lnurl server is ready to start diff --git a/server/tests/features/cli-offer-manage.feature b/server/tests/features/cli-offer-manage.feature index a5e790f..eee831e 100644 --- a/server/tests/features/cli-offer-manage.feature +++ b/server/tests/features/cli-offer-manage.feature @@ -32,17 +32,23 @@ Feature: Offer CLI management Then the command should succeed @offer-get - Scenario: Get an offer + Scenario Outline: Get an offer Given the lnurl server is ready to start When I start the lnurl server with the configuration Then the server should start successfully Given a valid offer JSON exists When I run "swgr offer post" with offer JSON Then the command should succeed - When I run "swgr offer get" for offer ID + When I run "swgr offer get" for offer ID with Then the command should succeed And offer details should be output + Examples: + | certificate-location | + | --trusted-roots | + | env var OFFER_STORE_HTTP_TRUSTED_ROOTS | + | env var SSL_CERT_FILE | + @offer-get-all Scenario Outline: Get all offers with parameters Given the lnurl server is ready to start @@ -125,17 +131,23 @@ Feature: Offer CLI management Then the command should succeed @offer-metadata-get - Scenario: Get offer metadata + Scenario Outline: Get offer metadata Given the lnurl server is ready to start When I start the lnurl server with the configuration Then the server should start successfully Given a valid offer metadata JSON exists When I run "swgr offer metadata post" with metadata JSON Then the command should succeed - When I run "swgr offer metadata get" for metadata ID + When I run "swgr offer metadata get" for metadata ID with Then the command should succeed And offer metadata details should be output + Examples: + | certificate-location | + | --trusted-roots | + | env var OFFER_STORE_HTTP_TRUSTED_ROOTS | + | env var SSL_CERT_FILE | + @offer-metadata-get-all Scenario Outline: Get all offer metadata with parameters Given the lnurl server is ready to start diff --git a/server/tests/features/cli_discovery_manage.rs b/server/tests/features/cli_discovery_manage.rs index 2dc9c9f..4641545 100644 --- a/server/tests/features/cli_discovery_manage.rs +++ b/server/tests/features/cli_discovery_manage.rs @@ -5,6 +5,8 @@ use crate::common::step_functions::*; use crate::FEATURE_TEST_CONFIG_PATH; use std::path::PathBuf; +use crate::common::context::server::CertificateLocation; + /// Feature: Discovery CLI management /// Scenario Outline: Generate cln-grpc backend JSON #[tokio::test] @@ -123,7 +125,7 @@ async fn test_discovery_post() { step_given_a_valid_backend_json_exists(&mut ctx, &mut cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -181,7 +183,7 @@ async fn test_discovery_post_conflict() { step_given_a_valid_backend_json_exists(&mut ctx, &mut cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -189,7 +191,7 @@ async fn test_discovery_post_conflict() { .expect("assert"); // Scenario steps - post same backend again - step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_fail(&mut cli_ctx) @@ -251,7 +253,7 @@ async fn test_discovery_ls() { .await .expect("assert"); let expected_backend = extract_backend(&cli_ctx).await.expect("assert"); - step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -273,73 +275,86 @@ async fn test_discovery_ls() { } /// Feature: Discovery CLI management -/// Scenario: Get a backend +/// Scenario Outline: Get a backend #[tokio::test] async fn test_discovery_get() { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let feature_test_config_path = manifest_dir.join(FEATURE_TEST_CONFIG_PATH); - let mut ctx = match GlobalContext::create(&feature_test_config_path).expect("assert") { - Some(ctx) => ctx, - None => return, - }; - - let server1 = "server1"; - let config_path = manifest_dir.join("config/memory-basic.yaml"); - ctx.add_server( - server1, - config_path, - Protocol::Https, - Protocol::Https, - Protocol::Https, - ) - .expect("assert"); - ctx.activate_server(server1); - - let mut cli_ctx = CliContext::create().expect("assert"); - - // Background - step_given_the_swgr_cli_is_available(&mut cli_ctx) - .await - .expect("assert"); - - // Start server - step_given_the_lnurl_server_is_ready_to_start(&mut ctx) - .await - .expect("assert"); - step_when_i_start_the_lnurl_server_with_the_configuration(&mut ctx) - .await - .expect("assert"); - step_then_the_server_should_start_successfully(&mut ctx) - .await - .expect("assert"); - step_and_all_services_should_be_listening_on_their_configured_ports(&mut ctx) - .await - .expect("assert"); - - // Setup - load backend - step_given_a_valid_backend_json_exists(&mut ctx, &mut cli_ctx) - .await - .expect("assert"); - let expected_backend = extract_backend(&cli_ctx).await.expect("assert"); - step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx) - .await - .expect("assert"); - step_then_the_command_should_succeed(&mut cli_ctx) - .await - .expect("assert"); - - // Scenario steps - step_when_i_run_swgr_discovery_get(&mut ctx, &mut cli_ctx, &expected_backend.address.encoded()) - .await - .expect("assert"); - step_then_the_command_should_succeed(&mut cli_ctx) - .await - .expect("assert"); - step_then_backend_details_should_be_output(&mut cli_ctx, &expected_backend) - .await - .expect("assert"); - - ctx.stop_all_servers().expect("assert"); + let root_locations = vec![ + CertificateLocation::Arg, + CertificateLocation::Env, + CertificateLocation::Native, + ]; + + for root_location in root_locations { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let feature_test_config_path = manifest_dir.join(FEATURE_TEST_CONFIG_PATH); + let mut ctx = match GlobalContext::create(&feature_test_config_path).expect("assert") { + Some(ctx) => ctx, + None => return, + }; + + let server1 = "server1"; + let config_path = manifest_dir.join("config/memory-basic.yaml"); + ctx.add_server( + server1, + config_path, + Protocol::Https, + Protocol::Https, + Protocol::Https, + ) + .expect("assert"); + ctx.activate_server(server1); + + let mut cli_ctx = CliContext::create().expect("assert"); + + // Background + step_given_the_swgr_cli_is_available(&mut cli_ctx) + .await + .expect("assert"); + + // Start server + step_given_the_lnurl_server_is_ready_to_start(&mut ctx) + .await + .expect("assert"); + step_when_i_start_the_lnurl_server_with_the_configuration(&mut ctx) + .await + .expect("assert"); + step_then_the_server_should_start_successfully(&mut ctx) + .await + .expect("assert"); + step_and_all_services_should_be_listening_on_their_configured_ports(&mut ctx) + .await + .expect("assert"); + + // Setup - load backend + step_given_a_valid_backend_json_exists(&mut ctx, &mut cli_ctx) + .await + .expect("assert"); + let expected_backend = extract_backend(&cli_ctx).await.expect("assert"); + step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx, root_location.clone()) + .await + .expect("assert"); + step_then_the_command_should_succeed(&mut cli_ctx) + .await + .expect("assert"); + + // Scenario steps + step_when_i_run_swgr_discovery_get( + &mut ctx, + &mut cli_ctx, + &expected_backend.address.encoded(), + root_location, + ) + .await + .expect("assert"); + step_then_the_command_should_succeed(&mut cli_ctx) + .await + .expect("assert"); + step_then_backend_details_should_be_output(&mut cli_ctx, &expected_backend) + .await + .expect("assert"); + + ctx.stop_all_servers().expect("assert"); + } } /// Feature: Discovery CLI management @@ -391,7 +406,7 @@ async fn test_discovery_get_all() { .await .expect("assert"); let expected_backend = extract_backend(&cli_ctx).await.expect("assert"); - step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -461,7 +476,7 @@ async fn test_discovery_put() { .await .expect("assert"); let backend_address = extract_backend_address(&cli_ctx).await.expect("assert"); - step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -478,9 +493,14 @@ async fn test_discovery_put() { step_then_the_command_should_succeed(&mut cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_discovery_get(&mut ctx, &mut cli_ctx, &backend_address) - .await - .expect("assert"); + step_when_i_run_swgr_discovery_get( + &mut ctx, + &mut cli_ctx, + &backend_address, + CertificateLocation::Arg, + ) + .await + .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) .await .expect("assert"); @@ -540,7 +560,7 @@ async fn test_discovery_delete() { .await .expect("assert"); let backend_address = extract_backend_address(&cli_ctx).await.expect("assert"); - step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -554,9 +574,14 @@ async fn test_discovery_delete() { step_then_the_command_should_succeed(&mut cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_discovery_get(&mut ctx, &mut cli_ctx, &backend_address) - .await - .expect("assert"); + step_when_i_run_swgr_discovery_get( + &mut ctx, + &mut cli_ctx, + &backend_address, + CertificateLocation::Arg, + ) + .await + .expect("assert"); step_then_the_command_should_fail(&mut cli_ctx) .await .expect("assert"); @@ -616,7 +641,7 @@ async fn test_discovery_patch() { .await .expect("assert"); let backend_address = extract_backend_address(&cli_ctx).await.expect("assert"); - step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -633,9 +658,14 @@ async fn test_discovery_patch() { step_then_the_command_should_succeed(&mut cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_discovery_get(&mut ctx, &mut cli_ctx, &backend_address) - .await - .expect("assert"); + step_when_i_run_swgr_discovery_get( + &mut ctx, + &mut cli_ctx, + &backend_address, + CertificateLocation::Arg, + ) + .await + .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) .await .expect("assert"); @@ -695,7 +725,7 @@ async fn test_discovery_enable() { .await .expect("assert"); let backend_address = extract_backend_address(&cli_ctx).await.expect("assert"); - step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -715,9 +745,14 @@ async fn test_discovery_enable() { step_then_the_command_should_succeed(&mut cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_discovery_get(&mut ctx, &mut cli_ctx, &backend_address) - .await - .expect("assert"); + step_when_i_run_swgr_discovery_get( + &mut ctx, + &mut cli_ctx, + &backend_address, + CertificateLocation::Arg, + ) + .await + .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) .await .expect("assert"); @@ -777,7 +812,7 @@ async fn test_discovery_disable() { .await .expect("assert"); let backend_address = extract_backend_address(&cli_ctx).await.expect("assert"); - step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_discovery_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -791,9 +826,14 @@ async fn test_discovery_disable() { step_then_the_command_should_succeed(&mut cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_discovery_get(&mut ctx, &mut cli_ctx, &backend_address) - .await - .expect("assert"); + step_when_i_run_swgr_discovery_get( + &mut ctx, + &mut cli_ctx, + &backend_address, + CertificateLocation::Arg, + ) + .await + .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) .await .expect("assert"); diff --git a/server/tests/features/cli_offer_manage.rs b/server/tests/features/cli_offer_manage.rs index 22780a5..ec491cb 100644 --- a/server/tests/features/cli_offer_manage.rs +++ b/server/tests/features/cli_offer_manage.rs @@ -5,6 +5,8 @@ use crate::common::step_functions::*; use crate::FEATURE_TEST_CONFIG_PATH; use std::path::PathBuf; +use crate::common::context::server::CertificateLocation; + /// Feature: Offer CLI management /// Scenario: Generate offer JSON #[tokio::test] @@ -99,7 +101,7 @@ async fn test_offer_post() { step_given_a_valid_offer_json_exists(&mut ctx, &mut cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -110,73 +112,87 @@ async fn test_offer_post() { } /// Feature: Offer CLI management -/// Scenario: Get an offer +/// Scenario Outline: Get an offer #[tokio::test] async fn test_offer_get() { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let feature_test_config_path = manifest_dir.join(FEATURE_TEST_CONFIG_PATH); - let mut ctx = match GlobalContext::create(&feature_test_config_path).expect("assert") { - Some(ctx) => ctx, - None => return, - }; + // Test all three root certificate location methods + let certificate_locations = vec![ + CertificateLocation::Arg, // --trusted-roots CLI flag + CertificateLocation::Env, // DISCOVERY_STORE_HTTP_TRUSTED_ROOTS env var + CertificateLocation::Native, // SSL_CERT_FILE env var + ]; - let server1 = "server1"; - let config_path = manifest_dir.join("config/memory-basic.yaml"); - ctx.add_server( - server1, - config_path, - Protocol::Https, - Protocol::Https, - Protocol::Https, - ) - .expect("assert"); - ctx.activate_server(server1); + for certificate_location in certificate_locations { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let feature_test_config_path = manifest_dir.join(FEATURE_TEST_CONFIG_PATH); + let mut ctx = match GlobalContext::create(&feature_test_config_path).expect("assert") { + Some(ctx) => ctx, + None => return, + }; + + let server1 = "server1"; + let config_path = manifest_dir.join("config/memory-basic.yaml"); + ctx.add_server( + server1, + config_path, + Protocol::Https, + Protocol::Https, + Protocol::Https, + ) + .expect("assert"); + ctx.activate_server(server1); - let mut cli_ctx = CliContext::create().expect("assert"); + let mut cli_ctx = CliContext::create().expect("assert"); - // Background - step_given_the_swgr_cli_is_available(&mut cli_ctx) - .await - .expect("assert"); + // Background + step_given_the_swgr_cli_is_available(&mut cli_ctx) + .await + .expect("assert"); - // Start server - step_given_the_lnurl_server_is_ready_to_start(&mut ctx) - .await - .expect("assert"); - step_when_i_start_the_lnurl_server_with_the_configuration(&mut ctx) - .await - .expect("assert"); - step_then_the_server_should_start_successfully(&mut ctx) - .await - .expect("assert"); - step_and_all_services_should_be_listening_on_their_configured_ports(&mut ctx) - .await - .expect("assert"); + // Start server + step_given_the_lnurl_server_is_ready_to_start(&mut ctx) + .await + .expect("assert"); + step_when_i_start_the_lnurl_server_with_the_configuration(&mut ctx) + .await + .expect("assert"); + step_then_the_server_should_start_successfully(&mut ctx) + .await + .expect("assert"); + step_and_all_services_should_be_listening_on_their_configured_ports(&mut ctx) + .await + .expect("assert"); - // Setup - load offer - step_given_a_valid_offer_json_exists(&mut ctx, &mut cli_ctx) - .await - .expect("assert"); - let expected_offer = extract_offer(&cli_ctx).await.expect("assert"); - step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx) - .await - .expect("assert"); - step_then_the_command_should_succeed(&mut cli_ctx) - .await - .expect("assert"); + // Setup - load offer + step_given_a_valid_offer_json_exists(&mut ctx, &mut cli_ctx) + .await + .expect("assert"); + let expected_offer = extract_offer(&cli_ctx).await.expect("assert"); + step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx, certificate_location.clone()) + .await + .expect("assert"); + step_then_the_command_should_succeed(&mut cli_ctx) + .await + .expect("assert"); - // Scenario steps - step_when_i_run_swgr_offer_get(&mut ctx, &mut cli_ctx, &expected_offer.id) - .await - .expect("assert"); - step_then_the_command_should_succeed(&mut cli_ctx) - .await - .expect("assert"); - step_then_offer_details_should_be_output(&mut cli_ctx, &expected_offer) + // Scenario steps + step_when_i_run_swgr_offer_get( + &mut ctx, + &mut cli_ctx, + &expected_offer.id, + certificate_location, + ) .await .expect("assert"); + step_then_the_command_should_succeed(&mut cli_ctx) + .await + .expect("assert"); + step_then_offer_details_should_be_output(&mut cli_ctx, &expected_offer) + .await + .expect("assert"); - ctx.stop_all_servers().expect("assert"); + ctx.stop_all_servers().expect("assert"); + } } /// Feature: Offer CLI management @@ -232,7 +248,7 @@ async fn test_offer_get_all() { let offer = extract_offer(&cli_ctx).await.expect("assert"); expected_offers.push(offer); - step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -323,7 +339,7 @@ async fn test_offer_get_all_bounds_error() { step_given_a_valid_offer_json_exists(&mut ctx, &mut cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -394,7 +410,7 @@ async fn test_offer_put() { .await .expect("assert"); let offer_id = extract_offer_id(&cli_ctx).await.expect("assert"); - step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -411,7 +427,7 @@ async fn test_offer_put() { step_then_the_command_should_succeed(&mut cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_offer_get(&mut ctx, &mut cli_ctx, &offer_id) + step_when_i_run_swgr_offer_get(&mut ctx, &mut cli_ctx, &offer_id, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -473,7 +489,7 @@ async fn test_offer_delete() { .await .expect("assert"); let offer_id = extract_offer_id(&cli_ctx).await.expect("assert"); - step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -487,7 +503,7 @@ async fn test_offer_delete() { step_then_the_command_should_succeed(&mut cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_offer_get(&mut ctx, &mut cli_ctx, &offer_id) + step_when_i_run_swgr_offer_get(&mut ctx, &mut cli_ctx, &offer_id, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_fail(&mut cli_ctx) @@ -594,7 +610,7 @@ async fn test_offer_metadata_post() { step_given_a_valid_offer_metadata_json_exists(&mut ctx, &mut cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_offer_metadata_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_offer_metadata_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -605,73 +621,91 @@ async fn test_offer_metadata_post() { } /// Feature: Offer CLI management -/// Scenario: Get offer metadata +/// Scenario Outline: Get offer metadata #[tokio::test] async fn test_offer_metadata_get() { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let feature_test_config_path = manifest_dir.join(FEATURE_TEST_CONFIG_PATH); - let mut ctx = match GlobalContext::create(&feature_test_config_path).expect("assert") { - Some(ctx) => ctx, - None => return, - }; + // Test all three root certificate location methods + let certificate_locations = vec![ + CertificateLocation::Arg, // --trusted-roots CLI flag + CertificateLocation::Env, // DISCOVERY_STORE_HTTP_TRUSTED_ROOTS env var + CertificateLocation::Native, // SSL_CERT_FILE env var + ]; - let server1 = "server1"; - let config_path = manifest_dir.join("config/memory-basic.yaml"); - ctx.add_server( - server1, - config_path, - Protocol::Https, - Protocol::Https, - Protocol::Https, - ) - .expect("assert"); - ctx.activate_server(server1); + for certificate_location in certificate_locations { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let feature_test_config_path = manifest_dir.join(FEATURE_TEST_CONFIG_PATH); + let mut ctx = match GlobalContext::create(&feature_test_config_path).expect("assert") { + Some(ctx) => ctx, + None => return, + }; + + let server1 = "server1"; + let config_path = manifest_dir.join("config/memory-basic.yaml"); + ctx.add_server( + server1, + config_path, + Protocol::Https, + Protocol::Https, + Protocol::Https, + ) + .expect("assert"); + ctx.activate_server(server1); - let mut cli_ctx = CliContext::create().expect("assert"); + let mut cli_ctx = CliContext::create().expect("assert"); - // Background - step_given_the_swgr_cli_is_available(&mut cli_ctx) - .await - .expect("assert"); + // Background + step_given_the_swgr_cli_is_available(&mut cli_ctx) + .await + .expect("assert"); - // Start server - step_given_the_lnurl_server_is_ready_to_start(&mut ctx) - .await - .expect("assert"); - step_when_i_start_the_lnurl_server_with_the_configuration(&mut ctx) - .await - .expect("assert"); - step_then_the_server_should_start_successfully(&mut ctx) - .await - .expect("assert"); - step_and_all_services_should_be_listening_on_their_configured_ports(&mut ctx) - .await - .expect("assert"); + // Start server + step_given_the_lnurl_server_is_ready_to_start(&mut ctx) + .await + .expect("assert"); + step_when_i_start_the_lnurl_server_with_the_configuration(&mut ctx) + .await + .expect("assert"); + step_then_the_server_should_start_successfully(&mut ctx) + .await + .expect("assert"); + step_and_all_services_should_be_listening_on_their_configured_ports(&mut ctx) + .await + .expect("assert"); - // Setup - load metadata - step_given_a_valid_offer_metadata_json_exists(&mut ctx, &mut cli_ctx) - .await - .expect("assert"); - let expected_metadata = extract_metadata(&cli_ctx).await.expect("assert"); - step_when_i_run_swgr_offer_metadata_post(&mut ctx, &mut cli_ctx) - .await - .expect("assert"); - step_then_the_command_should_succeed(&mut cli_ctx) + // Setup - load metadata + step_given_a_valid_offer_metadata_json_exists(&mut ctx, &mut cli_ctx) + .await + .expect("assert"); + let expected_metadata = extract_metadata(&cli_ctx).await.expect("assert"); + step_when_i_run_swgr_offer_metadata_post( + &mut ctx, + &mut cli_ctx, + certificate_location.clone(), + ) .await .expect("assert"); + step_then_the_command_should_succeed(&mut cli_ctx) + .await + .expect("assert"); - // Scenario steps - step_when_i_run_swgr_offer_metadata_get(&mut ctx, &mut cli_ctx, &expected_metadata.id) - .await - .expect("assert"); - step_then_the_command_should_succeed(&mut cli_ctx) - .await - .expect("assert"); - step_then_offer_metadata_details_should_be_output(&mut cli_ctx, &expected_metadata) + // Scenario steps + step_when_i_run_swgr_offer_metadata_get( + &mut ctx, + &mut cli_ctx, + &expected_metadata.id, + certificate_location, + ) .await .expect("assert"); + step_then_the_command_should_succeed(&mut cli_ctx) + .await + .expect("assert"); + step_then_offer_metadata_details_should_be_output(&mut cli_ctx, &expected_metadata) + .await + .expect("assert"); - ctx.stop_all_servers().expect("assert"); + ctx.stop_all_servers().expect("assert"); + } } /// Feature: Offer CLI management @@ -728,7 +762,7 @@ async fn test_offer_metadata_get_all() { let metadata = extract_metadata(&cli_ctx).await.expect("assert"); expected_metadata.push(metadata); - step_when_i_run_swgr_offer_metadata_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_offer_metadata_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -819,7 +853,7 @@ async fn test_offer_metadata_get_all_bounds_error() { step_given_a_valid_offer_metadata_json_exists(&mut ctx, &mut cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_offer_metadata_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_offer_metadata_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -890,7 +924,7 @@ async fn test_offer_metadata_put() { .await .expect("assert"); let metadata_id = extract_metadata_id(&cli_ctx).await.expect("assert"); - step_when_i_run_swgr_offer_metadata_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_offer_metadata_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -907,9 +941,14 @@ async fn test_offer_metadata_put() { step_then_the_command_should_succeed(&mut cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_offer_metadata_get(&mut ctx, &mut cli_ctx, &metadata_id) - .await - .expect("assert"); + step_when_i_run_swgr_offer_metadata_get( + &mut ctx, + &mut cli_ctx, + &metadata_id, + CertificateLocation::Arg, + ) + .await + .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) .await .expect("assert"); @@ -969,7 +1008,7 @@ async fn test_offer_metadata_delete() { .await .expect("assert"); let metadata_id = extract_metadata_id(&cli_ctx).await.expect("assert"); - step_when_i_run_swgr_offer_metadata_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_offer_metadata_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -983,9 +1022,14 @@ async fn test_offer_metadata_delete() { step_then_the_command_should_succeed(&mut cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_offer_metadata_get(&mut ctx, &mut cli_ctx, &metadata_id) - .await - .expect("assert"); + step_when_i_run_swgr_offer_metadata_get( + &mut ctx, + &mut cli_ctx, + &metadata_id, + CertificateLocation::Arg, + ) + .await + .expect("assert"); step_then_the_command_should_fail(&mut cli_ctx) .await .expect("assert"); @@ -1044,7 +1088,7 @@ async fn test_offer_post_invalid_metadata() { step_given_an_offer_json_with_non_existent_metadata_id_exists(&mut ctx, &mut cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_fail(&mut cli_ctx) @@ -1108,7 +1152,7 @@ async fn test_offer_metadata_delete_referenced() { let metadata_id = extract_metadata_id_from_offer(&cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -1291,7 +1335,7 @@ async fn test_offer_post_conflict() { step_given_a_valid_offer_json_exists(&mut ctx, &mut cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -1299,7 +1343,7 @@ async fn test_offer_post_conflict() { .expect("assert"); // Scenario steps - post same offer again - step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_offer_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_fail(&mut cli_ctx) @@ -1476,7 +1520,7 @@ async fn test_offer_metadata_post_conflict() { step_given_a_valid_offer_metadata_json_exists(&mut ctx, &mut cli_ctx) .await .expect("assert"); - step_when_i_run_swgr_offer_metadata_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_offer_metadata_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_succeed(&mut cli_ctx) @@ -1484,7 +1528,7 @@ async fn test_offer_metadata_post_conflict() { .expect("assert"); // Scenario steps - post same metadata again - step_when_i_run_swgr_offer_metadata_post(&mut ctx, &mut cli_ctx) + step_when_i_run_swgr_offer_metadata_post(&mut ctx, &mut cli_ctx, CertificateLocation::Arg) .await .expect("assert"); step_then_the_command_should_fail(&mut cli_ctx) diff --git a/server/tests/features/common/context/global.rs b/server/tests/features/common/context/global.rs index 476437a..dae3862 100644 --- a/server/tests/features/common/context/global.rs +++ b/server/tests/features/common/context/global.rs @@ -1,7 +1,7 @@ use crate::common::client::{LnUrlTestClient, TcpProbe}; use crate::common::context::certs::gen_root_cert; use crate::common::context::pay::{OfferRequest, PayeeContext}; -use crate::common::context::server::ServerContext; +use crate::common::context::server::{CertificateLocation, ServerContext}; use crate::common::context::{Protocol, TestConfiguration, TestConfigurationServiceDomains}; use crate::common::context::{Service, ServiceProfile}; use anyhow::{anyhow, Context}; @@ -11,7 +11,7 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use switchgear_service::components::discovery::http::HttpDiscoveryBackendStore; use switchgear_service::components::offer::http::HttpOfferStore; -use switchgear_testing::credentials::{ +use switchgear_testing::credentials::lightning::{ ClnRegTestLnNode, LnCredentials, LndRegTestLnNode, RegTestLnNode, }; use tempfile::TempDir; @@ -31,6 +31,10 @@ pub struct GlobalContext { impl GlobalContext { pub fn create(feature_test_config_path: &Path) -> anyhow::Result> { + let _ = rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .map_err(|_| anyhow!("failed to stand up rustls encryption platform")); + let credentials = LnCredentials::create()?; let ln_nodes = credentials.get_backends()?; if ln_nodes.is_empty() { @@ -317,6 +321,21 @@ impl GlobalContext { Ok(()) } + pub fn set_discovery_store_database_url( + &mut self, + server_key_id: &str, + database_url: String, + ) -> anyhow::Result<()> { + let server = self + .servers + .get_mut(server_key_id) + .ok_or_else(|| anyhow!("server not found"))?; + + server.set_discovery_store_database_url(database_url); + + Ok(()) + } + pub fn set_discovery_store_authorization( &mut self, service_server_key_id: &str, @@ -363,6 +382,21 @@ impl GlobalContext { Ok(()) } + pub fn set_certificate_location( + &mut self, + server_key_id: &str, + certificate_location: CertificateLocation, + ) -> anyhow::Result<()> { + let server = self + .servers + .get_mut(server_key_id) + .ok_or_else(|| anyhow!("server not found"))?; + + server.set_certificate_location(certificate_location); + + Ok(()) + } + pub fn set_offer_store_url( &mut self, service_server_key_id: &str, @@ -384,6 +418,36 @@ impl GlobalContext { Ok(()) } + pub fn set_offer_store_database_url( + &mut self, + server_key_id: &str, + database_url: String, + ) -> anyhow::Result<()> { + let server = self + .servers + .get_mut(server_key_id) + .ok_or_else(|| anyhow!("server not found"))?; + + server.set_offer_store_database_url(database_url); + + Ok(()) + } + + pub fn set_ln_trusted_roots_path( + &mut self, + server_key_id: &str, + ln_trusted_roots_path: Option, + ) -> anyhow::Result<()> { + let server = self + .servers + .get_mut(server_key_id) + .ok_or_else(|| anyhow!("server not found"))?; + + server.set_ln_trusted_roots_path(ln_trusted_roots_path); + + Ok(()) + } + fn get_service_url(profile: ServiceProfile) -> String { format!( "{}://{}:{}", diff --git a/server/tests/features/common/context/pay.rs b/server/tests/features/common/context/pay.rs index 6d40777..74668f4 100644 --- a/server/tests/features/common/context/pay.rs +++ b/server/tests/features/common/context/pay.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use switchgear_service::api::lnurl::LnUrlOffer; -use switchgear_testing::credentials::RegTestLnNode; +use switchgear_testing::credentials::lightning::RegTestLnNode; use uuid::Uuid; #[derive(Clone)] diff --git a/server/tests/features/common/context/server.rs b/server/tests/features/common/context/server.rs index a2ec4e7..034da4c 100644 --- a/server/tests/features/common/context/server.rs +++ b/server/tests/features/common/context/server.rs @@ -6,7 +6,10 @@ use crate::common::context::{ DiscoveryServiceConfigOverride, LnUrlBalancerServiceConfigOverride, OfferServiceConfigOverride, ServerConfigOverrides, Service, ServiceProfile, }; +use anyhow::{bail, Context}; use rcgen::{Issuer, KeyPair}; +use rustls::pki_types::pem::PemObject; +use rustls::pki_types::CertificateDer; use std::path::{Path, PathBuf}; use std::process::{Child, Command}; use std::sync::{Arc, Mutex}; @@ -16,6 +19,14 @@ use switchgear_service::components::discovery::http::HttpDiscoveryBackendStore; use switchgear_service::components::offer::http::HttpOfferStore; use uuid::Uuid; +#[derive(Debug, Clone)] +pub enum CertificateLocation { + Arg, + Env, + Native, + NativePath(String), +} + pub struct ServerContext { id: Uuid, config_path: PathBuf, @@ -32,9 +43,14 @@ pub struct ServerContext { offer_store_url: Option, discovery_store_url: Option, + offer_store_database_url: String, + discovery_store_database_url: String, + discovery_store_authorization: Option, offer_store_authorization: Option, + certificate_location: CertificateLocation, + lnurl_client: LnUrlTestClient, discovery_client: HttpDiscoveryBackendStore, offer_client: HttpOfferStore, @@ -50,6 +66,8 @@ pub struct ServerContext { offer_probe: TcpProbe, server_config_overrides: ServerConfigOverrides, + + ln_trusted_roots_path: Option, } impl ServerContext { @@ -143,8 +161,8 @@ impl ServerContext { Ok(Self { id, config_path, - discovery_store_dir, - offer_store_dir, + discovery_store_dir: discovery_store_dir.clone(), + offer_store_dir: offer_store_dir.clone(), pki_root_certificate_path, server_process: None, exit_code: -1, @@ -153,9 +171,21 @@ impl ServerContext { offer_store_url: None, discovery_store_url: None, + discovery_store_database_url: format!( + "sqlite://{}?mode=rwc", + discovery_store_dir.join("discovery.db").to_string_lossy() + ), + + offer_store_database_url: format!( + "sqlite://{}?mode=rwc", + offer_store_dir.join("offers.db").to_string_lossy() + ), + discovery_store_authorization: None, offer_store_authorization: None, + certificate_location: CertificateLocation::Env, + lnurl_client, lnurl_probe: TcpProbe::new(lnurl_service_profile.address, Duration::from_millis(500)), @@ -210,6 +240,7 @@ impl ServerContext { domain: offer_service_profile.domain, }, }, + ln_trusted_roots_path: None, }) } @@ -249,22 +280,28 @@ impl ServerContext { match service_profile.protocol { Protocol::Https => { - let cert_data = std::fs::read(root_certificate)?; - let cert = reqwest::Certificate::from_pem(&cert_data)?; + let certs = CertificateDer::pem_file_iter(root_certificate) + .with_context(|| { + format!("parsing root certificate: {}", root_certificate.display()) + })? + .collect::, _>>() + .with_context(|| { + format!("parsing root certificate: {}", root_certificate.display()) + })?; Ok(HttpDiscoveryBackendStore::create( - url.parse()?, + url, Duration::from_secs(10), Duration::from_secs(10), - vec![cert], + &certs, authorization, )?) } Protocol::Http => Ok(HttpDiscoveryBackendStore::create( - url.parse()?, + url, Duration::from_secs(10), Duration::from_secs(10), - vec![], + &[], authorization, )?), } @@ -279,22 +316,28 @@ impl ServerContext { match service_profile.protocol { Protocol::Https => { - let cert_data = std::fs::read(root_certificate)?; - let cert = reqwest::Certificate::from_pem(&cert_data)?; + let certs = CertificateDer::pem_file_iter(root_certificate) + .with_context(|| { + format!("parsing root certificate: {}", root_certificate.display()) + })? + .collect::, _>>() + .with_context(|| { + format!("parsing root certificate: {}", root_certificate.display()) + })?; Ok(HttpOfferStore::create( - url.parse()?, + url, Duration::from_secs(10), Duration::from_secs(10), - vec![cert], + &certs, authorization, )?) } Protocol::Http => Ok(HttpOfferStore::create( - url.parse()?, + url, Duration::from_secs(10), Duration::from_secs(10), - vec![], + &[], authorization, )?), } @@ -391,31 +434,37 @@ impl ServerContext { .env("RUST_LOG", rust_log) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) - .env("OFFER_SERVICE_ADDRESS", offers_svc.address.to_string()) - .env( - "DISCOVERY_STORE_HTTP_TRUSTED_ROOTS", - &self.pki_root_certificate_path, - ) - .env( - "OFFER_STORE_HTTP_TRUSTED_ROOTS", - &self.pki_root_certificate_path, - ) + .env("OFFER_SERVICE_ADDRESS", offers_svc.address.to_string()); + + match &self.certificate_location { + CertificateLocation::Env => { + command + .env( + "DISCOVERY_STORE_HTTP_TRUSTED_ROOTS", + &self.pki_root_certificate_path, + ) + .env( + "OFFER_STORE_HTTP_TRUSTED_ROOTS", + &self.pki_root_certificate_path, + ); + } + CertificateLocation::Native => { + command.env("SSL_CERT_FILE", &self.pki_root_certificate_path); + } + CertificateLocation::NativePath(path) => { + command.env("SSL_CERT_FILE", path); + } + CertificateLocation::Arg => { + bail!("not supported: server cannot be configured with cli arguments for trusted root locations") + } + } + + command .env( "DISCOVERY_STORE_DATABASE_URL", - format!( - "sqlite://{}?mode=rwc", - self.discovery_store_dir - .join("discovery.db") - .to_string_lossy() - ), - ) - .env( - "OFFER_STORE_DATABASE_URL", - format!( - "sqlite://{}?mode=rwc", - self.offer_store_dir.join("offers.db").to_string_lossy() - ), + &self.discovery_store_database_url, ) + .env("OFFER_STORE_DATABASE_URL", &self.offer_store_database_url) .env( "DISCOVERY_SERVICE_AUTH_AUTHORITY_PATH", &self.discovery_authority, @@ -462,6 +511,9 @@ impl ServerContext { if let Some(offer_store_authorization) = &self.offer_store_authorization { command.env("OFFER_STORE_HTTP_AUTHORIZATION", offer_store_authorization); } + if let Some(ln_trusted_roots_path) = &self.ln_trusted_roots_path { + command.env("LN_TRUSTED_ROOTS", ln_trusted_roots_path); + } if has_rust_log { println!("[STDOUT] Executing command: {command:?}"); let lnurl_profile = self.get_service_profile(Service::LnUrl)?; @@ -636,4 +688,20 @@ impl ServerContext { pub fn offer_authorization(&self) -> &Path { &self.offer_authorization } + + pub fn set_certificate_location(&mut self, certificate_location: CertificateLocation) { + self.certificate_location = certificate_location; + } + + pub fn set_offer_store_database_url(&mut self, offer_store_database_url: String) { + self.offer_store_database_url = offer_store_database_url; + } + + pub fn set_discovery_store_database_url(&mut self, discovery_store_database_url: String) { + self.discovery_store_database_url = discovery_store_database_url; + } + + pub fn set_ln_trusted_roots_path(&mut self, ln_trusted_roots_path: Option) { + self.ln_trusted_roots_path = ln_trusted_roots_path; + } } diff --git a/server/tests/features/common/step_functions.rs b/server/tests/features/common/step_functions.rs index 9c9bf60..6574ced 100644 --- a/server/tests/features/common/step_functions.rs +++ b/server/tests/features/common/step_functions.rs @@ -1,6 +1,7 @@ use crate::common::context::cli::CliContext; use crate::common::context::global::GlobalContext; use crate::common::context::pay::OfferRequest; +use crate::common::context::server::CertificateLocation; use crate::common::context::{Protocol, Service}; use crate::common::helpers::{ check_all_services_health, check_services_listening_status, count_log_patterns, @@ -32,9 +33,10 @@ use switchgear_service::components::pool::cln::grpc::config::{ use switchgear_service::components::pool::lnd::grpc::config::{ LndGrpcClientAuth, LndGrpcClientAuthPath, LndGrpcDiscoveryBackendImplementation, }; -use switchgear_testing::credentials::{RegTestLnNode, RegTestLnNodeType}; +use switchgear_testing::credentials::lightning::{RegTestLnNode, RegTestLnNodeType}; use tokio::time::sleep as tokio_sleep; use uuid::Uuid; + // ============================================================================= // STEP FUNCTIONS - Mapped to Gherkin steps in feature files // ============================================================================= @@ -411,6 +413,7 @@ pub async fn step_when_the_payee_creates_an_offer_for_their_lightning_node( pub async fn step_when_the_payee_registers_their_lightning_node_as_a_backend( ctx: &mut GlobalContext, payee_id: &str, + include_ca: bool, ) -> Result<()> { // Use the selected node from context let payee = get_payee_from_context(ctx, payee_id)?; @@ -427,18 +430,26 @@ pub async fn step_when_the_payee_registers_their_lightning_node_as_a_backend( DiscoveryBackendImplementation::ClnGrpc(ClnGrpcDiscoveryBackendImplementation { url, auth: ClnGrpcClientAuth::Path(ClnGrpcClientAuthPath { - ca_cert_path: cln.ca_cert_path.clone(), + ca_cert_path: if include_ca { + cln.ca_cert_path.clone().into() + } else { + None + }, client_cert_path: cln.client_cert_path.clone(), client_key_path: cln.client_key_path.clone(), }), - domain: Some(cln.sni.clone()), + domain: None, }) } RegTestLnNode::Lnd(lnd) => { DiscoveryBackendImplementation::LndGrpc(LndGrpcDiscoveryBackendImplementation { url, auth: LndGrpcClientAuth::Path(LndGrpcClientAuthPath { - tls_cert_path: lnd.tls_cert_path.clone(), + tls_cert_path: if include_ca { + lnd.tls_cert_path.clone().into() + } else { + None + }, macaroon_path: lnd.macaroon_path.clone(), }), amp_invoice: false, @@ -697,18 +708,18 @@ pub async fn register_payee_node_as_backend(ctx: &mut GlobalContext, payee_id: & DiscoveryBackendImplementation::ClnGrpc(ClnGrpcDiscoveryBackendImplementation { url, auth: ClnGrpcClientAuth::Path(ClnGrpcClientAuthPath { - ca_cert_path: cln.ca_cert_path.clone(), + ca_cert_path: cln.ca_cert_path.clone().into(), client_cert_path: cln.client_cert_path.clone(), client_key_path: cln.client_key_path.clone(), }), - domain: Some(cln.sni.clone()), + domain: None, }) } RegTestLnNode::Lnd(lnd) => { DiscoveryBackendImplementation::LndGrpc(LndGrpcDiscoveryBackendImplementation { url, auth: LndGrpcClientAuth::Path(LndGrpcClientAuthPath { - tls_cert_path: lnd.tls_cert_path.clone(), + tls_cert_path: lnd.tls_cert_path.clone().into(), macaroon_path: lnd.macaroon_path.clone(), }), amp_invoice: false, @@ -2573,6 +2584,7 @@ pub async fn step_given_a_valid_backend_json_exists( pub async fn step_when_i_run_swgr_discovery_post( ctx: &mut GlobalContext, cli_ctx: &mut CliContext, + root_location: CertificateLocation, ) -> Result<()> { let discovery_profile = ctx.get_active_discovery_service_profile()?; let base_url = format!( @@ -2591,21 +2603,32 @@ pub async fn step_when_i_run_swgr_discovery_post( let authorization = ctx.get_active_discovery_authorization()?; let authorization_str = authorization.to_str().unwrap().to_string(); - let args = vec![ - "discovery", - "post", - "--base-url", - &base_url, - "--trusted-roots", - &trusted_roots_str, - "--authorization-path", - &authorization_str, - "--input", - &backend_json_path, - ]; + let mut args = vec!["discovery", "post", "--base-url", &base_url]; - let empty_env: Vec<(&str, &str)> = vec![]; - cli_ctx.command(empty_env, args)?; + // Configure root certificate location + let mut env: Vec<(&str, &str)> = vec![]; + match &root_location { + CertificateLocation::Arg => { + args.push("--trusted-roots"); + args.push(&trusted_roots_str); + } + CertificateLocation::Env => { + env.push(("DISCOVERY_STORE_HTTP_TRUSTED_ROOTS", &trusted_roots_str)); + } + CertificateLocation::Native => { + env.push(("SSL_CERT_FILE", &trusted_roots_str)); + } + CertificateLocation::NativePath(path) => { + env.push(("SSL_CERT_FILE", path)); + } + } + + args.push("--authorization-path"); + args.push(&authorization_str); + args.push("--input"); + args.push(&backend_json_path); + + cli_ctx.command(env, args)?; Ok(()) } @@ -2740,6 +2763,7 @@ pub async fn step_when_i_run_swgr_discovery_get( ctx: &mut GlobalContext, cli_ctx: &mut CliContext, backend_address: &str, + root_location: CertificateLocation, ) -> Result<()> { let discovery_profile = ctx.get_active_discovery_service_profile()?; let base_url = format!( @@ -2757,20 +2781,30 @@ pub async fn step_when_i_run_swgr_discovery_get( let authorization = ctx.get_active_discovery_authorization()?; let authorization_str = authorization.to_str().unwrap().to_string(); - let args = vec![ - "discovery", - "get", - backend_address, - "--base-url", - &base_url, - "--trusted-roots", - &trusted_roots_str, - "--authorization-path", - &authorization_str, - ]; + let mut args = vec!["discovery", "get", backend_address, "--base-url", &base_url]; - let empty_env: Vec<(&str, &str)> = vec![]; - cli_ctx.command(empty_env, args)?; + // Configure root certificate location + let mut env: Vec<(&str, &str)> = vec![]; + match &root_location { + CertificateLocation::Arg => { + args.push("--trusted-roots"); + args.push(&trusted_roots_str); + } + CertificateLocation::Env => { + env.push(("DISCOVERY_STORE_HTTP_TRUSTED_ROOTS", &trusted_roots_str)); + } + CertificateLocation::Native => { + env.push(("SSL_CERT_FILE", &trusted_roots_str)); + } + CertificateLocation::NativePath(path) => { + env.push(("SSL_CERT_FILE", path)); + } + } + + args.push("--authorization-path"); + args.push(&authorization_str); + + cli_ctx.command(env, args)?; Ok(()) } @@ -3620,6 +3654,7 @@ pub async fn step_given_a_valid_offer_json_exists( pub async fn step_when_i_run_swgr_offer_post( ctx: &mut GlobalContext, cli_ctx: &mut CliContext, + certificate_location: CertificateLocation, ) -> Result<()> { let service_profile = ctx.get_active_offer_service_profile()?; let protocol = service_profile.protocol; @@ -3635,21 +3670,37 @@ pub async fn step_when_i_run_swgr_offer_post( let input_path = cli_ctx.offer_json_path.to_str().unwrap().to_string(); - let args = vec![ + let mut args = vec![ "offer", "post", "--base-url", &base_url, "--authorization-path", &authorization_str, - "--trusted-roots", - &ca_bundle_str, - "--input", - &input_path, ]; - let empty_env: Vec<(&str, &str)> = vec![]; - cli_ctx.command(empty_env, args)?; + // Configure root certificate location + let mut env: Vec<(&str, &str)> = vec![]; + match &certificate_location { + CertificateLocation::Arg => { + args.push("--trusted-roots"); + args.push(&ca_bundle_str); + } + CertificateLocation::Env => { + env.push(("OFFER_STORE_HTTP_TRUSTED_ROOTS", &ca_bundle_str)); + } + CertificateLocation::Native => { + env.push(("SSL_CERT_FILE", &ca_bundle_str)); + } + CertificateLocation::NativePath(path) => { + env.push(("SSL_CERT_FILE", path)); + } + } + + args.push("--input"); + args.push(&input_path); + + cli_ctx.command(env, args)?; Ok(()) } @@ -3659,6 +3710,7 @@ pub async fn step_when_i_run_swgr_offer_get( ctx: &mut GlobalContext, cli_ctx: &mut CliContext, offer_id: &Uuid, + certificate_location: CertificateLocation, ) -> Result<()> { let service_profile = ctx.get_active_offer_service_profile()?; let protocol = service_profile.protocol; @@ -3674,7 +3726,7 @@ pub async fn step_when_i_run_swgr_offer_get( let id = offer_id.to_string(); - let args = vec![ + let mut args = vec![ "offer", "get", "default", @@ -3683,12 +3735,27 @@ pub async fn step_when_i_run_swgr_offer_get( &base_url, "--authorization-path", &authorization_str, - "--trusted-roots", - &ca_bundle_str, ]; - let empty_env: Vec<(&str, &str)> = vec![]; - cli_ctx.command(empty_env, args)?; + // Configure root certificate location + let mut env: Vec<(&str, &str)> = vec![]; + match &certificate_location { + CertificateLocation::Arg => { + args.push("--trusted-roots"); + args.push(&ca_bundle_str); + } + CertificateLocation::Env => { + env.push(("OFFER_STORE_HTTP_TRUSTED_ROOTS", &ca_bundle_str)); + } + CertificateLocation::Native => { + env.push(("SSL_CERT_FILE", &ca_bundle_str)); + } + CertificateLocation::NativePath(path) => { + env.push(("SSL_CERT_FILE", path)); + } + } + + cli_ctx.command(env, args)?; Ok(()) } @@ -4083,6 +4150,7 @@ pub async fn step_given_a_valid_offer_metadata_json_exists( pub async fn step_when_i_run_swgr_offer_metadata_post( ctx: &mut GlobalContext, cli_ctx: &mut CliContext, + certificate_location: CertificateLocation, ) -> Result<()> { let service_profile = ctx.get_active_offer_service_profile()?; let protocol = service_profile.protocol; @@ -4098,7 +4166,7 @@ pub async fn step_when_i_run_swgr_offer_metadata_post( let input_path = cli_ctx.metadata_json_path.to_str().unwrap().to_string(); - let args = vec![ + let mut args = vec![ "offer", "metadata", "post", @@ -4106,14 +4174,30 @@ pub async fn step_when_i_run_swgr_offer_metadata_post( &base_url, "--authorization-path", &authorization_str, - "--trusted-roots", - &ca_bundle_str, - "--input", - &input_path, ]; - let empty_env: Vec<(&str, &str)> = vec![]; - cli_ctx.command(empty_env, args)?; + // Configure root certificate location + let mut env: Vec<(&str, &str)> = vec![]; + match &certificate_location { + CertificateLocation::Arg => { + args.push("--trusted-roots"); + args.push(&ca_bundle_str); + } + CertificateLocation::Env => { + env.push(("OFFER_STORE_HTTP_TRUSTED_ROOTS", &ca_bundle_str)); + } + CertificateLocation::Native => { + env.push(("SSL_CERT_FILE", &ca_bundle_str)); + } + CertificateLocation::NativePath(path) => { + env.push(("SSL_CERT_FILE", path)); + } + } + + args.push("--input"); + args.push(&input_path); + + cli_ctx.command(env, args)?; Ok(()) } @@ -4123,6 +4207,7 @@ pub async fn step_when_i_run_swgr_offer_metadata_get( ctx: &mut GlobalContext, cli_ctx: &mut CliContext, metadata_id: &Uuid, + certificate_location: CertificateLocation, ) -> Result<()> { let service_profile = ctx.get_active_offer_service_profile()?; let protocol = service_profile.protocol; @@ -4138,7 +4223,7 @@ pub async fn step_when_i_run_swgr_offer_metadata_get( let id = metadata_id.to_string(); - let args = vec![ + let mut args = vec![ "offer", "metadata", "get", @@ -4148,12 +4233,27 @@ pub async fn step_when_i_run_swgr_offer_metadata_get( &base_url, "--authorization-path", &authorization_str, - "--trusted-roots", - &ca_bundle_str, ]; - let empty_env: Vec<(&str, &str)> = vec![]; - cli_ctx.command(empty_env, args)?; + // Configure root certificate location + let mut env: Vec<(&str, &str)> = vec![]; + match &certificate_location { + CertificateLocation::Arg => { + args.push("--trusted-roots"); + args.push(&ca_bundle_str); + } + CertificateLocation::Env => { + env.push(("OFFER_STORE_HTTP_TRUSTED_ROOTS", &ca_bundle_str)); + } + CertificateLocation::Native => { + env.push(("SSL_CERT_FILE", &ca_bundle_str)); + } + CertificateLocation::NativePath(path) => { + env.push(("SSL_CERT_FILE", path)); + } + } + + cli_ctx.command(env, args)?; Ok(()) } diff --git a/server/tests/features/http-remote-stores.feature b/server/tests/features/http-remote-stores.feature index 7a9d268..9177987 100644 --- a/server/tests/features/http-remote-stores.feature +++ b/server/tests/features/http-remote-stores.feature @@ -14,33 +14,33 @@ Feature: Http Remote Stores And the server is not already running @http-stores @remote-data-access @multi-server - Scenario: Complete HTTP remote stores workflow with distributed services + Scenario Outline: Complete HTTP remote stores workflow with distributed services # Setup first server with offers and discovery services using memory stores Given a server 1 configuration with memory stores exists When I start server 1 with offers and discovery services Then server 1 should have offers and discovery services listening - + # Setup second server with only lnurl service using HTTP stores - Given a server 2 configuration with HTTP stores pointing to server 1 exists + Given a server 2 configuration with HTTP stores pointing to server 1 exists with When I start server 2 with only lnurl service Then server 2 should have only lnurl service listening - + # Create offer and backend on server 1 (data storage server) When the single payee creates an offer for their lightning node And the single payee registers their lightning node as a backend And the system waits for backend readiness - + # Test LNURL Pay flow through server 2 (using HTTP remote stores) When the payer requests the LNURL offer from the payee Then the payee offer should contain valid sendable amounts And the payee offer should contain valid metadata And the payee offer should provide a callback URL - + When the payer requests an invoice for 100 sats using the payee's callback URL Then the payer should receive a valid Lightning invoice And the invoice amount should be 100000 millisatoshis And the invoice description hash should match the metadata hash - + # Stop servers and validate logs When I stop all servers Then server 1 logs should contain offer creation requests @@ -49,4 +49,9 @@ Feature: Http Remote Stores And server 1 logs should contain HTTP requests from server 2 for offers and discovery And server 2 logs should contain offer retrieval requests via HTTP stores And server 2 logs should contain invoice generation requests - And server 2 logs should contain health check requests for lnurl service \ No newline at end of file + And server 2 logs should contain health check requests for lnurl service + + Examples: + | certificate-location | + | env var OFFER_STORE_HTTP_TRUSTED_ROOTS + DISCOVERY_STORE_HTTP_TRUSTED_ROOTS | + | env var SSL_CERT_FILE | \ No newline at end of file diff --git a/server/tests/features/http_remote_stores.rs b/server/tests/features/http_remote_stores.rs index d22ae54..7f31ef9 100644 --- a/server/tests/features/http_remote_stores.rs +++ b/server/tests/features/http_remote_stores.rs @@ -3,160 +3,167 @@ use crate::common::context::Protocol; use crate::common::step_functions::*; use crate::FEATURE_TEST_CONFIG_PATH; use std::path::PathBuf; -use switchgear_testing::credentials::RegTestLnNodeType; +use switchgear_testing::credentials::lightning::RegTestLnNodeType; + +use crate::common::context::server::CertificateLocation; #[tokio::test] async fn test_complete_http_remote_stores_workflow_with_distributed_services() { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let feature_test_config_path = manifest_dir.join(FEATURE_TEST_CONFIG_PATH); - let mut ctx = match GlobalContext::create(&feature_test_config_path).expect("assert") { - Some(ctx) => ctx, - None => return, - }; - let server1 = "server1"; - let config_path = manifest_dir.join("config/memory-basic.yaml"); - ctx.add_server( - server1, - config_path, - Protocol::Https, - Protocol::Https, - Protocol::Https, - ) - .expect("assert"); - - let server2 = "server2"; - let config_path = manifest_dir.join("config/lnurl-standalone.yaml"); - ctx.add_server( - server2, - config_path, - Protocol::Https, - Protocol::Https, - Protocol::Https, - ) - .expect("assert"); - - ctx.set_discovery_store_url(server1, server2) - .expect("assert"); - ctx.set_discovery_store_authorization(server1, server2) - .expect("assert"); - - ctx.set_offer_store_url(server1, server2).expect("assert"); - ctx.set_offer_store_authorization(server1, server2) - .expect("assert"); - - ctx.activate_server(server1); - - // Background - step_given_the_payee_has_a_lightning_node_available(&mut ctx, RegTestLnNodeType::Cln) - .await - .expect("assert"); - step_given_the_server_is_not_already_running(&mut ctx) - .await - .expect("assert"); - - // Setup first server with offers and discovery services using memory stores - step_given_the_lnurl_server_is_ready_to_start(&mut ctx) - .await - .expect("assert"); - step_when_i_start_server_1_with_offers_and_discovery_services(&mut ctx) - .await - .expect("assert"); - step_then_server_1_should_have_offers_and_discovery_services_listening(&mut ctx) - .await - .expect("assert"); - - ctx.activate_server(server2); - step_given_the_lnurl_server_is_ready_to_start(&mut ctx) - .await - .expect("assert"); - - step_when_i_start_server_2_with_only_lnurl_service(&mut ctx) - .await - .expect("assert"); - step_then_server_2_should_have_only_lnurl_service_listening(&mut ctx) - .await - .expect("assert"); - - ctx.activate_server(server1); - - // Create offer and register backend on server1 (offers and discovery services) - step_when_the_payee_creates_an_offer_for_their_lightning_node(&mut ctx, "single") - .await - .expect("assert"); - step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "single") - .await - .expect("assert"); - - ctx.activate_server(server2); - - // Request LNURL offer and invoice through server2 (which uses HTTP stores to access server1) - step_when_the_payer_requests_the_lnurl_offer_from_the_payee(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_payee_offer_should_contain_valid_sendable_amounts(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_payee_offer_should_contain_valid_metadata(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_payee_offer_should_provide_a_callback_url(&mut ctx, "single") - .await - .expect("assert"); - - step_when_the_payer_requests_an_invoice_for_100_sats_using_the_payee_callback_url( - &mut ctx, - "single", - &Protocol::Https, - ) - .await - .expect("assert"); - step_then_the_payer_should_receive_a_valid_lightning_invoice(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_invoice_amount_should_be_100000_millisatoshis(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_invoice_description_hash_should_match_the_metadata_hash(&mut ctx, "single") - .await - .expect("assert"); - - // Stop servers and validate logs - step_when_i_stop_all_servers(&mut ctx) - .await - .expect("assert"); - - ctx.activate_server(server1); - - // Validate server1 logs (offers and discovery services) - step_then_server_1_logs_should_contain_offer_creation_requests(&mut ctx) - .await - .expect("assert"); - step_and_server_1_logs_should_contain_backend_registration_requests(&mut ctx) - .await - .expect("assert"); - step_and_server_1_logs_should_contain_health_check_requests_for_offers_and_discovery_services( - &mut ctx, - ) - .await - .expect("assert"); - step_and_server_1_logs_should_contain_http_requests_from_server_2_for_offers_and_discovery( - &mut ctx, - ) - .await - .expect("assert"); - - ctx.activate_server(server2); - - // Validate server2 logs (LNURL service with HTTP stores) - step_and_server_2_logs_should_contain_offer_retrieval_requests_via_http_stores(&mut ctx) - .await - .expect("assert"); - step_and_server_2_logs_should_contain_invoice_generation_requests(&mut ctx) - .await - .expect("assert"); - step_and_server_2_logs_should_contain_health_check_requests_for_lnurl_service(&mut ctx) - .await - .expect("assert"); - - ctx.stop_all_servers().expect("assert"); + let certificate_locations = vec![CertificateLocation::Env, CertificateLocation::Native]; + + for certificate_location in certificate_locations { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let feature_test_config_path = manifest_dir.join(FEATURE_TEST_CONFIG_PATH); + let mut ctx = match GlobalContext::create(&feature_test_config_path).expect("assert") { + Some(ctx) => ctx, + None => return, + }; + let server1 = "server1"; + let config_path = manifest_dir.join("config/memory-basic.yaml"); + ctx.add_server( + server1, + config_path, + Protocol::Https, + Protocol::Https, + Protocol::Https, + ) + .expect("assert"); + + let server2 = "server2"; + let config_path = manifest_dir.join("config/lnurl-standalone.yaml"); + ctx.add_server( + server2, + config_path, + Protocol::Https, + Protocol::Https, + Protocol::Https, + ) + .expect("assert"); + + ctx.set_discovery_store_url(server1, server2) + .expect("assert"); + ctx.set_discovery_store_authorization(server1, server2) + .expect("assert"); + + ctx.set_offer_store_url(server1, server2).expect("assert"); + ctx.set_offer_store_authorization(server1, server2) + .expect("assert"); + + // Set certificate location for server2 + ctx.set_certificate_location(server2, certificate_location) + .expect("assert"); + + ctx.activate_server(server1); + + // Background + step_given_the_payee_has_a_lightning_node_available(&mut ctx, RegTestLnNodeType::Cln) + .await + .expect("assert"); + step_given_the_server_is_not_already_running(&mut ctx) + .await + .expect("assert"); + + // Setup first server with offers and discovery services using memory stores + step_given_the_lnurl_server_is_ready_to_start(&mut ctx) + .await + .expect("assert"); + step_when_i_start_server_1_with_offers_and_discovery_services(&mut ctx) + .await + .expect("assert"); + step_then_server_1_should_have_offers_and_discovery_services_listening(&mut ctx) + .await + .expect("assert"); + + ctx.activate_server(server2); + step_given_the_lnurl_server_is_ready_to_start(&mut ctx) + .await + .expect("assert"); + + step_when_i_start_server_2_with_only_lnurl_service(&mut ctx) + .await + .expect("assert"); + step_then_server_2_should_have_only_lnurl_service_listening(&mut ctx) + .await + .expect("assert"); + + ctx.activate_server(server1); + + // Create offer and register backend on server1 (offers and discovery services) + step_when_the_payee_creates_an_offer_for_their_lightning_node(&mut ctx, "single") + .await + .expect("assert"); + step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "single", true) + .await + .expect("assert"); + + ctx.activate_server(server2); + + // Request LNURL offer and invoice through server2 (which uses HTTP stores to access server1) + step_when_the_payer_requests_the_lnurl_offer_from_the_payee(&mut ctx, "single") + .await + .expect("assert"); + step_then_the_payee_offer_should_contain_valid_sendable_amounts(&mut ctx, "single") + .await + .expect("assert"); + step_then_the_payee_offer_should_contain_valid_metadata(&mut ctx, "single") + .await + .expect("assert"); + step_then_the_payee_offer_should_provide_a_callback_url(&mut ctx, "single") + .await + .expect("assert"); + + step_when_the_payer_requests_an_invoice_for_100_sats_using_the_payee_callback_url( + &mut ctx, + "single", + &Protocol::Https, + ) + .await + .expect("assert"); + step_then_the_payer_should_receive_a_valid_lightning_invoice(&mut ctx, "single") + .await + .expect("assert"); + step_then_the_invoice_amount_should_be_100000_millisatoshis(&mut ctx, "single") + .await + .expect("assert"); + step_then_the_invoice_description_hash_should_match_the_metadata_hash(&mut ctx, "single") + .await + .expect("assert"); + + // Stop servers and validate logs + step_when_i_stop_all_servers(&mut ctx) + .await + .expect("assert"); + + ctx.activate_server(server1); + + // Validate server1 logs (offers and discovery services) + step_then_server_1_logs_should_contain_offer_creation_requests(&mut ctx) + .await + .expect("assert"); + step_and_server_1_logs_should_contain_backend_registration_requests(&mut ctx) + .await + .expect("assert"); + step_and_server_1_logs_should_contain_health_check_requests_for_offers_and_discovery_services( + &mut ctx, ).await.expect("assert"); + step_and_server_1_logs_should_contain_http_requests_from_server_2_for_offers_and_discovery( + &mut ctx, + ) + .await + .expect("assert"); + + ctx.activate_server(server2); + + // Validate server2 logs (LNURL service with HTTP stores) + step_and_server_2_logs_should_contain_offer_retrieval_requests_via_http_stores(&mut ctx) + .await + .expect("assert"); + step_and_server_2_logs_should_contain_invoice_generation_requests(&mut ctx) + .await + .expect("assert"); + step_and_server_2_logs_should_contain_health_check_requests_for_lnurl_service(&mut ctx) + .await + .expect("assert"); + + ctx.stop_all_servers().expect("assert"); + } } diff --git a/server/tests/features/lnurl-pay-invoice-generation.feature b/server/tests/features/lnurl-pay-invoice-generation.feature index d1fa162..e715b0d 100644 --- a/server/tests/features/lnurl-pay-invoice-generation.feature +++ b/server/tests/features/lnurl-pay-invoice-generation.feature @@ -13,10 +13,10 @@ Feature: LNURL Pay invoice generation And all services should be listening on their configured ports @lightning-backend @happy-path - Scenario Outline: Payer requests invoice from single payee's lightning offer using + Scenario Outline: Payer requests invoice from single payee's lightning offer using and ln trust root location # Test LNURL Pay flow where payee creates offer and payer requests invoice Given a valid configuration file exists for - And the single payee has a lightning node available + And the single payee has a lightning node available with trust root When the single payee creates an offer for their lightning node And the single payee registers their lightning node as a backend And the system waits for backend readiness @@ -30,8 +30,12 @@ Feature: LNURL Pay invoice generation And the invoice description hash should match the metadata hash Examples: - | backend_type | protocol | - | CLN | http | - | CLN | https | - | LND | http | - | LND | https | \ No newline at end of file + | backend_type | protocol | trust | + | CLN | http | creds | + | CLN | https | creds | + | CLN | https | config | + | CLN | https | native | + | LND | http | creds | + | LND | https | creds | + | LND | https | config | + | LND | https | native | diff --git a/server/tests/features/lnurl_pay_invoice_generation.rs b/server/tests/features/lnurl_pay_invoice_generation.rs index 13f4026..1428389 100644 --- a/server/tests/features/lnurl_pay_invoice_generation.rs +++ b/server/tests/features/lnurl_pay_invoice_generation.rs @@ -1,367 +1,189 @@ use crate::common::context::global::GlobalContext; +use crate::common::context::server::CertificateLocation; use crate::common::context::Protocol; +use crate::common::helpers::get_payee_from_context; use crate::common::step_functions::*; use crate::FEATURE_TEST_CONFIG_PATH; use std::path::PathBuf; -use switchgear_testing::credentials::RegTestLnNodeType; +use switchgear_testing::credentials::lightning::{RegTestLnNode, RegTestLnNodeType}; -#[tokio::test] -async fn test_payer_requests_invoice_from_payee_cln_lightning_offer_using_http() { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let feature_test_config_path = manifest_dir.join(FEATURE_TEST_CONFIG_PATH); - let mut ctx = match GlobalContext::create(&feature_test_config_path).expect("assert") { - Some(ctx) => ctx, - None => return, - }; +enum LnTrustRootsLocation { + Credentials, + Configuration, + Native, +} - let server1 = "server1"; - let config_path = manifest_dir.join("config/memory-basic-no-tls.yaml"); - ctx.add_server( - server1, - config_path, - Protocol::Http, - Protocol::Http, +#[tokio::test] +async fn test_payer_requests_invoice_from_payee_cln_lightning_offer_using_http_creds() { + test_payer_requests_invoice_from_payee_inner( Protocol::Http, + RegTestLnNodeType::Cln, + LnTrustRootsLocation::Credentials, ) + .await .expect("assert"); - ctx.activate_server(server1); - - // When: Start the server - step_when_i_start_the_lnurl_server_with_the_configuration(&mut ctx) - .await - .expect("assert"); - - // Then: Verify server starts successfully - step_then_the_server_should_start_successfully(&mut ctx) - .await - .expect("assert"); - step_and_all_services_should_be_listening_on_their_configured_ports(&mut ctx) - .await - .expect("assert"); - - // Background: Verify LNURL server is running - step_and_all_services_should_be_listening_on_their_configured_ports(&mut ctx) - .await - .expect("assert"); - - // Given: Specific backend type - step_given_the_payee_has_a_lightning_node_available(&mut ctx, RegTestLnNodeType::Cln) - .await - .expect("assert"); - - // When: Payee setup steps - step_when_the_payee_creates_an_offer_for_their_lightning_node(&mut ctx, "single") - .await - .expect("assert"); - step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "single") - .await - .expect("assert"); - - // When: Payer requests offer using specified protocol - step_when_the_payer_requests_the_lnurl_offer_from_the_payee(&mut ctx, "single") - .await - .expect("assert"); - - // Then: Verify offer properties - step_then_the_payee_offer_should_contain_valid_sendable_amounts(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_payee_offer_should_contain_valid_metadata(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_payee_offer_should_provide_a_callback_url(&mut ctx, "single") - .await - .expect("assert"); +} - // When: Request invoice using specified protocol - step_when_the_payer_requests_an_invoice_for_100_sats_using_the_payee_callback_url( - &mut ctx, - "single", - &Protocol::Http, +#[tokio::test] +async fn test_payer_requests_invoice_from_payee_cln_lightning_offer_using_https_creds() { + test_payer_requests_invoice_from_payee_inner( + Protocol::Https, + RegTestLnNodeType::Cln, + LnTrustRootsLocation::Credentials, ) .await .expect("assert"); - - // Then: Verify invoice - step_then_the_payer_should_receive_a_valid_lightning_invoice(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_invoice_amount_should_be_100000_millisatoshis(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_invoice_description_hash_should_match_the_metadata_hash(&mut ctx, "single") - .await - .expect("assert"); - - ctx.stop_all_servers().expect("assert"); } #[tokio::test] -async fn test_payer_requests_invoice_from_payee_cln_lightning_offer_using_https() { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let feature_test_config_path = manifest_dir.join(FEATURE_TEST_CONFIG_PATH); - let mut ctx = match GlobalContext::create(&feature_test_config_path).expect("assert") { - Some(ctx) => ctx, - None => return, - }; - let server1 = "server1"; - let config_path = manifest_dir.join("config/memory-basic.yaml"); - ctx.add_server( - server1, - config_path, - Protocol::Https, - Protocol::Https, +async fn test_payer_requests_invoice_from_payee_cln_lightning_offer_using_https_config() { + test_payer_requests_invoice_from_payee_inner( Protocol::Https, + RegTestLnNodeType::Cln, + LnTrustRootsLocation::Configuration, ) + .await .expect("assert"); - ctx.activate_server(server1); - - // When: Start the server - step_when_i_start_the_lnurl_server_with_the_configuration(&mut ctx) - .await - .expect("assert"); - - // Then: Verify server starts successfully - step_then_the_server_should_start_successfully(&mut ctx) - .await - .expect("assert"); - step_and_all_services_should_be_listening_on_their_configured_ports(&mut ctx) - .await - .expect("assert"); - - // Background: Verify LNURL server is running - step_and_all_services_should_be_listening_on_their_configured_ports(&mut ctx) - .await - .expect("assert"); - - // Given: Specific backend type - step_given_the_payee_has_a_lightning_node_available(&mut ctx, RegTestLnNodeType::Cln) - .await - .expect("assert"); - - // When: Payee setup steps - step_when_the_payee_creates_an_offer_for_their_lightning_node(&mut ctx, "single") - .await - .expect("assert"); - step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "single") - .await - .expect("assert"); - - // When: Payer requests offer using specified protocol - step_when_the_payer_requests_the_lnurl_offer_from_the_payee(&mut ctx, "single") - .await - .expect("assert"); - - // Then: Verify offer properties - step_then_the_payee_offer_should_contain_valid_sendable_amounts(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_payee_offer_should_contain_valid_metadata(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_payee_offer_should_provide_a_callback_url(&mut ctx, "single") - .await - .expect("assert"); +} - // When: Request invoice using specified protocol - step_when_the_payer_requests_an_invoice_for_100_sats_using_the_payee_callback_url( - &mut ctx, - "single", - &Protocol::Https, +#[tokio::test] +async fn test_payer_requests_invoice_from_payee_cln_lightning_offer_using_https_native() { + test_payer_requests_invoice_from_payee_inner( + Protocol::Https, + RegTestLnNodeType::Cln, + LnTrustRootsLocation::Native, ) .await .expect("assert"); - - // Then: Verify invoice - step_then_the_payer_should_receive_a_valid_lightning_invoice(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_invoice_amount_should_be_100000_millisatoshis(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_invoice_description_hash_should_match_the_metadata_hash(&mut ctx, "single") - .await - .expect("assert"); - - ctx.stop_all_servers().expect("assert"); } #[tokio::test] -async fn test_payer_requests_invoice_from_payee_lnd_lightning_offer_using_http() { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let feature_test_config_path = manifest_dir.join(FEATURE_TEST_CONFIG_PATH); - let mut ctx = match GlobalContext::create(&feature_test_config_path).expect("assert") { - Some(ctx) => ctx, - None => return, - }; - let server1 = "server1"; - let config_path = manifest_dir.join("config/memory-basic-no-tls.yaml"); - ctx.add_server( - server1, - config_path, - Protocol::Http, - Protocol::Http, +async fn test_payer_requests_invoice_from_payee_lnd_lightning_offer_using_http_creds() { + test_payer_requests_invoice_from_payee_inner( Protocol::Http, + RegTestLnNodeType::Lnd, + LnTrustRootsLocation::Credentials, ) + .await .expect("assert"); - ctx.activate_server(server1); - - // When: Start the server - step_when_i_start_the_lnurl_server_with_the_configuration(&mut ctx) - .await - .expect("assert"); - - // Then: Verify server starts successfully - step_then_the_server_should_start_successfully(&mut ctx) - .await - .expect("assert"); - step_and_all_services_should_be_listening_on_their_configured_ports(&mut ctx) - .await - .expect("assert"); - - // Background: Verify LNURL server is running - step_and_all_services_should_be_listening_on_their_configured_ports(&mut ctx) - .await - .expect("assert"); - - // Given: Specific backend type - step_given_the_payee_has_a_lightning_node_available(&mut ctx, RegTestLnNodeType::Lnd) - .await - .expect("assert"); - - // When: Payee setup steps - step_when_the_payee_creates_an_offer_for_their_lightning_node(&mut ctx, "single") - .await - .expect("assert"); - step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "single") - .await - .expect("assert"); - - // When: Payer requests offer using specified protocol - step_when_the_payer_requests_the_lnurl_offer_from_the_payee(&mut ctx, "single") - .await - .expect("assert"); - - // Then: Verify offer properties - step_then_the_payee_offer_should_contain_valid_sendable_amounts(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_payee_offer_should_contain_valid_metadata(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_payee_offer_should_provide_a_callback_url(&mut ctx, "single") - .await - .expect("assert"); +} - // When: Request invoice using specified protocol - step_when_the_payer_requests_an_invoice_for_100_sats_using_the_payee_callback_url( - &mut ctx, - "single", - &Protocol::Http, +#[tokio::test] +async fn test_payer_requests_invoice_from_payee_lnd_lightning_offer_using_https_creds() { + test_payer_requests_invoice_from_payee_inner( + Protocol::Https, + RegTestLnNodeType::Lnd, + LnTrustRootsLocation::Credentials, ) .await .expect("assert"); +} - // Then: Verify invoice - step_then_the_payer_should_receive_a_valid_lightning_invoice(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_invoice_amount_should_be_100000_millisatoshis(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_invoice_description_hash_should_match_the_metadata_hash(&mut ctx, "single") - .await - .expect("assert"); - - ctx.stop_all_servers().expect("assert"); +#[tokio::test] +async fn test_payer_requests_invoice_from_payee_lnd_lightning_offer_using_https_config() { + test_payer_requests_invoice_from_payee_inner( + Protocol::Https, + RegTestLnNodeType::Lnd, + LnTrustRootsLocation::Configuration, + ) + .await + .expect("assert"); } #[tokio::test] -async fn test_payer_requests_invoice_from_payee_lnd_lightning_offer_using_https() { +async fn test_payer_requests_invoice_from_payee_lnd_lightning_offer_using_https_native() { + test_payer_requests_invoice_from_payee_inner( + Protocol::Https, + RegTestLnNodeType::Lnd, + LnTrustRootsLocation::Native, + ) + .await + .expect("assert"); +} + +async fn test_payer_requests_invoice_from_payee_inner( + protocol: Protocol, + node_type: RegTestLnNodeType, + ln_trusted_roots_location: LnTrustRootsLocation, +) -> Result<(), Box> { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let feature_test_config_path = manifest_dir.join(FEATURE_TEST_CONFIG_PATH); - let mut ctx = match GlobalContext::create(&feature_test_config_path).expect("assert") { + let mut ctx = match GlobalContext::create(&feature_test_config_path)? { Some(ctx) => ctx, - None => return, + None => return Ok(()), }; + let server1 = "server1"; - let config_path = manifest_dir.join("config/memory-basic.yaml"); - ctx.add_server( - server1, - config_path, - Protocol::Https, - Protocol::Https, - Protocol::Https, - ) - .expect("assert"); + let config_path = match protocol { + Protocol::Http => manifest_dir.join("config/memory-basic-no-tls.yaml"), + Protocol::Https => manifest_dir.join("config/memory-basic.yaml"), + }; + + ctx.add_server(server1, config_path, protocol, protocol, protocol)?; ctx.activate_server(server1); + // Given: Specific backend type + step_given_the_payee_has_a_lightning_node_available(&mut ctx, node_type).await?; + + let payee = get_payee_from_context(&ctx, "single")?; + let node_cert_path = match &payee.node { + RegTestLnNode::Cln(n) => n.ca_cert_path.as_path(), + RegTestLnNode::Lnd(n) => n.tls_cert_path.as_path(), + }; + + let include_ca = match ln_trusted_roots_location { + LnTrustRootsLocation::Credentials => true, + LnTrustRootsLocation::Configuration => { + ctx.set_ln_trusted_roots_path(server1, Some(node_cert_path.to_path_buf())) + .expect("assert"); + false + } + LnTrustRootsLocation::Native => { + ctx.set_certificate_location( + server1, + CertificateLocation::NativePath(node_cert_path.to_string_lossy().to_string()), + ) + .expect("assert"); + false + } + }; + // When: Start the server - step_when_i_start_the_lnurl_server_with_the_configuration(&mut ctx) - .await - .expect("assert"); + step_when_i_start_the_lnurl_server_with_the_configuration(&mut ctx).await?; // Then: Verify server starts successfully - step_then_the_server_should_start_successfully(&mut ctx) - .await - .expect("assert"); - step_and_all_services_should_be_listening_on_their_configured_ports(&mut ctx) - .await - .expect("assert"); + step_then_the_server_should_start_successfully(&mut ctx).await?; + step_and_all_services_should_be_listening_on_their_configured_ports(&mut ctx).await?; // Background: Verify LNURL server is running - step_and_all_services_should_be_listening_on_their_configured_ports(&mut ctx) - .await - .expect("assert"); - - // Given: Specific backend type - step_given_the_payee_has_a_lightning_node_available(&mut ctx, RegTestLnNodeType::Lnd) - .await - .expect("assert"); + step_and_all_services_should_be_listening_on_their_configured_ports(&mut ctx).await?; // When: Payee setup steps - step_when_the_payee_creates_an_offer_for_their_lightning_node(&mut ctx, "single") - .await - .expect("assert"); - step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "single") - .await - .expect("assert"); + step_when_the_payee_creates_an_offer_for_their_lightning_node(&mut ctx, "single").await?; + step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "single", include_ca) + .await?; // When: Payer requests offer using specified protocol - step_when_the_payer_requests_the_lnurl_offer_from_the_payee(&mut ctx, "single") - .await - .expect("assert"); + step_when_the_payer_requests_the_lnurl_offer_from_the_payee(&mut ctx, "single").await?; // Then: Verify offer properties - step_then_the_payee_offer_should_contain_valid_sendable_amounts(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_payee_offer_should_contain_valid_metadata(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_payee_offer_should_provide_a_callback_url(&mut ctx, "single") - .await - .expect("assert"); + step_then_the_payee_offer_should_contain_valid_sendable_amounts(&mut ctx, "single").await?; + step_then_the_payee_offer_should_contain_valid_metadata(&mut ctx, "single").await?; + step_then_the_payee_offer_should_provide_a_callback_url(&mut ctx, "single").await?; // When: Request invoice using specified protocol step_when_the_payer_requests_an_invoice_for_100_sats_using_the_payee_callback_url( - &mut ctx, - "single", - &Protocol::Https, + &mut ctx, "single", &protocol, ) - .await - .expect("assert"); + .await?; // Then: Verify invoice - step_then_the_payer_should_receive_a_valid_lightning_invoice(&mut ctx, "single") - .await - .expect("assert"); - step_then_the_invoice_amount_should_be_100000_millisatoshis(&mut ctx, "single") - .await - .expect("assert"); + step_then_the_payer_should_receive_a_valid_lightning_invoice(&mut ctx, "single").await?; + step_then_the_invoice_amount_should_be_100000_millisatoshis(&mut ctx, "single").await?; step_then_the_invoice_description_hash_should_match_the_metadata_hash(&mut ctx, "single") - .await - .expect("assert"); + .await?; + + ctx.stop_all_servers()?; - ctx.stop_all_servers().expect("assert"); + Ok(()) } diff --git a/server/tests/features/lnurl_pay_multi_backend_invoice_generation.rs b/server/tests/features/lnurl_pay_multi_backend_invoice_generation.rs index 57ae432..f5ff91a 100644 --- a/server/tests/features/lnurl_pay_multi_backend_invoice_generation.rs +++ b/server/tests/features/lnurl_pay_multi_backend_invoice_generation.rs @@ -3,7 +3,7 @@ use crate::common::context::Protocol; use crate::common::step_functions::*; use crate::FEATURE_TEST_CONFIG_PATH; use std::path::PathBuf; -use switchgear_testing::credentials::RegTestLnNodeType; +use switchgear_testing::credentials::lightning::RegTestLnNodeType; #[tokio::test] async fn test_no_backends_no_invoices_for_either_offer() { diff --git a/server/tests/features/server-persistence.feature b/server/tests/features/server-persistence.feature index 7d23026..38f2bf2 100644 --- a/server/tests/features/server-persistence.feature +++ b/server/tests/features/server-persistence.feature @@ -9,7 +9,7 @@ Feature: Server resumes state after restart And the server is not already running @persistence @backend-recovery @offer-recovery @full-lifecycle - Scenario Outline: Complete persistence lifecycle across multiple server restarts with / storage + Scenario Outline: Complete persistence lifecycle across multiple server restarts with / storage Given a valid configuration file exists with backend storage and offer storage # First server instance: Create and persist data When I start the LNURL server with the configuration @@ -60,10 +60,14 @@ Feature: Server resumes state after restart Then the server should exit with code 0 Examples: Current storage combinations - | backend_store | offer_store | - | sqlite | sqlite | - - + | database | ssl | + | sqlite | none | + | postgres | none | + | postgres | param | + | postgres | native | + | mysql | none | + | mysql | param | + | mysql | native | @persistence @selective-cleanup @backend-only Scenario Outline: Backend data loss with offer persistence using / storage diff --git a/server/tests/features/server_persistence.rs b/server/tests/features/server_persistence.rs index a6ad116..b9487f8 100644 --- a/server/tests/features/server_persistence.rs +++ b/server/tests/features/server_persistence.rs @@ -1,20 +1,95 @@ use crate::common::context::global::GlobalContext; +use crate::common::context::server::CertificateLocation; use crate::common::context::Protocol; use crate::common::step_functions::*; use crate::FEATURE_TEST_CONFIG_PATH; +use std::cmp::PartialEq; use std::path::PathBuf; -use switchgear_testing::credentials::RegTestLnNodeType; +use switchgear_testing::credentials::db::{DbCredentials, TestDatabase}; +use switchgear_testing::credentials::lightning::RegTestLnNodeType; +use switchgear_testing::db::{TestMysqlDatabase, TestPostgresDatabase}; +use uuid::Uuid; #[tokio::test] -async fn test_complete_persistence_lifecycle_sqlite_sqlite() { +async fn test_complete_persistence_lifecycle_sqlite() { + test_complete_persistence_lifecycle_impl(DbType::Sqlite).await; +} + +#[tokio::test] +async fn test_complete_persistence_lifecycle_mysql() { + test_complete_persistence_lifecycle_impl(DbType::Mysql { + ssl: DbSslType::None, + }) + .await; +} + +#[tokio::test] +async fn test_complete_persistence_lifecycle_mysql_ssl() { + test_complete_persistence_lifecycle_impl(DbType::Mysql { + ssl: DbSslType::Parameter, + }) + .await; +} + +#[tokio::test] +async fn test_complete_persistence_lifecycle_mysql_ssl_native() { + test_complete_persistence_lifecycle_impl(DbType::Mysql { + ssl: DbSslType::Native, + }) + .await; +} + +#[tokio::test] +async fn test_complete_persistence_lifecycle_postgresql() { + test_complete_persistence_lifecycle_impl(DbType::Postgresql { + ssl: DbSslType::None, + }) + .await; +} + +#[tokio::test] +async fn test_complete_persistence_lifecycle_postgresql_ssl() { + test_complete_persistence_lifecycle_impl(DbType::Postgresql { + ssl: DbSslType::Parameter, + }) + .await; +} + +#[tokio::test] +async fn test_complete_persistence_lifecycle_postgresql_ssl_native() { + test_complete_persistence_lifecycle_impl(DbType::Postgresql { + ssl: DbSslType::Native, + }) + .await; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum DbType { + Sqlite, + Mysql { ssl: DbSslType }, + Postgresql { ssl: DbSslType }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum DbSslType { + None, + Parameter, + Native, +} + +async fn test_complete_persistence_lifecycle_impl(db_type: DbType) { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let feature_test_config_path = manifest_dir.join(FEATURE_TEST_CONFIG_PATH); + + let db_credentials = DbCredentials::create().expect("assert"); + let db = db_credentials.get_databases().expect("assert"); + let mut ctx = match GlobalContext::create(&feature_test_config_path).expect("assert") { Some(ctx) => ctx, None => return, }; let server1 = "server1"; - let config_path = manifest_dir.join("config/sqlite-persistent.yaml"); + let config_path = manifest_dir.join("config/persistence.yaml"); ctx.add_server( server1, config_path, @@ -23,6 +98,31 @@ async fn test_complete_persistence_lifecycle_sqlite_sqlite() { Protocol::Https, ) .expect("assert"); + + let _db_guard = match &db_type { + DbType::Sqlite => (None, None), + DbType::Mysql { ssl } => match &db.mysql { + None => { + eprintln!("MySQL database not available, skipping test"); + return; + } + Some(db) => ( + Some(install_mysql_databases(&mut ctx, server1, db, *ssl).expect("assert")), + None, + ), + }, + DbType::Postgresql { ssl } => match &db.postgres { + None => { + eprintln!("PostgreSQL database not available, skipping test"); + return; + } + Some(db) => ( + None, + Some(install_postgres_databases(&mut ctx, server1, db, *ssl).expect("assert")), + ), + }, + }; + ctx.activate_server(server1); // First server instance: Start server and create persistent data @@ -46,9 +146,10 @@ async fn test_complete_persistence_lifecycle_sqlite_sqlite() { step_when_the_payee_creates_an_offer_for_their_lightning_node(&mut ctx, "single") .await .expect("assert"); - step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "single") + step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "single", true) .await .expect("assert"); + step_when_the_payer_requests_the_lnurl_offer_from_the_payee(&mut ctx, "single") .await .expect("assert"); @@ -100,6 +201,7 @@ async fn test_complete_persistence_lifecycle_sqlite_sqlite() { step_and_all_services_should_be_listening_on_their_configured_ports(&mut ctx) .await .expect("assert"); + // Test that persisted offer and backend still work step_when_the_payer_requests_the_lnurl_offer_from_the_payee(&mut ctx, "single") .await @@ -149,7 +251,7 @@ async fn test_backend_data_loss_with_offer_persistence_sqlite_sqlite() { None => return, }; let server1 = "server1"; - let config_path = manifest_dir.join("config/sqlite-persistent.yaml"); + let config_path = manifest_dir.join("config/persistence.yaml"); ctx.add_server( server1, config_path, @@ -180,7 +282,7 @@ async fn test_backend_data_loss_with_offer_persistence_sqlite_sqlite() { step_when_the_payee_creates_an_offer_for_their_lightning_node(&mut ctx, "single") .await .expect("assert"); - step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "single") + step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "single", true) .await .expect("assert"); step_when_the_payer_requests_the_lnurl_offer_from_the_payee(&mut ctx, "single") @@ -232,3 +334,81 @@ async fn test_backend_data_loss_with_offer_persistence_sqlite_sqlite() { .await .expect("assert"); } + +fn install_mysql_databases( + ctx: &mut GlobalContext, + server: &str, + db: &TestDatabase, + ssl: DbSslType, +) -> anyhow::Result<(TestMysqlDatabase, TestMysqlDatabase)> { + if ssl == DbSslType::Native { + ctx.set_certificate_location( + server, + CertificateLocation::NativePath(db.ca_cert_path.to_string_lossy().to_string()), + )?; + } + let cert_path = if ssl == DbSslType::Parameter { + Some(db.ca_cert_path.as_path()) + } else { + None + }; + + let discovery_db = TestMysqlDatabase::new( + format!("discovery_{}", Uuid::new_v4().to_string().replace("-", "")), + &db.address, + ssl != DbSslType::None, + cert_path, + ); + + let offer_db = TestMysqlDatabase::new( + format!("offer_{}", Uuid::new_v4().to_string().replace("-", "")), + &db.address, + ssl != DbSslType::None, + cert_path, + ); + + ctx.set_discovery_store_database_url(server, discovery_db.connection_url().to_string())?; + + ctx.set_offer_store_database_url(server, offer_db.connection_url().to_string())?; + + Ok((discovery_db, offer_db)) +} + +fn install_postgres_databases( + ctx: &mut GlobalContext, + server: &str, + db: &TestDatabase, + ssl: DbSslType, +) -> anyhow::Result<(TestPostgresDatabase, TestPostgresDatabase)> { + if ssl == DbSslType::Native { + ctx.set_certificate_location( + server, + CertificateLocation::NativePath(db.ca_cert_path.to_string_lossy().to_string()), + )?; + } + let cert_path = if ssl == DbSslType::Parameter { + Some(db.ca_cert_path.as_path()) + } else { + None + }; + + let discovery_db = TestPostgresDatabase::new( + format!("discovery_{}", Uuid::new_v4().to_string().replace("-", "")), + &db.address, + ssl != DbSslType::None, + cert_path, + ); + + let offer_db = TestPostgresDatabase::new( + format!("offer_{}", Uuid::new_v4().to_string().replace("-", "")), + &db.address, + ssl != DbSslType::None, + cert_path, + ); + + ctx.set_discovery_store_database_url(server, discovery_db.connection_url().to_string())?; + + ctx.set_offer_store_database_url(server, offer_db.connection_url().to_string())?; + + Ok((discovery_db, offer_db)) +} diff --git a/server/tests/features/service_logs.rs b/server/tests/features/service_logs.rs index d437c34..0009a9c 100644 --- a/server/tests/features/service_logs.rs +++ b/server/tests/features/service_logs.rs @@ -3,7 +3,7 @@ use crate::common::context::Protocol; use crate::common::step_functions::*; use crate::FEATURE_TEST_CONFIG_PATH; use std::path::PathBuf; -use switchgear_testing::credentials::RegTestLnNodeType; +use switchgear_testing::credentials::lightning::RegTestLnNodeType; #[tokio::test] async fn test_service_health_check_logging() { @@ -119,7 +119,7 @@ async fn test_service_operation_request_logging() { step_when_the_payee_creates_an_offer_for_their_lightning_node(&mut ctx, "single") .await .expect("assert"); - step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "single") + step_when_the_payee_registers_their_lightning_node_as_a_backend(&mut ctx, "single", true) .await .expect("assert"); step_when_the_payer_requests_the_lnurl_offer_from_the_payee(&mut ctx, "single") diff --git a/service/Cargo.toml b/service/Cargo.toml index 0d78671..cc6cd2e 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -23,8 +23,8 @@ chrono = { version = "0.4", features = ["serde"] } client-ip = { version = "0.1", features = ["forwarded-header"] } email_address = "0.2" hex = "0.4" -http = "1.3" -hyper-rustls = { version = "0.27", features = ["http2", "tls12"], default-features = false } +http = "1.4" +hyper-rustls = { version = "0.27", features = ["http2", "tls12", "native-tokio"], default-features = false } hyper-timeout = "0.5" hyper-util = "0.1" image = { version = "0.25", default-features = false, features = ["png"] } @@ -32,24 +32,24 @@ jsonwebtoken = { version = "10.2", features = ["aws_lc_rs"] } log = "0.4" prost = { version = "0.14" } qrcode = { version = "0.14", default-features = false, features = ["image"] } -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } -rustls = { version = "0.23", features = ["ring"] } -rustls-pemfile = "2.0" -sea-orm = { version = "1.1", features = ["sqlx-mysql", "sqlx-sqlite", "sqlx-postgres", "runtime-tokio-rustls", "macros"] } +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-native-roots-no-provider"] } +rustls = { version = "0.23", default-features = false } +rustls-pemfile = "2.2" +sea-orm = { version = "1.1", default-features = false, features = ["with-chrono", "with-uuid", "with-json"] } secp256k1 = { version = "0.31", features = ["recovery", "serde"] } serde = "1.0" serde_json = "1.0" sha2 = "0.10" -sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "tls-rustls", "sqlite", "macros", "migrate"] } +sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "tls-rustls-no-provider-native-roots", "sqlite", "postgres", "mysql"] } switchgear-migration = { version = "0.1.7", path = "../migration" } -tempfile = "3.0" +tempfile = "3.23" thiserror = "2.0" tokio = { version = "1", features = ["full"] } -tonic = { version = "0.14", features = ["transport", "tls-ring"] } +tonic = { version = "0.14", default-features = false, features = ["codegen", "transport", "tls-native-roots"] } tonic-prost = "0.14" tower = { version = "0.5", features = ["balance"] } url = { version = "2.5", features = ["serde"] } -uuid = { version = "1.17", features = ["v4", "serde"] } +uuid = { version = "1.18", features = ["v4", "serde"] } [build-dependencies] tonic-prost-build = { version = "0.14" } @@ -65,5 +65,6 @@ pkcs8 = { version = "0.10", features = ["pem"] } png = "0.18" rand = "0.8" rqrr = "0.10" +rustls = { version = "0.23", features = ["aws-lc-rs"] } secp256k1_0_29 = { package = "secp256k1", version = "0.29", features = ["recovery", "serde"] } switchgear-testing = { version = "0.1.7", path = "../testing" } diff --git a/service/src/components/discovery/http.rs b/service/src/components/discovery/http.rs index ef74ec7..c0add53 100644 --- a/service/src/components/discovery/http.rs +++ b/service/src/components/discovery/http.rs @@ -6,7 +6,8 @@ use crate::api::service::ServiceErrorSource; use crate::components::discovery::error::DiscoveryBackendStoreError; use async_trait::async_trait; use reqwest::header::{HeaderMap, HeaderValue}; -use reqwest::{Certificate, Client, ClientBuilder, StatusCode}; +use reqwest::{Certificate, Client, ClientBuilder, IntoUrl, StatusCode}; +use rustls::pki_types::CertificateDer; use std::time::Duration; use url::Url; @@ -18,11 +19,11 @@ pub struct HttpDiscoveryBackendStore { } impl HttpDiscoveryBackendStore { - pub fn create( - base_url: Url, + pub fn create( + base_url: U, total_timeout: Duration, connect_timeout: Duration, - trusted_roots: Vec, + trusted_roots: &[CertificateDer], authorization: String, ) -> Result { let mut headers = HeaderMap::new(); @@ -30,7 +31,7 @@ impl HttpDiscoveryBackendStore { HeaderValue::from_str(&format!("Bearer {authorization}")).map_err(|e| { DiscoveryBackendStoreError::internal_error( ServiceErrorSource::Internal, - format!("creating http client with base url: {base_url}"), + format!("creating http client with base url: {}", base_url.as_str()), e.to_string(), ) })?; @@ -40,6 +41,13 @@ impl HttpDiscoveryBackendStore { let mut builder = ClientBuilder::new(); for root in trusted_roots { + let root = Certificate::from_der(root).map_err(|e| { + DiscoveryBackendStoreError::internal_error( + ServiceErrorSource::Internal, + format!("parsing certificate for url: {}", base_url.as_str()), + e.to_string(), + ) + })?; builder = builder.add_root_certificate(root); } @@ -52,14 +60,17 @@ impl HttpDiscoveryBackendStore { .map_err(|e| { DiscoveryBackendStoreError::http_error( ServiceErrorSource::Internal, - format!("creating http client with base url: {base_url}"), + format!("creating http client with base url: {}", base_url.as_str()), e, ) })?; Self::with_client(client, base_url) } - pub fn with_client(client: Client, base_url: Url) -> Result { + pub fn with_client( + client: Client, + base_url: U, + ) -> Result { let base_url = base_url.as_str().trim_end_matches('/').to_string(); let discovery_url = format!("{base_url}/discovery"); Url::parse(&discovery_url).map_err(|e| { @@ -309,10 +320,15 @@ impl HttpDiscoveryBackendClient for HttpDiscoveryBackendStore { mod tests { use crate::api::discovery::DiscoveryBackendAddress; use crate::components::discovery::http::HttpDiscoveryBackendStore; + use anyhow::anyhow; use url::Url; #[test] fn base_urls() { + let _ = rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .map_err(|_| anyhow!("failed to stand up rustls encryption platform")); + let client = HttpDiscoveryBackendStore::with_client( reqwest::Client::default(), Url::parse("https://base.com").unwrap(), diff --git a/service/src/components/offer/http.rs b/service/src/components/offer/http.rs index 6893705..a16cd3a 100644 --- a/service/src/components/offer/http.rs +++ b/service/src/components/offer/http.rs @@ -7,7 +7,8 @@ use crate::api::service::ServiceErrorSource; use crate::components::offer::error::OfferStoreError; use async_trait::async_trait; use axum::http::{HeaderMap, HeaderValue}; -use reqwest::{Certificate, Client, ClientBuilder, StatusCode}; +use reqwest::{Certificate, Client, ClientBuilder, IntoUrl, StatusCode}; +use rustls::pki_types::CertificateDer; use sha2::Digest; use std::time::Duration; use url::Url; @@ -22,11 +23,11 @@ pub struct HttpOfferStore { } impl HttpOfferStore { - pub fn create( - base_url: Url, + pub fn create( + base_url: U, total_timeout: Duration, connect_timeout: Duration, - trusted_roots: Vec, + trusted_roots: &[CertificateDer], authorization: String, ) -> Result { let mut headers = HeaderMap::new(); @@ -34,7 +35,7 @@ impl HttpOfferStore { HeaderValue::from_str(&format!("Bearer {authorization}")).map_err(|e| { OfferStoreError::internal_error( ServiceErrorSource::Internal, - format!("creating http client with base url: {base_url}"), + format!("creating http client with base url: {}", base_url.as_str()), e.to_string(), ) })?; @@ -43,6 +44,13 @@ impl HttpOfferStore { let mut builder = ClientBuilder::new(); for root in trusted_roots { + let root = Certificate::from_der(root).map_err(|e| { + OfferStoreError::internal_error( + ServiceErrorSource::Internal, + format!("parsing certificate for url: {}", base_url.as_str()), + e.to_string(), + ) + })?; builder = builder.add_root_certificate(root); } @@ -55,14 +63,14 @@ impl HttpOfferStore { .map_err(|e| { OfferStoreError::http_error( ServiceErrorSource::Internal, - format!("creating http client with base url: {base_url}"), + format!("creating http client with base url: {}", base_url.as_str()), e, ) })?; Self::with_client(client, base_url) } - fn with_client(client: Client, base_url: Url) -> Result { + fn with_client(client: Client, base_url: U) -> Result { let base_url = base_url.as_str().trim_end_matches('/').to_string(); let offer_url = format!("{base_url}/offers"); diff --git a/service/src/components/pool/cln/grpc/client.rs b/service/src/components/pool/cln/grpc/client.rs index f4ea1c2..3c6613e 100644 --- a/service/src/components/pool/cln/grpc/client.rs +++ b/service/src/components/pool/cln/grpc/client.rs @@ -1,18 +1,18 @@ use crate::api::service::ServiceErrorSource; use crate::components::pool::cln::grpc::config::{ - ClnGrpcClientAuth, ClnGrpcClientAuthPath, ClnGrpcDiscoveryBackendImplementation, + ClnGrpcClientAuth, ClnGrpcDiscoveryBackendImplementation, }; use crate::components::pool::error::LnPoolError; use crate::components::pool::{Bolt11InvoiceDescription, LnFeatures, LnMetrics, LnRpcClient}; use async_trait::async_trait; use hex::ToHex; +use rustls::pki_types::CertificateDer; use sha2::Digest; use std::fs; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tokio::sync::Mutex; -use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint, Identity}; -use url::Url; +use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; #[allow(clippy::all)] pub mod cln { @@ -21,20 +21,69 @@ pub mod cln { use cln::node_client::NodeClient; -type ClientCredentials = (Vec, Vec, Vec); - pub struct TonicClnGrpcClient { timeout: Duration, config: ClnGrpcDiscoveryBackendImplementation, features: Option, inner: Arc>>>, + ca_certificates: Vec, + identity: Identity, } impl TonicClnGrpcClient { pub fn create( timeout: Duration, config: ClnGrpcDiscoveryBackendImplementation, + trusted_roots: &[CertificateDer], ) -> Result { + let ClnGrpcClientAuth::Path(auth) = &config.auth; + + let mut ca_certificates = trusted_roots + .iter() + .map(|c| { + let c = Self::certificate_der_as_pem(c); + Certificate::from_pem(&c) + }) + .collect::>(); + + if let Some(ca_cert_path) = &auth.ca_cert_path { + let ca_certificate = fs::read(ca_cert_path).map_err(|e| { + LnPoolError::from_invalid_credentials( + e.to_string(), + ServiceErrorSource::Internal, + format!( + "loading CLN credentials and reading CA certificate from path {}", + ca_cert_path.to_string_lossy() + ), + ) + })?; + ca_certificates.push(Certificate::from_pem(&ca_certificate)); + } + + let client_cert = fs::read(&auth.client_cert_path).map_err(|e| { + LnPoolError::from_invalid_credentials( + e.to_string(), + ServiceErrorSource::Internal, + format!( + "loading CLN credentials and reading client certificate from path {}", + auth.client_cert_path.to_string_lossy() + ), + ) + })?; + + let client_key = fs::read(&auth.client_key_path).map_err(|e| { + LnPoolError::from_invalid_credentials( + e.to_string(), + ServiceErrorSource::Internal, + format!( + "loading CLN credentials and reading client key from path {}", + auth.client_key_path.to_string_lossy() + ), + ) + })?; + + let identity = Identity::from_pem(client_cert, client_key); + Ok(Self { timeout, config, @@ -42,6 +91,8 @@ impl TonicClnGrpcClient { invoice_from_desc_hash: false, }), inner: Arc::new(Default::default()), + ca_certificates, + identity, }) } @@ -52,8 +103,10 @@ impl TonicClnGrpcClient { let inner_connect = Arc::new( InnerTonicClnGrpcClient::connect( self.timeout, - self.config.clone(), - self.config.url.clone(), + self.ca_certificates.clone(), + self.identity.clone(), + self.config.url.to_string(), + self.config.domain.as_deref(), ) .await?, ); @@ -68,6 +121,12 @@ impl TonicClnGrpcClient { let mut inner = self.inner.lock().await; *inner = None; } + + fn certificate_der_as_pem(certificate: &CertificateDer) -> String { + use base64::Engine; + let base64_cert = base64::engine::general_purpose::STANDARD.encode(certificate.as_ref()); + format!("-----BEGIN CERTIFICATE-----\n{base64_cert}\n-----END CERTIFICATE-----") + } } #[async_trait] @@ -110,21 +169,18 @@ impl LnRpcClient for TonicClnGrpcClient { struct InnerTonicClnGrpcClient { client: NodeClient, - config: ClnGrpcDiscoveryBackendImplementation, + url: String, } impl InnerTonicClnGrpcClient { async fn connect( timeout: Duration, - config: ClnGrpcDiscoveryBackendImplementation, - url: Url, + ca_certificates: Vec, + identity: Identity, + url: String, + domain: Option<&str>, ) -> Result { - let ClnGrpcClientAuth::Path(auth) = config.auth.clone(); - - let (ca_cert_data, client_cert_data, client_key_data) = - Self::load_client_credentials(&auth)?; - - let endpoint = Channel::from_shared(url.to_string()).map_err(|e| { + let endpoint = Channel::from_shared(url.clone()).map_err(|e| { LnPoolError::from_invalid_configuration( format!("Invalid endpoint URI: {}", e), ServiceErrorSource::Internal, @@ -132,78 +188,9 @@ impl InnerTonicClnGrpcClient { ) })?; - let channel = Self::connect_with_tls( - timeout, - &url, - endpoint, - &ca_cert_data, - &client_cert_data, - &client_key_data, - config.domain.as_deref(), - ) - .await?; - - let client = NodeClient::new(channel); - Ok(Self { client, config }) - } - - fn load_client_credentials( - auth: &ClnGrpcClientAuthPath, - ) -> Result { - let ca_cert_path = &auth.ca_cert_path; - let client_cert_path = &auth.client_cert_path; - let client_key_path = &auth.client_key_path; - - let ca_cert = fs::read(ca_cert_path).map_err(|e| { - LnPoolError::from_invalid_credentials( - e.to_string(), - ServiceErrorSource::Internal, - format!( - "loading CLN credentials and reading CA certificate from path {}", - ca_cert_path.to_string_lossy() - ), - ) - })?; - - let client_cert = fs::read(client_cert_path).map_err(|e| { - LnPoolError::from_invalid_credentials( - e.to_string(), - ServiceErrorSource::Internal, - format!( - "loading CLN credentials and reading client certificate from path {}", - client_cert_path.to_string_lossy() - ), - ) - })?; - - let client_key = fs::read(client_key_path).map_err(|e| { - LnPoolError::from_invalid_credentials( - e.to_string(), - ServiceErrorSource::Internal, - format!( - "loading CLN credentials and reading client key from path {}", - client_key_path.to_string_lossy() - ), - ) - })?; - - Ok((ca_cert, client_cert, client_key)) - } - - async fn connect_with_tls( - timeout: Duration, - url: &Url, - endpoint: Endpoint, - ca_cert: &[u8], - client_cert: &[u8], - client_key: &[u8], - domain: Option<&str>, - ) -> Result { - let ca_cert = Certificate::from_pem(ca_cert); - let identity = Identity::from_pem(client_cert, client_key); - let mut tls_config = ClientTlsConfig::new() - .ca_certificate(ca_cert) + .with_native_roots() + .ca_certificates(ca_certificates) .identity(identity); if let Some(domain) = domain { @@ -218,18 +205,21 @@ impl InnerTonicClnGrpcClient { ) })?; - endpoint + let channel = endpoint .connect_timeout(timeout) .timeout(timeout) .connect() .await .map_err(|e| { - LnPoolError::from_cln_transport_error( + LnPoolError::from_transport_error( e, ServiceErrorSource::Upstream, format!("connecting CLN client to {url}"), ) - }) + })?; + + let client = NodeClient::new(channel); + Ok(Self { client, url }) } async fn get_invoice<'a>( @@ -250,7 +240,7 @@ impl InnerTonicClnGrpcClient { ServiceErrorSource::Internal, format!( "CLN get invoice from {}, parsing invoice description", - self.config.url + self.url ), )) } @@ -262,7 +252,7 @@ impl InnerTonicClnGrpcClient { ServiceErrorSource::Internal, format!( "CLN get invoice from {}, getting current time for label", - self.config.url + self.url ), ) })?; @@ -289,12 +279,9 @@ impl InnerTonicClnGrpcClient { .invoice(request) .await .map_err(|e| { - LnPoolError::from_cln_tonic_error( + LnPoolError::from_tonic_error( e, - format!( - "CLN get invoice from {}, requesting invoice", - self.config.url - ), + format!("CLN get invoice from {}, requesting invoice", self.url), ) })? .into_inner(); @@ -312,12 +299,9 @@ impl InnerTonicClnGrpcClient { .list_peer_channels(channels_request) .await .map_err(|e| { - LnPoolError::from_cln_tonic_error( + LnPoolError::from_tonic_error( e, - format!( - "CLN get metrics for {}, requesting channels", - self.config.url - ), + format!("CLN get metrics for {}, requesting channels", self.url), ) })? .into_inner(); diff --git a/service/src/components/pool/cln/grpc/config.rs b/service/src/components/pool/cln/grpc/config.rs index d7809ff..fa32170 100644 --- a/service/src/components/pool/cln/grpc/config.rs +++ b/service/src/components/pool/cln/grpc/config.rs @@ -20,7 +20,7 @@ pub enum ClnGrpcClientAuth { #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ClnGrpcClientAuthPath { - pub ca_cert_path: PathBuf, + pub ca_cert_path: Option, pub client_cert_path: PathBuf, pub client_key_path: PathBuf, } diff --git a/service/src/components/pool/default_pool.rs b/service/src/components/pool/default_pool.rs index fbd345a..117d88c 100644 --- a/service/src/components/pool/default_pool.rs +++ b/service/src/components/pool/default_pool.rs @@ -12,6 +12,7 @@ use std::collections::HashMap; use std::fmt::Debug; use std::sync::{Arc, Mutex}; use std::time::Duration; +use tonic::transport::CertificateDer; type LnClientMap = HashMap + Send + Sync + 'static>>>; @@ -24,17 +25,22 @@ where timeout: Duration, pool: Arc>>, metrics_cache: Arc>>, + trusted_roots: Vec>, } impl DefaultLnClientPool where K: Clone + std::hash::Hash + Eq + Debug, { - pub fn new(timeout: Duration) -> DefaultLnClientPool { + pub fn new( + timeout: Duration, + trusted_roots: Vec>, + ) -> DefaultLnClientPool { Self { timeout, pool: Default::default(), metrics_cache: Default::default(), + trusted_roots, } } @@ -107,14 +113,18 @@ where } fn connect(&self, key: Self::Key, backend: &DiscoveryBackend) -> Result<(), Self::Error> { - let client: Box + std::marker::Send + Sync> = + let client: Box + Send + Sync> = match &backend.backend.implementation { - DiscoveryBackendImplementation::ClnGrpc(c) => { - Box::new(TonicClnGrpcClient::create(self.timeout, c.clone())?) - } - DiscoveryBackendImplementation::LndGrpc(c) => { - Box::new(TonicLndGrpcClient::create(self.timeout, c.clone())?) - } + DiscoveryBackendImplementation::ClnGrpc(c) => Box::new(TonicClnGrpcClient::create( + self.timeout, + c.clone(), + &self.trusted_roots, + )?), + DiscoveryBackendImplementation::LndGrpc(c) => Box::new(TonicLndGrpcClient::create( + self.timeout, + c.clone(), + &self.trusted_roots, + )?), DiscoveryBackendImplementation::RemoteHttp => { return Err(LnPoolError::from_invalid_configuration( "RemoteHttp backends not available".to_string(), diff --git a/service/src/components/pool/error.rs b/service/src/components/pool/error.rs index 3fddebf..fe59870 100644 --- a/service/src/components/pool/error.rs +++ b/service/src/components/pool/error.rs @@ -1,5 +1,4 @@ use crate::api::service::{HasServiceErrorSource, ServiceErrorSource}; -use http; use std::borrow::Cow; use std::fmt::{Display, Formatter}; use thiserror::Error; @@ -8,19 +7,13 @@ use tonic::{transport, Code, Status}; #[derive(Error, Debug)] pub enum LnPoolErrorSourceKind { #[error("CLN tonic gRPC error: {0}")] - ClnTonicError(Status), - #[error("LND tonic gRPC error: {0}")] - LndTonicError(Status), + TonicError(Status), #[error("CLN transport connection error: {0}")] - ClnTransportError(transport::Error), - #[error("LND transport connection error: {0}")] - LndTransportError(transport::Error), + TransportError(transport::Error), #[error("invalid configuration for: {0}")] InvalidConfiguration(String), #[error("invalid credentials for {0}")] InvalidCredentials(String), - #[error("invalid endpoint URI: {0}")] - InvalidEndpointUri(http::uri::InvalidUri), #[error("memory error: {0}")] MemoryError(String), } @@ -81,67 +74,18 @@ impl LnPoolError { ) } - pub fn from_cln_invalid_endpoint_uri>>( - invalid_uri: http::uri::InvalidUri, - esource: ServiceErrorSource, - context: C, - ) -> Self { - Self::new( - LnPoolErrorSourceKind::InvalidEndpointUri(invalid_uri), - esource, - context.into(), - ) - } - - pub fn from_cln_tonic_error>>(source: Status, context: C) -> Self { - let esource = Self::from_tonic_code(source.code()); - Self::new( - LnPoolErrorSourceKind::ClnTonicError(source), - esource, - context, - ) - } - - pub fn from_cln_tonic_error_with_esource>>( - source: Status, - esource: ServiceErrorSource, - context: C, - ) -> Self { - Self::new( - LnPoolErrorSourceKind::ClnTonicError(source), - esource, - context, - ) - } - - pub fn from_lnd_tonic_error>>(source: Status, context: C) -> Self { + pub fn from_tonic_error>>(source: Status, context: C) -> Self { let esource = Self::from_tonic_code(source.code()); - Self::new( - LnPoolErrorSourceKind::LndTonicError(source), - esource, - context, - ) - } - - pub fn from_lnd_tonic_error_with_esource>>( - source: Status, - esource: ServiceErrorSource, - context: C, - ) -> Self { - Self::new( - LnPoolErrorSourceKind::LndTonicError(source), - esource, - context, - ) + Self::new(LnPoolErrorSourceKind::TonicError(source), esource, context) } - pub fn from_cln_transport_error>>( + pub fn from_transport_error>>( source: transport::Error, esource: ServiceErrorSource, context: C, ) -> Self { Self::new( - LnPoolErrorSourceKind::ClnTransportError(source), + LnPoolErrorSourceKind::TransportError(source), esource, context, ) diff --git a/service/src/components/pool/lnd/grpc/client.rs b/service/src/components/pool/lnd/grpc/client.rs index ccb61ce..062c84d 100644 --- a/service/src/components/pool/lnd/grpc/client.rs +++ b/service/src/components/pool/lnd/grpc/client.rs @@ -1,22 +1,18 @@ use crate::api::service::ServiceErrorSource; use crate::components::pool::error::LnPoolError; use crate::components::pool::lnd::grpc::config::{ - LndGrpcClientAuth, LndGrpcClientAuthPath, LndGrpcDiscoveryBackendImplementation, + LndGrpcClientAuth, LndGrpcDiscoveryBackendImplementation, }; use crate::components::pool::{Bolt11InvoiceDescription, LnFeatures, LnMetrics, LnRpcClient}; use async_trait::async_trait; +use rustls::pki_types::CertificateDer; use sha2::Digest; +use std::fs; use std::sync::Arc; use std::time::Duration; use tokio::sync::Mutex; -use tonic::service::{interceptor::InterceptedService, Interceptor}; - -use hyper_timeout::TimeoutConnector; -use hyper_util::client::legacy::connect::HttpConnector; -use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; -use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; -use rustls::{ClientConfig, DigitallySignedStruct, Error as TlsError, SignatureScheme}; -use rustls_pemfile; +use tonic::service::Interceptor; +use tonic::transport::{Certificate, Channel, ClientTlsConfig}; #[allow(clippy::all)] pub mod lnrpc { @@ -25,28 +21,57 @@ pub mod lnrpc { use lnrpc::lightning_client::LightningClient; -type ClientCredentials = (Vec, Vec); - -type Service = InterceptedService< - hyper_util::client::legacy::Client< - TimeoutConnector>, - tonic::body::Body, - >, - MacaroonInterceptor, ->; - pub struct TonicLndGrpcClient { timeout: Duration, config: LndGrpcDiscoveryBackendImplementation, features: Option, inner: Arc>>>, + ca_certificates: Vec, + macaroon: String, } impl TonicLndGrpcClient { pub fn create( timeout: Duration, config: LndGrpcDiscoveryBackendImplementation, + trusted_roots: &[CertificateDer], ) -> Result { + let LndGrpcClientAuth::Path(auth) = &config.auth; + + let mut ca_certificates = trusted_roots + .iter() + .map(|c| { + let c = Self::certificate_der_as_pem(c); + Certificate::from_pem(&c) + }) + .collect::>(); + + if let Some(tls_cert_path) = &auth.tls_cert_path { + let ca_certificate = fs::read(tls_cert_path).map_err(|e| { + LnPoolError::from_invalid_credentials( + e.to_string(), + ServiceErrorSource::Internal, + format!( + "loading LND credentials and reading CA certificate from path {}", + tls_cert_path.to_string_lossy() + ), + ) + })?; + ca_certificates.push(Certificate::from_pem(&ca_certificate)); + } + + let macaroon = fs::read(&auth.macaroon_path).map_err(|e| { + LnPoolError::from_invalid_credentials( + e.to_string(), + ServiceErrorSource::Internal, + format!( + "loading LND macaroon from {}", + auth.macaroon_path.to_string_lossy() + ), + ) + })?; + let macaroon = hex::encode(&macaroon); + Ok(Self { timeout, config, @@ -54,6 +79,8 @@ impl TonicLndGrpcClient { invoice_from_desc_hash: true, }), inner: Arc::new(Default::default()), + ca_certificates, + macaroon, }) } @@ -62,7 +89,15 @@ impl TonicLndGrpcClient { match inner.as_ref() { None => { let inner_connect = Arc::new( - InnerTonicLndGrpcClient::connect(self.timeout, self.config.clone()).await?, + InnerTonicLndGrpcClient::connect( + self.timeout, + self.ca_certificates.clone(), + self.macaroon.clone(), + self.config.url.to_string(), + self.config.domain.as_deref(), + self.config.amp_invoice, + ) + .await?, ); *inner = Some(inner_connect.clone()); Ok(inner_connect) @@ -75,6 +110,12 @@ impl TonicLndGrpcClient { let mut inner = self.inner.lock().await; *inner = None; } + + fn certificate_der_as_pem(certificate: &CertificateDer) -> String { + use base64::Engine; + let base64_cert = base64::engine::general_purpose::STANDARD.encode(certificate.as_ref()); + format!("-----BEGIN CERTIFICATE-----\n{base64_cert}\n-----END CERTIFICATE-----") + } } #[async_trait] @@ -116,133 +157,67 @@ impl LnRpcClient for TonicLndGrpcClient { } struct InnerTonicLndGrpcClient { - client: LightningClient, - config: LndGrpcDiscoveryBackendImplementation, + client: LightningClient< + tonic::service::interceptor::InterceptedService, + >, + url: String, + amp_invoice: bool, } impl InnerTonicLndGrpcClient { async fn connect( timeout: Duration, - config: LndGrpcDiscoveryBackendImplementation, + ca_certificates: Vec, + macaroon: String, + url: String, + domain: Option<&str>, + amp_invoice: bool, ) -> Result { - let LndGrpcClientAuth::Path(auth) = config.auth.clone(); - - let (tls_cert, macaroon) = Self::load_client_credentials(&config, &auth).await?; - - let service = Self::connect_with_tls(&config, &tls_cert, &macaroon, timeout)?; - - let uri = config.url.to_string().parse().map_err(|e| { + let endpoint = Channel::from_shared(url.clone()).map_err(|e| { LnPoolError::from_invalid_configuration( - format!("Invalid URI: {}", e), + format!("Invalid endpoint URI: {}", e), ServiceErrorSource::Internal, - format!("parsing LND URL {}", config.url), + format!("LND connecting to endpoint address {url}"), ) })?; - let client = LightningClient::with_origin(service, uri); - Ok(Self { client, config }) - } + let mut tls_config = ClientTlsConfig::new() + .with_native_roots() + .ca_certificates(ca_certificates); - async fn load_client_credentials( - _config: &LndGrpcDiscoveryBackendImplementation, - auth: &LndGrpcClientAuthPath, - ) -> Result { - let tls_cert_path = &auth.tls_cert_path; - let macaroon_path = &auth.macaroon_path; + if let Some(domain) = domain { + tls_config = tls_config.domain_name(domain); + } - let tls_cert = tokio::fs::read(tls_cert_path).await.map_err(|e| { + let endpoint = endpoint.tls_config(tls_config).map_err(|e| { LnPoolError::from_invalid_credentials( e.to_string(), ServiceErrorSource::Internal, - format!( - "loading LND TLS certificate from {}", - tls_cert_path.to_string_lossy() - ), + format!("loading LND TLS configuration into client for {url}"), ) })?; - let macaroon = tokio::fs::read(macaroon_path).await.map_err(|e| { - LnPoolError::from_invalid_credentials( - e.to_string(), - ServiceErrorSource::Internal, - format!( - "loading LND macaroon from {}", - macaroon_path.to_string_lossy() - ), - ) - })?; - - Ok((tls_cert, macaroon)) - } - - fn connect_with_tls( - config: &LndGrpcDiscoveryBackendImplementation, - tls_cert_pem: &[u8], - macaroon_bytes: &[u8], - timeout: Duration, - ) -> Result { - let mut cert_reader = std::io::Cursor::new(tls_cert_pem); - let cert_der = rustls_pemfile::certs(&mut cert_reader) - .collect::, _>>() + let channel = endpoint + .connect_timeout(timeout) + .timeout(timeout) + .connect() + .await .map_err(|e| { - LnPoolError::from_invalid_credentials( - e.to_string(), - ServiceErrorSource::Internal, - format!("parsing LND TLS certificate from: {config:?}"), - ) - })? - .into_iter() - .next() - .ok_or_else(|| { - LnPoolError::from_invalid_credentials( - "No certificate found in PEM file".to_string(), - ServiceErrorSource::Internal, - format!("parsing LND TLS certificate from: {config:?}"), + LnPoolError::from_transport_error( + e, + ServiceErrorSource::Upstream, + format!("connecting LND client to {url}"), ) })?; - let crypto_provider = rustls::crypto::CryptoProvider::get_default() - .ok_or_else(|| { - LnPoolError::from_invalid_configuration( - "No default crypto provider installed", - ServiceErrorSource::Internal, - "getting default crypto provider for LND TLS verification", - ) - })? - .clone(); - - let tls_config = ClientConfig::builder() - .dangerous() - .with_custom_certificate_verifier(Arc::new(LndCertificateVerifier::new( - cert_der.to_vec(), - crypto_provider, - ))) - .with_no_client_auth(); - - let https_connector = hyper_rustls::HttpsConnectorBuilder::new() - .with_tls_config(tls_config) - .https_or_http() - .enable_http2() - .build(); - - let mut timeout_connector = TimeoutConnector::new(https_connector); - timeout_connector.set_connect_timeout(Some(timeout)); - timeout_connector.set_read_timeout(Some(timeout)); - timeout_connector.set_write_timeout(Some(timeout)); - - let http_client = - hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) - .build(timeout_connector); - - let macaroon_hex = hex::encode(macaroon_bytes); - let service = InterceptedService::new( - http_client, - MacaroonInterceptor { - macaroon: macaroon_hex, - }, - ); + let interceptor = MacaroonInterceptor { macaroon }; - Ok(service) + let client = LightningClient::with_interceptor(channel, interceptor); + Ok(Self { + client, + url, + amp_invoice, + }) } async fn get_invoice<'a>( @@ -266,7 +241,7 @@ impl InnerTonicLndGrpcClient { value_msat: amount_msat.unwrap_or(0) as i64, description_hash, expiry: expiry_secs.unwrap_or(3600) as i64, - is_amp: self.config.amp_invoice, + is_amp: self.amp_invoice, ..Default::default() }; @@ -274,12 +249,9 @@ impl InnerTonicLndGrpcClient { .add_invoice(invoice_request) .await .map_err(|e| { - LnPoolError::from_lnd_tonic_error( + LnPoolError::from_tonic_error( e, - format!( - "LND get invoice from {}, requesting invoice", - self.config.url - ), + format!("LND get invoice from {}, requesting invoice", self.url), ) })? .into_inner(); @@ -295,12 +267,9 @@ impl InnerTonicLndGrpcClient { .channel_balance(channel_balance_request) .await .map_err(|e| { - LnPoolError::from_lnd_tonic_error( + LnPoolError::from_tonic_error( e, - format!( - "LND get metrics for {}, requesting channels", - self.config.url - ), + format!("LND get metrics for {}, requesting channels", self.url), ) })? .into_inner(); @@ -317,62 +286,6 @@ impl InnerTonicLndGrpcClient { } } -#[derive(Debug)] -struct LndCertificateVerifier { - expected_cert: Vec, - supported_algs: rustls::crypto::WebPkiSupportedAlgorithms, -} - -impl LndCertificateVerifier { - fn new(cert_der: Vec, crypto_provider: Arc) -> Self { - Self { - expected_cert: cert_der, - supported_algs: crypto_provider.signature_verification_algorithms, - } - } -} - -impl ServerCertVerifier for LndCertificateVerifier { - fn verify_server_cert( - &self, - end_entity: &CertificateDer, - _intermediates: &[CertificateDer], - _server_name: &ServerName, - _ocsp_response: &[u8], - _now: UnixTime, - ) -> Result { - if end_entity.as_ref() == self.expected_cert.as_slice() { - Ok(ServerCertVerified::assertion()) - } else { - Err(TlsError::General( - "Server certificate does not match expected".to_string(), - )) - } - } - - fn verify_tls12_signature( - &self, - message: &[u8], - cert: &CertificateDer, - dss: &DigitallySignedStruct, - ) -> Result { - rustls::crypto::verify_tls12_signature(message, cert, dss, &self.supported_algs) - } - - fn verify_tls13_signature( - &self, - message: &[u8], - cert: &CertificateDer, - dss: &DigitallySignedStruct, - ) -> Result { - rustls::crypto::verify_tls13_signature(message, cert, dss, &self.supported_algs) - } - - fn supported_verify_schemes(&self) -> Vec { - self.supported_algs.supported_schemes() - } -} - #[derive(Clone)] struct MacaroonInterceptor { macaroon: String, diff --git a/service/src/components/pool/lnd/grpc/config.rs b/service/src/components/pool/lnd/grpc/config.rs index 17abe38..c41e88a 100644 --- a/service/src/components/pool/lnd/grpc/config.rs +++ b/service/src/components/pool/lnd/grpc/config.rs @@ -21,6 +21,6 @@ pub enum LndGrpcClientAuth { #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct LndGrpcClientAuthPath { - pub tls_cert_path: PathBuf, + pub tls_cert_path: Option, pub macaroon_path: PathBuf, } diff --git a/service/tests/common/mod.rs b/service/tests/common/mod.rs index f2ea46c..e1b8307 100644 --- a/service/tests/common/mod.rs +++ b/service/tests/common/mod.rs @@ -1,4 +1,3 @@ -pub mod db; pub mod discovery; pub mod offer; pub mod service; diff --git a/service/tests/discovery/db_mysql.rs b/service/tests/discovery/db_mysql.rs index c9d62e6..00cf675 100644 --- a/service/tests/discovery/db_mysql.rs +++ b/service/tests/discovery/db_mysql.rs @@ -1,10 +1,15 @@ -use crate::common::db::TestMysqlDatabase; use crate::common::discovery; +use anyhow::anyhow; use switchgear_service::components::discovery::db::DbDiscoveryBackendStore; +use switchgear_testing::db::TestMysqlDatabase; use switchgear_testing::services::IntegrationTestServices; use uuid::Uuid; async fn create_mysql_store() -> Option<(DbDiscoveryBackendStore, TestMysqlDatabase)> { + let _ = rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .map_err(|_| anyhow!("failed to stand up rustls encryption platform")); + let db_name = format!( "test_discovery_{}", Uuid::new_v4().to_string().replace("-", "") @@ -15,7 +20,7 @@ async fn create_mysql_store() -> Option<(DbDiscoveryBackendStore, TestMysqlDatab None => return None, Some(v) => v, }; - let db = TestMysqlDatabase::new(db_name, mysql); + let db = TestMysqlDatabase::new(db_name, mysql, false, None); let store = DbDiscoveryBackendStore::connect(db.connection_url(), 5) .await diff --git a/service/tests/discovery/db_postgres.rs b/service/tests/discovery/db_postgres.rs index b06ea53..37ad22e 100644 --- a/service/tests/discovery/db_postgres.rs +++ b/service/tests/discovery/db_postgres.rs @@ -1,10 +1,15 @@ -use crate::common::db::TestPostgresDatabase; use crate::common::discovery; +use anyhow::anyhow; use switchgear_service::components::discovery::db::DbDiscoveryBackendStore; +use switchgear_testing::db::TestPostgresDatabase; use switchgear_testing::services::IntegrationTestServices; use uuid::Uuid; async fn create_postgres_store() -> Option<(DbDiscoveryBackendStore, TestPostgresDatabase)> { + let _ = rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .map_err(|_| anyhow!("failed to stand up rustls encryption platform")); + let db_name = format!( "test_discovery_{}", Uuid::new_v4().to_string().replace("-", "") @@ -15,7 +20,7 @@ async fn create_postgres_store() -> Option<(DbDiscoveryBackendStore, TestPostgre None => return None, Some(v) => v, }; - let db = TestPostgresDatabase::new(db_name, postgres); + let db = TestPostgresDatabase::new(db_name, postgres, false, None); let store = DbDiscoveryBackendStore::connect(db.connection_url(), 5) .await diff --git a/service/tests/discovery/http.rs b/service/tests/discovery/http.rs index 0f12117..903aabd 100644 --- a/service/tests/discovery/http.rs +++ b/service/tests/discovery/http.rs @@ -1,19 +1,24 @@ use crate::common::{discovery, service}; +use anyhow::anyhow; use std::path::PathBuf; use std::time::Duration; use switchgear_service::api::discovery::HttpDiscoveryBackendClient; use switchgear_service::components::discovery::http::HttpDiscoveryBackendStore; async fn create_http_store() -> (HttpDiscoveryBackendStore, service::TestService) { + let _ = rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .map_err(|_| anyhow!("failed to stand up rustls encryption platform")); + let ports_path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")); let test_service = service::TestService::start(&ports_path).await.unwrap(); let base_url = test_service.discovery_base_url(); let store = HttpDiscoveryBackendStore::create( - base_url.parse().unwrap(), + base_url, Duration::from_secs(10), Duration::from_secs(10), - vec![], + &[], test_service.discovery_authorization.clone(), ) .unwrap(); diff --git a/service/tests/ln/cln.rs b/service/tests/ln/cln.rs index 1c85b47..8cf0d9f 100644 --- a/service/tests/ln/cln.rs +++ b/service/tests/ln/cln.rs @@ -1,5 +1,5 @@ use crate::try_create_cln_backend; -use anyhow::bail; +use anyhow::{anyhow, bail}; use bitcoin_hashes::Hash; use lightning_invoice::Bolt11Invoice; use rand::{distributions::Alphanumeric, Rng}; @@ -9,7 +9,7 @@ use std::time::Duration; use switchgear_service::api::discovery::DiscoveryBackendImplementation; use switchgear_service::components::pool::cln::grpc::client::TonicClnGrpcClient; use switchgear_service::components::pool::{Bolt11InvoiceDescription, LnRpcClient}; -use switchgear_testing::credentials::LnCredentials; +use switchgear_testing::credentials::lightning::LnCredentials; async fn try_create_cln_tonic_client( credentials: &LnCredentials, @@ -23,8 +23,9 @@ async fn try_create_cln_tonic_client( >, >, > { - // Install default crypto provider for rustls - let _ = rustls::crypto::ring::default_provider().install_default(); + let _ = rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .map_err(|_| anyhow!("failed to stand up rustls encryption platform")); let backend = match try_create_cln_backend(credentials)? { None => return Ok(None), @@ -34,7 +35,7 @@ async fn try_create_cln_tonic_client( }, }; - let client = TonicClnGrpcClient::create(Duration::from_secs(1), backend)?; + let client = TonicClnGrpcClient::create(Duration::from_secs(1), backend, &[])?; Ok(Some(Box::new(client))) } diff --git a/service/tests/ln/lnd.rs b/service/tests/ln/lnd.rs index 0096f99..9382a09 100644 --- a/service/tests/ln/lnd.rs +++ b/service/tests/ln/lnd.rs @@ -1,5 +1,5 @@ use crate::try_create_lnd_backend; -use anyhow::bail; +use anyhow::{anyhow, bail}; use bitcoin_hashes::Hash; use lightning_invoice::Bolt11Invoice; use rand::{distributions::Alphanumeric, Rng}; @@ -9,7 +9,7 @@ use std::time::Duration; use switchgear_service::api::discovery::DiscoveryBackendImplementation; use switchgear_service::components::pool::lnd::grpc::client::TonicLndGrpcClient; use switchgear_service::components::pool::{Bolt11InvoiceDescription, LnRpcClient}; -use switchgear_testing::credentials::LnCredentials; +use switchgear_testing::credentials::lightning::LnCredentials; type LnClientBox = Box< dyn LnRpcClient @@ -21,7 +21,9 @@ type LnClientBox = Box< async fn try_create_lnd_tonic_client( credentials: &LnCredentials, ) -> anyhow::Result> { - let _ = rustls::crypto::ring::default_provider().install_default(); + let _ = rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .map_err(|_| anyhow!("failed to stand up rustls encryption platform")); let backend = match try_create_lnd_backend(credentials)? { None => return Ok(None), @@ -31,7 +33,7 @@ async fn try_create_lnd_tonic_client( }, }; - let client = TonicLndGrpcClient::create(Duration::from_secs(1), backend)?; + let client = TonicLndGrpcClient::create(Duration::from_secs(1), backend, &[])?; Ok(Some(Box::new(client))) } @@ -87,6 +89,8 @@ async fn test_lnd_tonic_invoice_with_direct_description() { // Validate expiry assert_eq!(invoice.expiry_time().as_secs(), expected_expiry_secs); + + eprintln!("lnd success! credentials: {:?}", credentials.get_backends()); } #[tokio::test] diff --git a/service/tests/ln/main.rs b/service/tests/ln/main.rs index 1081537..698b223 100644 --- a/service/tests/ln/main.rs +++ b/service/tests/ln/main.rs @@ -8,7 +8,7 @@ use switchgear_service::components::pool::cln::grpc::config::{ use switchgear_service::components::pool::lnd::grpc::config::{ LndGrpcClientAuth, LndGrpcClientAuthPath, LndGrpcDiscoveryBackendImplementation, }; -use switchgear_testing::credentials::{LnCredentials, RegTestLnNode}; +use switchgear_testing::credentials::lightning::{LnCredentials, RegTestLnNode}; use url::Url; #[path = "../common/mod.rs"] @@ -43,11 +43,11 @@ pub fn try_create_cln_backend( DiscoveryBackendImplementation::ClnGrpc(ClnGrpcDiscoveryBackendImplementation { url, auth: ClnGrpcClientAuth::Path(ClnGrpcClientAuthPath { - ca_cert_path: cln_node.ca_cert_path, + ca_cert_path: cln_node.ca_cert_path.into(), client_cert_path: cln_node.client_cert_path, client_key_path: cln_node.client_key_path, }), - domain: Some(cln_node.sni), + domain: None, }); let backend = DiscoveryBackend { @@ -90,7 +90,7 @@ pub fn try_create_lnd_backend( DiscoveryBackendImplementation::LndGrpc(LndGrpcDiscoveryBackendImplementation { url, auth: LndGrpcClientAuth::Path(LndGrpcClientAuthPath { - tls_cert_path: lnd_node.tls_cert_path, + tls_cert_path: lnd_node.tls_cert_path.into(), macaroon_path: lnd_node.macaroon_path, }), amp_invoice: false, diff --git a/service/tests/offer/db_mysql.rs b/service/tests/offer/db_mysql.rs index 277031a..2abc39f 100644 --- a/service/tests/offer/db_mysql.rs +++ b/service/tests/offer/db_mysql.rs @@ -1,10 +1,15 @@ -use crate::common::db::TestMysqlDatabase; use crate::common::offer; +use anyhow::anyhow; use switchgear_service::components::offer::db::DbOfferStore; +use switchgear_testing::db::TestMysqlDatabase; use switchgear_testing::services::IntegrationTestServices; use uuid::Uuid; async fn create_mysql_store() -> Option<(DbOfferStore, TestMysqlDatabase)> { + let _ = rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .map_err(|_| anyhow!("failed to stand up rustls encryption platform")); + let db_name = format!( "test_discovery_{}", Uuid::new_v4().to_string().replace("-", "") @@ -15,7 +20,7 @@ async fn create_mysql_store() -> Option<(DbOfferStore, TestMysqlDatabase)> { None => return None, Some(v) => v, }; - let db = TestMysqlDatabase::new(db_name, mysql); + let db = TestMysqlDatabase::new(db_name, mysql, false, None); let store = DbOfferStore::connect(db.connection_url(), 5).await.unwrap(); store.migrate_up().await.unwrap(); diff --git a/service/tests/offer/db_postgres.rs b/service/tests/offer/db_postgres.rs index 1877d8e..3084525 100644 --- a/service/tests/offer/db_postgres.rs +++ b/service/tests/offer/db_postgres.rs @@ -1,10 +1,15 @@ -use crate::common::db::TestPostgresDatabase; use crate::common::offer; +use anyhow::anyhow; use switchgear_service::components::offer::db::DbOfferStore; +use switchgear_testing::db::TestPostgresDatabase; use switchgear_testing::services::IntegrationTestServices; use uuid::Uuid; async fn create_postgres_store() -> Option<(DbOfferStore, TestPostgresDatabase)> { + let _ = rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .map_err(|_| anyhow!("failed to stand up rustls encryption platform")); + let db_name = format!("test_offer_{}", Uuid::new_v4().to_string().replace("-", "")); let services = IntegrationTestServices::create().unwrap(); @@ -12,7 +17,7 @@ async fn create_postgres_store() -> Option<(DbOfferStore, TestPostgresDatabase)> None => return None, Some(v) => v, }; - let db = TestPostgresDatabase::new(db_name, postgres); + let db = TestPostgresDatabase::new(db_name, postgres, false, None); let store = DbOfferStore::connect(db.connection_url(), 5).await.unwrap(); store.migrate_up().await.unwrap(); diff --git a/service/tests/offer/http.rs b/service/tests/offer/http.rs index 0bd9636..92d3686 100644 --- a/service/tests/offer/http.rs +++ b/service/tests/offer/http.rs @@ -1,19 +1,24 @@ use crate::common::{offer, service}; +use anyhow::anyhow; use std::path::PathBuf; use std::time::Duration; use switchgear_service::api::offer::HttpOfferClient; use switchgear_service::components::offer::http::HttpOfferStore; async fn create_http_store() -> (HttpOfferStore, service::TestService) { + let _ = rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .map_err(|_| anyhow!("failed to stand up rustls encryption platform")); + let ports_path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")); let test_service = service::TestService::start(&ports_path).await.unwrap(); let base_url = test_service.offer_base_url(); let store = HttpOfferStore::create( - base_url.parse().unwrap(), + base_url, Duration::from_secs(10), Duration::from_secs(10), - vec![], + &[], test_service.offer_authorization.clone(), ) .unwrap(); diff --git a/switchgear/README.md b/switchgear/README.md index 4f5ef88..33fcf67 100644 --- a/switchgear/README.md +++ b/switchgear/README.md @@ -134,6 +134,18 @@ docker run bitshock/switchgear The image is configured with a default configuration file path of `/etc/swgr/config.yaml` . Mount a volume on top of `/etc/swgr` to provide your own configuration file. +To build the Docker image: + +```shell +docker buildx build --platform linux/arm64,linux/amd64 -t swgr . +``` + +Set the build arg `WEBPKI_ROOTS=true` if you need Mozilla's web PKI roots bundle installed on the image: + +```shell +docker buildx build --platform linux/arm64,linux/amd64 --build-arg WEBPKI_ROOTS=true -t swgr . +``` + ## Administration Switchgear can be configured by both the REST API and the CLI. @@ -372,6 +384,9 @@ lnurl-service: # Timeout in seconds for Lightning node client connections (float) ln-client-timeout-secs: 2.0 + + # Optional trusted roots pem bundle for all LN clients + ln-trusted-roots: "/etc/ssl/certs/ln-ca.pem" # List of allowed host headers for incoming requests # Used for safely generating callback/invoice URLs. @@ -418,7 +433,7 @@ lnurl-service: cert-path: "/etc/ssl/certs/lnurl-cert.pem" # Path to TLS private key file key-path: "/etc/ssl/certs/lnurl-key.pem" - + # QR module width x height bech32-qr-scale: 8 # QR light gray level @@ -562,8 +577,8 @@ store: connect-timeout-secs: 2.0 # Total timeout in seconds for complete request/response total-timeout-secs: 5.0 - # List of trusted CA certificate paths for TLS verification - trusted-roots: ["/etc/ssl/certs/ca.pem"] + # Optional pem bundle of trusted CA certificate paths for TLS verification + trusted-roots: "/etc/ssl/certs/ca.pem" # Path to bearer token file for authentication authorization: "/etc/ssl/certs/auth.token" ``` @@ -622,7 +637,7 @@ store: base-url: "https://discovery.internal:8081" connect-timeout-secs: 2.0 total-timeout-secs: 5.0 - trusted-roots: ["/etc/ssl/certs/internal-ca.pem"] + trusted-roots: "/etc/ssl/certs/internal-ca.pem" authorization: "/etc/ssl/certs/discovery.token" # Local database for Offers diff --git a/testing/Cargo.toml b/testing/Cargo.toml index 4ff8ab8..7978057 100644 --- a/testing/Cargo.toml +++ b/testing/Cargo.toml @@ -16,8 +16,10 @@ dotenvy = "0.15" flate2 = "1.1" fs4 = { version ="0.13" } hex = "0.4" -indexmap = "2.10" +indexmap = "2.12" secp256k1 = { version = "0.31", features = ["recovery", "serde"] } tar = "0.4" tempfile = "3.23" -ureq = "3.1" +ureq = { version = "3.1", default-features = false } +tokio = { version = "1", features = ["full"] } +sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "tls-rustls-no-provider-native-roots", "sqlite", "postgres", "mysql"] } diff --git a/testing/cln/Dockerfile b/testing/cln/Dockerfile new file mode 100644 index 0000000..f5a5b67 --- /dev/null +++ b/testing/cln/Dockerfile @@ -0,0 +1,9 @@ +FROM elementsproject/lightningd:v25.09 + +RUN apt-get update && apt-get install -y openssl && rm -rf /var/lib/apt/lists/* + +COPY init-tls.sh /usr/local/bin/init-tls.sh + +RUN chmod +x /usr/local/bin/init-tls.sh + +ENTRYPOINT ["/usr/local/bin/init-tls.sh"] diff --git a/testing/cln/init-tls.sh b/testing/cln/init-tls.sh new file mode 100644 index 0000000..3df309a --- /dev/null +++ b/testing/cln/init-tls.sh @@ -0,0 +1,129 @@ +#!/bin/sh +set -e + +CERT_DIR="/root/.lightning/regtest" +mkdir -p "$CERT_DIR" + +HOSTNAME="${CLN_HOSTNAME:-cln}" + +# Generate CA certificate and key +cat > "$CERT_DIR/openssl-ca.cnf" < "$CERT_DIR/openssl-server.cnf" < "$CERT_DIR/openssl-client.cnf" < "$CERT_DIR/openssl.cnf" < "$CERT_DIR/openssl.cnf" < "$CERT_DIR/openssl.cnf" < "$CREDS_DIR/cln/node_id" @@ -19,21 +21,14 @@ echo "$LND_PUBKEY" > "$CREDS_DIR/lnd/node_id" docker cp lnd-regtest:/root/.lnd/tls.cert "$CREDS_DIR/lnd/" docker cp lnd-regtest:/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon "$CREDS_DIR/lnd/" +docker cp postgres-db:/var/lib/postgresql/server.pem "$CREDS_DIR/postgres/" + +docker cp mysql-db:/etc/mysql/certs/server.pem "$CREDS_DIR/mysql/" + chmod -R 644 "$CREDS_DIR" chmod -R +X "$CREDS_DIR" -echo "" -echo "=== CREDENTIALS SUMMARY ===" -echo "CLN:" -echo " - Node ID: $(cat $CREDS_DIR/cln/node_id)" -echo " - ca.pem: $CREDS_DIR/cln/ca.pem" -echo " - client.pem: $CREDS_DIR/cln/client.pem" -echo " - client-key.pem: $CREDS_DIR/cln/client-key.pem" -echo "" -echo "LND:" -echo " - Node ID: $(cat $CREDS_DIR/lnd/node_id)" -echo " - tls.cert: $CREDS_DIR/lnd/tls.cert" -echo " - admin.macaroon: $CREDS_DIR/lnd/admin.macaroon" - cd /shared -tar -czf credentials.tar.gz credentials/ + +echo "=== CREDENTIALS ===" +tar cvzf credentials.tar.gz credentials/ diff --git a/testing/src/credentials/db.rs b/testing/src/credentials/db.rs new file mode 100644 index 0000000..f22dd88 --- /dev/null +++ b/testing/src/credentials/db.rs @@ -0,0 +1,111 @@ +use crate::credentials::download_credentials; +use crate::services::IntegrationTestServices; +use anyhow::Context; +use std::fs; +use std::path::PathBuf; +use tempfile::TempDir; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TestDatabase { + pub address: String, + pub ca_cert_path: PathBuf, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TestDatabases { + pub postgres: Option, + pub mysql: Option, +} + +pub struct DbCredentials { + inner: Option, +} + +struct DbCredentialsInner { + credentials_dir: TempDir, + postgres: String, + mysql: String, +} + +impl DbCredentials { + pub fn create() -> anyhow::Result { + let services = IntegrationTestServices::create()?; + + let inner = match ( + services.credentials(), + services.postgres(), + services.mysql(), + ) { + (Some(credentials), Some(postgres), Some(mysql)) => { + let credentials_dir = TempDir::new()?; + download_credentials(credentials_dir.path(), credentials)?; + Some(DbCredentialsInner { + credentials_dir, + postgres: postgres.to_string(), + mysql: mysql.to_string(), + }) + } + _ => None, + }; + Ok(Self { inner }) + } + + pub fn get_databases(&self) -> anyhow::Result { + let inner = match &self.inner { + None => { + return Ok(TestDatabases { + postgres: None, + mysql: None, + }) + } + Some(inner) => inner, + }; + + let credentials = inner.credentials_dir.path().join("credentials"); + let base_path = credentials.as_path(); + + let entries = fs::read_dir(base_path) + .with_context(|| format!("reading directory {}", base_path.display()))?; + + let mut postgres = None; + let mut mysql = None; + for entry in entries { + let entry = entry + .with_context(|| format!("reading directory entry in {}", base_path.display(),))?; + + let path = entry.path(); + + if !path.is_dir() { + continue; + } + + let dir_name = match path.file_name() { + Some(name) => match name.to_str() { + Some(s) => s, + None => continue, + }, + None => continue, + }; + + if dir_name == "postgres" { + postgres = Some(TestDatabase { + address: inner.postgres.to_string(), + ca_cert_path: path.join("server.pem"), + }); + } + + if dir_name == "mysql" { + mysql = Some(TestDatabase { + address: inner.mysql.to_string(), + ca_cert_path: path.join("server.pem"), + }); + } + + if postgres.is_some() && mysql.is_some() { + break; + } + } + + Ok(TestDatabases { postgres, mysql }) + } +} diff --git a/testing/src/credentials.rs b/testing/src/credentials/lightning.rs similarity index 82% rename from testing/src/credentials.rs rename to testing/src/credentials/lightning.rs index 5f81a86..4eb98db 100644 --- a/testing/src/credentials.rs +++ b/testing/src/credentials/lightning.rs @@ -1,10 +1,9 @@ +use crate::credentials::download_credentials; use crate::services::{IntegrationTestServices, LightningIntegrationTestServices}; use anyhow::Context; -use flate2::read::GzDecoder; use secp256k1::PublicKey; use std::fs; use std::path::{Path, PathBuf}; -use tar::Archive; use tempfile::TempDir; #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -14,7 +13,6 @@ pub struct ClnRegTestLnNode { pub ca_cert_path: PathBuf, pub client_cert_path: PathBuf, pub client_key_path: PathBuf, - pub sni: String, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -72,45 +70,20 @@ struct LnCredentialsInner { impl LnCredentials { pub fn create() -> anyhow::Result { let services = IntegrationTestServices::create()?; - let inner = match services.lightning() { - None => None, - Some(lightning) => { + let inner = match (services.credentials(), services.lightning()) { + (Some(credentials), Some(lightning)) => { let credentials_dir = TempDir::new()?; - Self::download_credentials(credentials_dir.path(), &lightning.credentials)?; + download_credentials(credentials_dir.path(), credentials)?; Some(LnCredentialsInner { credentials_dir, lightning: lightning.clone(), }) } + _ => None, }; Ok(Self { inner }) } - fn download_credentials(credentials_dir: &Path, credentials_url: &str) -> anyhow::Result<()> { - let download_path = credentials_dir.join("credentials.tar.gz"); - let response = ureq::get(credentials_url) - .call() - .with_context(|| format!("Downloading credentials from {}", credentials_url))?; - - let bytes = response - .into_body() - .read_to_vec() - .with_context(|| format!("Downloading credentials from {}", credentials_url))?; - - fs::write(&download_path, &bytes) - .with_context(|| format!("Downloading credentials from {}", credentials_url))?; - - let tar_gz = fs::File::open(&download_path) - .with_context(|| format!("Downloading credentials from {}", credentials_url))?; - - let tar = GzDecoder::new(tar_gz); - let mut archive = Archive::new(tar); - archive - .unpack(credentials_dir) - .with_context(|| format!("Downloading credentials from {}", credentials_url))?; - Ok(()) - } - pub fn get_backends(&self) -> anyhow::Result> { let inner = match &self.inner { None => return Ok(vec![]), @@ -212,7 +185,6 @@ impl LnCredentials { ca_cert_path, client_cert_path, client_key_path, - sni: "localhost".to_string(), }) } diff --git a/testing/src/credentials/mod.rs b/testing/src/credentials/mod.rs new file mode 100644 index 0000000..ec4117e --- /dev/null +++ b/testing/src/credentials/mod.rs @@ -0,0 +1,33 @@ +use anyhow::Context; +use flate2::read::GzDecoder; +use std::fs; +use std::path::Path; +use tar::Archive; + +pub mod db; +pub mod lightning; + +pub fn download_credentials(credentials_dir: &Path, credentials_url: &str) -> anyhow::Result<()> { + let download_path = credentials_dir.join("credentials.tar.gz"); + let response = ureq::get(credentials_url) + .call() + .with_context(|| format!("Downloading credentials from {}", credentials_url))?; + + let bytes = response + .into_body() + .read_to_vec() + .with_context(|| format!("Downloading credentials from {}", credentials_url))?; + + fs::write(&download_path, &bytes) + .with_context(|| format!("Downloading credentials from {}", credentials_url))?; + + let tar_gz = fs::File::open(&download_path) + .with_context(|| format!("Downloading credentials from {}", credentials_url))?; + + let tar = GzDecoder::new(tar_gz); + let mut archive = Archive::new(tar); + archive + .unpack(credentials_dir) + .with_context(|| format!("Downloading credentials from {}", credentials_url))?; + Ok(()) +} diff --git a/service/tests/common/db.rs b/testing/src/db.rs similarity index 82% rename from service/tests/common/db.rs rename to testing/src/db.rs index acd6443..cd60ca0 100644 --- a/service/tests/common/db.rs +++ b/testing/src/db.rs @@ -1,3 +1,4 @@ +use std::path::Path; use std::thread; pub struct TestMysqlDatabase { @@ -7,7 +8,7 @@ pub struct TestMysqlDatabase { } impl TestMysqlDatabase { - pub fn new(db_name: String, addr: &str) -> Self { + pub fn new(db_name: String, addr: &str, ssl: bool, ssl_ca: Option<&Path>) -> Self { let addr_c = addr.to_string(); let db_name_clone = db_name.clone(); let _ = thread::spawn(move || { @@ -34,7 +35,14 @@ impl TestMysqlDatabase { }) .join(); - let connection_url = format!("mysql://root:mysql@{addr}/{db_name}"); + let ssl = if ssl { "?ssl-mode=VERIFY_IDENTITY" } else { "" }; + + let ssl_ca = match (!ssl.is_empty(), ssl_ca) { + (true, Some(ssl_ca)) => format!("&ssl-ca={}", ssl_ca.to_string_lossy()), + (_, _) => "".to_string(), + }; + + let connection_url = format!("mysql://root:mysql@{addr}/{db_name}{ssl}{ssl_ca}"); Self { db_name, connection_url, @@ -84,7 +92,7 @@ pub struct TestPostgresDatabase { } impl TestPostgresDatabase { - pub fn new(db_name: String, addr: &str) -> Self { + pub fn new(db_name: String, addr: &str, ssl: bool, ssl_root_cert: Option<&Path>) -> Self { let db_name_clone = db_name.clone(); let addr_c = addr.to_string(); let _ = thread::spawn(move || { @@ -111,7 +119,18 @@ impl TestPostgresDatabase { }) .join(); - let connection_url = format!("postgres://postgres:postgres@{addr}/{db_name}"); + let ssl = if ssl { "?sslmode=verify-full" } else { "" }; + + let ssl_root_cert = match (!ssl.is_empty(), ssl_root_cert) { + (true, Some(ssl_root_cert)) => { + format!("&sslrootcert={}", ssl_root_cert.to_string_lossy()) + } + (_, _) => "".to_string(), + }; + + let connection_url = + format!("postgres://postgres:postgres@{addr}/{db_name}{ssl}{ssl_root_cert}"); + Self { db_name, connection_url, diff --git a/testing/src/lib.rs b/testing/src/lib.rs index c984fe9..2d1e647 100644 --- a/testing/src/lib.rs +++ b/testing/src/lib.rs @@ -1,3 +1,4 @@ pub mod credentials; +pub mod db; pub mod ports; pub mod services; diff --git a/testing/src/services.rs b/testing/src/services.rs index ea4acbc..8be8936 100644 --- a/testing/src/services.rs +++ b/testing/src/services.rs @@ -4,6 +4,7 @@ pub const SKIP_INTEGRATION_TESTS_ENV: &str = "SWGR_SKIP_INTEGRATION_TESTS"; #[derive(Debug)] pub struct IntegrationTestServices { + credentials: Option, postgres: Option, mysql: Option, lightning: Option, @@ -11,7 +12,6 @@ pub struct IntegrationTestServices { #[derive(Debug, Clone)] pub struct LightningIntegrationTestServices { - pub credentials: String, pub cln: String, pub lnd: String, } @@ -20,29 +20,38 @@ impl IntegrationTestServices { pub fn create() -> anyhow::Result { let _ = dotenvy::dotenv(); - let postgres = match Self::env_or_panic("POSTGRES_PORT") { + let credentials = match Self::env_or_panic("CREDENTIALS_SERVER_PORT") { None => None, Some(port) => { let port = port.parse::()?; - Self::env_or_panic("POSTGRES_HOSTNAME").map(|s| format!("{s}:{port}")) + Self::env_or_panic("CREDENTIALS_SERVER_HOSTNAME") + .map(|s| format!("http://{s}:{port}/credentials.tar.gz")) } }; - let mysql = match Self::env_or_panic("MYSQL_PORT") { - None => None, - Some(port) => { + if credentials.is_none() { + return Ok(Self { + credentials, + postgres: None, + mysql: None, + lightning: None, + }); + } + + let postgres = match (&credentials, Self::env_or_panic("POSTGRES_PORT")) { + (Some(_), Some(port)) => { let port = port.parse::()?; - Self::env_or_panic("MYSQL_HOSTNAME").map(|s| format!("{s}:{port}")) + Self::env_or_panic("POSTGRES_HOSTNAME").map(|s| format!("{s}:{port}")) } + _ => None, }; - let credentials = match Self::env_or_panic("CREDENTIALS_SERVER_PORT") { - None => None, - Some(port) => { + let mysql = match (&credentials, Self::env_or_panic("MYSQL_PORT")) { + (Some(_), Some(port)) => { let port = port.parse::()?; - Self::env_or_panic("CREDENTIALS_SERVER_HOSTNAME") - .map(|s| format!("http://{s}:{port}/credentials.tar.gz")) + Self::env_or_panic("MYSQL_HOSTNAME").map(|s| format!("{s}:{port}")) } + _ => None, }; let cln = match Self::env_or_panic("CLN_PORT") { @@ -61,16 +70,13 @@ impl IntegrationTestServices { } }; - let lightning = match (credentials, cln, lnd) { - (Some(credentials), Some(cln), Some(lnd)) => Some(LightningIntegrationTestServices { - credentials, - cln, - lnd, - }), + let lightning = match (&credentials, cln, lnd) { + (Some(_), Some(cln), Some(lnd)) => Some(LightningIntegrationTestServices { cln, lnd }), _ => None, }; Ok(Self { + credentials, postgres, mysql, lightning, @@ -112,6 +118,10 @@ SKIP INTEGRATION TESTS } } + pub fn credentials(&self) -> Option<&String> { + self.credentials.as_ref() + } + pub fn postgres(&self) -> Option<&String> { self.postgres.as_ref() }