From 52b4a56debe18c82d495bc3d873e0e499dba4f8d Mon Sep 17 00:00:00 2001 From: "Laurence A. Lee" Date: Wed, 15 Jul 2026 11:17:07 -1000 Subject: [PATCH] Add pluggable curb/Faraday HTTP client backends Makes the HTTP client backend pluggable (SOAP4R_HTTP_CLIENTS, mirroring the existing SOAP4R_PARSERS mechanism), adding curb and Faraday (typhoeus adapter) as opt-in backends alongside the existing httpclient/net_http. Broadens test coverage that used to be httpclient-only (backend-neutral wiredump parsing, SSL config coverage across all SSL-capable backends), fixing several real bugs surfaced in the process (Faraday :ciphers SSLOptions crash, URI.parse vs. already-a-URI object, curb Curl::Easy#ciphers= not existing, Tempfile finalizer bugs, Ruby 1.8.7 syntax regressions). Gates curb (>= 2.4, libcurl version requirement) and Faraday (>= 2.6, an enhancement for modern Rubies rather than a legacy capability -- older Faraday releases don't match this project's adapter assumptions) to their real floors, with CI skipping cleanly below those floors instead of hard-failing. Fixes a latent CI bug where `bundle config set with "..."` (Bundler 2.x-only syntax) silently no-opped on the Bundler 1.17.3 every Ruby below 3.2 falls back to -- curb/faraday were never actually being exercised in CI on those versions. Replaced with BUNDLE_WITH. Corrects an Ox dependency issue found via full-matrix regression testing: Ruby 2.2.x-2.6.x resolve Ox 2.14.14 unconstrained (Ox's own gemspec added a hard Ruby >= 2.7 floor starting at 2.14.15), and 2.14.14 segfaults inside Ox.sax_parse's :convert_special => true path on complex documents. htmlentities avoids that path entirely and is restored as a required dependency for that range; Ruby 1.9.3-2.1.x pins Ox exactly to 2.14.6 (decodes entities natively, no crash); Ruby 1.8.7 stays on the older Ox 2.4.5 (2.14.6 fails to build there). Scopes curb's already-documented libcurl/OpenSSL SSL exception on Ruby 2.4.10/2.5.9 to continue-on-error on just that step, instead of failing the whole job. Adds CHANGELOG.md holding the full investigation detail (version-by- version Ox testing, Faraday's architecture mismatch, the BUNDLE_WITH bug, CI's Docker-per-version architecture, full known-test-exceptions writeup) that had accumulated as long inline comments in Gemfile, ci.yml, and README.md -- those three files now carry short pointers instead. Full regression matrix (all 5 XML parsers x all applicable HTTP backends) re-run clean across 1.8.7, 1.9.3, 2.0.0, 2.1.10, 2.2.10, 2.3.8, 2.6.10 -- all green except 1.8.7, which shows only its pre-existing, already-documented exceptions. --- .github/workflows/ci.yml | 252 ++++++--------- CHANGELOG.md | 406 ++++++++++++++++++++++++ Gemfile | 180 ++++++----- README.md | 197 ++++-------- lib/soap/curbClient.rb | 286 +++++++++++++++++ lib/soap/faradayClient.rb | 290 +++++++++++++++++ lib/soap/httpbackend.rb | 11 +- lib/soap/httpbackend/curb.rb | 7 + lib/soap/httpbackend/faraday.rb | 7 + lib/soap/httpbackend/http_access2.rb | 14 - lib/wsdl/xmlSchema/importer.rb | 14 +- lib/xsd/datatypes.rb | 14 +- lib/xsd/xmlparser/oxparser.rb | 12 +- test/soap/asp.net/test_aspdotnet.rb | 35 +- test/soap/auth/test_basic.rb | 19 +- test/soap/auth/test_digest.rb | 19 +- test/soap/ssl/test_ssl.rb | 134 +++++++- test/soap/test_cookie.rb | 20 +- test/soap/test_nethttpclient.rb | 12 +- test/soap/test_no_indent.rb | 12 +- test/soap/test_streamhandler.rb | 35 +- test/testutil.rb | 21 ++ test/wsdl/anonymous/test_anonymous.rb | 9 - test/wsdl/qualified/test_qualified.rb | 11 +- test/wsdl/qualified/test_unqualified.rb | 11 +- test/wsdl/rpc/test_rpc_lit.rb | 13 +- 26 files changed, 1518 insertions(+), 523 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 lib/soap/curbClient.rb create mode 100644 lib/soap/faradayClient.rb create mode 100644 lib/soap/httpbackend/curb.rb create mode 100644 lib/soap/httpbackend/faraday.rb delete mode 100644 lib/soap/httpbackend/http_access2.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66014d7d8..1922dbbd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,48 +8,15 @@ on: workflow_dispatch: jobs: - # Runs test:deep (the complete suite, not just the surface subset) inside - # the exact same official Docker Hub `ruby` image used for local - # validation of each version -- confirmed clean for every version here - # across all 5 parsers (see .github/docker/ and the "Known Test Suite - # Exceptions" section in the README for the few narrow, understood - # exceptions on 3.0/3.1/3.2). - # - # This replaces an earlier ruby/setup-ruby-based matrix that ran directly - # on the GitHub-hosted runner VM (Ubuntu). That base kept drifting out from - # under local validation in ways that cost a lot of debugging time: Ubuntu - # rolling ubuntu-latest forward from under a pinned Ruby version, - # ruby-builder not always publishing a binary for every Ruby x Ubuntu - # combination, and Ubuntu 22.04's ICU-enabled system libxml2 colliding with - # libxml-ruby's C extension in a way that never reproduced locally against - # Debian. Running inside the identical container we test against locally - # makes "works locally" and "works in CI" the same claim by construction. - # - # This deliberately does NOT use the job-level `container:` key. GitHub - # Actions runs container jobs by injecting its own Node.js runtime into the - # container to execute JS-based steps like actions/checkout -- and most of - # these official `ruby` images are old enough (Debian jessie/stretch-era - # glibc) that the injected Node 24 binary can't even execute - # ("version `GLIBC_2.27' not found"), failing checkout before a single test - # runs. Confirmed empirically: every job up through Ruby 2.7 failed this - # way when first tried with `container:`. Instead, checkout runs on the - # (modern) runner VM as normal, and a plain `docker run` does the actual - # bundle install / test run inside the target image -- exactly the pattern - # already used below for 1.8.7/1.9.3/2.0.0, which never had this problem - # because they never used `container:` in the first place. - # - # Exact patch-level tags are pinned (not the bare X.Y floating tag) to - # match precisely what was validated -- these are all EOL/frozen lines on - # Docker Hub so the floating tag would resolve to the same image anyway, - # but pinning removes any ambiguity. + # Runs test:deep inside the exact same official Docker Hub `ruby` image + # used for local validation, via plain `docker run` (not the job-level + # `container:` key -- that injects a Node runtime these old images can't + # run). See CHANGELOG.md for the full architecture rationale. test: name: Ruby ${{ matrix.ruby }} runs-on: ubuntu-latest - # continue-on-error is set for 3.0/3.1/3.2 specifically: all three have - # one confirmed, understood, non-soap4r-bug failure (test_exception in - # marshaltestlib.rb compares a live Test::Unit::TestCase#inspect dump - # that happens to render differently on this narrow range of Ruby - # patch versions) -- see "Known Test Suite Exceptions" in the README. + # 3.0.7/3.1.7/3.2.11 have one known, non-soap4r-bug failure each -- see + # "Known Test Suite Exceptions" in the README. continue-on-error: ${{ contains(fromJSON('["3.0.7", "3.1.7", "3.2.11"]'), matrix.ruby) }} strategy: fail-fast: false @@ -81,29 +48,14 @@ jobs: || gem install bundler -v 1.17.3 --no-document 2>&1 \ || gem install bundler -v 1.17.3 --no-ri --no-rdoc bundle install - # set -e above covers setup (nothing to test if that fails), but - # must NOT apply to the loop itself: rake test:deep exits non-zero - # on any test failure, and 3.0.7/3.1.7/3.2.11 have exactly one - # guaranteed failure on every single parser (see README) -- under - # set -e that killed the script after oxparser alone, silently - # skipping nokogiri/libxml/oga/rexml every run without anyone - # noticing (confirmed: CI logs showed only one parser ever ran - # for those versions). Track failures instead and exit at the end - # so every parser actually runs regardless of earlier ones. + # set -e must not wrap the loop itself: rake test:deep exits + # non-zero per failure, and some versions have exactly one + # guaranteed failure on every parser (see README) -- track + # failures instead so every parser still runs. See CHANGELOG.md. set +e FAILED=0 for parser in oxparser nokogiriparser libxmlparser ogaparser rexmlparser; do - # All 5 parsers run as separate `rake test:deep` invocations - # sequentially in this one long-lived container, sharing a - # hardcoded port (17171) used throughout the test suite -- - # confirmed via CI logs (Ruby 2.7.8, run 28854082576) that - # something can be left - # bound to that port across that process boundary for minutes, - # well past the widened retry budget in test/testutil.rb (60 - # tries/1s). Never reproduced locally, and not root-caused - # (suspected a WEBrick/CGI-handler subprocess not fully - # released), so force-clear the port before every parser - # regardless of cause rather than keep guessing at timing. + # Force-clear the shared test port between runs -- see CHANGELOG.md. fuser -k -n tcp 17171 >/dev/null 2>&1 || true echo "--- SOAP4R_PARSERS=$parser ---" SOAP4R_PARSERS=$parser bundle exec rake test:deep @@ -113,17 +65,10 @@ jobs: exit $FAILED ' - # HTTP client backend is independently selectable too (see - # lib/soap/httpbackend.rb, same mechanism as SOAP4R_PARSERS above). - # httpclient is already exercised by every run of the loop above (it's - # the default), so this only adds the net_http fallback -- the one - # backend that, prior to this, no test in the suite could ever - # actually reach (it's only selected when both httpclient and - # http-access2 fail to load, and this project's Gemfile always - # installs httpclient). http-access2 itself is left out: it's no - # longer published on RubyGems.org at all, so there's nothing - # installable to sweep. A single parser (oxparser) is enough here -- - # this is about exercising the HTTP backend, not the XML parser. + # HTTP client backend is independently selectable (lib/soap/httpbackend.rb). + # httpclient is already exercised above (the default); this adds the + # net_http fallback. One parser (oxparser) is enough -- this tests the + # HTTP backend, not the XML parser. - name: Run net_http HTTP client backend (oxparser) run: | docker run --rm -v "${{ github.workspace }}":/repo-ro:ro ruby:${{ matrix.ruby }} bash -c ' @@ -138,20 +83,72 @@ jobs: SOAP4R_HTTP_CLIENTS=net_http SOAP4R_PARSERS=oxparser bundle exec rake test:deep ' - # 1.9.3 and 2.0.0 have official Docker Hub images, but every tag in both - # lines was published in the legacy Docker manifest v1 schema and is no - # longer pullable at all -- confirmed empirically ("not implemented: media - # type ... manifest.v1+prettyjws ... no longer supported since containerd - # v2.1") against every 1.9.x/2.0.x tag tried (bare, -slim, every specific - # patch level). GitHub-hosted runners are on similarly current - # Docker/containerd and hit the same wall, so `container: image: ruby:1.9.3` - # is a dead end here, not just a local quirk. Built from source instead via - # rbenv/ruby-build on Debian bullseye (Dockerfile.legacy) -- confirmed - # clean across all 5 parsers for both versions, including libxmlparser - # (Debian bullseye's libxml2-dev doesn't have the ICU/`bool`-collision - # problem ubuntu-22.04's does) and ogaparser (oga's ruby-ll dependency - # installs and works fine on both, contrary to earlier assumptions that it - # needed Ruby >= 2.1 -- that constraint turned out to only apply to 1.8). + # curb is opt-in (Gemfile group :http_curb, optional: true) and gated + # to Ruby >= 2.4; below that floor this step skips explicitly rather + # than hard-failing the job. Uses BUNDLE_WITH (env var), not `bundle + # config set with "..."` -- the latter is Bundler 2.x-only syntax and + # silently no-ops on the Bundler 1.17.3 every Ruby below 3.2 falls + # back to. See CHANGELOG.md for both bugs in full. + # + # continue-on-error scoped to just this step (not the whole job) for + # 2.4.10/2.5.9 specifically: their Docker images ship a libcurl/OpenSSL + # pair that fails CA chain validation on test_ca_verification/ + # test_ciphers -- a known, environment-specific CANTFIX (see "Known + # Test Suite Exceptions" in README.md), not a real regression. Every + # other step for these two versions (main parser sweep, net_http, + # faraday) is left fully enforced. + - name: Run curb HTTP client backend (oxparser) + continue-on-error: ${{ contains(fromJSON('["2.4.10", "2.5.9"]'), matrix.ruby) }} + run: | + docker run --rm -v "${{ github.workspace }}":/repo-ro:ro ruby:${{ matrix.ruby }} bash -c ' + set -e + if ! ruby -e "exit(RUBY_VERSION.to_f >= 2.4 ? 0 : 1)"; then + echo "Skipping: curb is gated to Ruby >= 2.4 in the Gemfile." + exit 0 + fi + export BUNDLE_WITH="http_curb" + apt-get update -qq && apt-get install -y -qq psmisc libcurl4-openssl-dev pkg-config build-essential + mkdir -p /app && cp -r /repo-ro/. /app/ && cd /app + gem install bundler --no-document 2>&1 \ + || gem install bundler -v 1.17.3 --no-document 2>&1 \ + || gem install bundler -v 1.17.3 --no-ri --no-rdoc + bundle install + fuser -k -n tcp 17171 >/dev/null 2>&1 || true + SOAP4R_HTTP_CLIENTS=curb SOAP4R_PARSERS=oxparser bundle exec rake test:deep + ' + + # Faraday's default adapter (:net_http) doesn't get its own step -- + # net_http above already covers that path end-to-end. :typhoeus + # (libcurl-based, via ethon/FFI) is the adapter this project tests + # against instead, and gated to Ruby >= 2.6 same as Faraday itself + # (see Gemfile/CHANGELOG.md); below that floor this step skips + # explicitly rather than hard-failing the job. + - name: Run faraday HTTP client backend, typhoeus adapter (oxparser) + run: | + docker run --rm -v "${{ github.workspace }}":/repo-ro:ro ruby:${{ matrix.ruby }} bash -c ' + set -e + if ! ruby -e "exit((RUBY_VERSION.to_f >= 2.6 && RUBY_PLATFORM !~ /java/) ? 0 : 1)"; then + echo "Skipping: Faraday is gated to Ruby >= 2.6 in the Gemfile." + exit 0 + fi + export BUNDLE_WITH="http_faraday" + apt-get update -qq && apt-get install -y -qq psmisc libcurl4-openssl-dev + mkdir -p /app && cp -r /repo-ro/. /app/ && cd /app + gem install bundler --no-document 2>&1 \ + || gem install bundler -v 1.17.3 --no-document 2>&1 \ + || gem install bundler -v 1.17.3 --no-ri --no-rdoc + bundle install + fuser -k -n tcp 17171 >/dev/null 2>&1 || true + SOAP4R_HTTP_CLIENTS=faraday SOAP4R_FARADAY_ADAPTER=typhoeus SOAP4R_PARSERS=oxparser bundle exec rake test:deep + ' + + # :patron was tried as a third spot-check adapter and dropped from CI: + # a real upstream patron bug, not ours to fix -- see "Known Test Suite + # Exceptions" in README.md. + + # 1.9.3/2.0.0's official Docker Hub images use the legacy manifest v1 + # schema and are no longer pullable at all. Built from source instead via + # rbenv/ruby-build on Debian bullseye (Dockerfile.legacy). See CHANGELOG.md. test-legacy-source-build: name: Ruby ${{ matrix.ruby }} (built from source via rbenv) runs-on: ubuntu-latest @@ -180,9 +177,7 @@ jobs: || gem install bundler -v 1.17.3 --no-ri --no-rdoc rbenv rehash 2>&1 || true bundle install - # See the `test` job above for why set -e must not wrap this loop, - # and why each iteration force-clears port 17171 first (psmisc is - # baked into this Dockerfile already). + # See the `test` job above re: set -e and port-clearing. set +e FAILED=0 for parser in oxparser nokogiriparser libxmlparser ogaparser rexmlparser; do @@ -195,30 +190,11 @@ jobs: exit $FAILED ' - # Ruby 1.8.7 has no official Docker Hub image at all (the "ruby" library - # starts at 1.9) and no ruby-build definition with OpenSSL handling, so - # rbenv/ruby-build can't get this one for free the way 1.9.3/2.0.0 above - # do. Dockerfile.legacy187 builds it from source against a vendored - # OpenSSL 1.0.2u instead (confirmed working: openssl.so builds, and - # OpenSSL::SSL::SSLContext.new instantiates correctly at runtime). - # - # oga is intentionally excluded from the parser loop: it was never - # installed for Ruby <= 1.8 in the first place (its ruby-ll dependency - # needs Ruby >= 2.1 -- confirmed this constraint is real for 1.8, unlike - # 1.9.3 above where ruby-ll installs and works fine), so xsd/xmlparser.rb's - # cascade correctly reports it unavailable -- that's an expected, - # non-zero-exit "failure", not a real one, so it's left out here rather - # than treated as a test result. - # - # continue-on-error is set: this job has two confirmed, understood, - # non-soap4r-bug failure categories -- Kernel#singleton_class not - # existing until Ruby 1.9.2 (see README), and a deeper interaction - # between Logger::Application and the CGI test suite's stripped-ENV - # subprocess spawn that wasn't fully root-caused (narrowed to - # lib/soap/rpc/cgistub.rb#run's `logger` call raising NameError only in - # that specific spawned-subprocess context; not reproducible in an - # isolated minimal case) -- both confined to CGI-based tests - # (test_calc_cgi, test_authheader_cgi) on this one Ruby version. + # Ruby 1.8.7 has no official Docker Hub image; Dockerfile.legacy187 builds + # it from source against a vendored OpenSSL 1.0.2u. oga is excluded from + # the parser loop (its ruby-ll dependency genuinely needs Ruby >= 2.1). + # continue-on-error: known, understood failures -- see "Known Test Suite + # Exceptions" in README.md / CHANGELOG.md. test-ruby-1_8_7: name: Ruby 1.8.7 (built from source, vendored OpenSSL) runs-on: ubuntu-latest @@ -238,12 +214,7 @@ jobs: || gem install bundler -v 1.17.3 --no-document 2>&1 \ || gem install bundler -v 1.17.3 --no-ri --no-rdoc bundle install - # See the comment on the `test` job above for why set -e must not wrap this - # loop -- 1.8.7 has guaranteed failures on every parser - # (singleton_class), so this job in particular was only ever - # actually testing oxparser before this fix. Also force-clears - # port 17171 before each iteration (psmisc is baked into this - # Dockerfile already) -- see the comment on the `test` job above for why. + # See the `test` job above re: set -e and port-clearing. set +e FAILED=0 for parser in oxparser nokogiriparser libxmlparser rexmlparser; do @@ -256,23 +227,10 @@ jobs: exit $FAILED ' - # JRuby: nokogiri, oga, and rexml all work correctly. ox and libxml-ruby - # have no viable JRuby-compatible gem at all (ox has never had a JRuby - # port; libxml-jruby's one and only release is from 2010 and calls a - # since-removed JRuby API) -- both are excluded from the parser loop for - # the same reason oga is excluded from the 1.8.7 job above. - # - # Runs inside the official jruby Docker Hub image (same as local - # validation) via plain `docker run` rather than the job-level `container:` - # key -- see the long comment on the `test` job above for why: GitHub - # Actions injects its own Node.js runtime into `container:` jobs to run - # actions/checkout, and that crashed here too (exit 127) against this - # image's libc. - # - # continue-on-error is set because the remaining JRuby-specific behavioral - # differences are known and confined to environment/dependency quirks - # rather than soap4r-ng bugs -- see "Known Test Suite Exceptions" in the - # README for the root-cause breakdown. + # JRuby: ox and libxml-ruby have no viable JRuby gem, so both are excluded + # from the parser loop. continue-on-error: remaining differences are known + # environment/dependency quirks, not soap4r-ng bugs -- see "Known Test + # Suite Exceptions" in README.md. test-jruby: name: JRuby ${{ matrix.ruby }} runs-on: ubuntu-latest @@ -290,22 +248,12 @@ jobs: run: | docker run --rm -v "${{ github.workspace }}":/repo-ro:ro jruby:${{ matrix.ruby }} bash -c ' set -e - # The official jruby image does not ship git, but the Gemfile - # pulls rubyjedi-testunitxml straight from its GitHub repo, so - # bundle install needs it -- confirmed missing locally too - # (apt-get install was already required there, just got dropped - # when this job was rewritten to a plain docker run). psmisc - # (fuser) is for the port-clearing step below. + # git: Gemfile pulls rubyjedi-testunitxml from GitHub. psmisc: port-clearing below. apt-get update -qq && apt-get install -y -qq git psmisc mkdir -p /app && cp -r /repo-ro/. /app/ && cd /app gem install bundler --no-document bundle install - # See the comment on the `test` job above for why set -e must not wrap this - # loop -- JRuby has the same guaranteed-every-parser failure - # count (see README), so this job was also only ever actually - # testing nokogiriparser before this fix. Also force-clears port - # 17171 before each iteration for the same reason as the `test` - # job. + # See the `test` job above re: set -e and port-clearing. set +e FAILED=0 for parser in nokogiriparser ogaparser rexmlparser; do @@ -351,15 +299,9 @@ jobs: } }; - // core.summary.addTable() emits an actual element rather - // than hand-rolled markdown pipe syntax. That matters here: a - // markdown table placed immediately after addHeading()'s

- // HTML with no blank line in between doesn't get parsed as a - // table at all (GFM requires a fresh block boundary before table - // syntax) -- confirmed on a live run, where the whole thing - // rendered as one literal-pipe-character paragraph instead of a - // table. An HTML table sidesteps that block-adjacency rule - // entirely. + // addTable() emits a real

, not markdown pipe syntax -- + // a markdown table right after addHeading()'s

doesn't + // render as a table at all (GFM block-adjacency rule). See CHANGELOG.md. const rows = [ [ { data: 'Job', header: true }, diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..930f97029 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,406 @@ +# Changelog + +Detailed rationale and investigation notes for non-obvious dependency and +compatibility decisions. Kept separate from README.md/code comments so +those stay skimmable; this is the place to look for the "why" in full. + +## Unreleased + +### Faraday gated to Ruby >= 2.6 + +`gem 'faraday'` was previously unconstrained, which resolved *some* old +release for every Ruby down to 1.9.3 -- but that old-Faraday territory +turned out to be broken, not just untested: + +- Below Faraday 1.10 (resolved as far up as Ruby 2.3.8), net_http support + is bundled directly into the main `faraday` gem with no separate + `faraday-net_http` package. `lib/soap/faradayClient.rb`'s + `require "faraday/#{adapter}"` convention assumes the modern + split-into-per-adapter-gems architecture, so it can't load any adapter + at all there -- an architecture mismatch, not a version-selection bug. +- Ruby 2.4.10/2.5.9 resolve Faraday 1.10.6, which does carry a real + `faraday-net_http` dependency and gets further, but hits a distinct bug: + `TypeError: wrong argument (String)! (Expected kind of + OpenSSL::X509::Certificate)` deep inside `Net::HTTP#connect`, from + `faradayClient.rb` passing a raw file-path string where curb's own + client already correctly converts to a parsed certificate object. + +Decision: Faraday is a modern-transport enhancement, not a legacy +capability worth carrying forward two Ruby versions for. Gated to +`>= 2.6` outright, matching `faraday-typhoeus`'s own real floor +(confirmed: its oldest RubyGems release needs Ruby >= 2.5's `logger`, and +the next release requires Ruby >= 2.6 directly). `faraday-patron` bumped +to the same floor since an adapter gem is inert without its host. + +`curb` has the equivalent floor for an unrelated reason: its C extension +references `CURL_SSLVERSION_MAX_*` constants absent before libcurl 7.54.0. +The official `ruby:2.2.10`/`ruby:2.3.8` Docker Hub images' system +`libcurl-dev` predates that (confirmed hard compile failure, not a +graceful skip); `ruby:2.4.10`'s image is new enough. + +CI had a latent bug matching both of these: `bundle config set with "..."` +is Bundler 2.x-only syntax. Bundler 1.17.3 (what every Ruby < 3.2 falls +back to) has no `set` verb and silently creates a bogus config key +literally named `"set"` -- meaning curb/faraday were never actually +exercised in CI below Ruby 3.2. Fixed with the `BUNDLE_WITH="group1:group2"` +env var instead (confirmed identical behavior on both Bundler +generations), exported (not just prefixed on the `bundle install` line) +since `bundle exec`'s own `Bundler.setup` re-checks group inclusion +independently for that invocation too. Both `ci.yml` steps also needed +explicit skip-and-exit-0 logic below their respective floors -- +previously a Ruby version below the floor would hard-fail the whole job +(`RuntimeError: HTTP client backend not found`) rather than skip cleanly. + +### Ox pin split three ways (dropping htmlentities where possible) + +The Gemfile pins Ox differently depending on Ruby version because +compatibility and entity-decoding behavior don't move together, or even +monotonically, across releases -- confirmed by installing every Ox +release from 2.4.5 through 2.14.28 across Ruby 1.8.7, 1.9.3, 2.0.0, and +2.1.10 and testing both (a) whether it loads and (b) whether it decodes +named HTML entities (`…`, `—`) natively via +`:convert_special => true`: + +- Entity decoding never improves anywhere in 2.4.5-2.8.0 (identical, + undecoded output throughout every version tested). It first improves at + **2.13.4** (2020-09-12), which decodes the same named-entity set + htmlentities does, byte-identical on direct comparison. +- **2.14.7** is the first release to call `rb_utf8_str_new`, a symbol not + exported by Ruby's shared lib before 2.2 -- `undefined symbol: + rb_utf8_str_new` at native-ext load time on Ruby 2.0.0/2.1.10. Oddly, + this does *not* reproduce on Ruby 1.9.3 even with the same Ox version -- + Ox's extconf must feature-detect the symbol differently depending on + which Ruby's headers are present at compile time, so the boundary isn't + a simple "older Ruby is safer" gradient; each version needs testing + directly rather than assumed from a neighbor. +- **2.14.6** is therefore the newest release confirmed to work, with + correct entity decoding, on 1.9.3, 2.0.0, *and* 2.1.10 uniformly. Pinned + exactly (`= 2.14.6`), since the very next patch release breaks it. +- **Ruby 1.8.7** can't use 2.14.6 at all: its `extconf.rb` fails outright + with `undefined method 'slice!' for nil:NilClass`, an mkmf/extconf + incompatibility with 1.8.7's build tooling, unrelated to the + `rb_utf8_str_new` issue above. Falls back to the older `= 2.4.5` pin, + which does compile and load there but only decodes the basic 5 XML + entities -- htmlentities is still required, and only there. + - Pinned exactly (`= 2.4.5`, not `~>`): later 2.4.x patches (confirmed + with 2.4.13) hit a *second*, different unresolvable-symbol crash + (`RSTRUCT_GET`) on Ruby 2.0.0/2.1.10 -- though notably *not* on + 1.8.7/1.9.3, the same non-monotonic pattern as the 2.14.x boundary + above. Moot now that 1.8.7 is the only Ruby left in this branch, but a + floating patch-level constraint would silently regress this the + moment that changes. + - `htmlentities 4.3.1` pinned exactly there too: 4.3.3+ hard-fails on + 1.8.7 with `NameError: uninitialized constant + HTMLEntities::Decoder::Encoding` (references Ruby's `Encoding` class, + introduced in 1.9, absent on 1.8.7). + +A CRLF regression was found and fixed along the way, incidental to this +investigation: `lib/xsd/xmlparser/oxparser.rb`'s no-decoder branch used +`:skip => :skip_return`, which discards `\r` -- unrelated to entity +decoding, but broke CRLF round-tripping (`test_string_crlf`) for anyone +running without htmlentities installed. Changed to `:skip_none` +unconditionally. + +Regression matrix re-run after these changes: 1.9.3, 2.0.0, and 2.1.10 all +green; 1.8.7 shows only its pre-existing, already-documented exceptions +(see "Known Test Suite Exceptions" below) -- no new failures introduced. +**This re-run did not include Ruby >= 2.2** (its `gem 'ox'` branch wasn't +touched by this change, so it seemed out of scope at the time) -- see the +correction directly below, found days later while reordering these same +Gemfile branches for an unrelated reason. + +### Correction: htmlentities still required on Ruby 2.2.x - 2.6.x + +The "htmlentities is dead weight above 1.9.3" conclusion above was wrong +for part of that range. `gem 'ox'` unconstrained (the `>= 2.2` branch) +resolves whatever the newest Ox release is for the running Ruby -- and Ox +added a hard `Ruby >= 2.7.0` requirement to its own gemspec starting at +2.14.15. That means Ruby 2.2.x-2.6.x are stuck resolving **2.14.14** (the +last release before that floor), while Ruby >= 2.7 gets 2.14.15+. + +2.14.14 segfaults inside `Ox.sax_parse`'s `:convert_special => true` path +on complex documents -- confirmed via a real crash in `test_mapping.rb` +(`[BUG] Segmentation fault` at `oxparser.rb:33`), reproduced identically on +Ruby 2.2.10, 2.3.8, 2.4.10, and 2.6.10, while a bare, simple XML string +through the same code path does *not* crash (so it's data/complexity +dependent, not a trivial always-fails bug -- a quick smoke test wouldn't +have caught it). Ruby 2.7.8 (resolving Ox 2.14.28) and Ruby 3.0.7/3.4.10/ +4.0.5 all run the same test cleanly. + +`htmlentities` avoids the bug entirely by design, not by luck: +`oxparser.rb` only passes `:convert_special => true` when no decoder is +present; with htmlentities installed, Ox is called without that option and +never enters the buggy path. So for Ruby 2.2.x-2.6.x, htmlentities is a +required workaround, not an optional speed boost -- `gem 'htmlentities'` +was restored there (Gemfile), unconstrained Ox unconstrained-and-safe only +from Ruby >= 2.7. + +This was only caught because a later, unrelated task (reordering the +Gemfile's version-gate branches newest-first) prompted a fresh full-matrix +re-run that happened to include Ruby 2.2.10 for the first time since the +original htmlentities change. The lesson: a version-gate boundary that +touches one branch's *dependency count* (adding/removing a gem) can change +behavior in a sibling branch that looks untouched, if both branches share +downstream code (`oxparser.rb`) that behaves differently based on which +gems happen to be present -- worth re-running the full matrix after any +Gemfile change that touches a shared code path, not just the Ruby +versions whose own branch changed. + +### CI: Docker-per-version architecture, not ruby/setup-ruby + +`ci.yml`'s `test` job runs each Ruby version via a plain `docker run` of +the exact same official `ruby:X.Y.Z` image used for local validation, +rather than a `ruby/setup-ruby`-based matrix on the GitHub-hosted runner +VM directly, and rather than the job-level `container:` key. + +- The earlier `ruby/setup-ruby` approach kept drifting from local + validation in ways that cost real debugging time: `ubuntu-latest` + rolling forward under a pinned Ruby version, `ruby-builder` not always + publishing a binary for every Ruby x Ubuntu combination, and Ubuntu + 22.04's ICU-enabled system libxml2 colliding with `libxml-ruby`'s C + extension in a way that never reproduced locally against Debian. Running + the identical container makes "works locally" and "works in CI" the same + claim by construction. +- `container:` was tried and rejected: GitHub Actions injects its own + Node.js runtime into container jobs to run JS-based steps like + `actions/checkout`, and most of these official `ruby` images are old + enough (Debian jessie/stretch-era glibc) that the injected Node 24 + binary can't execute (`version 'GLIBC_2.27' not found`), failing + checkout before a single test runs. Confirmed empirically: every job up + through Ruby 2.7 failed this way. Checkout instead runs on the runner VM + as normal, and a plain `docker run` does the actual `bundle install`/test + run inside the target image. +- Exact patch-level tags (e.g. `2.4.10`, not `2.4`) are pinned to match + precisely what was locally validated, even though these are all + EOL/frozen lines where the floating tag would resolve identically. + +### CI: BUNDLE_WITH bug (curb/faraday silently never tested below Ruby 3.2) + +`bundle config set with "http_curb"` is Bundler 2.x-only syntax. Every +Ruby below 3.2 in this matrix falls back to Bundler 1.17.3 (the last +release installable there), whose `config` CLI has no `set` verb at all -- +under 1.17.3 that command silently creates a bogus config key literally +named `"set"` (value `"with http_curb"`) instead of configuring the `with` +group list. Confirmed empirically: `bundle config` afterward showed the +bogus key, and `SOAP4R_HTTP_CLIENTS=curb` then failed with "HTTP client +backend not found" against a gem that was never installed. Meaning: every +curb/faraday CI step below Ruby 3.2 had been silently testing nothing. + +Fixed with the `BUNDLE_WITH="group1:group2"` env var instead -- the same +underlying mechanism a persisted `bundle config` writes to, confirmed +working identically on both Bundler generations. It must be `export`ed +(not just prefixed on the `bundle install` line): `bundle install` alone +happily installs an optional group's gems to disk even without +`BUNDLE_WITH` set for a later command, but `bundle exec`'s own +`Bundler.setup` excludes `:optional => true` groups from the load path by +default unless told to include them for that invocation too -- confirmed +empirically that a `bundle exec` immediately after a successful, +curb-installing `bundle install` still raised `LoadError` on `require +'curb'` without `BUNDLE_WITH` set again. + +Both the curb and faraday CI steps also needed explicit skip-and-exit-0 +logic below their respective Gemfile floors (Ruby >= 2.4 / >= 2.6): without +it, a matrix entry below the floor would hard-fail the whole job +(`RuntimeError: HTTP client backend not found`) under `set -e` with no +`continue-on-error` at that granularity, rather than skip cleanly. + +### CI: port 17171 clearing between parser runs + +All parsers/backends run as separate `rake test:deep` invocations +sequentially in one long-lived container, sharing a hardcoded port (17171) +used throughout the test suite. Confirmed via CI logs (Ruby 2.7.8, run +28854082576) that something can stay bound to that port across a process +boundary for minutes, well past the widened retry budget in +`test/testutil.rb` (60 tries/1s). Never reproduced locally, and not fully +root-caused (suspected a WEBrick/CGI-handler subprocess not fully +released) -- `fuser -k -n tcp 17171` runs before every iteration +regardless of cause rather than keep guessing at timing. + +Relatedly, `set -e` must not wrap the parser loop itself (only the setup +steps before it): `rake test:deep` exits non-zero on any test failure, and +several Ruby versions have exactly one guaranteed failure on every parser +(see "Known Test Suite Exceptions" below) -- under `set -e` that killed the +script after the first parser alone, silently skipping the rest without +anyone noticing (confirmed: CI logs showed only one parser ever ran for +those versions). Failures are tracked in a variable and the loop always +runs to completion instead. + +### CI: summary job's GFM table-adjacency gotcha + +The `summary` job uses `core.summary.addTable()` (an actual HTML +`

`) rather than hand-rolled markdown pipe syntax, because a +markdown table placed immediately after `addHeading()`'s `

` HTML with +no blank line in between doesn't get parsed as a table at all (GitHub +Flavored Markdown requires a fresh block boundary before table syntax) -- +confirmed on a live run, where the whole thing rendered as one +literal-pipe-character paragraph instead of a table. + +### CI: legacy Ruby build strategy (1.9.3, 2.0.0, 1.8.7) + +1.9.3 and 2.0.0 have official Docker Hub images, but every tag in both +lines was published in the legacy Docker manifest v1 schema and is no +longer pullable at all (confirmed empirically: `not implemented: media +type ... manifest.v1+prettyjws ... no longer supported since containerd +v2.1`, against every tag tried). GitHub-hosted runners hit the same wall. +Built from source instead via rbenv/ruby-build on Debian bullseye +(`Dockerfile.legacy`) -- confirmed clean across all 5 parsers for both +versions, including libxmlparser (bullseye's libxml2-dev doesn't have +Ubuntu 22.04's ICU/`bool`-collision problem) and ogaparser (oga's ruby-ll +dependency installs and works fine on both, unlike 1.8.7 below). + +1.8.7 has no official Docker Hub image at all (the `ruby` library starts +at 1.9) and no ruby-build definition with OpenSSL handling, so +rbenv/ruby-build can't get it for free. `Dockerfile.legacy187` builds it +from source against a vendored OpenSSL 1.0.2u instead (confirmed working: +`openssl.so` builds, `OpenSSL::SSL::SSLContext.new` instantiates +correctly). `oga` is intentionally excluded from its parser loop: its +`ruby-ll` dependency genuinely needs Ruby >= 2.1 on this version (unlike +1.9.3 above), so it's correctly never installed there. + +### HTTP client backend rollout (curb/faraday, wiredump parity, TLS trust) + +`http-access2` (httpclient's old name, same author) is no longer published +on RubyGems.org at all; retired from the backend cascade for the same +reason. See git history for the adapter that used to sit here. + +Before `SOAP4R_HTTP_CLIENTS` existed as an independently selectable env +var, nothing in the test suite could ever actually reach the `net_http` +fallback -- this project's Gemfile always installs `httpclient`, so the +cascade never had a reason to fall through to it. Making backends +independently selectable surfaced (and fixed) a real bug in the process: +`SOAP::NetHttpClient`'s wiredump output duplicated request/response bodies +and was missing the raw request-line/header block entirely, corrupting +any test or tool parsing `wiredump_dev` output. + +Several tests (WSDL-driven codegen round-trips, request-envelope +assertions, ASP.NET-handler interop) used to be gated to `httpclient` only +because they parsed `wiredump_dev` output assuming its specific block +layout. That layout turned out to already be backend-neutral by design +once `NetHttpClient`/`CurbClient`/`FaradayClient` were all built to mirror +it -- confirmed by simply removing the gates and running the suite +unmodified under every backend. The duplicated parsing logic across those +test files is now consolidated into +`TestUtil.parse_wiredump_request_body`/`parse_wiredump_response_body` +(`test/testutil.rb`). + +`test/soap/ssl/test_ssl.rb` runs its SSL config-loading coverage against +every SSL-capable backend now, not just httpclient: `test_ca_verification` +and `test_ciphers` (rewritten to expect each backend's own real exception +class -- `OpenSSL::SSL::SSLError` for httpclient, `Curl::Err::CurlError` +for curb, `Faraday::Error` for Faraday; confirmed empirically, since +test-unit's `assert_raise` wants an exact class match). +`test_options`/`test_verification`/`test_property` stay httpclient-only: +they're built around `ssl_config.verify_callback`, and neither +libcurl-based backend (curb, or Faraday on typhoeus/patron) exposes a +per-certificate Ruby callback hook at all -- confirmed against both +libraries' public APIs. Faraday's own adapter list is deliberately not +swept exhaustively in CI -- `lib/soap/faradayClient.rb` is what's under +test, and sweeping every adapter Faraday supports would mostly re-test +Faraday's own correctness. `:typhoeus` was chosen as the one CI adapter +because it's also the one people actually reach for in practice (28 +reverse dependencies on RubyGems vs. `faraday-patron`'s 3, per [Ruby +Toolbox](https://www.ruby-toolbox.com/projects/faraday-typhoeus)); Faraday's +own `faraday-typhoeus` adapter was confirmed to silently drop the +`ciphers` option (never forwards it to `ethon`'s `ssl_cipher_list`) -- a +real gap in that third-party adapter, not this bridge. + +`httpclient`'s `SSLConfig` doesn't trust the system CA bundle unless told +to -- left unconfigured, it lazily falls back to its own gem-vendored +`cacert.pem` snapshot, which can go stale relative to a real server's +certificate chain as CAs rotate intermediates (confirmed directly: a real +Let's Encrypt-signed endpoint failed verification against an older bundled +snapshot while verifying fine against the host's own CA bundle). +`lib/soap/httpbackend/httpclient.rb` now calls `set_default_paths` on +every connection's `ssl_config` by default, deferring to whatever CA store +OpenSSL was built to trust on the platform, layered before any +`ssl_config.ca_file`/`ca_path`/`cert_store` set explicitly. `NetHttpClient` +was never affected -- it has no SSL config surface of its own and defers +to stdlib `Net::HTTP`/OpenSSL defaults directly. + +## Known Test Suite Exceptions (full detail) + +Short summary lives in README.md; full root-cause narrative here. + +- **Ruby 1.8.7**: + - 3 errors (`test_time`, `test_time_ivar`, `test_time_subclass` in + `marshaltestlib.rb`): `Kernel#singleton_class` doesn't exist until Ruby + 1.9.2. CANTFIX -- the method is absent on that Ruby, full stop. + - CGI-based tests (`test_calc_cgi`, `test_authheader_cgi`) fail with + `SOAP::ResponseFormatError: ... Internal Server Error`. Root-caused + partway: WEBrick's `CGIHandler` wipes the spawned CGI subprocess's + entire environment before exec'ing it, so `logger-application` and + `webrick` need forwarding via a raw `-I` load-path flag (already fixed, + in both test files). That forwarding alone wasn't enough on 1.8.7 + specifically: the spawned subprocess still hits `NameError: undefined + local variable or method 'logger'` inside + `lib/soap/rpc/cgistub.rb#run`, even though `Logger::Application#logger` + is a real public method and a minimal reproduction of the same class + hierarchy works fine in isolation. Not fully root-caused -- something + specific to the real `CGIStub`/`AuthHeaderPortServer`/`CalcServer` + class graph, only reproducible via the actual spawned-CGI-subprocess + path. Narrow enough (2 of ~330 tests, both CGI smoke tests) that it + wasn't worth further chasing. + - Collateral damage from the CGI issue above: `test_nil_attribute` and + `test_wsdl_with_map` (`test/wsdl/document/test_rpc.rb`) intermittently + receive a garbage `dateTime` value (`XSD::ValueSpaceError`) when run as + part of the full suite, but pass cleanly in isolation + (`SCOPE=wsdl/document`). Confirmed state leaking from a still-lingering + CGI subprocess/WEBrick thread earlier in the same run, not a genuine + 1.8.7 `Date`/`DateTime` bug -- `XSDDateTimeImpl#screen_data`'s `Time` + branch produces a correct result in isolation. Not independently + fixable -- same underlying CGI/WEBrick fragility, different test. +- **Ruby 2.4.10, 2.5.9, `SOAP4R_HTTP_CLIENTS=curb`**: `test_ca_verification` + and `test_ciphers` (`test/soap/ssl/test_ssl.rb`) fail with + `Curl::Err::SSLPeerCertificateError: ... unable to get issuer + certificate`, even with a correct, complete CA chain supplied. CANTFIX -- + confirmed environment-specific: the official `ruby:2.4.10`/`ruby:2.5.9` + images ship libcurl 7.64.0/OpenSSL 1.1.1d, while `ruby:2.6.10`+ ship + libcurl 7.74.0/OpenSSL 1.1.1n; the same chain validates cleanly under the + newer pair. httpclient/net_http (unaffected by libcurl version) pass + this same test cleanly everywhere, confirming this is that older libcurl + build, not this bridge's CA-file wiring. +- **Ruby 3.0.7, 3.1.7, 3.2.11**: 1 failure in `test_exception` + (`marshaltestlib.rb`), which marshals an exception whose `.message` + embeds a live `#inspect` dump of the entire running + `Test::Unit::TestCase` (memory addresses, internal framework state and + all). WONTFIX -- confirmed via minimal reproduction that soap4r-ng's own + exception-marshaling round-trips correctly; the test itself compares + volatile process/framework internals that render differently across this + narrow patch-version range. +- **JRuby** (9.4.15.0 and 10.1.0.0, identical on both) -- 13 confirmed + environment-specific items: + - 7 `XSD::ValueSpaceError` tests (`test_SOAPInteger`, `test_XSDInteger`, + etc.): JRuby's `Kernel#Integer()` silently stops validating trailing + garbage once the digit count gets long enough, where MRI always raises + `ArgumentError`. CANTFIX -- JRuby core-method behavior difference. + - `test_singleton` (`marshaltestlib.rb`): expects marshaling `ENV` to + raise `TypeError`. JRuby's `ENV` is backed by a `Hash`-flavored object + rather than MRI's anonymous-object singleton, so it never trips the + check. CANTFIX -- JRuby object-representation difference. + - `test_ciphers`, `test_property`, `test_verification` + (`test/soap/ssl/test_ssl.rb`): `TypeError: failed to coerce + java.lang.String to [Ljava.lang.String;`, confirmed entirely inside + `httpclient`'s own JRuby SSL socket bridge + (`httpclient/jruby_ssl_socket.rb`). CANTFIX (upstream dependency). + - `test_nestedexception` (both variants): JRuby's backtrace formatting + differs from every MRI format already branched on. CANTFIX -- the + engine differs, not the Ruby version. + - A handful of other JRuby-only failures (`test_calc`, `test_calc2`, + `test_calc_cgi`, `test_authfailure` x2, `test_mu`) turned out to be a + real, fixable bug: `SOAP::Mapping.fault2exception` assumed a + reconstructed fault exception always has a non-nil `.backtrace`, false + on JRuby for a programmatically-constructed exception. Fixed with a + nil-guard. +- **`SOAP4R_HTTP_CLIENTS=faraday SOAP4R_FARADAY_ADAPTER=patron`** -- not run + in CI, but documented for anyone who reaches for it: CGI-based tests + fail with `Patron::Aborted: Callback aborted`. Root-caused to `patron` + itself: reproduced with a bare `Patron::Session#post` against the exact + same WEBrick CGI-subprocess server, no soap4r-ng or Faraday code in the + path. A raw TCP-level dump ruled out a missing `Content-Length`. Most + likely explanation, unconfirmed without reading patron's C extension + directly: patron implements its own request timeouts via a libcurl + progress callback, and the CGI handler's per-request subprocess spawn (a + few hundred ms before the first byte, unlike every other test's + in-process handler) is the one thing distinguishing failing requests + from passing ones, including under `curb` (also libcurl-based) and both + adapters CI actually runs. CANTFIX without a patron-side fix. diff --git a/Gemfile b/Gemfile index c76f2fc09..a470ad1ed 100644 --- a/Gemfile +++ b/Gemfile @@ -1,11 +1,42 @@ source 'http://rubygems.org' -if RUBY_VERSION.to_f <= 1.8 - gem 'htmlentities', '4.3.1' # Require this if OxParser's built-in "Special Character" conversion isn't sufficient for your needs. - gem 'httpclient', '~> 2.7.0.1' -else +if RUBY_VERSION.to_f > 1.8 gem 'httpclient' # 2.1.5.2 - gem 'htmlentities', '~> 4.3.3' # Require this if OxParser's built-in "Special Character" conversion isn't sufficient for your needs. +else + gem 'httpclient', '~> 2.7.0.1' +end + +# --------------------------------------------------------------------------- +# Additional (opt-in) HTTP client backends -- see "HTTP Client Backends" in +# README.md and lib/soap/httpbackend.rb. Not installed by default; requires +# curb needs a system libcurl-dev at compile time. Install with: +# bundle install --with http_curb http_faraday +# Neither has a JRuby port (no native-extension support there). +group :http_curb, :optional => true do + # >= 2.4: curb.c needs CURL_SSLVERSION_MAX_* (libcurl >= 7.54.0), which + # predates the system libcurl-dev on ruby:2.2.10/2.3.8's Docker images. + # See CHANGELOG.md. + gem 'curb' if RUBY_PLATFORM !~ /java/ && RUBY_VERSION.to_f >= 2.4 +end +group :http_faraday, :optional => true do + # >= 2.6: an enhancement for modern Rubies, not a legacy capability -- + # older Faraday releases don't match lib/soap/faradayClient.rb's adapter + # architecture. See CHANGELOG.md. + gem 'faraday' if RUBY_VERSION.to_f >= 2.6 + # faraday-typhoeus is the real second adapter this project tests against + # (see SOAP4R_FARADAY_ADAPTER in lib/soap/faradayClient.rb). + gem 'faraday-typhoeus' if RUBY_PLATFORM !~ /java/ && RUBY_VERSION.to_f >= 2.6 + gem 'typhoeus' if RUBY_PLATFORM !~ /java/ && RUBY_VERSION.to_f >= 2.6 + # ethon (typhoeus's dep) pulls in an unconstrained ffi needing Ruby >= 3.0; + # pin the newest release that still installs below that. + gem 'ffi', '~> 1.12.2' if RUBY_PLATFORM !~ /java/ && RUBY_VERSION.to_f >= 2.6 && RUBY_VERSION.to_f < 3.0 + # faraday-patron: third, optional spot-check adapter (see + # SOAP4R_FARADAY_ADAPTER=patron). + gem 'faraday-patron' if RUBY_PLATFORM !~ /java/ && RUBY_VERSION.to_f >= 2.6 + gem 'patron' if RUBY_PLATFORM !~ /java/ && RUBY_VERSION.to_f >= 2.6 + # faradayClient.rb's manual Basic-auth header encoding needs this from + # Ruby >= 3.4 (see logger/getoptlong below for the same demotion pattern). + gem 'base64' if RUBY_VERSION.to_f >= 3.4 end # --------------------------------------------------------------------------- @@ -17,115 +48,102 @@ end if RUBY_PLATFORM =~ /java/ # ox has no JRuby port at all -- nothing to require here; oxparser will # gracefully report unavailable via xsd/xmlparser.rb's parser cascade. -elsif RUBY_VERSION.to_f >= 2.2 +elsif RUBY_VERSION.to_f >= 2.7 gem 'ox' # oxparser ; Uses its own custom C-library +elsif RUBY_VERSION.to_f >= 2.2 + # Ruby 2.2.x - 2.6.x: unconstrained resolves Ox 2.14.14, the newest release + # before Ox's own gemspec required Ruby >= 2.7. 2.14.14 segfaults inside + # Ox.sax_parse's :convert_special => true path on complex documents + # (confirmed via test_mapping.rb on 2.2.10/2.3.8/2.4.10/2.6.10) -- + # htmlentities avoids that path entirely (see oxparser.rb), so it's a + # required workaround here, not an optional speed boost. + gem 'ox' + gem 'htmlentities', '~> 4.3.3' +elsif RUBY_VERSION.to_f > 1.8 + # Ruby 1.9.3 - 2.1.x: newest Ox that loads without the rb_utf8_str_new + # crash here and decodes named entities natively (htmlentities not + # needed). Confirmed empirically -- 2.14.7 introduces the crash. Pin + # exact: patch releases matter here. + gem 'ox', '= 2.14.6' else - # current ox needs rb_utf8_str_new (Ruby >= 2.2); older Rubies fail with - # an unresolvable-symbol crash at native-ext load time (not a catchable - # LoadError). 2.4.5 predates that and is already the known-good pin used - # for Ruby <= 1.8 above; it also covers 1.9.x - 2.1.x. - # - # Must be pinned exactly ("= 2.4.5", not "~> 2.4.5"): later 2.4.x patches - # (confirmed with 2.4.13) hit a second, different unresolvable-symbol - # crash of their own (RSTRUCT_GET) on these old Rubies, so a floating - # patch-level constraint silently re-breaks this the same way. + # Ruby 1.8.7: 2.14.6's extconf.rb fails to build here (unrelated build + # tooling issue), so fall back to 2.4.5. Pin exact: 2.4.13 hits a + # different crash (RSTRUCT_GET) elsewhere in this range. gem 'ox', '= 2.4.5' + # Only needed here: 2.4.5 decodes just the 5 basic XML entities, not the + # full named set. 4.3.1 pinned -- 4.3.3+ needs Encoding, absent on 1.8.7. + gem 'htmlentities', '4.3.1' end -if RUBY_VERSION.to_f <= 1.8 - gem 'nokogiri', '~> 1.5.11' # nokogiriparser ; Uses libxml2, libxslt, and zlib -elsif RUBY_VERSION.to_f <= 2.2 +if RUBY_VERSION.to_f > 2.2 + gem 'nokogiri' # let Bundler pick a version compatible with the running Ruby +elsif RUBY_VERSION.to_f > 1.8 gem 'nokogiri', '~> 1.6.6' # nokogiriparser ; Uses libxml2, libxslt, and zlib else - gem 'nokogiri' # let Bundler pick a version compatible with the running Ruby + gem 'nokogiri', '~> 1.5.11' # nokogiriparser ; Uses libxml2, libxslt, and zlib end if RUBY_PLATFORM =~ /java/ - # libxml-jruby's one and only release is from 2010 and calls - # `include_class`, a JRuby API removed long ago -- it's a NoMethodError at - # require time (not a catchable LoadError) on any current JRuby, and - # there's no newer version or replacement gem to pin instead. Leaving - # libxmlparser unavailable here is consistent with ox above, which simply - # has no JRuby port either. -elsif RUBY_VERSION.to_f <= 1.9 - gem 'libxml-ruby', '~> 2.8.0' -else + # libxml-jruby's only release (2010) calls a since-removed JRuby API + # (NoMethodError at require time) -- no replacement exists, so libxmlparser + # is simply unavailable here, same as ox above. +elsif RUBY_VERSION.to_f > 1.9 gem 'libxml-ruby', '~> 3.1.0' +else + gem 'libxml-ruby', '~> 2.8.0' end if RUBY_VERSION.to_f > 1.8 - # oga itself supports Ruby >= 1.9.3, but its own `ruby-ll` dependency - # constraint (~> 2.1) permits drifting all the way up to ruby-ll 2.2.0, - # whose C ext needs RUBY_TYPED_FREE_IMMEDIATELY (Ruby >= 2.1) -- Bundler - # has no way to know that from the declared constraint alone. Pin ruby-ll - # directly to the last release that still declares Ruby >= 1.9.3 support. + # oga's own ruby-ll dependency (~> 2.1) can drift to 2.2.0, whose C ext + # needs RUBY_TYPED_FREE_IMMEDIATELY (Ruby >= 2.1). Pin the last ruby-ll + # release that still supports Ruby >= 1.9.3. gem 'ruby-ll', '~> 2.1.2' if RUBY_VERSION.to_f < 2.1 gem 'oga' # ogaparser ; Pure-Ruby Alternative end if RUBY_VERSION.to_f > 1.8 - # rexml/webrick/logger were all implicit default gems once, pulled in - # automatically without a Gemfile entry -- but only up to the Ruby version - # where each was demoted to a bundled gem (rexml/webrick: 3.0, logger: 4.0). - # Below that floor they're still implicitly available and don't need (or - # want) a Gemfile entry: rubygems.org only hosts *current* rexml/webrick/ - # logger releases, built with syntax (e.g. `**opts`, `&.`) that predates-Ruby - # can't even parse, so pulling them in unconditionally shadows a perfectly - # working built-in with a gem that hard-crashes at require time. - gem 'rexml' if RUBY_VERSION.to_f >= 3.0 # no longer an implicit default gem under Bundler as of Ruby 3.0 - gem 'webrick' if RUBY_VERSION.to_f >= 3.0 # same; needed by lib/soap/rpc/{httpserver,cgistub,soaplet}.rb - # dropped in Ruby 4.0, but the "will no longer be part of the default - # gems" notice itself already starts firing a full version earlier, on a - # plain `require 'logger'` with no Bundler pinning involved at all -- - # confirmed silent on 3.1.7/3.2.11/3.3.11, present starting 3.4.10. Gated - # here from 3.4 (not 4.0) to actually silence it, matching getoptlong - # below which has the same early-warning behavior. + # rexml/webrick/logger were implicit default gems until demoted to bundled + # gems (rexml/webrick: Ruby 3.0, logger: 4.0). Below that floor they're + # still built in and must NOT get a Gemfile entry: current releases on + # rubygems.org use syntax predates-Ruby can't parse. + gem 'rexml' if RUBY_VERSION.to_f >= 3.0 + gem 'webrick' if RUBY_VERSION.to_f >= 3.0 # needed by lib/soap/rpc/{httpserver,cgistub,soaplet}.rb + # logger's deprecation warning starts firing a version early (3.4, not + # 4.0) on a plain `require` with no pinning at all; gated here to silence + # it, matching getoptlong below. gem 'logger' if RUBY_VERSION.to_f >= 3.4 - gem 'getoptlong' if RUBY_VERSION.to_f >= 3.4 # same; bin/{xsd2ruby,wsdl2ruby}.rb both need it unconditionally + gem 'getoptlong' if RUBY_VERSION.to_f >= 3.4 # bin/{xsd2ruby,wsdl2ruby}.rb need it unconditionally gem 'logger-application', :require=>'logger-application' end ## # Testing Support ### group :test do - if RUBY_VERSION.to_f <= 1.8 + if RUBY_VERSION.to_f > 1.9 + gem 'test-unit' + gem 'rake' + elsif RUBY_VERSION.to_f > 1.8 + # current test-unit uses Ruby >= 2.0 keyword-argument syntax (SyntaxError + # on 1.9.3); pin an old-enough release. + gem 'test-unit', '~> 3.0.5' + gem 'rake' + else gem 'test-unit', '~> 1.2.3' gem 'rake', '~> 10.4.2' - # test-unit 1.2.3 lists hoe as a *runtime* dependency (a common - # old-ecosystem quirk from that era); hoe's current release needs - # Ruby >= 2.7, so pin the oldest version that still satisfies - # test-unit's own floor (>= 1.5.1). hoe in turn pulls in rubyforge, - # which pulls in json_pure -- same story, pin that too. + # test-unit 1.2.3 lists hoe as a runtime dependency; current hoe needs + # Ruby >= 2.7, so pin the oldest release satisfying test-unit's floor + # (>= 1.5.1). hoe pulls in rubyforge -> json_pure; pin that too. gem 'hoe', '1.5.1' gem 'json_pure', '~> 1.7.6' - elsif RUBY_VERSION.to_f <= 1.9 - # current test-unit (like rexml/webrick/logger above) is built with - # Ruby >=2.0 keyword-argument syntax and is a SyntaxError on 1.9.3; pin an - # old-enough release instead of relying on Bundler to steer around it. - gem 'test-unit', '~> 3.0.5' - gem 'rake' - else - gem 'test-unit' - gem 'rake' end - # test-unit pulls in power_assert, which pulls in ansi -- ansi 1.6.0 needs - # Ruby >= 3.1, and Bundler's resolver picks it for some Ruby versions in - # our supported range (2.3.8 confirmed) but not others (2.2.10, 1.9.3 - # both land on the older, compatible 1.5.0) depending on how the rest of - # the bundle resolves. Pin it directly rather than rely on that. + # test-unit -> power_assert -> ansi; ansi 1.6.0 needs Ruby >= 3.1 and + # Bundler's resolver doesn't always avoid it below that. Pin directly. gem 'ansi', '~> 1.5.0' if RUBY_VERSION.to_f < 3.1 - # Not used directly anywhere in this codebase -- it's here only because - # simplecov (below) depends on 'json' unconstrained, and current json - # releases use **opts keyword-splat syntax that's a SyntaxError on Ruby - # <= 1.9. Without this pin Bundler resolves json 2.3.0 even on 1.9.3 and - # every test run fails at require time before a single test executes. + # Not used directly -- simplecov (below) depends on unconstrained 'json', + # whose current releases use **opts syntax (SyntaxError on Ruby <= 1.9). gem 'json', '~> 1.8' if RUBY_VERSION.to_f <= 1.9 gem 'rubyjedi-testunitxml', :git=>'https://github.com/rubyjedi/testunitxml.git', :branch=>'master' - # test/helper.rb requires this directly (RUBY_VERSION.to_f >= 1.9); it used - # to arrive only transitively via codeclimate-test-reporter, so removing - # that gem means it needs its own declaration now. Pinned to the exact - # version that dependency always resolved to, since that's what every - # Ruby version in this project's test matrix has actually been verified - # against. + # test/helper.rb requires this directly; pinned to the version this + # project's test matrix has actually been verified against. gem 'simplecov', '0.13.0' if RUBY_VERSION.to_f >= 1.9 ### Misc Debugging Aids ### diff --git a/README.md b/README.md index 576457b79..c6837e2a3 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,9 @@ gem 'webrick' gem 'logger' gem 'httpclient' # Strongly recommended. See "HTTP Client Backends" below for the Net::HTTP fallback's limitations. +## curb and faraday are additional, opt-in HTTP client backends -- not +## needed unless you specifically want one of them; see "HTTP Client +## Backends" below. gem 'soap4r-ng', :git=>'https://github.com/rubyjedi/soap4r.git', :branch=>"master" ``` @@ -89,140 +92,78 @@ is a drop-in file rather than a change to library code. * **[httpclient](https://github.com/nahi/httpclient)** -- the default and strongly recommended backend. Fully featured: proxying, basic/digest auth, - cookies, SSL/TLS configuration, request/response filters (used for things - like cookie handling -- see `test/soap/test_cookie.rb`). -* **[http-access2](https://rubygems.org/gems/http-access2)** -- httpclient's - predecessor, by the same author. Historically the second fallback, but it's - no longer published on RubyGems.org at all, so in practice this backend is - unreachable today unless you vendor the gem yourself. + cookies, SSL/TLS configuration, request/response filters. (Formerly named + `http-access2`; see CHANGELOG.md if you need that old adapter.) +* **[curb](https://github.com/taf2/curb)** -- libcurl bindings. Opt-in (see + below), needs system `libcurl-dev` at compile time. Supports proxying, + SSL/TLS config (ca_file, client cert/key, cipher-list restriction), and + basic/digest WWW-Authenticate auth. No cookies or request/response + filters, and no `ssl_config.verify_callback` (libcurl's C API exposes no + per-certificate Ruby callback hook). No JRuby port; gated to Ruby >= 2.4. +* **[faraday](https://github.com/lostisland/faraday)** -- gated to Ruby + >= 2.6 (see CHANGELOG.md for why). Has its own adapter system underneath + our backend selection, defaulting to `:net_http` and overridable with + `SOAP4R_FARADAY_ADAPTER` (e.g. `SOAP4R_FARADAY_ADAPTER=typhoeus`) -- a + second, independent knob layered under `SOAP4R_HTTP_CLIENTS`. Opt-in (see + below). Supports proxying, basic auth (sent proactively via a + manually-built header), and SSL/TLS config (ca_file, client cert/key as + file paths). No challenge-response auth, cookies, request/response + filters, or `verify_callback`. Cipher-list restriction passes through to + the active adapter, but `:typhoeus` silently ignores it (an upstream gap, + not this bridge). `SOAP4R_FARADAY_ADAPTER=patron` also works for ordinary + requests but isn't part of the automated test matrix (see "Known Test + Suite Exceptions" below). * **`SOAP::NetHttpClient`** -- this project's own wrapper around stdlib - `Net::HTTP`, used only when neither gem above is installed. It does **not** - support basic/digest auth, cookies, or request/response filters (all raise - `NotImplementedError`, or are silently inert for filters) -- these were - never wired up for this backend. It's a reasonable fallback for simple - unauthenticated SOAP calls when you can't add a gem dependency, but - `httpclient` is what most of this library's real-world testing and feature - support assumes. + `Net::HTTP`, used only when none of the above are installed. No + basic/digest auth, cookies, or request/response filters. A reasonable + fallback for simple unauthenticated SOAP calls when you can't add a gem + dependency, but `httpclient` is what most of this library's real-world + testing assumes. -Force a specific backend (e.g. to debug something backend-specific, or to -run the test suite against a backend other than the default) with: +Force a specific backend with: ``` SOAP4R_HTTP_CLIENTS=net_http bundle exec rake test:deep ``` -Valid names are `httpclient`, `http_access2`, and `net_http`, matching the -files under `lib/soap/httpbackend/`. CI runs the full suite against -`net_http` too (single-parser, since this is about the HTTP layer, not XML -parsing) precisely because, before this option existed, nothing in the test -suite could ever actually reach that fallback -- this project's own Gemfile -always installs `httpclient`, so the fallback cascade never had a reason to -fall through. Making it independently selectable surfaced (and fixed) a real -bug in the process: `SOAP::NetHttpClient`'s wiredump output duplicated -request/response bodies and was missing the raw request-line/header block -entirely, corrupting any test or tool that parsed `wiredump_dev` output. - -**A note on TLS trust for the `httpclient` backend**: `httpclient`'s -`SSLConfig` doesn't trust your system's CA bundle unless told to -- left -unconfigured, it lazily falls back to its own gem-vendored `cacert.pem` -snapshot, which can go stale relative to a real server's certificate chain as -CAs rotate their intermediates (confirmed directly: a real Let's -Encrypt-signed endpoint failed verification against an older bundled -snapshot while verifying fine against the host's own, actively-maintained CA -bundle). `lib/soap/httpbackend/httpclient.rb` calls `set_default_paths` on -every `httpclient` connection's `ssl_config` by default now, which defers to -whatever CA store OpenSSL was actually built to trust on your platform -- -portable across Debian/RHEL/Alpine/etc, and layered *before* any -`ssl_config.ca_file`/`ca_path`/`cert_store` you set yourself, so explicit -configuration still works exactly as before. `SOAP::NetHttpClient` was never -affected by this -- it has no SSL configuration surface of its own and -simply defers to Ruby's own stdlib `Net::HTTP`/OpenSSL defaults, which -already trust the system store without any help. +Valid names are `httpclient`, `curb`, `faraday`, and `net_http`, matching +the files under `lib/soap/httpbackend/`. `curb` and `faraday` are opt-in +Bundler groups: `BUNDLE_WITH="http_curb:http_faraday" bundle install` +(an env var, not `bundle config set with "..."` -- see CHANGELOG.md). + +CI runs the full suite against `net_http`, `curb`, and `faraday` too +(single-parser each -- this is about the HTTP layer, not XML parsing). +Faraday runs with the `:typhoeus` adapter specifically (real correctness +check plus the most-used adapter in practice); `:patron` was tried and +dropped from CI (real upstream bug, not ours -- see "Known Test Suite +Exceptions" below). Full backstory on the backend rollout (wiredump-parity +work, TLS-trust defaults, per-backend test coverage decisions) is in +CHANGELOG.md. #### Known Test Suite Exceptions -Running `rake test:deep` (the complete suite) across the full version matrix -surfaces a small, understood set of failures that aren't soap4r-ng bugs -- -they're either something the target Ruby genuinely can't do, or a test that's -checking something too environment-specific to be a fair test. Documented -here so they don't get mistaken for regressions: - -CI runs every version inside the same Docker image used for local -validation (official `ruby:X.Y.Z` / `jruby:X.Y.Z.W` images where one exists; -a from-source `rbenv` build on Debian bullseye for 1.9.3 and 2.0.0, whose -official Docker Hub images are permanently unpullable -- every tag in both -lines was published in the long-deprecated Docker manifest v1 schema; a -from-source build against a vendored OpenSSL 1.0.2u for 1.8.7, which has no -official image at all). Matching the exact environment means a clean local -run and a clean CI run are the same claim -- there's no separate "works on -my machine" environment for version-specific gotchas to hide in. - -* **Ruby 1.8.7** -- two separate issues, neither a soap4r-ng regression: - * 3 errors (`test_time`, `test_time_ivar`, `test_time_subclass` in - `marshaltestlib.rb`) from `Kernel#singleton_class`, which doesn't exist - until Ruby 1.9.2. **CANTFIX**: the method is absent on that Ruby, full - stop. - * The CGI-based tests (`test_calc_cgi`, `test_authheader_cgi`) fail with - `SOAP::ResponseFormatError: ... Internal Server Error`. Root-caused - partway: WEBrick's `CGIHandler` wipes the spawned CGI subprocess's - entire environment before exec'ing it, so `logger-application` and - `webrick` need forwarding via a raw `-I` load-path flag (already fixed, - in `test/soap/calc/test_calc_cgi.rb` and - `test/soap/header/test_authheader_cgi.rb`). That forwarding alone - wasn't enough on 1.8.7 specifically: the spawned - subprocess still hits `NameError: undefined local variable or method - 'logger'` inside `lib/soap/rpc/cgistub.rb#run`, even though - `Logger::Application#logger` is a real public method and a minimal - reproduction of the exact same class hierarchy (`Logger::Application` - combined with `include SOAP` and `include WEBrick`) works fine in - isolation. - **Not fully root-caused** -- something specific to the real - `CGIStub`/`AuthHeaderPortServer`/`CalcServer` class graph, only - reproducible via the actual spawned-CGI-subprocess path, not a - simplified one. Narrow enough in scope (2 of ~330 tests, both CGI - smoke tests rather than core library behavior) that it wasn't worth - further chasing this round; flagged here rather than silently - swallowed by `continue-on-error`. -* **Ruby 3.0.7, 3.1.7, 3.2.11** -- 1 failure in `test_exception` - (`test/soap/marshal/marshaltestlib.rb`), which marshals an exception whose - `.message` embeds a live `#inspect` dump of the entire running - `Test::Unit::TestCase` (memory addresses, internal test-framework state and - all). **WONTFIX**: confirmed via a minimal reproduction that soap4r-ng's own - exception-marshaling code round-trips correctly; the test itself is - fragile because it's comparing volatile process/framework internals that - happen to render differently across this narrow range of Ruby patch - versions, not anything under this library's control. -* **JRuby** (9.4.15.0 and 10.1.0.0, identical on both) -- 13 confirmed - environment-specific items, none of them soap4r-ng bugs: - * 7 `XSD::ValueSpaceError` tests (`test_SOAPInteger`, `test_XSDInteger`, - and friends) -- JRuby's `Kernel#Integer()` silently stops validating - trailing garbage characters (e.g. a trailing `.`) once the digit count - gets long enough, where MRI correctly raises `ArgumentError` regardless - of length. **CANTFIX**: a JRuby core-method behavior difference. - * `test_singleton` (`marshaltestlib.rb`) -- expects marshaling `ENV` to - raise `TypeError` (Ruby singleton objects can't be dumped). JRuby's - `ENV` is backed by a `Hash`-flavored object rather than MRI's - anonymous-object singleton, so it never trips the singleton-detection - check at all. **CANTFIX**: a JRuby object-representation difference, - and not a realistic thing to special-case for one object. - * `test_ciphers`, `test_property`, `test_verification` - (`test/soap/ssl/test_ssl.rb`) -- fail with - `TypeError: failed to coerce java.lang.String to [Ljava.lang.String;`. - Confirmed via backtrace inspection that this is entirely inside the - `httpclient` gem's own JRuby-specific SSL socket bridge - (`httpclient/jruby_ssl_socket.rb`), not soap4r-ng's code. - **CANTFIX** (upstream, in a dependency). - * `test_nestedexception` (both `SOAP::TestMapping` and - `SOAP::TestNestedException`) -- JRuby's backtrace formatting differs - from every MRI format the test already branches on by Ruby version. - **CANTFIX**: nothing to version-branch against, since it's the Ruby - engine that differs, not the Ruby version. - - A handful of other JRuby-only failures (`test_calc`, `test_calc2`, - `test_calc_cgi`, `test_authfailure` x2, `test_mu`) turned out to be a real, - fixable bug rather than a JRuby limitation: `SOAP::Mapping.fault2exception` - (`lib/soap/mapping/mapping.rb`) assumed a reconstructed fault exception - always has a non-nil `.backtrace`, which doesn't hold on JRuby for a - programmatically-constructed (never actually `raise`d-and-caught) - exception object. Fixed with a nil-guard; confirmed clean on both JRuby and - MRI afterward. +Running `rake test:deep` across the full version matrix surfaces a small, +understood set of failures that aren't soap4r-ng bugs -- either something +the target Ruby/engine genuinely can't do, or a test checking something too +environment-specific to be fair. Documented here so they don't get mistaken +for regressions; full root-cause writeups are in CHANGELOG.md. + +* **Ruby 1.8.7** -- 3 errors from `Kernel#singleton_class` not existing + until Ruby 1.9.2 (CANTFIX), plus the CGI-based tests + (`test_calc_cgi`, `test_authheader_cgi`) and two collateral WSDL tests + failing from a not-fully-root-caused WEBrick/CGI-subprocess environment + issue. +* **Ruby 2.4.10, 2.5.9, `SOAP4R_HTTP_CLIENTS=curb`** -- `test_ca_verification`/ + `test_ciphers` fail from those Docker images' older libcurl/OpenSSL + (CANTFIX, environment-specific, not a soap4r-ng/curb bug). +* **Ruby 3.0.7, 3.1.7, 3.2.11** -- `test_exception` fails because it compares + a live `Test::Unit::TestCase#inspect` dump that renders differently across + this patch-version range (WONTFIX, not under this library's control). +* **JRuby** (9.4.15.0, 10.1.0.0) -- 13 confirmed environment/engine + differences (integer parsing, `ENV` object model, SSL socket bridge, + backtrace formatting), all CANTFIX/upstream. A handful of other JRuby-only + failures were a real, fixable bug (`SOAP::Mapping.fault2exception` assumed + a non-nil `.backtrace`) -- fixed with a nil-guard. +* **`SOAP4R_HTTP_CLIENTS=faraday SOAP4R_FARADAY_ADAPTER=patron`** -- not run + in CI; the CGI-based tests fail with `Patron::Aborted: Callback aborted`, + root-caused to patron itself, not this project's code (CANTFIX). #### How to Use * [NaHi's Original documentation](https://web.archive.org/web/20101212040735/http://dev.ctor.org/soap4r/wiki/) -- the authoritative reference material is still available through the Wayback Machine, thankfully! @@ -231,7 +172,7 @@ my machine" environment for version-specific gotchas to hide in. #### How to Get a Speed Boost : Use Nokogiri or Ox, not REXML Be sure to have Nokogiri or Ox available in your Gemset. Soap4R-ng will find and use what's available (Ox has highest precedence, then Nokogiri, then LibXML, then Oga, falling back to REXML as the last resort if needed). -I personally recommend **Nokogiri** as the best performing, most flexible parser at this time, as it handles "special characters" like HTML ampersand-escaped characters internally. Ox doesn't handle such an extensive set of special-characters natively, so to get things up to par, I added **htmlentities** support if it's available when using the Ox parser. Using **htmlentities** with **Ox** in this manner adds a bit of a performance penalty, however. +I personally recommend **Nokogiri** as the best performing, most flexible parser at this time, as it handles "special characters" like HTML ampersand-escaped characters internally. Ox needs **htmlentities** as a fallback on Ruby 1.8.7 (stuck on the old Ox 2.4.5, which only decodes the 5 basic XML entities) and on Ruby 2.2.x-2.6.x (stuck on Ox 2.14.14, which segfaults on complex documents without it -- see CHANGELOG.md). Ruby 1.9.3-2.1.x and Ruby >= 2.7 both resolve an Ox release that decodes entities natively and safely, so htmlentities is dead weight there. If you know your incoming XML is "clean", Ox is a really great alternative. diff --git a/lib/soap/curbClient.rb b/lib/soap/curbClient.rb new file mode 100644 index 000000000..2824484f8 --- /dev/null +++ b/lib/soap/curbClient.rb @@ -0,0 +1,286 @@ +# encoding: UTF-8 +# SOAP4R - curb (libcurl) wrapper. + +require 'curb' +require 'tempfile' +require 'soap/filter/filterchain' + + +module SOAP + + +class CurbClient + attr_reader :proxy + attr_accessor :no_proxy + attr_accessor :debug_dev + attr_reader :ssl_config + attr_accessor :protocol_version # ignored -- libcurl negotiates this itself. + attr_accessor :connect_timeout + attr_accessor :send_timeout # ignored -- libcurl has one overall #timeout, not separate send/receive phases. + attr_accessor :receive_timeout + attr_reader :test_loopback_response + attr_reader :request_filter # ignored for now, same as SOAP::NetHttpClient. + + def initialize(proxy = nil, agent = nil) + @proxy = proxy + @no_proxy = nil + @agent = agent + @debug_dev = nil + @ssl_config = SSLConfig.new + @connect_timeout = @receive_timeout = @send_timeout = nil + @basic_auth = nil # [user, pass], set via #set_basic_auth + @challenge_auth = nil # [user, pass], set via #set_auth + @cookie_store = nil + @test_loopback_response = [] + @request_filter = Filter::FilterChain.new + end + + def proxy=(value) + if value + uri = value.is_a?(URI) ? value : URI.parse(value) + if uri.scheme.nil? or uri.scheme.downcase != 'http' or uri.host.nil? or uri.port.nil? + raise ArgumentError.new("unsupported proxy `#{value}'") + end + end + @proxy = value + end + + def set_basic_auth(uri, user_id, passwd) + @basic_auth = [user_id, passwd] + end + + # soap4r's "auth" (as opposed to "basic_auth") is fed by a server + # challenge (WWW-Authenticate: Basic or Digest) rather than sent + # proactively -- :any lets libcurl negotiate whichever the server + # actually asks for, covering both test/soap/auth/test_basic.rb and + # test/soap/auth/test_digest.rb with the same code path. + def set_auth(uri, user_id, passwd) + @challenge_auth = [user_id, passwd] + end + + def set_cookie_store(filename) + @cookie_store = filename + end + + def save_cookie_store + # curb's cookiejar is written incrementally as requests complete; + # nothing to flush here. + end + + def reset(url) + # no persistent connection state kept between requests; ignored. + end + + def reset_all + # ditto. + end + + def post(url, req_body, header = {}) + if str = @test_loopback_response.shift + if @debug_dev + @debug_dev << "= Request\n\n" + @debug_dev << req_body + @debug_dev << "\n\n= Response\n\n" + @debug_dev << str + end + return Response.new(200, nil, 'text/xml', Hash.new { |h, k| h[k] = [] }, str) + end + curl = build_curl(url, header) + curl.http_post(req_body) + dump_wiredump(curl, req_body) if @debug_dev + Response.from_curl(curl) + end + +private + + def build_curl(url, header) + # Curl::Easy.new needs a String -- given a URI object instead (the + # driver's endpoint can be constructed from either, see test_uri in + # test/soap/test_streamhandler.rb), curb's C extension doesn't coerce + # it and silently stores nil for the URL internally, surfacing much + # later as "TypeError: no implicit conversion of nil into String" + # inside curl/multi.rb's own perform machinery rather than failing + # here where the actual mistake is. + curl = Curl::Easy.new(url.to_s) + curl.headers = header.dup + curl.headers['User-Agent'] = @agent if @agent + curl.follow_location = false # streamHandler.rb's own send_post loop handles redirects. + curl.connect_timeout = @connect_timeout if @connect_timeout + curl.timeout = @receive_timeout if @receive_timeout + unless no_proxy?(url.is_a?(URI) ? url : URI.parse(url)) + curl.proxy_url = @proxy + end + if @challenge_auth + curl.http_auth_types = :any + curl.username, curl.password = @challenge_auth + elsif @basic_auth + curl.http_auth_types = :basic + curl.username, curl.password = @basic_auth + end + curl.cookiejar = @cookie_store if @cookie_store + apply_ssl_config(curl) + curl + end + + def apply_ssl_config(curl) + cfg = @ssl_config + return unless cfg + curl.ssl_verify_peer = (cfg.verify_mode != OpenSSL::SSL::VERIFY_NONE) + curl.ssl_verify_host = (cfg.verify_mode == OpenSSL::SSL::VERIFY_NONE) ? 0 : 2 + curl.cacert = cfg.ca_file if cfg.ca_file + # Curl::Easy has no dedicated ciphers=/cipher_list= method at all + # (confirmed empirically -- NoMethodError) despite libcurl itself + # supporting CURLOPT_SSL_CIPHER_LIST; #setopt with the raw constant is + # curb's only way to reach it. + curl.setopt(Curl::CURLOPT_SSL_CIPHER_LIST, cfg.ciphers) if cfg.ciphers + # Curl::Easy#cert=/#cert_key= want file paths, but + # HTTPConfigLoader#cert_from_file/#key_from_file (lib/soap/httpconfigloader.rb) + # already parsed the configured files into OpenSSL::X509::Certificate/ + # OpenSSL::PKey::RSA objects (that's what httpclient's SSLConfig wants) -- + # round-trip them back out to PEM tempfiles for curb's sake. + curl.cert = write_pem_tempfile(cfg.client_cert, 'cert') if cfg.client_cert + curl.cert_key = write_pem_tempfile(cfg.client_key, 'key') if cfg.client_key + end + + def write_pem_tempfile(openssl_obj, basename) + f = Tempfile.new(["soap4r-curb-#{basename}-", '.pem']) + f.write(openssl_obj.to_pem) + f.close + # The finalizer proc must not be created in an instance-method context + # closing over self -- that makes self itself unreachable-but-not-quite + # (reachable only via its own finalizer), so it never actually becomes + # eligible for GC and the finalizer never runs (confirmed: Ruby warns + # "finalizer references object to be finalized" and the tempfile never + # got cleaned up). Building the proc in a class method keeps the + # closure's only capture to f, not self. + ObjectSpace.define_finalizer(self, self.class.tempfile_unlinker(f)) + f.path + end + + def self.tempfile_unlinker(file) + proc { file.unlink rescue nil } + end + + def dump_wiredump(curl, req_body) + # Mirrors httpclient's wiredump block layout (marker line, blank line, + # raw request-line + headers, blank line, body) -- callers that parse + # wiredump_dev output by block position or by scanning for a "POST ..." + # line (e.g. test/soap/test_streamhandler.rb's parse_req_header) depend + # on that exact shape regardless of which backend produced it. + uri = URI.parse(curl.url) + request_line = (curl.proxy_url ? curl.url : uri.request_uri) + @debug_dev << "= Request\n\n" + @debug_dev << "POST #{request_line} HTTP/1.1\n" + curl.headers.each { |k, v| @debug_dev << "#{k}: #{v}\n" } + @debug_dev << "\n" + @debug_dev << req_body + @debug_dev << "\n\n= Response\n\n" + @debug_dev << "HTTP/1.1 #{curl.response_code} #{Response.reason_from_header_str(curl.header_str)}\n" + @debug_dev << "Content-Type: #{curl.content_type}\n" if curl.content_type + @debug_dev << "\n" + @debug_dev << curl.body + end + + NO_PROXY_HOSTS = ['localhost'] + + def no_proxy?(uri) + if !@proxy or NO_PROXY_HOSTS.include?(uri.host) + return true + end + unless @no_proxy + return false + end + @no_proxy.scan(/([^:,]+)(?::(\d+))?/) do |host, port| + if /(\A|\.)#{Regexp.quote(host)}\z/i =~ uri.host && + (!port || uri.port == port.to_i) + return true + end + end + false + end + + class SSLConfig + attr_accessor :client_cert + attr_accessor :client_key + attr_accessor :client_ca + attr_accessor :verify_mode + attr_accessor :verify_depth # not directly settable via libcurl's simpler peer/host verify model; stored only. + attr_accessor :options # ditto -- no libcurl equivalent to OpenSSL::SSL::SSLContext#options. + attr_accessor :ciphers + attr_accessor :verify_callback # not supported -- libcurl has no per-certificate Ruby callback hook. + attr_accessor :cert_store # not supported -- libcurl manages its own trust store internally. + attr_reader :ca_file + + def set_trust_ca(value) + @ca_file = value + end + + def set_crl(value) + @crl = value # not applied -- libcurl has no direct CRL-file option exposed via curb. + end + + # curb/libcurl never bundles its own CA snapshot the way httpclient + # does (see lib/soap/httpconfigloader.rb) -- unless #set_trust_ca + # overrides it, it always defers to whatever CA bundle libcurl itself + # was built against (the system's, on every mainstream Linux + # distribution), so there's nothing to override here. + def set_default_paths + end + end + + class Response + attr_reader :status + attr_reader :reason + attr_reader :contenttype + attr_reader :header + attr_reader :content + + def initialize(status, reason, contenttype, header, content) + @status = status + @reason = reason + @contenttype = contenttype + @header = header + @content = content + end + + def self.from_curl(curl) + header = parse_headers(curl.header_str) + reason = reason_from_header_str(curl.header_str) + new(curl.response_code, reason, curl.content_type, header, curl.body) + end + + # curb has no accessor for the HTTP reason phrase (e.g. "OK", "Not + # Found") separate from the numeric status -- pull it off the first + # line of the raw header block ourselves ("HTTP/1.1 200 OK" -> "OK"). + # With redirects (multiple status lines in header_str, one per hop) + # this reads the LAST one, matching #parse_headers below which also + # only reflects the final response's headers. + def self.reason_from_header_str(header_str) + return nil unless header_str + line = header_str.each_line.select { |l| l.start_with?('HTTP/') }.last + return nil unless line + line.strip.split(' ', 3)[2] + end + + # curb only exposes the raw response header block (#header_str), not a + # parsed hash -- build one ourselves, keyed lowercase like + # streamHandler.rb expects (it reads header['content-encoding'][0] and + # header['location'][0] directly), with an Array of values per key to + # match the other backends' Hash-of-Arrays shape. + def self.parse_headers(header_str) + result = Hash.new { |h, k| h[k] = [] } + return result unless header_str + header_str.each_line do |line| + line = line.strip + next if line.empty? || line.start_with?('HTTP/') + key, value = line.split(':', 2) + next unless key && value + result[key.strip.downcase] << value.strip + end + result + end + end +end + + +end diff --git a/lib/soap/faradayClient.rb b/lib/soap/faradayClient.rb new file mode 100644 index 000000000..f10e3a5eb --- /dev/null +++ b/lib/soap/faradayClient.rb @@ -0,0 +1,290 @@ +# encoding: UTF-8 +# SOAP4R - Faraday wrapper. +# +# Faraday is itself pluggable (it has its own adapter selection, on top of +# ours) -- default to its own default adapter (:net_http, bundled via the +# faraday-net_http gem that ships as a runtime dependency of faraday itself) +# and let SOAP4R_FARADAY_ADAPTER override it for anyone who wants Faraday to +# ride on a different underlying transport (e.g. :patron for a libcurl-based +# one). This is a second, independent config knob layered under our own +# SOAP4R_HTTP_CLIENTS -- see lib/soap/httpbackend.rb -- not a replacement +# for it. + +require 'faraday' +require 'base64' +require 'tempfile' +require 'soap/filter/filterchain' + + +module SOAP + + +class FaradayClient + attr_reader :proxy + attr_accessor :no_proxy + attr_accessor :debug_dev + attr_reader :ssl_config + attr_accessor :protocol_version # ignored -- the underlying Faraday adapter negotiates this itself. + attr_accessor :connect_timeout + attr_accessor :send_timeout # ignored -- mapped onto Faraday's single #timeout option instead (see #receive_timeout). + attr_accessor :receive_timeout + attr_reader :test_loopback_response + attr_reader :request_filter # ignored for now, same as SOAP::NetHttpClient. + + ADAPTER = (ENV['SOAP4R_FARADAY_ADAPTER'] || 'net_http').to_sym + + def initialize(proxy = nil, agent = nil) + @proxy = proxy + @no_proxy = nil + @agent = agent + @debug_dev = nil + @ssl_config = SSLConfig.new + @connect_timeout = @receive_timeout = @send_timeout = nil + @basic_auth = nil # [user, pass] + @cookie_store = nil + @test_loopback_response = [] + @request_filter = Filter::FilterChain.new + require_adapter! + end + + def proxy=(value) + if value + uri = value.is_a?(URI) ? value : URI.parse(value) + if uri.scheme.nil? or uri.scheme.downcase != 'http' or uri.host.nil? or uri.port.nil? + raise ArgumentError.new("unsupported proxy `#{value}'") + end + end + @proxy = value + end + + def set_basic_auth(uri, user_id, passwd) + @basic_auth = [user_id, passwd] + end + + def set_auth(uri, user_id, passwd) + # Faraday's core has no bundled challenge-response (WWW-Authenticate) + # negotiation -- that lives in third-party middleware, not something + # this bridge can assume is installed. Digest auth in particular has no + # core equivalent at all. + raise NotImplementedError.new("challenge-response auth is not supported under soap4r + faraday.") + end + + def set_cookie_store(filename) + raise NotImplementedError.new("cookie persistence is not supported under soap4r + faraday.") + end + + def save_cookie_store + raise NotImplementedError.new("cookie persistence is not supported under soap4r + faraday.") + end + + def reset(url) + # no persistent connection state kept between requests; ignored. + end + + def reset_all + # ditto. + end + + def post(url, req_body, header = {}) + if str = @test_loopback_response.shift + if @debug_dev + @debug_dev << "= Request\n\n" + @debug_dev << req_body + @debug_dev << "\n\n= Response\n\n" + @debug_dev << str + end + return Response.new(200, nil, 'text/xml', Hash.new { |h, k| h[k] = [] }, str) + end + response = build_connection(url).post do |req| + header.each { |k, v| req.headers[k] = v } + req.headers['User-Agent'] = @agent if @agent + if @basic_auth + req.headers['Authorization'] = + "Basic #{Base64.strict_encode64(@basic_auth.join(':'))}" + end + req.body = req_body + end + dump_wiredump(url, header, req_body, response) if @debug_dev + Response.from_faraday(response) + end + +private + + def require_adapter! + require "faraday/#{ADAPTER}" + rescue LoadError + raise LoadError.new( + "Faraday adapter #{ADAPTER.inspect} could not be loaded -- is its " \ + "gem (e.g. \"faraday-#{ADAPTER}\" and whatever it wraps) installed?") + end + + def build_connection(url) + cfg = @ssl_config + ssl_opts = { + :ca_file => cfg && cfg.ca_file, + :verify => !(cfg && cfg.verify_mode == OpenSSL::SSL::VERIFY_NONE), + # Faraday::SSLOptions documents client_cert/client_key as accepting + # OpenSSL objects directly, but confirmed empirically that the + # :typhoeus adapter rejects them ("Problem with the local SSL + # certificate") and only accepts file paths -- same requirement as + # curb's C binding (see curbClient.rb), so round-trip the same way + # here for whichever adapter is actually active. + :client_cert => cfg && cfg.client_cert && write_pem_tempfile(cfg.client_cert, 'cert'), + :client_key => cfg && cfg.client_key && write_pem_tempfile(cfg.client_key, 'key'), + :verify_depth => cfg && cfg.verify_depth, + :cert_store => cfg && cfg.cert_store, + } + # Faraday::SSLOptions is a Struct, and Faraday's own connection setup + # merges this hash into it via []= -- which raises NameError for any + # key that isn't a struct member, rather than just ignoring it the way + # a Hash would. :ciphers was dropped from that struct for a stretch of + # faraday 2.x releases and later restored (confirmed: present on + # 2.14.3, absent on 2.8.1 -- exactly what Ruby 3.3+ vs. 2.6/2.7 + # resolve to respectively) -- confirmed crashing every single request + # on the versions missing it. Only include the key at all when this + # faraday actually has it; it's passed through for adapters that honor + # SSLOptions#ciphers, but confirmed empirically that :typhoeus silently + # ignores it either way (Faraday's own typhoeus adapter doesn't + # forward it to ethon's ssl_cipher_list at all) -- a real, external gap + # in that adapter, not something this bridge can paper over. + # verify_depth/cert_store have the same no-guarantee-every-adapter- + # honors-them caveat. + ssl_opts[:ciphers] = cfg && cfg.ciphers if Faraday::SSLOptions.members.include?(:ciphers) + Faraday.new( + :url => url, + :proxy => no_proxy?(url.is_a?(URI) ? url : URI.parse(url)) ? nil : @proxy, + :ssl => ssl_opts + ) do |f| + f.adapter ADAPTER + end.tap do |conn| + conn.options.open_timeout = @connect_timeout if @connect_timeout + conn.options.timeout = @receive_timeout if @receive_timeout + end + end + + def write_pem_tempfile(openssl_obj, basename) + f = Tempfile.new(["soap4r-faraday-#{basename}-", '.pem']) + f.write(openssl_obj.to_pem) + f.close + # The finalizer proc must not be created in an instance-method context + # closing over self -- see the identical comment in curbClient.rb's own + # write_pem_tempfile for why (confirmed: Ruby warns "finalizer + # references object to be finalized" and the tempfile never actually + # got cleaned up otherwise). + ObjectSpace.define_finalizer(self, self.class.tempfile_unlinker(f)) + f.path + end + + def self.tempfile_unlinker(file) + proc { file.unlink rescue nil } + end + + def dump_wiredump(url, header, req_body, response) + # Mirrors httpclient's wiredump block layout (marker line, blank line, + # raw request-line + headers, blank line, body) -- callers that parse + # wiredump_dev output by block position or by scanning for a "POST ..." + # line (e.g. test/soap/test_streamhandler.rb's parse_req_header) depend + # on that exact shape regardless of which backend produced it. + uri = url.is_a?(URI) ? url : URI.parse(url) + request_line = (@proxy && !no_proxy?(uri)) ? url : uri.request_uri + @debug_dev << "= Request\n\n" + @debug_dev << "POST #{request_line} HTTP/1.1\n" + header.each { |k, v| @debug_dev << "#{k}: #{v}\n" } + @debug_dev << "\n" + @debug_dev << req_body + @debug_dev << "\n\n= Response\n\n" + @debug_dev << "HTTP/1.1 #{response.status} #{response.reason_phrase}\n" + contenttype = response.headers['content-type'] + @debug_dev << "Content-Type: #{contenttype}\n" if contenttype + @debug_dev << "\n" + @debug_dev << response.body + end + + NO_PROXY_HOSTS = ['localhost'] + + def no_proxy?(uri) + if !@proxy or NO_PROXY_HOSTS.include?(uri.host) + return true + end + unless @no_proxy + return false + end + @no_proxy.scan(/([^:,]+)(?::(\d+))?/) do |host, port| + if /(\A|\.)#{Regexp.quote(host)}\z/i =~ uri.host && + (!port || uri.port == port.to_i) + return true + end + end + false + end + + class SSLConfig + # client_cert/client_key/ciphers/verify_depth/cert_store are all applied + # via Faraday::SSLOptions in build_connection -- Faraday's options + # struct accepts the same OpenSSL::X509::Certificate/OpenSSL::PKey::RSA + # objects HTTPConfigLoader already builds for httpclient's sake. Whether + # each one is actually *honored* still depends on which concrete adapter + # Faraday is riding on (SOAP4R_FARADAY_ADAPTER) -- confirmed working for + # :net_http and :typhoeus, not verified against every adapter Faraday + # supports. + attr_accessor :client_cert + attr_accessor :client_key + attr_accessor :client_ca + attr_accessor :verify_mode + attr_accessor :verify_depth + attr_accessor :options # no unified Faraday equivalent across adapters (OpenSSL::SSL::SSLContext#options bitmask); stored only. + attr_accessor :ciphers + attr_accessor :verify_callback # NOT supported -- no adapter Faraday supports exposes a per-certificate Ruby callback hook (same underlying limitation as libcurl-based backends generally). + attr_accessor :cert_store + attr_reader :ca_file + + def set_trust_ca(value) + @ca_file = value + end + + def set_crl(value) + @crl = value # not applied -- no unified Faraday equivalent. + end + + # Whichever concrete adapter Faraday is riding on (net_http, patron, + # etc.) determines the real default CA trust behavior; none of them + # bundle their own snapshot the way httpclient does, so there's nothing + # to override here. + def set_default_paths + end + end + + class Response + attr_reader :status + attr_reader :reason + attr_reader :contenttype + attr_reader :header + attr_reader :content + + def initialize(status, reason, contenttype, header, content) + @status = status + @reason = reason + @contenttype = contenttype + @header = header + @content = content + end + + def self.from_faraday(response) + # Faraday::Utils::Headers keeps one value per key (last write wins for + # anything the adapter received more than once, e.g. repeated + # Set-Cookie lines) -- wrap each in a single-element Array so callers + # get the same Hash-of-Arrays shape the other backends provide + # (streamHandler.rb reads header['content-encoding'][0] and + # header['location'][0]). A known, accepted limitation: multi-valued + # response headers only reflect the last occurrence under this + # backend. + header = Hash.new { |h, k| h[k] = [] } + response.headers.each { |k, v| header[k.downcase] = [v] } + new(response.status, response.reason_phrase, response.headers['content-type'], + header, response.body) + end + end +end + + +end diff --git a/lib/soap/httpbackend.rb b/lib/soap/httpbackend.rb index 9f2b0400c..58b39ed3a 100644 --- a/lib/soap/httpbackend.rb +++ b/lib/soap/httpbackend.rb @@ -11,12 +11,21 @@ # anyone debugging a backend-specific issue) actually exercise # SOAP::NetHttpClient end-to-end instead of it only ever being reachable # when every other backend happens to fail to load. +# http-access2 (httpclient's own predecessor, by the same author) used to +# sit in this cascade too. Removed: the gem was renamed to httpclient years +# ago and is no longer published on RubyGems.org at all, so that entry +# could never actually load -- confirmed empirically (`gem install +# http-access2` fails outright). Anyone still vendoring the old gem +# directly (e.g. via a git ref) can select it by placing a matching +# lib/soap/httpbackend/http_access2.rb adapter back on their own +# $LOAD_PATH; see git history for the version that shipped here. if ENV.has_key?('SOAP4R_HTTP_CLIENTS') backend_list = ENV['SOAP4R_HTTP_CLIENTS'].to_s.split(',') else backend_list = [ 'httpclient', ## Uses the httpclient gem - 'http_access2', ## Uses the http-access2 gem ; no longer published on RubyGems.org + 'curb', ## Uses the curb gem (libcurl bindings) ; not installed by default, opt-in + 'faraday', ## Uses the faraday gem (itself pluggable -- see soap/faradayClient.rb) ; not installed by default, opt-in 'net_http', ## Falls back to this project's own wrapper around stdlib Net::HTTP ] end diff --git a/lib/soap/httpbackend/curb.rb b/lib/soap/httpbackend/curb.rb new file mode 100644 index 000000000..0ade1dfa5 --- /dev/null +++ b/lib/soap/httpbackend/curb.rb @@ -0,0 +1,7 @@ +# encoding: UTF-8 +# SOAP4R - HTTP client backend: the curb gem (libcurl bindings). + +require 'soap/httpbackend/registry' +require 'soap/curbClient' + +SOAP::HTTPBackend.register(SOAP::CurbClient, true) diff --git a/lib/soap/httpbackend/faraday.rb b/lib/soap/httpbackend/faraday.rb new file mode 100644 index 000000000..d0a1644ae --- /dev/null +++ b/lib/soap/httpbackend/faraday.rb @@ -0,0 +1,7 @@ +# encoding: UTF-8 +# SOAP4R - HTTP client backend: the faraday gem. + +require 'soap/httpbackend/registry' +require 'soap/faradayClient' + +SOAP::HTTPBackend.register(SOAP::FaradayClient, true) diff --git a/lib/soap/httpbackend/http_access2.rb b/lib/soap/httpbackend/http_access2.rb deleted file mode 100644 index 274fc5df2..000000000 --- a/lib/soap/httpbackend/http_access2.rb +++ /dev/null @@ -1,14 +0,0 @@ -# encoding: UTF-8 -# SOAP4R - HTTP client backend: the http-access2 gem (httpclient's -# predecessor, by the same author; no longer published on RubyGems.org, so -# this adapter is effectively unreachable in practice today, but kept for -# anyone still vendoring the gem directly). - -require 'soap/httpbackend/registry' -require 'http-access2' - -if HTTPAccess2::VERSION < "2.0" - raise LoadError.new("http-access2/2.0 or later is required.") -end - -SOAP::HTTPBackend.register(HTTPAccess2::Client, true) diff --git a/lib/wsdl/xmlSchema/importer.rb b/lib/wsdl/xmlSchema/importer.rb index 2f4743957..34a6214cb 100644 --- a/lib/wsdl/xmlSchema/importer.rb +++ b/lib/wsdl/xmlSchema/importer.rb @@ -82,17 +82,9 @@ def web_client require 'httpclient' @web_client = HTTPClient rescue LoadError - begin - require 'http-access2' - if HTTPAccess2::VERSION < "2.0" - raise LoadError.new("http-access/2.0 or later is required.") - end - @web_client = HTTPAccess2::Client - rescue LoadError - warn("Loading http-access2 failed. Net/http is used.") if $DEBUG - require 'soap/netHttpClient' - @web_client = ::SOAP::NetHttpClient - end + warn("Loading httpclient failed. Net/http is used.") if $DEBUG + require 'soap/netHttpClient' + @web_client = ::SOAP::NetHttpClient end @web_client end diff --git a/lib/xsd/datatypes.rb b/lib/xsd/datatypes.rb index f1bd246ad..85c487dcd 100644 --- a/lib/xsd/datatypes.rb +++ b/lib/xsd/datatypes.rb @@ -584,10 +584,18 @@ def screen_data(t) t <<= 12 if t.year < 0 t elsif t.is_a?(Time) - jd = DateTime.send(:civil_to_jd, t.year, t.mon, t.mday, DateTime::ITALY) - fr = DateTime.send(:time_to_day_fraction, t.hour, t.min, [t.sec, 59].min) + t.usec.to_r / DayInMicro + # Reached only on Rubies without Time#to_datetime (in practice, 1.8.7 + # only, since the to_datetime branch above wins on every later + # version). The civil_to_jd/jd_to_ajd route below built a Rational + # day-fraction and hit the exact same Ruby 1.8.7 DateTime/Rational + # arithmetic bug documented in screen_data_str's secfrac handling + # below (confirmed: produced a bogus ~4714 BC date). Build via + # DateTime.civil directly with the sub-second fraction baked into + # the seconds argument instead, avoiding the buggy code path + # entirely, same fix as that other call site. of = t.utc_offset.to_r / DayInSec - DateTime.new!(DateTime.send(:jd_to_ajd, jd, fr, of), of, DateTime::ITALY) + secfrac = t.usec.to_r / SecInMicro + DateTime.civil(t.year, t.mon, t.mday, t.hour, t.min, t.sec + secfrac, of) else screen_data_str(t) end diff --git a/lib/xsd/xmlparser/oxparser.rb b/lib/xsd/xmlparser/oxparser.rb index 52bf60689..ad03d3908 100644 --- a/lib/xsd/xmlparser/oxparser.rb +++ b/lib/xsd/xmlparser/oxparser.rb @@ -23,11 +23,17 @@ def do_parse(string_or_readable) handler = OxDocHandler.new(self, @decoder) string = string_or_readable.respond_to?(:read) ? string_or_readable.read : StringIO.new(string_or_readable) + # :skip_none regardless of decoder: the old no-decoder default + # (:skip_return) discarded \r entirely, breaking CRLF round-tripping + # (see test_string_crlf) whenever htmlentities wasn't installed. if @decoder.nil? - # Use the built-in conversion with Ox. - ::Ox.sax_parse(handler, string, {:symbolize=> false, :convert_special=> true, :skip=> :skip_return} ) + # Ox's own :convert_special decodes the full named-entity set, but this + # path segfaults on complex documents under Ox 2.14.14 (the version + # Ruby 2.2.x-2.6.x are stuck on) -- htmlentities is required there + # specifically to avoid this branch entirely (see Gemfile). + ::Ox.sax_parse(handler, string, {:symbolize=> false, :convert_special=> true, :skip=> :skip_none} ) else - # Use HTMLEntities Decoder. Leave the special-character conversion alone and let HTMLEntities decode it for us. + # Let HTMLEntities decode instead; leave Ox's own conversion off. ::Ox.sax_parse(handler, string, {:skip=> :skip_none}) end end diff --git a/test/soap/asp.net/test_aspdotnet.rb b/test/soap/asp.net/test_aspdotnet.rb index 776ddb772..5451b25e2 100644 --- a/test/soap/asp.net/test_aspdotnet.rb +++ b/test/soap/asp.net/test_aspdotnet.rb @@ -94,15 +94,8 @@ def test_aspdotnethandler assert_equal("Hello Mike", @client.sayHello("Mike")) end - # Fixture/assertions below assume httpclient's wiredump format, so this - # must check the ACTIVE backend (SOAP4R_HTTP_CLIENTS can force a - # different one -- see lib/soap/httpbackend.rb), not merely whether the - # gem happens to be loaded in this process (e.g. test/soap/ssl/test_ssl.rb - # requires it unconditionally regardless of the active backend). - if defined?(HTTPClient) and SOAP::HTTPStreamHandler::Client == HTTPClient - - # qualified! - REQUEST_ASPDOTNETHANDLER = + # qualified! + REQUEST_ASPDOTNETHANDLER = %q[ ] - def test_aspdotnethandler_envelope - @client = SOAP::RPC::Driver.new(Endpoint, Server::Namespace) - @client.wiredump_dev = str = String.new - @client.add_method_with_soapaction('sayHello', Server::Namespace + 'SayHello', 'name') - @client.default_encodingstyle = SOAP::EncodingStyle::ASPDotNetHandler::Namespace - assert_equal("Hello Mike", @client.sayHello("Mike")) - assert_xml_equal(REQUEST_ASPDOTNETHANDLER, parse_requestxml(str), - [REQUEST_ASPDOTNETHANDLER, parse_requestxml(str)].join("\n\n")) - end - - def parse_requestxml(str) - str.split(/\r?\n\r?\n/)[3] - end + def test_aspdotnethandler_envelope + @client = SOAP::RPC::Driver.new(Endpoint, Server::Namespace) + @client.wiredump_dev = str = String.new + @client.add_method_with_soapaction('sayHello', Server::Namespace + 'SayHello', 'name') + @client.default_encodingstyle = SOAP::EncodingStyle::ASPDotNetHandler::Namespace + assert_equal("Hello Mike", @client.sayHello("Mike")) + assert_xml_equal(REQUEST_ASPDOTNETHANDLER, parse_requestxml(str), + [REQUEST_ASPDOTNETHANDLER, parse_requestxml(str)].join("\n\n")) + end + def parse_requestxml(str) + TestUtil.parse_wiredump_request_body(str) end end diff --git a/test/soap/auth/test_basic.rb b/test/soap/auth/test_basic.rb index 912b39ad0..a61ebc874 100644 --- a/test/soap/auth/test_basic.rb +++ b/test/soap/auth/test_basic.rb @@ -130,14 +130,19 @@ def test_proxy private - # SOAP::NetHttpClient#set_auth explicitly raises NotImplementedError -- - # soap4r + net/http never supported WWW-Authenticate-style auth. This is - # a real, permanent backend limitation, not a bug, so skip cleanly under - # SOAP4R_HTTP_CLIENTS=net_http instead of failing (see - # lib/soap/httpbackend.rb; test_streamhandler.rb's test_basic_auth uses - # the same pattern). + # SOAP::NetHttpClient#set_auth and SOAP::FaradayClient#set_auth both + # explicitly raise NotImplementedError -- neither soap4r + net/http nor + # soap4r + faraday (whose core has no bundled challenge-response + # middleware) support WWW-Authenticate-style auth. This is a real, + # permanent limitation for those backends, not a bug, so skip cleanly + # rather than fail (see lib/soap/httpbackend.rb; + # test_streamhandler.rb's test_basic_auth uses the same pattern). + # SOAP::CurbClient DOES support this (libcurl negotiates the challenge + # itself), so it's deliberately absent from this list. + AUTH_UNSUPPORTED_BACKENDS = %w[SOAP::NetHttpClient SOAP::FaradayClient] + def auth_supported? - SOAP::HTTPStreamHandler::Client != SOAP::NetHttpClient + !AUTH_UNSUPPORTED_BACKENDS.include?(SOAP::HTTPStreamHandler::Client.name) end end diff --git a/test/soap/auth/test_digest.rb b/test/soap/auth/test_digest.rb index 53647c74a..e73ee6262 100644 --- a/test/soap/auth/test_digest.rb +++ b/test/soap/auth/test_digest.rb @@ -130,14 +130,19 @@ def test_proxy private - # SOAP::NetHttpClient#set_auth explicitly raises NotImplementedError -- - # soap4r + net/http never supported WWW-Authenticate-style auth. This is - # a real, permanent backend limitation, not a bug, so skip cleanly under - # SOAP4R_HTTP_CLIENTS=net_http instead of failing (see - # lib/soap/httpbackend.rb; test_streamhandler.rb's test_basic_auth uses - # the same pattern). + # SOAP::NetHttpClient#set_auth and SOAP::FaradayClient#set_auth both + # explicitly raise NotImplementedError -- neither soap4r + net/http nor + # soap4r + faraday (whose core has no bundled challenge-response + # middleware) support WWW-Authenticate-style auth. This is a real, + # permanent limitation for those backends, not a bug, so skip cleanly + # rather than fail (see lib/soap/httpbackend.rb; + # test_streamhandler.rb's test_basic_auth uses the same pattern). + # SOAP::CurbClient DOES support this (libcurl negotiates the challenge + # itself), so it's deliberately absent from this list. + AUTH_UNSUPPORTED_BACKENDS = %w[SOAP::NetHttpClient SOAP::FaradayClient] + def auth_supported? - SOAP::HTTPStreamHandler::Client != SOAP::NetHttpClient + !AUTH_UNSUPPORTED_BACKENDS.include?(SOAP::HTTPStreamHandler::Client.name) end end diff --git a/test/soap/ssl/test_ssl.rb b/test/soap/ssl/test_ssl.rb index 717085f5b..f750e0c84 100644 --- a/test/soap/ssl/test_ssl.rb +++ b/test/soap/ssl/test_ssl.rb @@ -5,17 +5,30 @@ require 'httpclient' rescue LoadError end +begin + require 'curb' +rescue LoadError +end +begin + require 'faraday' +rescue LoadError +end require 'soap/rpc/driver' # Checking defined?(HTTPClient) alone isn't enough now that the HTTP client # backend is independently selectable (SOAP4R_HTTP_CLIENTS -- see -# lib/soap/httpbackend.rb): the require above pulls in the gem regardless of -# which backend SOAP::HTTPStreamHandler actually picked, so HTTPClient can be -# defined while, say, SOAP::NetHttpClient (whose #ssl_config is always nil) -# is the active one. Every assertion below assumes httpclient's SSLConfig -# shape, so this must check the ACTIVE backend, not merely whether the gem -# loaded. -if SOAP::HTTPStreamHandler::Client == HTTPClient and defined?(OpenSSL) +# lib/soap/httpbackend.rb): the requires above pull in these gems +# regardless of which backend SOAP::HTTPStreamHandler actually picked, so +# e.g. HTTPClient can be defined while SOAP::NetHttpClient (whose +# #ssl_config is always nil) is the active one. This must check the ACTIVE +# backend, not merely whether a gem happens to be loaded. +# +# httpclient, curb, and faraday (any SOAP4R_FARADAY_ADAPTER) all get real +# SSL config plumbing exercised here -- SOAP::NetHttpClient is excluded +# since it has no SSL configuration surface of its own at all (see +# lib/soap/netHttpClient.rb). +SSL_TESTABLE_BACKENDS = %w[HTTPClient SOAP::CurbClient SOAP::FaradayClient] +if SSL_TESTABLE_BACKENDS.include?(SOAP::HTTPStreamHandler::Client.name) and defined?(OpenSSL) module SOAP; module SSL @@ -43,7 +56,38 @@ def teardown teardown_server end + def httpclient_backend? + SOAP::HTTPStreamHandler::Client == HTTPClient + end + + def faraday_backend? + SOAP::HTTPStreamHandler::Client.name == 'SOAP::FaradayClient' + end + + # Each backend surfaces a TLS verification/handshake failure as a + # different exception class -- confirmed empirically against the real + # server this file spins up, not guessed from documentation: + # httpclient: OpenSSL::SSL::SSLError + # curb: Curl::Err::CurlError (base class for its whole SSL error + # family -- SSLPeerCertificateError, SSLCACertificateError, + # SSLCypherError, etc. -- rather than pinning to one) + # faraday: Faraday::Error (SSLError and ConnectionFailed are BOTH + # direct subclasses of it, and different Faraday adapters + # raise different ones for the same failure -- confirmed + # :typhoeus raises ConnectionFailed, not SSLError) + def expected_ssl_error_class + case SOAP::HTTPStreamHandler::Client.name + when 'SOAP::CurbClient' + Curl::Err::CurlError + when 'SOAP::FaradayClient' + Faraday::Error + else + OpenSSL::SSL::SSLError + end + end + def test_options + return unless httpclient_backend? cfg = @client.streamhandler.client.ssl_config assert_nil(cfg.client_cert) assert_nil(cfg.client_key) @@ -70,7 +114,14 @@ def test_options end end + # verify_callback is not portable across backends: no libcurl-based + # backend (curb, or Faraday riding on typhoeus/patron/etc.) exposes a + # per-certificate Ruby callback hook the way OpenSSL::SSL::SSLContext + # does -- confirmed there's no equivalent in either library's public API. + # See test_ca_verification below for the backend-neutral equivalent of + # what this test covers, minus the callback-specific assertions. def test_verification + return unless httpclient_backend? cfg = @client.options cfg["protocol.http.ssl_config.verify_callback"] = method(:verify_callback).to_proc begin @@ -132,7 +183,9 @@ def test_verification assert_equal("Hello World, from ssl client", @client.hello_world("ssl client")) end + # Also verify_callback-dependent throughout -- see test_verification above. def test_property + return unless httpclient_backend? testpropertyname = File.join(DIR, 'soapclient.properties') File.open(testpropertyname, "w") do |f| f<<<<__EOP__ @@ -200,18 +253,73 @@ def test_ciphers #cfg.timeout = 123 cfg["protocol.http.ssl_config.ciphers"] = "!ALL" # - begin - @client.hello_world("ssl client") - assert(false) - rescue OpenSSL::SSL::SSLError => ssle - # depends on OpenSSL version. (?:0.9.8|0.9.7) - assert_match(/\A(?:SSL_CTX_set_cipher_list:+ no cipher match|no ciphers available)\z/, ssle.message) + if faraday_backend? + # Confirmed empirically: Faraday's own :typhoeus adapter never + # forwards Faraday::SSLOptions#ciphers to ethon's ssl_cipher_list at + # all, so "!ALL" is silently ignored rather than rejected -- a real + # gap in that third-party adapter (not this bridge, and not + # something soap4r-ng's own code can fix). The handshake succeeds + # here regardless of the (unhonored) cipher restriction. + assert_equal("Hello World, from ssl client", @client.hello_world("ssl client")) + else + begin + @client.hello_world("ssl client") + assert(false) + rescue expected_ssl_error_class => ssle + if httpclient_backend? + # depends on OpenSSL version. (?:0.9.8|0.9.7) + assert_match(/\A(?:SSL_CTX_set_cipher_list:+ no cipher match|no ciphers available)\z/, ssle.message) + end + # curb's own message ("Could not use specified SSL cipher: failed + # setting cipher list: !ALL") is asserted only by class above -- + # its exact wording isn't pinned since it's libcurl/OpenSSL-version + # dependent the same way httpclient's is. + end end # cfg["protocol.http.ssl_config.ciphers"] = "ALL" assert_equal("Hello World, from ssl client", @client.hello_world("ssl client")) end + # Backend-neutral equivalent of test_verification/test_property, minus + # the verify_callback-specific assertions those can't support on + # anything but httpclient (see the comment on test_verification). This + # is what actually matters for "does soap4r-ng's own config-loading + # bridge correctly plumb ca_file/client_cert/client_key through to a real + # TLS handshake" -- runs identically for every SSL-testable backend. + def test_ca_verification + cfg = @client.options + cfg["protocol.http.ssl_config.client_cert"] = File.join(DIR, "client.cert") + cfg["protocol.http.ssl_config.client_key"] = File.join(DIR, "client.key") + # + # No ca_file configured at all -- client has no reason to trust this + # private test CA, so verification must fail regardless of backend. + assert_ssl_verification_fails { @client.hello_world("ssl client") } + # + # Wrong CA (the root, not the intermediate that actually signed the + # server's cert) -- still fails. + cfg["protocol.http.ssl_config.ca_file"] = File.join(DIR, "ca.cert") + assert_ssl_verification_fails { @client.hello_world("ssl client") } + # + # Correct CA (the signing intermediate) -- succeeds. + cfg["protocol.http.ssl_config.ca_file"] = File.join(DIR, "subca.cert") + assert_equal("Hello World, from ssl client", @client.hello_world("ssl client")) + end + + # test-unit's assert_raise wants an EXACT class match, not + # kind_of?/subclass-inclusive like plain Ruby rescue -- confirmed + # empirically (curb/Faraday both raise a specific subclass of the base + # class expected_ssl_error_class returns, e.g. Curl::Err::SSLPeerCertificateError + # rather than bare Curl::Err::CurlError, and assert_raise rejected it). + # Every other SSL-exception check in this file already routes around + # that via begin/rescue instead of assert_raise; this just gives + # test_ca_verification the same treatment. + def assert_ssl_verification_fails + yield + assert(false, "expected an SSL verification failure") + rescue expected_ssl_error_class + end + private def setup_server diff --git a/test/soap/test_cookie.rb b/test/soap/test_cookie.rb index 9db26d75c..5463bdf7f 100644 --- a/test/soap/test_cookie.rb +++ b/test/soap/test_cookie.rb @@ -103,15 +103,19 @@ def do_server_proc(req, res) end + # SOAP::NetHttpClient/CurbClient/FaradayClient all expose #request_filter + # (so streamHandler.rb's respond_to?(:request_filter) check passes and + # wires this filter in), but none of them actually invoke it anywhere in + # their own request/response code -- it's a genuinely inert accessor on + # all three, like #ssl_config is for NetHttpClient. Only httpclient (which + # has its own native filter-chain support) really honors it. + FILTER_CHAIN_INERT_BACKENDS = %w[SOAP::NetHttpClient SOAP::CurbClient SOAP::FaradayClient] + def test_normal - # SOAP::NetHttpClient exposes #request_filter (so streamHandler.rb's - # respond_to?(:request_filter) check passes and wires this filter in), - # but never actually invokes it anywhere in its own request/response - # code -- it's a genuinely inert accessor, like #ssl_config. Skip - # cleanly under that backend rather than fail on a feature it was never - # wired up to support (see lib/soap/httpbackend.rb for how the active - # backend is selected/forced). - return if SOAP::HTTPStreamHandler::Client == SOAP::NetHttpClient + # Skip cleanly under a backend that never wired this up, rather than + # fail on a feature it doesn't support (see lib/soap/httpbackend.rb for + # how the active backend is selected/forced). + return if FILTER_CHAIN_INERT_BACKENDS.include?(SOAP::HTTPStreamHandler::Client.name) @client.wiredump_dev = STDOUT if $DEBUG filter = CookieFilter.new @client.streamhandler.filterchain << filter diff --git a/test/soap/test_nethttpclient.rb b/test/soap/test_nethttpclient.rb index f0cabf3f0..f8733b943 100644 --- a/test/soap/test_nethttpclient.rb +++ b/test/soap/test_nethttpclient.rb @@ -7,12 +7,12 @@ module SOAP # Covers SOAP::NetHttpClient#create_connection directly (via #send, since # it's private) rather than through the full driver/streamHandler stack: -# by default HTTPStreamHandler only falls back to NetHttpClient when both -# httpclient and http-access2 fail to load, and this project's Gemfile -# always installs httpclient, so nothing else in the suite exercises this -# file under the default backend order. create_connection only builds and -# configures a Net::HTTP object (no #start call), so this is safe to test -# without a real network call or proxy server. +# by default HTTPStreamHandler only falls back to NetHttpClient when +# httpclient fails to load, and this project's Gemfile always installs +# httpclient, so nothing else in the suite exercises this file under the +# default backend order. create_connection only builds and configures a +# Net::HTTP object (no #start call), so this is safe to test without a real +# network call or proxy server. # # For real end-to-end coverage of this backend (live WEBrick round trips, # proxying, basic auth, etc.), force it via SOAP4R_HTTP_CLIENTS=net_http -- diff --git a/test/soap/test_no_indent.rb b/test/soap/test_no_indent.rb index 844548790..3fab57d88 100644 --- a/test/soap/test_no_indent.rb +++ b/test/soap/test_no_indent.rb @@ -1,15 +1,9 @@ # encoding: UTF-8 require 'helper' +require 'testutil' require 'soap/rpc/standaloneServer' require 'soap/rpc/driver' -# Assertions below assume httpclient's wiredump format, so this must check -# the ACTIVE backend (SOAP4R_HTTP_CLIENTS can force a different one -- see -# lib/soap/httpbackend.rb), not merely whether the gem happens to be loaded -# in this process (e.g. test/soap/ssl/test_ssl.rb requires it unconditionally -# regardless of the active backend). -if defined?(HTTPClient) and SOAP::HTTPStreamHandler::Client == HTTPClient - module SOAP @@ -90,11 +84,9 @@ def test_no_indent end def parse_requestxml(str) - str.split(/\r?\n\r?\n/)[3] + TestUtil.parse_wiredump_request_body(str) end end -end - end diff --git a/test/soap/test_streamhandler.rb b/test/soap/test_streamhandler.rb index 0ec509ab4..e81050872 100644 --- a/test/soap/test_streamhandler.rb +++ b/test/soap/test_streamhandler.rb @@ -189,16 +189,26 @@ def test_basic_auth assert_nil(@client.do_server_proc_basic_auth) end + # Every backend that owns a NO_PROXY_HOSTS constant treats 'localhost' as + # never-proxied by default (see e.g. lib/soap/netHttpClient.rb, + # lib/soap/curbClient.rb, lib/soap/faradayClient.rb) -- this test needs + # the opposite for its own assertions, so it clears whichever class is + # ACTIVE, not a hardcoded HTTPClient-or-NetHttpClient binary choice (that + # binary check predates SOAP4R_HTTP_CLIENTS making more than two backends + # reachable -- see lib/soap/httpbackend.rb). + def no_proxy_hosts_holder + # const_defined?'s 2-arg (inherit) form is Ruby 1.9+ only -- the 1-arg + # form works identically here across every supported Ruby version since + # SOAP::HTTPStreamHandler::Client itself (not some ancestor) is always + # what actually defines NO_PROXY_HOSTS on every backend that has one. + SOAP::HTTPStreamHandler::Client.const_defined?(:NO_PROXY_HOSTS) ? + SOAP::HTTPStreamHandler::Client : nil + end + def test_proxy - # See test_basic_auth above for why this checks the ACTIVE backend - # rather than merely whether HTTPClient happens to be loaded. - if defined?(HTTPClient) and SOAP::HTTPStreamHandler::Client == HTTPClient - backup = HTTPClient::NO_PROXY_HOSTS.dup - HTTPClient::NO_PROXY_HOSTS.clear - else - backup = SOAP::NetHttpClient::NO_PROXY_HOSTS.dup - SOAP::NetHttpClient::NO_PROXY_HOSTS.clear - end + holder = no_proxy_hosts_holder + backup = holder ? holder::NO_PROXY_HOSTS.dup : nil + holder::NO_PROXY_HOSTS.clear if holder setup_proxyserver str = String.new @client.wiredump_dev = str @@ -211,11 +221,8 @@ def test_proxy @client.options["protocol.http.proxy"] = 'ftp://foo:8080' end ensure - if defined?(HTTPClient) and SOAP::HTTPStreamHandler::Client == HTTPClient - HTTPClient::NO_PROXY_HOSTS.replace(backup) - else - SOAP::NetHttpClient::NO_PROXY_HOSTS.replace(backup) - end + holder = no_proxy_hosts_holder + holder::NO_PROXY_HOSTS.replace(backup) if holder && backup end def test_charset diff --git a/test/testutil.rb b/test/testutil.rb index 815e3c09a..369e8c5c6 100644 --- a/test/testutil.rb +++ b/test/testutil.rb @@ -61,6 +61,27 @@ def self.webrick_proxy_server(options) webrick_server(WEBrick::HTTPProxyServer, options) end + # Extracts the request body XML out of a wiredump_dev capture. Every HTTP + # client backend (see lib/soap/httpbackend.rb) writes the same block + # layout -- "Wire dump:", "= Request", the raw request-line+headers, the + # body, "= Response", the raw response-line+headers, the response body -- + # separated by blank lines, so the body is always the 4th block (index 3) + # regardless of which backend produced the dump. This was confirmed by + # design, not by accident: SOAP::NetHttpClient/CurbClient/FaradayClient + # were all built to match httpclient's own block layout exactly (see the + # "Mirrors httpclient's wiredump block layout" comments in + # lib/soap/netHttpClient.rb, curbClient.rb, and faradayClient.rb) precisely + # so callers like this one don't need to special-case any of them. + def self.parse_wiredump_request_body(str) + str.split(/\r?\n\r?\n/)[3] + end + + # Same deal, for the response body (block index 6 -- see + # parse_wiredump_request_body above for the full block layout). + def self.parse_wiredump_response_body(str) + str.split(/\r?\n\r?\n/)[6] + end + def self.webrick_server(klass, options) try = 0 begin diff --git a/test/wsdl/anonymous/test_anonymous.rb b/test/wsdl/anonymous/test_anonymous.rb index 9d3e1ed11..28810ceff 100644 --- a/test/wsdl/anonymous/test_anonymous.rb +++ b/test/wsdl/anonymous/test_anonymous.rb @@ -6,13 +6,6 @@ require 'soap/wsdlDriver' -# Assertions below assume httpclient's wiredump format, so this must check -# the ACTIVE backend (SOAP4R_HTTP_CLIENTS can force a different one -- see -# lib/soap/httpbackend.rb), not merely whether the gem happens to be loaded -# in this process (e.g. test/soap/ssl/test_ssl.rb requires it unconditionally -# regardless of the active backend). -if defined?(HTTPClient) and SOAP::HTTPStreamHandler::Client == HTTPClient - module WSDL; module Anonymous @@ -138,5 +131,3 @@ def test_anonymous_mapping end; end - -end diff --git a/test/wsdl/qualified/test_qualified.rb b/test/wsdl/qualified/test_qualified.rb index 5540128ba..515882ad5 100644 --- a/test/wsdl/qualified/test_qualified.rb +++ b/test/wsdl/qualified/test_qualified.rb @@ -6,13 +6,6 @@ require 'soap/wsdlDriver' -# Assertions below assume httpclient's wiredump format, so this must check -# the ACTIVE backend (SOAP4R_HTTP_CLIENTS can force a different one -- see -# lib/soap/httpbackend.rb), not merely whether the gem happens to be loaded -# in this process (e.g. test/soap/ssl/test_ssl.rb requires it unconditionally -# regardless of the active backend). -if defined?(HTTPClient) and SOAP::HTTPStreamHandler::Client == HTTPClient - module WSDL @@ -143,11 +136,9 @@ def test_naive end def parse_requestxml(str) - str.split(/\r?\n\r?\n/)[3] + TestUtil.parse_wiredump_request_body(str) end end -end - end diff --git a/test/wsdl/qualified/test_unqualified.rb b/test/wsdl/qualified/test_unqualified.rb index 1d3163a2b..cc87cabda 100644 --- a/test/wsdl/qualified/test_unqualified.rb +++ b/test/wsdl/qualified/test_unqualified.rb @@ -6,13 +6,6 @@ require 'soap/wsdlDriver' -# Assertions below assume httpclient's wiredump format, so this must check -# the ACTIVE backend (SOAP4R_HTTP_CLIENTS can force a different one -- see -# lib/soap/httpbackend.rb), not merely whether the gem happens to be loaded -# in this process (e.g. test/soap/ssl/test_ssl.rb requires it unconditionally -# regardless of the active backend). -if defined?(HTTPClient) and SOAP::HTTPStreamHandler::Client == HTTPClient - module WSDL @@ -144,11 +137,9 @@ def test_naive end def parse_requestxml(str) - str.split(/\r?\n\r?\n/)[3] + TestUtil.parse_wiredump_request_body(str) end end -end - end diff --git a/test/wsdl/rpc/test_rpc_lit.rb b/test/wsdl/rpc/test_rpc_lit.rb index 4f27f1763..66995b1ee 100644 --- a/test/wsdl/rpc/test_rpc_lit.rb +++ b/test/wsdl/rpc/test_rpc_lit.rb @@ -6,13 +6,6 @@ require 'soap/wsdlDriver' -# Assertions below assume httpclient's wiredump format, so this must check -# the ACTIVE backend (SOAP4R_HTTP_CLIENTS can force a different one -- see -# lib/soap/httpbackend.rb), not merely whether the gem happens to be loaded -# in this process (e.g. test/soap/ssl/test_ssl.rb requires it unconditionally -# regardless of the active backend). -if defined?(HTTPClient) and SOAP::HTTPStreamHandler::Client == HTTPClient and defined?(OpenSSL) - module WSDL; module RPC @@ -468,15 +461,13 @@ def test_stub_echoStructArray end def parse_requestxml(str) - str.split(/\r?\n\r?\n/)[3] + TestUtil.parse_wiredump_request_body(str) end def parse_responsexml(str) - str.split(/\r?\n\r?\n/)[6] + TestUtil.parse_wiredump_response_body(str) end end end; end - -end