. "$BST_LIB" # BST-TIMEOUT: 1200 # FOUR python3 starts, and they are the whole cost. Every # # other assertion below reads the filesystem or dpkg with # # shell builtins, or greps a line out of the transcript the # # fourth start left behind. # # # # The four are: a bare `import socks', an `import # # sockshandler' (its own case because it is the one with # # real dependencies), a third whose STDERR is the assertion # # (`import-quiet'), and the probe, which takes ONE start for # # two hundred and thirty-eight measurements. # # # # The work inside the probe is a few hundred bytes of # # struct and string handling against a BytesIO. There is no # # I/O in it at all -- no connect, no listen, no resolver -- # # so it is microseconds beside an interpreter start on this # # arm, which is tens of seconds before the first line of the # # program runs. # # # # 1200 is 300s per start, against the ~60s a python3 start # # has measured at here. Nothing in this suite is quadratic # # in anything, so the start is the only thing the number has # # to cover. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify, and the measured elapsed replaces # # this note in the test plan rather than in this comment, # # which would cost an image build to keep the shipped suite # # byte-identical. # # python3-pysocks (PySocks 1.7.1, poky meta/recipes-devtools/python/ # python3-pysocks_1.7.1.bb -- `inherit pypi setuptools3', PyPI sdist) on the # wasm32-hwjs glibc image. # # ============================================================================ # WHERE THESE EXPECTATIONS COME FROM, WHICH IS NOT WHERE THEY USUALLY DO. # ============================================================================ # # THE PACKAGE WAS NOT BUILT WHEN THIS SUITE WAS WRITTEN. BRINTOS-2132's # recipe-scope build ran 1579 tasks and one failed -- python3's own do_compile, # on `paste: not found', a defect in meta-brintos at this batch's pinned base # (python3_%.bbappend:892, commit 5eebdc1a) and nothing to do with this port. # python3-pysocks is downstream of python3's sysroot, so no .deb was produced. # # So the DPKG-SIDE cases below -- the file list, the dist-info, the Depends line # -- are DERIVED from the recipe and from a wheel built from the same sdist on # the build host, not read off a package this image produced. They are # predictions, and the batch verify is what measures them. # # The BEHAVIOURAL cases are a different matter and are not predictions: every # expected value below was MEASURED, by running this suite's own probe against # this sdist's own socks.py on Python 3.12.3 -- the same 3.12 the image ships. # What the target can still change is the platform underneath (a socket that # cannot be created, an absent AF_INET6), and those are the cases that would go # red for a reason worth knowing. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # TWO FLAT MODULES, no package directory: `socks' is the library and # `sockshandler' is a urllib glue layer on top of it. `socks.socksocket' is a # `socket.socket' SUBCLASS that speaks SOCKS4, SOCKS4a, SOCKS5 or HTTP CONNECT # to a proxy during `connect()' and is an ordinary socket afterwards -- so the # drop-in claim is literally a class swap, and `wrap_module' is the module that # performs one for you. # # The protocol work is byte computation and the transport is the standard # socket, which is what lets this suite assert the whole of it OFFLINE: the # probe overrides `makefile' and `sendall' on a socksocket subclass, so every # handshake below is driven through the module's own negotiators against an # in-memory wire, with no peer, no listener and no port. # # ============================================================================ # THE HEADLINE FINDING: A CREDENTIAL OF 128 CHARACTERS OR MORE BUILDS A PACKET # NO SERVER CAN PARSE, AND IT IS THE CLIENT THAT BUILDS IT. # ============================================================================ # # RFC 1929's ULEN and PLEN are ONE byte each. `_SOCKS5_request' writes them as # # chr(len(username)).encode() # # and `str.encode()' is UTF-8, so any length from 128 up encodes to TWO bytes. # `s5-ulen-boundary' pins 128 as the first length at which it happens; # `s5-long-user-ulen-bytes' is the `c3 88' that a 200-character username writes # where `c8' was meant, and `s5-long-user-auth-len' is the resulting 212-byte # packet against the 211 a correct encoder produces. Nothing raises: the client # sends it and the server reads the length as 195 and the rest as garbage. # # `_write_SOCKS5_address' HAS THE SAME DEFECT ON THE HOSTNAME, `s5-long-host- # prefix' being the `03 c2 88' of a 136-byte name. That one is reachable # WITHOUT a long credential, by a caller who simply has a long hostname -- and # a DNS name may be up to 253 bytes, so this is inside the protocol's own # limits rather than an abuse of them. # # ============================================================================ # `wrap_module' DOES NOT PATCH THE MODULE IT IS GIVEN. IT PATCHES `socket'. # ============================================================================ # # The whole body is `module.socket.socket = socksocket'. `module.socket' is not # a copy -- it is THE socket module object -- so the assignment rebinds # `socket.socket' for the entire interpreter. `wrap-patched-globally' asserts # that through `sys.modules', which is as far from the module that was passed as # it is possible to get. # # The docstring's "will only work on modules that import socket directly into # the namespace" reads as a limit on the BLAST RADIUS and is not one: it # describes how the module is REACHED. Hand it any module that has imported # socket and every socket the process creates afterwards is proxied, including # ones in libraries the caller never named. # # And there is no `unwrap_module' (`has-unwrap-module'). Undoing it means having # kept the original class yourself, which nothing in the API hands back -- # `restored-cleanly' is the probe doing exactly that by hand, which is the only # way there is. # # ============================================================================ # `get_proxy_peername' RETURNS THE DESTINATION, NOT THE PROXY. # ============================================================================ # # The class binds `getpeername = get_peername' and `get_proxy_peername' is # `return self.getpeername()'. That lookup finds the REBINDING, so both methods # answer `self.proxy_peername' -- the destination. `proxy-peername-equals- # peername' asserts the two are the same object and `get-proxy-peername' pins # what it is. # # The docstring above it says "Returns the IP and port number of the proxy." # There is no way through this API to ask a connected socksocket for its proxy's # address: the base class's `getpeername' is shadowed and nothing exposes it. # # ============================================================================ # EVERY EXCEPTION IT RAISES IS AN OSError WITH NOTHING IN IT. # ============================================================================ # # `ProxyError.__init__' sets `self.msg' and never calls its base, and # `OSError.__new__' fills in `args'/`errno'/`strerror' only for a subclass that # leaves `__init__' alone. So `args' is the EMPTY TUPLE (`proxyerror-args-len') # and `errno' is None (`proxyerror-errno') on all six of them. # # That costs something real, in this module's own code. `connect_ex' is # documented to return an error indicator rather than raise, and it does so by # testing `e.errno' -- which is None here, so it RE-RAISES (`connect-ex-ipv6'). # A caller who chose `connect_ex' precisely to avoid exceptions gets one. And # `except OSError as e: e.args[0]', the idiom a caller reaches for next, is an # IndexError. # # ============================================================================ # `connect()' REPORTS A MALFORMED PAIR AS AN IPv6 REFUSAL, OR NOT AT ALL. # ============================================================================ # # The first line of `connect' is # # if len(dest_pair) != 2 or dest_pair[0].startswith("["): # # and both branches raise the SAME message. So a one-tuple -- a forgotten port -- # comes back as "PySocks doesn't support IPv6: ('h',)", naming a feature the # caller was not using (`connect-short-pair-msg'). # # And `dest_pair[0].startswith' runs BEFORE the type check that exists to raise # GeneralProxyError for a bad pair, so a NON-STRING host escapes as a bare # AttributeError from the guard itself (`connect-non-string-host'). The # documented error is unreachable for that shape of bad input. # # ============================================================================ # THE SMALLER SURPRISES, EACH ASSERTED WHERE IT SITS. # ============================================================================ # # s5-domain-empty-host an EMPTY hostname is accepted and put on the wire as # a zero-length domain, `03 00' and a port. Python's # idna codec length-checks only the labels BEFORE the # last one, so `""' passes both it and this module. # s5-domain-case-preserved # `encode("idna")' is a PASS-THROUGH for an ASCII name # -- not a normaliser. No lowercasing, and a trailing # root dot is kept (`s5-domain-trailing-dot'). It # transforms only a non-ASCII name, which it punycodes # (`s5-domain-punycode'). # s5-bnd-domain-type the bound address comes back as `str' from an IPv4 # reply and as `bytes' from a domain one, because one # goes through `inet_ntoa' and the other is the raw # read. What `proxy_sockname[0]' IS depends on what # the server chose to answer with. # http-sockname-type ...and the HTTP path sets `proxy_sockname' to a # BYTES literal where both SOCKS paths set a str. # Nothing normalises it. # s4a-peername-is-marker on the SOCKS4a path the recorded peername is the # protocol's invalid-address MARKER, `0.0.0.1', and # not the hostname the caller asked for. # s4-userid-with-nul the SOCKS4 user id is written RAW and terminated by # the NUL that follows it, so a user id CONTAINING one # is silently truncated at it. Nothing rejects one. # http-auth-scheme-lowercase # the scheme is spelled `basic' where RFC 7617 spells # it `Basic'. A proxy matching case-sensitively # rejects this client. # http-username-only ...and the header is appended only when BOTH a # username and a password are set, so a username alone # is dropped in silence rather than refused. # http-two-field-status `split(" ", 2)' needs three fields, so a status line # with no reason phrase -- which RFC 9112 makes # optional -- is "HTTP proxy server sent invalid # response". # http-reason-keeps-crlf and where there IS a reason phrase, its CRLF goes # into the HTTPError's message. # savenames-count `_BaseSocket' exists to re-override `send'/`recv' # for Python 2. On 3.12 all four test as callable, so # `_savenames' is EMPTY and the whole mechanism is # inert. A release that changed the test would say so # here. # leaked-name there is no `__all__', and the class-body loop's # index and value are left bound at module level -- so # `from socks import *' exports `name' and `method' # along with `os', `sys', `socket' and `struct'. # unknown-type-with-port `_proxy_addr' guards on the resolved PORT, not the # type, so an unknown proxy type WITH a port sails # past it and fails later as a KeyError out of # `_proxy_negotiators' rather than as a ProxyError. # dist-version-socks the import name is `socks' and the distribution is # `PySocks', so asking importlib.metadata for the name # you imported is a PackageNotFoundError. The # lowercase spelling IS accepted, because the lookup # normalises. # # ============================================================================ # WHAT THIS SUITE DELIBERATELY DOES NOT DO. # ============================================================================ # # IT NEVER OPENS A CONNECTION, and two cases exist to say so rather than to # measure anything: `rdns-false-is-untested' and `bool-port-is-untested'. # # A HOSTNAME WITH `rdns=False' is the one input on which this library resolves # names ITSELF -- `_write_SOCKS5_address' calls `getaddrinfo', and both # `_negotiate_SOCKS4' and `_negotiate_HTTP' call `gethostbyname'. On a guest # with no resolver that blocks for the platform's DNS timeout, inside a suite # whose whole premise is that it needs no peer. Every `rdns=False' case here # passes a NUMERIC address, for which the resolver branch is never reached. # # `connect(("h", True))' is the other one: `True' is an `int', so it passes every # check and the call goes on to CONNECT. It is the one shape of bad pair this # module accepts, and asserting it would mean opening a socket. # # A hang costs the whole BATCH its verify, where a wrong answer costs one case. # # ============================================================================ # ON TARGET: 238 probe measurements over four python3 starts. # ============================================================================ # # EVERY STRING RESULT IS WRAPPED IN ANGLE BRACKETS and the empty string is `<>'. # Load-bearing here: several of this module's answers are empty -- an unset # proxy field, a request with no user id, an auth header that was not appended -- # and `grep | cut' cannot otherwise tell an empty ANSWER from a key the probe # never reached. A refusal is `!'. Wire bytes are HEX, and the # HTTP request has its CRLFs spelled `|', because the runner parses line by line. # # ============================================================================ P=python3-pysocks # The canary is first so that a suite which dies before its own first assertion # is distinguishable from one that never started. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages DI=$SITE/PySocks-1.7.1.dist-info PKGLIST=/var/lib/dpkg/info/python3-pysocks.list PROBE=$BST_DATA/python3-pysocks-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-pysocks.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- layout ----- # TWO FLAT MODULES, NOT A PACKAGE. `py_modules=["socks", "sockshandler"]' in # setup.py, so site-packages gets two .py files and no directory. A release that # turned this into a package would still import and still pass every behavioural # case below, so the shape is asserted on its own. check $P/site-packages-dir test -d "$SITE" check $P/socks-module test -s "$SITE/socks.py" check $P/sockshandler-module test -s "$SITE/sockshandler.py" check $P/no-package-dir test ! -d "$SITE/socks" check $P/no-pysocks-dir test ! -d "$SITE/pysocks" # The two .pyc ship in the .deb, which is what lets the modules import on a # READ-ONLY root: nothing has to be compiled at first import. check $P/pycache-socks test -s "$SITE/__pycache__/socks.cpython-312.pyc" check $P/pycache-sockshandler test -s "$SITE/__pycache__/sockshandler.cpython-312.pyc" # ------------------------------------------------------------- the .deb ------ cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # PURE PYTHON, AND NOT MERELY "NO .so OF ITS OWN". This image's python3 is ONE # STATIC BINARY with an empty lib-dynload and no dynamic loader (BRINTOS-1476), # so a module carrying a compiled extension could not be imported here at all. # PySocks ships no C -- no .c, no ext_modules in setup.py -- so this is a # REGRESSION TRIPWIRE on a version bump rather than a statement about today. # It is also why BRINTOS-2046, the empty Py* export surface that refuses a # CPython extension at dlopen on this image, does not reach this port. check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # ...and nothing but Python in the payload, which is the same claim from the # other side: a data file or a script appearing here would be a packaging change # nobody asked for. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # THE PACKAGE INSTALLS NO EXECUTABLE. setup.py declares no `entry_points' and no # `scripts', so there is nothing in ${bindir} and nothing to reach through a # shebang. Worth asserting because sockshandler.py carries a `#!/usr/bin/env # python' line AND a `__main__' block that opens a connection to httpbin.org: # the shebang is there, the install bit is not, and that is the only thing # standing between this package and a network call on this image. check_eq $P/pkg-no-bindir "" \ "$(grep -E '^/usr/s?bin/' "$W/pkgf" 2>/dev/null)" # EXACTLY TWO .py FILES. A repackaging that started shipping a third module # would show up here first, before any behavioural case noticed. check_eq $P/pkg-py-count "2" \ "$(grep -c '/site-packages/[a-z_]*\.py$' "$W/pkgf" 2>/dev/null)" check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-name "Name: PySocks" cat "$DI/METADATA" check_out $P/dist-info-version "Version: 1.7.1" cat "$DI/METADATA" check_out $P/dist-info-author "Author: Anorov" cat "$DI/METADATA" check $P/dist-info-record test -s "$DI/RECORD" # `setuptools3' BUILT THIS, and it leaves three traces the `python_hatchling' # ports beside it on this image do not: the generator names `bdist_wheel', there # IS a `top_level.txt', and the licence lands FLAT in the dist-info rather than # under `licenses/'. All three are asserted, so a recipe moved onto another # backend says so here rather than silently. The generator is matched WITHOUT # its version, which belongs to the image's python3-wheel and not to this # package -- pinning it would turn a wheel bump into this port's red. check_out $P/dist-info-generator "Generator: bdist_wheel" cat "$DI/WHEEL" check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag-py3 "Tag: py3-none-any" cat "$DI/WHEEL" # ONE TAG, NOT TWO. setup.cfg declares no `universal', so despite the Python 2 # classifiers this is a py3-only wheel -- unlike python3-pathtools3 beside it, # whose 2018 setup.cfg does declare one and which therefore carries a `py2' tag. check_eq $P/dist-info-tag-only-py3 "1" \ "$(grep -c '^Tag:' "$DI/WHEEL" 2>/dev/null)" check $P/dist-info-top-level test -s "$DI/top_level.txt" check_eq $P/dist-info-top-level-names "socks sockshandler" \ "$(tr '\n' ' ' < "$DI/top_level.txt" 2>/dev/null | sed 's/ *$//')" check $P/dist-info-flat-license test -s "$DI/LICENSE" check $P/dist-info-no-licenses-dir test ! -d "$DI/licenses" # The RECORD names seven entries: two payload modules and five dist-info files. # `no-installer-file' is the other half of that number -- an installer that wrote # one would make it eight -- and it is what makes the count a statement about # the wheel rather than about the install tool. check_out $P/record-socks "socks.py,sha256=" cat "$DI/RECORD" check_out $P/record-sockshandler "sockshandler.py,sha256=" cat "$DI/RECORD" check_eq $P/record-lines "7" "$(grep -c . "$DI/RECORD" 2>/dev/null)" check $P/no-installer-file test ! -e "$DI/INSTALLER" sed -n '/^Package: python3-pysocks$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" check_out $P/pkg-version "Version: 1.7.1-r0" cat "$W/st" # THE FIVE RDEPENDS ARE ALL SUB-PACKAGES OF THE PORTED python3 (BRINTOS-1476) # and are already on this image. Each is asserted SEPARATELY rather than as one # exact line: the line's ORDER is bitbake's to choose and a package the class # adds later would break an exact match without anything being wrong. # # NOTE FOR THE READER: BRINTOS-2132's own description says this recipe "declares # no target RDEPENDS at all". That is not so -- poky's recipe declares these # five under `RDEPENDS:${PN}:class-target'. The description is wrong on the # point and these cases are what settles it. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_out $P/pkg-depends-email "python3-email" cat "$W/dep" check_out $P/pkg-depends-io "python3-io" cat "$W/dep" check_out $P/pkg-depends-logging "python3-logging" cat "$W/dep" check_out $P/pkg-depends-netclient "python3-netclient" cat "$W/dep" check_out $P/pkg-depends-shell "python3-shell" cat "$W/dep" # ...AND NOTHING PROVIDES `ssl', WHICH IS THE ONE GAP IN THAT LIST. # `sockshandler' imports `ssl' at MODULE SCOPE, and no ssl package is named # here. On THIS image that is survivable, because the image installs the whole # `python3' meta-package rather than a hand-picked set of splits -- but the # recipe's own declaration does not cover its second module, and on a leaner # image `import sockshandler' would fail. `import-sockshandler' below is the # case that measures whether it does; this one records that the declaration is # not what makes it work. check_eq $P/pkg-depends-no-ssl "" \ "$(grep -oE 'python3-ssl' "$W/dep" 2>/dev/null | head -n 1)" # ---------------------------------------------------------------- import ----- # THREE imports, and the second is separate from the first ON PURPOSE. `socks' # imports only from the standard library's core; `sockshandler' adds # `http.client' and `ssl'. A failure of the second alone is a PACKAGING answer # -- the RDEPENDS gap above -- and not a statement about the library. check $P/import python3 -c 'import socks' check $P/import-sockshandler python3 -c 'import sockshandler' # A DeprecationWarning or a stray print on import would be invisible to the # cases above, which score only the exit status. There is a plausible source: # socks.py's `from collections.abc import Callable' has a Python-2 fallback to # `from collections import Callable', which on 3.12 would be an ImportError # rather than a warning -- and the fallback branch is not supposed to be taken. check_eq $P/import-quiet "" \ "$(python3 -c 'import socks, sockshandler' 2>&1)" # ----------------------------------------------------------------- probe ----- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # The probe's own two bookends. `probe-complete' says it reached its last line # and `probe-count' says it emitted every key it meant to: a probe that died # two-thirds of the way through would otherwise leave the missing cases scoring # against an empty string, which `check_eq' would read as a wrong answer rather # than as an absent one. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "238" "$(pv emitted_count)" # -------------------------------------------------------------- identity ----- # WHICH COPY ANSWERED. Every behavioural case below is a statement about the # module named here. check_eq $P/socks-answered "" "$(pv socks_module_file)" check_eq $P/is-flat-module "yes" "$(pv is_flat_module)" check_eq $P/version-attr "<1.7.1>" "$(pv version_attr)" check_eq $P/version-type "" "$(pv version_type)" # THE IMPORT NAME IS NOT THE DISTRIBUTION NAME, asserted from all three sides. # `PySocks' and `pysocks' both answer because importlib.metadata normalises the # name it is given; `socks', the name a caller actually typed at the import # statement, does not. check_eq $P/dist-version-pysocks "<1.7.1>" "$(pv dist_version_pysocks)" check_eq $P/dist-version-lowercase "<1.7.1>" "$(pv dist_version_lowercase)" check_eq $P/dist-version-socks "!" "$(pv dist_version_socks)" # NO `__all__', AND TWO LEAKED LOOP VARIABLES. The class-body loop over # ("sendto", "send", "recvfrom", "recv") leaves its index and its last lookup # bound at module level, so `from socks import *' hands the caller `name' and # `method' along with `os', `sys', `socket', `struct' and `logging'. check_eq $P/has-all "no" "$(pv has_all)" check_eq $P/leaked-name "" "$(pv leaked_name)" check_eq $P/leaked-method-type "" "$(pv leaked_method_type)" check_eq $P/star-exports-socket "yes" "$(pv star_exports_socket)" # THE PYTHON 2 DELEGATION MACHINERY IS INERT. `_BaseSocket' exists so that # Python 2 could override `send'/`recv', which were slot descriptors there # rather than functions; the module decides at import time by testing each for # callability. On 3.12 all four ARE callable, so `_savenames' stays empty and # `_BaseSocket.__init__' saves and deletes nothing. check_eq $P/savenames "<>" "$(pv savenames)" check_eq $P/savenames-count "0" "$(pv savenames_count)" # ------------------------------------------------------------- constants ----- check_eq $P/socks4-const "1" "$(pv socks4_const)" check_eq $P/socks5-const "2" "$(pv socks5_const)" check_eq $P/http-const "3" "$(pv http_const)" check_eq $P/alias-socks4 "yes" "$(pv alias_socks4)" check_eq $P/alias-socks5 "yes" "$(pv alias_socks5)" check_eq $P/alias-http "yes" "$(pv alias_http)" check_eq $P/proxy-types "" "$(pv proxy_types)" check_eq $P/printable-types "<1=SOCKS4,2=SOCKS5,3=HTTP>" "$(pv printable_types)" check_eq $P/default-ports "<1=1080,2=1080,3=8080>" "$(pv default_ports)" check_eq $P/socks4-error-codes "<0x5b,0x5c,0x5d>" "$(pv socks4_error_codes)" check_eq $P/socks5-error-codes "<0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08>" "$(pv socks5_error_codes)" check_eq $P/socks4-error-5b "" "$(pv socks4_error_5b)" check_eq $P/socks5-error-02 "" "$(pv socks5_error_02)" check_eq $P/orig-socket-is-socket "yes" "$(pv orig_socket_is_socket)" check_eq $P/orgsocket-alias "yes" "$(pv orgsocket_alias)" # ------------------------------------------------------------ exceptions ----- # ALL SIX ARE OSError SUBCLASSES, so `except OSError' around a plain socket # already catches every one -- which is the drop-in claim's other half. check_eq $P/proxyerror-is-oserror "yes" "$(pv proxyerror_is_oserror)" check_eq $P/exception-bases "" "$(pv exception_bases)" # ...AND ALL SIX CARRY NOTHING. See the header: `args' is empty and `errno' is # None, so the two idioms a caller reaches for after catching one -- `e.args[0]' # and `e.errno' -- are an IndexError and a None. The message is reachable only # as `str(e)' or `e.msg'. check_eq $P/proxyerror-str "" "$(pv proxyerror_str)" check_eq $P/proxyerror-args "<()>" "$(pv proxyerror_args)" check_eq $P/proxyerror-args-len "0" "$(pv proxyerror_args_len)" check_eq $P/proxyerror-errno "" "$(pv proxyerror_errno)" check_eq $P/proxyerror-strerror "" "$(pv proxyerror_strerror)" check_eq $P/proxyerror-msg-attr "" "$(pv proxyerror_msg_attr)" check_eq $P/proxyerror-socket-err "" "$(pv proxyerror_socket_err)" check_eq $P/proxyerror-msg-is-appended "yes" "$(pv proxyerror_msg_is_appended)" check_eq $P/proxyerror-bare-str "" "$(pv proxyerror_bare_str)" check_eq $P/proxyerror-bare-socket-err "" "$(pv proxyerror_bare_socket_err)" # ------------------------------------------------ socksocket's own shape ----- # THE DROP-IN CLAIM, ASSERTED AS A CLAIM: it is a socket.socket subclass, an # instance IS one, and the socket API a caller keeps calling after the swap is # all present. The list is asserted whole rather than name by name because the # names are INHERITED -- what would break it is a release that started # overriding one badly, and that shows in the behavioural cases below. check_eq $P/is-socket-subclass "yes" "$(pv is_socket_subclass)" check_eq $P/basesocket-is-socket-subclass "yes" "$(pv basesocket_is_socket_subclass)" check_eq $P/mro "" "$(pv mro)" check_eq $P/class-default-proxy "" "$(pv class_default_proxy)" check_eq $P/instance-is-socket "yes" "$(pv instance_is_socket)" check_eq $P/instance-proxy "<(None, None, None, None, None, None)>" "$(pv instance_proxy)" check_eq $P/instance-proxy-sockname "" "$(pv instance_proxy_sockname)" check_eq $P/instance-proxy-peername "" "$(pv instance_proxy_peername)" check_eq $P/instance-family "2" "$(pv instance_family)" check_eq $P/instance-type "1" "$(pv instance_type)" check_eq $P/socket-api-present "" "$(pv socket_api_present)" # `__init__' REFUSES A RAW SOCKET before it calls its base, and names the type. check_eq $P/raw-refused "!" "$(pv raw_refused)" check_eq $P/raw-refused-msg ">" "$(pv raw_refused_msg)" check_eq $P/dgram-accepted "yes" "$(pv dgram_accepted)" # `settimeout' IS SHADOWED and keeps its own `_timeout', which is what # `gettimeout' answers -- so the value survives on a socket that was never # connected, where the base class's own timeout was never applied. `setblocking' # is defined in terms of it, which is why False reads back as 0.0 and not False. check_eq $P/timeout-initial "" "$(pv timeout_initial)" check_eq $P/timeout-roundtrip "<12.5>" "$(pv timeout_roundtrip)" check_eq $P/setblocking-false "<0.0>" "$(pv setblocking_false)" check_eq $P/setblocking-true "" "$(pv setblocking_true)" # ------------------------------------------------------------- set_proxy ----- # THE CREDENTIALS ARE ENCODED AND THE REST IS NOT, so the tuple a caller reads # back is not the tuple it wrote: the address stays `str' and the username # becomes `bytes'. check_eq $P/set-proxy-tuple "<(2, 'proxy.example', 9050, True, b'user', b'pass')>" "$(pv set_proxy_tuple)" check_eq $P/set-proxy-username-type "" "$(pv set_proxy_username_type)" check_eq $P/set-proxy-addr-type "" "$(pv set_proxy_addr_type)" # ...and an EMPTY username is stored as None rather than as b"", because the # encode is guarded by the string's own truthiness. That is what makes the # SOCKS4 "no user id" and SOCKS5 "no auth" branches fire for `username=""'. check_eq $P/set-proxy-empty-username "" "$(pv set_proxy_empty_username)" check_eq $P/setproxy-alias-type "1" "$(pv setproxy_alias_type)" check_eq $P/setproxy-is-not-set-proxy "yes" "$(pv setproxy_is_not_set_proxy)" # `_proxy_addr' IS WHERE THE DEFAULT PORT COMES FROM -- 1080 for both SOCKS # versions, 8080 for HTTP -- and its guard is on the RESOLVED PORT rather than # on the type. So an unknown type with no port is a clean GeneralProxyError, and # an unknown type WITH a port sails past to fail later as a KeyError out of # `_proxy_negotiators'. The pair is what shows the guard is in the wrong place. check_eq $P/default-port-socks4 "<('h', 1080)>" "$(pv default_port_socks4)" check_eq $P/default-port-socks5 "<('h', 1080)>" "$(pv default_port_socks5)" check_eq $P/default-port-http "<('h', 8080)>" "$(pv default_port_http)" check_eq $P/explicit-port-wins "<('h', 3128)>" "$(pv explicit_port_wins)" check_eq $P/unknown-type-refused "!" "$(pv unknown_type_refused)" check_eq $P/unknown-type-msg "" "$(pv unknown_type_msg)" check_eq $P/unknown-type-with-port "<('h', 1080)>" "$(pv unknown_type_with_port)" check_eq $P/negotiator-keys "<1,2,3>" "$(pv negotiator_keys)" # ----------------------------------------- the default proxy, wrap_module ---- # `set_default_proxy' WRITES A CLASS ATTRIBUTE, so it reaches every socksocket # created AFTERWARDS and none created before. Both halves are asserted, because # a caller who sets it late is a caller whose earlier sockets are unproxied and # silent about it. check_eq $P/default-proxy-initially "" "$(pv default_proxy_initially)" check_eq $P/default-proxy-set "<(2, 'd.example', 1080, True, b'du', b'dp')>" "$(pv default_proxy_set)" check_eq $P/default-proxy-is-class-attr "yes" "$(pv default_proxy_is_class_attr)" check_eq $P/default-proxy-reaches-new "<(2, 'd.example', 1080, True, b'du', b'dp')>" "$(pv default_proxy_reaches_new)" check_eq $P/default-proxy-misses-old "<(None, None, None, None, None, None)>" "$(pv default_proxy_misses_old)" check_eq $P/getdefaultproxy-alias "yes" "$(pv getdefaultproxy_alias)" check_eq $P/setdefaultproxy-is-not-set-default "yes" "$(pv setdefaultproxy_is_not_set_default)" # THE INSTALL-AND-RESTORE ROUND TRIP. See the header for why `wrap-patched- # globally' is the case that matters: the patch is not scoped to the module # handed in, and `sys.modules["socket"]' is as far from that module as it is # possible to reach. check_eq $P/wrapped-module-is-socket-module "yes" "$(pv wrapped_module_is_socket_module)" check_eq $P/wrap-patched-socket-module "yes" "$(pv wrap_patched_socket_module)" check_eq $P/wrap-patched-globally "yes" "$(pv wrap_patched_globally)" check_eq $P/wrapmodule-alias "yes" "$(pv wrapmodule_alias)" check_eq $P/has-unwrap-module "no" "$(pv has_unwrap_module)" check_eq $P/restored-cleanly "yes" "$(pv restored_cleanly)" check_eq $P/restored-is-not-socksocket "yes" "$(pv restored_is_not_socksocket)" check_eq $P/post-restore-instance "" "$(pv post_restore_instance)" check_eq $P/default-proxy-cleared "" "$(pv default_proxy_cleared)" # ...and with no default set, `wrap_module' REFUSES rather than installing a # socket that would connect direct while the caller believed it was proxied. check_eq $P/wrap-without-default "!" "$(pv wrap_without_default)" check_eq $P/wrap-without-default-msg "" "$(pv wrap_without_default_msg)" check_eq $P/wrap-left-socket-alone "yes" "$(pv wrap_left_socket_alone)" # ------------------------------------------------- the peername pair --------- # See the header. `get_proxy_peername' answers the DESTINATION, its docstring # notwithstanding, and there is no way through this API to reach the proxy's own # address. check_eq $P/get-peername "<('dest.example', 443)>" "$(pv get_peername)" check_eq $P/getpeername-alias "yes" "$(pv getpeername_alias)" check_eq $P/get-proxy-peername "<('dest.example', 443)>" "$(pv get_proxy_peername)" check_eq $P/proxy-peername-equals-peername "yes" "$(pv proxy_peername_equals_peername)" check_eq $P/get-proxy-sockname "<('10.0.0.1', 1080)>" "$(pv get_proxy_sockname)" check_eq $P/getproxysockname-alias "yes" "$(pv getproxysockname_alias)" check_eq $P/getproxypeername-alias "yes" "$(pv getproxypeername_alias)" # ------------------------------------------------- SOCKS5: the handshake ----- # THE GREETING SAYS WHICH METHODS THE CLIENT SUPPORTS, and which one it sends is # decided by whether BOTH a username and a password were set: `05 01 00' offers # only "no authentication", `05 02 00 02' offers that and username/password. check_eq $P/s5-noauth-wire "<05010005010001010203040050>" "$(pv s5_noauth_wire)" check_eq $P/s5-greeting-noauth "<050100>" "$(pv s5_greeting_noauth)" check_eq $P/s5-request-ipv4 "<05010001010203040050>" "$(pv s5_request_ipv4)" check_eq $P/s5-ipv4-peername "<('1.2.3.4', 80)>" "$(pv s5_ipv4_peername)" check_eq $P/s5-ipv4-sockname "<('127.0.0.1', 8080)>" "$(pv s5_ipv4_sockname)" # A username WITHOUT a password does not reach the username/password method: the # offer is guarded by `username and password', so a half-configured proxy sends # the no-auth greeting and fails later, at the server's choice, rather than here. check_eq $P/s5-username-only-greeting "<050100>" "$(pv s5_username_only_greeting)" check_eq $P/s5-userpass-greeting "<05020002>" "$(pv s5_userpass_greeting)" check_eq $P/s5-auth-packet "<0104757365720470617373>" "$(pv s5_auth_packet)" check_eq $P/s5-userpass-request "<05010001010203040050>" "$(pv s5_userpass_request)" # THE HEADLINE FINDING, four cases. See the header: `chr(len(u)).encode()' is # UTF-8 and RFC 1929's ULEN is one byte, so 128 is where a correct packet stops # being produced -- silently, by the client. check_eq $P/s5-long-user-auth-len "212" "$(pv s5_long_user_auth_len)" check_eq $P/s5-long-user-ulen-bytes "" "$(pv s5_long_user_ulen_bytes)" check_eq $P/s5-long-user-ulen-width "2" "$(pv s5_long_user_ulen_width)" check_eq $P/s5-short-user-ulen-width "1" "$(pv s5_short_user_ulen_width)" check_eq $P/s5-ulen-boundary "128" "$(pv s5_ulen_boundary)" # THE THREE ADDRESS FORMS. IPv4 and IPv6 are detected by `inet_pton' and take # the numeric forms; anything else is a name and takes the domain form -- whose # length prefix has the SAME defect as the username's. check_eq $P/s5-domain-request "<050100030b6578616d706c652e636f6d0050>" "$(pv s5_domain_request)" check_eq $P/s5-domain-peername "<('example.com', 80)>" "$(pv s5_domain_peername)" check_eq $P/s5-domain-case-preserved "<050100030b4558414d504c452e434f4d0050>" "$(pv s5_domain_case_preserved)" check_eq $P/s5-domain-trailing-dot "<050100030c6578616d706c652e636f6d2e0050>" "$(pv s5_domain_trailing_dot)" check_eq $P/s5-domain-empty-host "<05010003000050>" "$(pv s5_domain_empty_host)" check_eq $P/s5-domain-punycode "<0501000315786e2d2d62636865722d6b76612e6578616d706c650050>" "$(pv s5_domain_punycode)" check_eq $P/s5-domain-long-label "!" "$(pv s5_domain_long_label)" check_eq $P/s5-long-host-len "136" "$(pv s5_long_host_len)" check_eq $P/s5-long-host-prefix "<03c288>" "$(pv s5_long_host_prefix)" check_eq $P/s5-long-host-request-len "144" "$(pv s5_long_host_request_len)" # THE IPv6 FORM IS THE ONE CASE HERE THAT DEPENDS ON THE PLATFORM rather than on # the library: it needs `inet_pton(AF_INET6, ...)' to work on this arm. A red # here is an answer about the image's socket module, not about PySocks. check_eq $P/s5-ipv6-request "<05010004000000000000000000000000000000010050>" "$(pv s5_ipv6_request)" check_eq $P/s5-port-big-endian "<1f90>" "$(pv s5_port_big_endian)" check_eq $P/s5-port-max "" "$(pv s5_port_max)" # `struct.pack(">H", port)' is the only bound on the port, so one out of range # is a struct.error from the packer rather than a checked argument. check_eq $P/s5-port-out-of-range "!" "$(pv s5_port_out_of_range)" check_eq $P/s5-rdns-false-numeric "<05010001010203040050>" "$(pv s5_rdns_false_numeric)" check_eq $P/rdns-false-is-untested "by-design" "$(pv rdns_false_is_untested)" # THE BOUND ADDRESS COMES BACK IN WHICHEVER FORM THE SERVER CHOSE, and the forms # do not agree on a TYPE -- see the header. check_eq $P/s5-bnd-domain "<(b'abc', 80)>" "$(pv s5_bnd_domain)" check_eq $P/s5-bnd-domain-type "" "$(pv s5_bnd_domain_type)" check_eq $P/s5-bnd-ipv4-type "" "$(pv s5_bnd_ipv4_type)" # UDP_ASSOCIATE is the other command this request builder serves, reached through # `bind' rather than `connect'. Driving `_SOCKS5_request' directly is what lets # the command byte be asserted without a relay. check_eq $P/s5-udp-cmd-byte "<0503>" "$(pv s5_udp_cmd_byte)" check_eq $P/s5-udp-resolved "<('0', 5000)>" "$(pv s5_udp_resolved)" check_eq $P/s5-udp-bnd "<('127.0.0.1', 8080)>" "$(pv s5_udp_bnd)" # ------------------------------------------- SOCKS5: the refusal paths ------- # EACH REPLY CODE RAISES ITS OWN CLASS, and the code is carried in the message # formatted `{:#04x}' -- so `0x02' rather than `2', and an unrecognised code is # "Unknown error" with the code still in front of it, which is what keeps a new # server-side status legible. check_eq $P/s5-bad-version "!" "$(pv s5_bad_version)" check_eq $P/s5-bad-version-msg "" "$(pv s5_bad_version_msg)" check_eq $P/s5-no-methods "!" "$(pv s5_no_methods)" check_eq $P/s5-no-methods-msg "" "$(pv s5_no_methods_msg)" check_eq $P/s5-unknown-method "!" "$(pv s5_unknown_method)" check_eq $P/s5-unknown-method-msg "" "$(pv s5_unknown_method_msg)" check_eq $P/s5-userpass-unasked "!" "$(pv s5_userpass_unasked)" check_eq $P/s5-userpass-unasked-msg "" "$(pv s5_userpass_unasked_msg)" check_eq $P/s5-auth-bad-version "!" "$(pv s5_auth_bad_version)" check_eq $P/s5-auth-failed "!" "$(pv s5_auth_failed)" check_eq $P/s5-auth-failed-msg "" "$(pv s5_auth_failed_msg)" check_eq $P/s5-status-02 "!" "$(pv s5_status_02)" check_eq $P/s5-status-02-msg "<0x02: Connection not allowed by ruleset>" "$(pv s5_status_02_msg)" check_eq $P/s5-status-05-msg "<0x05: Connection refused>" "$(pv s5_status_05_msg)" check_eq $P/s5-status-08-msg "<0x08: Address type not supported>" "$(pv s5_status_08_msg)" check_eq $P/s5-status-unknown-msg "<0x7f: Unknown error>" "$(pv s5_status_unknown_msg)" check_eq $P/s5-reply-bad-version "!" "$(pv s5_reply_bad_version)" check_eq $P/s5-bad-atyp "!" "$(pv s5_bad_atyp)" check_eq $P/s5-bad-atyp-msg "" "$(pv s5_bad_atyp_msg)" # A SHORT READ IS A GeneralProxyError out of `_readall', and it is the one # refusal that names a closed connection rather than a protocol fault. check_eq $P/s5-truncated "!" "$(pv s5_truncated)" check_eq $P/s5-truncated-msg "" "$(pv s5_truncated_msg)" check_eq $P/s5-empty-reply "!" "$(pv s5_empty_reply)" # ------------------------------------------------ SOCKS4 and SOCKS4a --------- # THE REQUEST IS VN=4, CD=1, a big-endian port, four address bytes, the user id # and a NUL. With no user id the field is empty and the packet is nine bytes. check_eq $P/s4-request-no-userid "<040100500102030400>" "$(pv s4_request_no_userid)" check_eq $P/s4-request-len "9" "$(pv s4_request_len)" check_eq $P/s4-sockname "<('127.0.0.1', 8080)>" "$(pv s4_sockname)" check_eq $P/s4-peername "<('1.2.3.4', 80)>" "$(pv s4_peername)" check_eq $P/s4-request-userid "<0401005001020304626f6200>" "$(pv s4_request_userid)" check_eq $P/s4-userid-with-nul "<040100500102030461006200>" "$(pv s4_userid_with_nul)" check_eq $P/s4-userid-nul-accepted "yes" "$(pv s4_userid_nul_accepted)" # SOCKS4a IS THE HOSTNAME EXTENSION: the address becomes the invalid 0.0.0.1 and # the name follows the user id's NUL as a second NUL-terminated string. The # peername recorded afterwards is that MARKER and not the name the caller asked # for -- see the header. check_eq $P/s4a-request "<0401005000000001006578616d706c652e636f6d00>" "$(pv s4a_request)" check_eq $P/s4a-marker "<00000001>" "$(pv s4a_marker)" check_eq $P/s4a-peername "<('0.0.0.1', 80)>" "$(pv s4a_peername)" check_eq $P/s4a-peername-is-marker "yes" "$(pv s4a_peername_is_marker)" check_eq $P/s4a-case-preserved "<4558414d504c452e636f6d00>" "$(pv s4a_case_preserved)" check_eq $P/s4a-punycode "<786e2d2d62636865722d6b76612e6578616d706c6500>" "$(pv s4a_punycode)" check_eq $P/s4a-long-label "!" "$(pv s4a_long_label)" check_eq $P/s4-port-big-endian "<1f90>" "$(pv s4_port_big_endian)" check_eq $P/s4-rdns-false-numeric "<040100500102030400>" "$(pv s4_rdns_false_numeric)" check_eq $P/s4-bad-first-byte "!" "$(pv s4_bad_first_byte)" check_eq $P/s4-bad-first-byte-msg "" "$(pv s4_bad_first_byte_msg)" check_eq $P/s4-status-5b "!" "$(pv s4_status_5b)" check_eq $P/s4-status-5b-msg "<0x5b: Request rejected or failed>" "$(pv s4_status_5b_msg)" check_eq $P/s4-status-5c-msg "<0x5c: Request rejected because SOCKS server cannot connect to identd on the client>" "$(pv s4_status_5c_msg)" check_eq $P/s4-status-5d-msg "<0x5d: Request rejected because the client program and identd report different user-ids>" "$(pv s4_status_5d_msg)" check_eq $P/s4-status-unknown-msg "<0x01: Unknown error>" "$(pv s4_status_unknown_msg)" check_eq $P/s4-truncated "!" "$(pv s4_truncated)" check_eq $P/s4-truncated-msg "" "$(pv s4_truncated_msg)" # ------------------------------------------------------------- HTTP ---------- # CRLFs ARE SPELLED `|' in the request cases, because the runner parses its # input line by line and a CONNECT request is CRLF-separated by construction. check_eq $P/http-request "" "$(pv http_request)" check_eq $P/http-request-hex "<434f4e4e454354206578616d706c652e636f6d3a34343320485454502f312e310d0a486f73743a206578616d706c652e636f6d0d0a0d0a>" "$(pv http_request_hex)" check_eq $P/http-peername "<('example.com', 443)>" "$(pv http_peername)" check_eq $P/http-sockname "<(b'0.0.0.0', 0)>" "$(pv http_sockname)" check_eq $P/http-sockname-type "" "$(pv http_sockname_type)" check_eq $P/http-sockname-is-bytes "yes" "$(pv http_sockname_is_bytes)" check_eq $P/http-auth-request "" "$(pv http_auth_request)" check_eq $P/http-auth-scheme-lowercase "yes" "$(pv http_auth_scheme_lowercase)" check_eq $P/http-username-only "" "$(pv http_username_only)" check_eq $P/http-no-proxy-connection-header "yes" "$(pv http_no_proxy_connection_header)" check_eq $P/http-case-preserved "" "$(pv http_case_preserved)" check_eq $P/http-punycode "" "$(pv http_punycode)" check_eq $P/http-reason-keeps-crlf "yes" "$(pv http_reason_keeps_crlf)" # THE STATUS PATHS. 400, 403 and 405 get an extra note appended, on the theory # that a proxy answering them is not a CONNECT tunnel at all; 404 does not, which # is what makes the pair a statement about the list rather than about the code. check_eq $P/http-status-407 "!" "$(pv http_status_407)" check_eq $P/http-status-407-msg "<407: Denied\\r\\n>" "$(pv http_status_407_msg)" check_eq $P/http-status-403-msg "<403: Forbidden\\r\\n\\n[*] Note: The HTTP proxy server may not be supported by PySocks (must be a CONNECT tunnel proxy)>" "$(pv http_status_403_msg)" check_eq $P/http-status-403-has-note "yes" "$(pv http_status_403_has_note)" check_eq $P/http-status-404-has-note "no" "$(pv http_status_404_has_note)" check_eq $P/http-status-non-numeric "!" "$(pv http_status_non_numeric)" check_eq $P/http-status-non-numeric-msg "" "$(pv http_status_non_numeric_msg)" check_eq $P/http-bad-proto "!" "$(pv http_bad_proto)" check_eq $P/http-bad-proto-msg "" "$(pv http_bad_proto_msg)" check_eq $P/http-two-field-status "!" "$(pv http_two_field_status)" check_eq $P/http-two-field-status-msg "" "$(pv http_two_field_status_msg)" check_eq $P/http-empty-reply "!" "$(pv http_empty_reply)" check_eq $P/http-empty-reply-msg "" "$(pv http_empty_reply_msg)" # ------------------------------------------- connect()'s input checks -------- # EVERY REFUSAL HERE HAPPENS BEFORE ANY NETWORK CALL, which is what makes them # assertable at all. See the header for the two that are wrong: a malformed pair # reported as an IPv6 refusal, and a non-string host escaping as an # AttributeError from the guard that was supposed to catch it. check_eq $P/connect-ipv6-bracket "!" "$(pv connect_ipv6_bracket)" check_eq $P/connect-ipv6-bracket-msg "" "$(pv connect_ipv6_bracket_msg)" check_eq $P/connect-short-pair "!" "$(pv connect_short_pair)" check_eq $P/connect-short-pair-msg "" "$(pv connect_short_pair_msg)" check_eq $P/connect-three-tuple "!" "$(pv connect_three_tuple)" check_eq $P/connect-non-string-host "!" "$(pv connect_non_string_host)" check_eq $P/connect-non-string-host-msg "<'int' object has no attribute 'startswith'>" "$(pv connect_non_string_host_msg)" check_eq $P/connect-empty-host "!" "$(pv connect_empty_host)" check_eq $P/connect-empty-host-msg "" "$(pv connect_empty_host_msg)" check_eq $P/connect-string-port "!" "$(pv connect_string_port)" check_eq $P/connect-string-port-msg "" "$(pv connect_string_port_msg)" check_eq $P/bool-port-is-untested "by-design" "$(pv bool_port_is_untested)" # `connect_ex' RE-RAISES WHERE IT MEANT TO RETURN, because it tests `e.errno' # and this module's exceptions have none. See the header. check_eq $P/connect-ex-ipv6 "!" "$(pv connect_ex_ipv6)" check_eq $P/connect-ex-empty-host "!" "$(pv connect_ex_empty_host)" # `set_self_blocking' wraps both, and `functools.wraps' keeps the names, so a # caller introspecting the API still sees `connect' rather than `wrapper'. check_eq $P/connect-name "" "$(pv connect_name)" check_eq $P/connect-ex-name "" "$(pv connect_ex_name)" check_eq $P/connect-has-wrapped "yes" "$(pv connect_has_wrapped)" # ------------------------------- create_connection and sockshandler ---------- # `create_connection' IS THE ONLY ENTRY POINT THAT OPENS A SOCKET ITSELF, so it # is not driven here -- what is asserted is the signature a caller writes # against, and that `rdns' defaults to TRUE in both places it appears. That # default is what keeps the resolver on the PROXY's side, and it is the only # reason a guest with no resolver can use this library at all. check_eq $P/create-connection-is-function "yes" "$(pv create_connection_is_function)" check_eq $P/create-connection-params "" "$(pv create_connection_params)" check_eq $P/create-connection-rdns-default "" "$(pv create_connection_rdns_default)" check_eq $P/set-proxy-rdns-default "" "$(pv set_proxy_rdns_default)" # `sockshandler' IS THE SECOND MODULE THE .deb SHIPS and the one with real # dependencies. Its import is measured twice -- once as a shell case above and # once inside the probe -- because a failure is a PACKAGING answer, and the six # cases after it report `!' rather than vanishing, so the count # still adds up. check_eq $P/sockshandler-import "yes" "$(pv sockshandler_import)" check_eq $P/sockshandler-import-msg "<>" "$(pv sockshandler_import_msg)" check_eq $P/sockshandler-socks-is-socks "yes" "$(pv sockshandler_socks_is_socks)" check_eq $P/sockshandler-classes "" "$(pv sockshandler_classes)" check_eq $P/sockshandler-no-rdns-empty "yes" "$(pv sockshandler_no_rdns_empty)" check_eq $P/sockshandler-is-ip-v4 "" "$(pv sockshandler_is_ip_v4)" check_eq $P/sockshandler-is-ip-name "" "$(pv sockshandler_is_ip_name)" # `is_ip' TESTS FOR A SEPARATOR BEFORE IT TESTS ANYTHING ELSE, so a bare name # with neither `:' nor `.' returns False without ever reaching `inet_aton'. check_eq $P/sockshandler-is-ip-bare "" "$(pv sockshandler_is_ip_bare)" # The probe tidies up after itself: every socket it created is closed. A red # here would mean file descriptors leaking one per boot into a guest that has # few of them. check_eq $P/closed-all "yes" "$(pv closed_all)" rm -rf "$W"