pax_global_header00006660000000000000000000000064145761054200014516gustar00rootroot0000000000000052 comment=651388d1eac070746959e1f57ed888213fdc8cc6 pushpin-1.39.1/000077500000000000000000000000001457610542000132775ustar00rootroot00000000000000pushpin-1.39.1/.github/000077500000000000000000000000001457610542000146375ustar00rootroot00000000000000pushpin-1.39.1/.github/workflows/000077500000000000000000000000001457610542000166745ustar00rootroot00000000000000pushpin-1.39.1/.github/workflows/test.yml000066400000000000000000000034341457610542000204020ustar00rootroot00000000000000on: pull_request name: Test jobs: test: strategy: matrix: rust-toolchain: [1.66.1] platform: [ubuntu-20.04] runs-on: ${{ matrix.platform }} steps: - name: Checkout code uses: actions/checkout@v2 with: submodules: recursive - name: Install Rust uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust-toolchain }} - name: Cache cargo registry uses: actions/cache@v1 with: path: ~/.cargo/registry key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - name: Cache cargo index uses: actions/cache@v1 with: path: ~/.cargo/git key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} - name: Cache cargo build uses: actions/cache@v1 with: path: target key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} - name: Install fmt run: rustup component add rustfmt shell: bash - name: Install clippy run: rustup component add clippy shell: bash - name: Install audit run: cargo install --version 0.17.6 --locked cargo-audit shell: bash - name: Install deps run: sudo apt-get update && sudo apt-get install -y make g++ libssl-dev libzmq3-dev qtbase5-dev libboost-dev - name: fmt run: cargo fmt --check shell: bash - name: build run: RUSTFLAGS="-D warnings" make shell: bash - name: check run: RUSTFLAGS="-D warnings" make check shell: bash - name: cargo bench --no-run run: RUSTFLAGS="-D warnings" cargo bench --no-run shell: bash - name: clippy run: cargo clippy -- -D warnings shell: bash - name: audit run: cargo audit shell: bash pushpin-1.39.1/.gitignore000066400000000000000000000004121457610542000152640ustar00rootroot00000000000000*.swp *~ qrc_* *.moc *.o *.pdb ui_* moc_* *.pyc .qmake.stash target_wrapper.sh /target **/*.rs.bk .cargo vendor .vscode conf.pri /pushpin /pushpin-legacy /bin/* /src/runner/certs/*.crt /src/runner/certs/*.key /postbuild/Makefile /postbuild/*.inst /config /run /log pushpin-1.39.1/CHANGELOG.md000066400000000000000000000315541457610542000151200ustar00rootroot00000000000000Pushpin Changelog ================= v. 1.39.1 (2024-03-18) * Regenerate pushpin.conf.inst post-build to ensure up-to-date configuration. * Update legacy runner use revised Qt linking logic, aligning with main branch improvements. v. 1.39.0 (2024-03-14) * Add support for multiple proxy worker threads. * New config option: workers (under [proxy]). * Fix memory leak when proxying requests. * Various build system fixes/improvements. * Use Boost for signals & slots to reduce dependence on Qt's event loop. v. 1.38.0 (2024-01-08) * Publish refresh action for triggering WebSocket-over-HTTP requests. * Ability to read signing secrets from files. * Move Condure into Pushpin and name the program pushpin-condure. * Fix crash when writing partial uncompressed WebSocket frame. * Fix WebSocket proxying flow control. * Support receiving non-chunked responses of indefinite length. * Qt 6 compatibility. * Remove configure script. Configure via environment variables instead. v. 1.37.0 (2023-06-29) * Ability to use Condure instead of Zurl for outgoing connections. * Ability to set mode/user/group when listening on Unix socket. * WebSocket performance optimizations. * New config options: allow_compression, stats_connection_send, cdn_loop. * Relicense to Apache 2.0. v. 1.36.0 (2022-11-14) * Ability to accept client connections via IPv6. * Ability to sign requests using EC or RSA public keys. * Include bytes/messages in report stats. v. 1.35.0 (2022-03-11) * Add support for Prometheus metrics. * Ability to listen on a Unix socket for client connections. * New config options: prometheus_port, prometheus_prefix. * New config option: local_ports. * New config option: accept_pushpin_route. * New route condition option: no_grip. * Use the route of the initial request for retries and link requests. * pushpin-publish: fix sending hint action for http-response format. v. 1.34.0 (2021-11-30) * New config option: message_wait. * Publish command for publishing via command socket. v. 1.33.1 (2021-08-09) * Build system fixes. v. 1.33.0 (2021-08-08) * Performance optimizations. * New config option: sig_iss. v. 1.32.2 (2021-06-09) * Fix publishing to SockJS WebSocket connections. v. 1.32.1 (2021-05-13) * Build system fixes. v. 1.32.0 (2021-05-11) * pushpin-publish: support sending via HTTP, and do this by default. * pushpin-publish: support authentication. * pushpin-publish: use GRIP_URL environment variable if present. * Add Rust code to the build process. v. 1.31.0 (2020-11-06) * Use Condure instead of Mongrel2, by default. * Ability to refresh WebSocket-over-HTTP sessions by channel. * Fix crash when sending delayed WebSocket messages. v. 1.30.0 (2020-07-29) * Optional support for Condure instead of Mongrel2. * ZHTTP compatibility fixes. v. 1.29.0 (2020-07-15) * Fix crash when parsing Accept header received on control port. * Fix crash when response hold times out while pausing. * Fix handling of hints in response mode. * Fix handling of ZeroMQ errors, including EINTR. * ZHTTP compatibility fixes. v. 1.28.0 (2020-04-08) * New route target option: one_event. v. 1.27.0 (2020-03-10) * WebSocket: ability to publish close reason. * WebSocket: proxy the content of ping and pong frames. v. 1.26.0 (2019-12-11) * Respond with status 200 on HTTP control port root path. v. 1.25.0 (2019-11-20) * Set the Mongrel2 log level and capture debug output. * Ability to set different log levels per subprocess. v. 1.24.0 (2019-08-06) * runner: capture Mongrel2 logs when --merge-output is used. v. 1.23.0 (2019-07-03) * Support log levels 0 and 1. * Don't write to Mongrel2 access log for log levels < 2. * Support JSON framing on the input PULL and SUB sockets. * New config option: push_in_sub_specs. * New config option: push_in_sub_connect. v. 1.22.0 (2019-06-17) * New filter: var-subst. * Support content-filters field in ws-message format. v. 1.21.0 (2019-05-01) * GRIP keep-alive modes: idle (default) and interval. * Don't put GRIP headers in Access-Control-Expose-Headers. v. 1.20.3 (2019-04-08) * Fix Grip-Last values when route prefix is used. v. 1.20.2 (2019-03-25) * WebSocket-Over-HTTP: fix mem leak when clients disconnect during close. v. 1.20.1 (2019-02-20) * WebSocket-Over-HTTP: don't forward Content-Length header. v. 1.20.0 (2019-02-19) * WebSocket-Over-HTTP: break up response messages to fit session buffers. * New config option: stats_format. * New config option: client_buffer_size. v. 1.19.1 (2019-01-10) * WebSocket: fix crash when receiving frames after close frame. * WebSocket: include reason and headers in rejection responses. v. 1.19.0 (2018-12-18) * WebSocket: support close reasons. v. 1.18.0 (2018-08-20) * WebSocket-Over-HTTP: update headers (mainly Grip-Sig) for each request. * WebSocket-Over-HTTP: properly report errors and handle target failover. * WebSocket: support debug responses. * Option to not send non-standard X-Forwarded-Protocol header. * Increase default request buffer size to 8k. * Make http_port optional. * runner: remove mongrel2 pid file before starting. * runner: return non-zero status code if failing due to subprocess error. * runner: prevent SIGINT from being copied to subprocesses. v. 1.17.2 (2018-01-11) * Fix close actions with HTTP streaming and WebSockets. v. 1.17.1 (2017-12-12) * Fix compilation with Qt 5.10. v. 1.17.0 (2017-11-06) * De-dup published messages based on recently seen IDs (default 60s). * Limit number of subscriptions per connection (default 20). * Ensure filters update after following next links. * Support content-filters field in http-stream and http-response formats. * Include subscribers field in subscription stats. * Include duration field in report stats. * New config options: connection_subscription_max, subscription_linger. * New config options: stats_connection_ttl, stats_subscription_ttl. * New config option: stats_report_interval. v. 1.16.0 (2017-07-14) * Reliable streaming fixes. * SockJS: XHR transport fixes. * WebSocket-Over-HTTP: more fixes to ensure DISCONNECT events get sent. * Fix routes file change detection when file is replaced. * Set Grip-Last headers when retrying long-polling request. * Enable client-side TCP keep-alives. * Stats: report logical IP address rather than physical. * Published items can include no-seq flag to bypass sequencing buffer. * New config options: log_from, log_user_agent. * New filters: skip-users, build-id, require-sub. * Add randomness to stream keep alives. * pushpin-publish: --meta option. * pushpin-publish: --no-seq option. * Announce more features using Grip-Feature request header. * Fix GRIP session detection. * sub target parameter works for both HTTP and WebSocket, forbids unsub. * Packet logging uses new format that only trims content, not headers. v. 1.15.0 (2017-01-22) * Publish hint action for triggering recovery requests. * Recover command for triggering recovery requests. * Refresh command for triggering WebSocket-Over-HTTP requests. * Improve reliability of long-polling when previous ID is used. * WebSocket-Over-HTTP: ensure DISCONNECT events get sent. * WebSocket: new control messages: send-delayed, flush-delayed. * WebSocket: break large published messages into frames. * Allow unknown previous ID for first message to channel. * Forget previous ID when channel has no subscribers. * Reduce timeout of out-of-order messages to 5 seconds. * pushpin-publish: --hint option * pushpin-publish: --no-eol option * pushpin-publish: ability to use file source (@filename) * New config option: message_block_size * Remove docs files from repository. Content moved to pushpin.org. v. 1.14.0 (2016-11-15) * Reliable HTTP streaming (stream hold + "GRIP Next"). * Process messages in order if received out of order. v. 1.13.1 (2016-10-27) * Fix crash when publishing to a long-polling client that is closing. * More conservative message_rate default. v. 1.13.0 (2016-10-22) * Optimizations for higher concurrent connections. * New config options: message_rate and message_hwm. * New stats message: report. * Handle next links internally if relative. * Log accepted requests as "accept", not "hold". * Log handler-initiated requests in handler, not proxy. * Fix memory leaks. * Send anonymous usage statistics to Fanout. v. 1.12.0 (2016-09-03) * "GRIP Next" feature for streaming many responses as a single response. * header route parameter for sending custom headers when proxying. * trust_connect_host target parameter for trusting cert of connect host. * SockJS: fix bug with not receiving messages from client. * More correct handling of Host header. * Set X-Forwarded-Proto in addition to X-Forwarded-Protocol. * Various bugfixes. v. 1.11.0 (2016-07-11) * Debug mode, to get more information about errors while proxying. * Command line option to log subprocess output: --merge-output. * Command line option to log merged output to file: --logfile. * Command line options for quick config: --port, --route. * Command line option to easily run multiple instances: --id. * Rewrite runner from Python to C++. * Don't relay Content-Encoding (fixes compressed long-polling timeouts). * Fixes to log output. v. 1.10.1 (2016-05-30) * Fix SockJS crash. * Fix bug that logged successful requests as errors. v. 1.10.0 (2016-05-25) * Streaming: initial response now has no size limit. * WebSocket-Over-HTTP: retry requests to the origin server. * WebSocket: ability to disconnect clients by publishing a close action. * WebSocket: ability to publish ping/pong frames. * WebSocket: keep-alives. * New route target "test", for testing without an origin server. * Fix publishing of large payloads through HTTP control port. * New config option: log_level. * Ability to set bind interface in config (use addr:port form). * Grip-Status header, for setting alternate response code and reason. v. 1.9.0 (2016-04-14) * More practical logging. Non-verbose output more informative. * New config option: accept_x_forwarded_protocol. * Support JSON responses in HTTP control endpoint. * More accurate WebSocket activity counting. v. 1.8.0 (2016-02-22) * Fix issue proxying large responses. * Refactor README. * Port server code to Qt 5. * Rewrite pushpin-publish tool from Python to C++. * Move internal.conf into LIBDIR. v. 1.7.0 (2016-01-10) * Rewrite pushpin-handler from Python to C++. * Initial support for subscription filters and skip-self filter. * Fix sending of large responses when flow control not used. * Speed up shutdown. * Pass WebSocket GRIP logic upstream if GRIP proxy detected. * Don't forward WebSocket-Over-HTTP requests unless client trusted. * WebSocket-Over-HTTP: strip private headers from responses. * Long-polling: finish support for JSON patch. * m2adapter: dynamically enable/disable control port as needed. * publish tool: add id, prev-id, patch, and sender options. * Add monitorsubsock tool for monitoring SUB socket. * Refactor docs/grip-protocol.md. v. 1.6.0 (2015-09-24) * Fix rare assert when publishing to a WebSocket. * Remove libdir from pushpin.conf. * Mongrel2: use download flow control. * Mongrel2: enable relaxed parsing. * Auto Cross-Origin: include Access-Control-Max-Age. * Throw error if can't create runtime directories on startup. * Various cleanups. v. 1.5.0 (2015-07-23) * replace_beg route parameter. * Fixed bug where non-persistent connections were closed before data sent. * Accept invalid characters in request URIs and URL-encode them. v. 1.4.0 (2015-07-16) * Improved handling of streamed input while proxying. * WebSocket over_http mode: relay error responses rather than 502. * Various WebSocket bugfixes. * Prefer using sortedcontainers.SortedDict rather than blist.sorteddict. v. 1.3.3 (2015-07-05) * Fix crash on conflict retry introduced in previous version. v. 1.3.2 (2015-07-05) * Better handling of responses with no explicit body (HEAD, 204, 304). * Persistent connection fixes. * Proxy flow control fixes. * WebSocket over_http mode: buffer fragmented messages before sending. v. 1.3.1 (2015-06-19) * Fix http-response conflict recovery. * Correctly proxy WebSocket ping and pong frames. * Fix WebSocket compatibility with latest Zurl. v. 1.3.0 (2015-06-03) * Many fixes with subscription reporting via stats and SUB socket. * Tweaks to enable higher concurrent connection counts. * WebSocket over_http mode sends DISCONNECT events. v. 1.2.0 (2015-05-09) * http-stream: close action, keep-alive. * Check for new pushpin versions. * ZeroMQ endpoint discovery via command socket. * pushpin-publish command line tool. v. 1.1.1 (2015-04-17) * Fix auto-cross-origin feature. v. 1.1.0 (2015-03-08) * SUB socket input. SockJS client support. v. 1.0.0 (2014-09-16) * Stable version. pushpin-1.39.1/Cargo.lock000066400000000000000000001314121457610542000152060ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" dependencies = [ "getrandom", "once_cell", "version_check", ] [[package]] name = "aho-corasick" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "342258dd14006105c2b75ab1bd7543a03bdf0cfc94383303ac212a04939dff6f" dependencies = [ "anstyle", "anstyle-parse", "anstyle-wincon", "concolor-override", "concolor-query", "is-terminal", "utf8parse", ] [[package]] name = "anstyle" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23ea9e81bd02e310c216d080f6223c179012256e5151c41db88d12c88a1684d2" [[package]] name = "anstyle-parse" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7d1bb534e9efed14f3e5f44e7dd1a4f709384023a4165199a4241e18dff0116" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-wincon" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3127af6145b149f3287bb9a0d10ad9c5692dba8c53ad48285e5bec4063834fa" dependencies = [ "anstyle", "windows-sys 0.45.0", ] [[package]] name = "arrayvec" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "async-trait" version = "0.1.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bumpalo" version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "libc", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "ciborium" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" dependencies = [ "ciborium-io", "ciborium-ll", "serde", ] [[package]] name = "ciborium-io" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" [[package]] name = "ciborium-ll" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" dependencies = [ "ciborium-io", "half", ] [[package]] name = "clap" version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046ae530c528f252094e4a77886ee1374437744b2bff1497aa898bbddbbb29b3" dependencies = [ "clap_builder", "clap_derive", "once_cell", ] [[package]] name = "clap_builder" version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "223163f58c9a40c3b0a43e1c4b50a9ce09f007ea2cb1ec258a687945b4b7929f" dependencies = [ "anstream", "anstyle", "bitflags 1.3.2", "clap_lex", "once_cell", "strsim", "terminal_size", ] [[package]] name = "clap_derive" version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" [[package]] name = "colorchoice" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "concolor-override" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40a4925288e39d5923e024781971aab940995fa31bab3ffceebbadfc87591e90" dependencies = [ "colorchoice", ] [[package]] name = "concolor-query" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d11d52c3d7ca2e6d0040212be9e4dbbcd78b6447f535b6b561f449427944cf" dependencies = [ "windows-sys 0.45.0", ] [[package]] name = "config" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" dependencies = [ "async-trait", "json5", "lazy_static", "nom", "pathdiff", "ron", "rust-ini", "serde", "serde_json", "toml 0.5.11", "yaml-rust", ] [[package]] name = "core-foundation" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] [[package]] name = "criterion" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" dependencies = [ "anes", "cast", "ciborium", "clap", "criterion-plot", "is-terminal", "itertools", "num-traits", "once_cell", "oorandom", "plotters", "rayon", "regex", "serde", "serde_derive", "serde_json", "tinytemplate", "walkdir", ] [[package]] name = "criterion-plot" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", "itertools", ] [[package]] name = "crossbeam-deque" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset", "scopeguard", ] [[package]] name = "crossbeam-utils" version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", ] [[package]] name = "dlv-list" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" [[package]] name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "env_logger" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" dependencies = [ "log", ] [[package]] name = "errno" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" dependencies = [ "libc", "windows-sys 0.48.0", ] [[package]] name = "error-chain" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9435d864e017c3c6afeac1654189b06cdb491cf2ff73dbf0d73b0f292f42ff8" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "half" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ "ahash", ] [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "httparse" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "idna" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", "libc", "windows-sys 0.48.0", ] [[package]] name = "ipnet" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is-terminal" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", "rustix 0.38.24", "windows-sys 0.48.0", ] [[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" dependencies = [ "wasm-bindgen", ] [[package]] name = "json5" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" dependencies = [ "pest", "pest_derive", "serde", ] [[package]] name = "jsonwebtoken" version = "8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" dependencies = [ "base64 0.21.5", "pem", "ring 0.16.20", "serde", "serde_json", "simple_asn1", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" [[package]] name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memoffset" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] [[package]] name = "metadeps" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b122901b3a675fac8cecf68dcb2f0d3036193bc861d1ac0e1c337f7d5254c2" dependencies = [ "error-chain", "pkg-config", "toml 0.2.1", ] [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" dependencies = [ "adler", ] [[package]] name = "mio" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", "wasi", "windows-sys 0.48.0", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "num-bigint" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-integer" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", ] [[package]] name = "num-traits" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] [[package]] name = "num_threads" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" dependencies = [ "libc", ] [[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "oorandom" version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "openssl" version = "0.10.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800" dependencies = [ "bitflags 2.4.1", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" version = "0.9.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "ordered-multimap" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" dependencies = [ "dlv-list", "hashbrown", ] [[package]] name = "paste" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pathdiff" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "pem" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" dependencies = [ "base64 0.13.1", ] [[package]] name = "percent-encoding" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pest" version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" dependencies = [ "memchr", "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" dependencies = [ "pest", "pest_generator", ] [[package]] name = "pest_generator" version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", "syn", ] [[package]] name = "pest_meta" version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" dependencies = [ "once_cell", "pest", "sha2", ] [[package]] name = "pkg-config" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "plotters" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" dependencies = [ "num-traits", "plotters-backend", "plotters-svg", "wasm-bindgen", "web-sys", ] [[package]] name = "plotters-backend" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" [[package]] name = "plotters-svg" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" dependencies = [ "plotters-backend", ] [[package]] name = "proc-macro2" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] [[package]] name = "pushpin" version = "1.39.0-dev" dependencies = [ "arrayvec", "base64 0.13.1", "clap", "config", "criterion", "env_logger", "httparse", "ipnet", "jsonwebtoken", "libc", "log", "miniz_oxide", "mio", "openssl", "paste", "pkg-config", "rustls", "rustls-native-certs", "serde", "serde_json", "sha1", "signal-hook", "slab", "socket2", "test-log", "thiserror", "time", "url", "zmq", ] [[package]] name = "quote" version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "rayon" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "regex" version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "ring" version = "0.16.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" dependencies = [ "cc", "libc", "once_cell", "spin 0.5.2", "untrusted 0.7.1", "web-sys", "winapi", ] [[package]] name = "ring" version = "0.17.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" dependencies = [ "cc", "getrandom", "libc", "spin 0.9.8", "untrusted 0.9.0", "windows-sys 0.48.0", ] [[package]] name = "ron" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" dependencies = [ "base64 0.13.1", "bitflags 1.3.2", "serde", ] [[package]] name = "rust-ini" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" dependencies = [ "cfg-if", "ordered-multimap", ] [[package]] name = "rustix" version = "0.37.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", "linux-raw-sys 0.3.8", "windows-sys 0.48.0", ] [[package]] name = "rustix" version = "0.38.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234" dependencies = [ "bitflags 2.4.1", "errno", "libc", "linux-raw-sys 0.4.11", "windows-sys 0.48.0", ] [[package]] name = "rustls" version = "0.21.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" dependencies = [ "log", "ring 0.17.5", "rustls-webpki", "sct", ] [[package]] name = "rustls-native-certs" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", "rustls-pemfile", "schannel", "security-framework", ] [[package]] name = "rustls-pemfile" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ "base64 0.21.5", ] [[package]] name = "rustls-webpki" version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ "ring 0.17.5", "untrusted 0.9.0", ] [[package]] name = "ryu" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "schannel" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ "windows-sys 0.48.0", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ "ring 0.17.5", "untrusted 0.9.0", ] [[package]] name = "security-framework" version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "serde" version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "signal-hook" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", ] [[package]] name = "signal-hook-registry" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "simple_asn1" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" dependencies = [ "num-bigint", "num-traits", "thiserror", "time", ] [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "socket2" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", ] [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "terminal_size" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" dependencies = [ "rustix 0.37.27", "windows-sys 0.48.0", ] [[package]] name = "test-log" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f66edd6b6cd810743c0c71e1d085e92b01ce6a72782032e3f794c8284fe4bcdd" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thiserror" version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "time" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af0097eaf301d576d0b2aead7a59facab6d53cc636340f0291fab8446a2e8613" dependencies = [ "itoa", "libc", "num_threads", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" [[package]] name = "time-macros" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" dependencies = [ "time-core", ] [[package]] name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", ] [[package]] name = "tinyvec" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "736b60249cb25337bc196faa43ee12c705e426f3d55c214d73a4e7be06f92cb4" [[package]] name = "toml" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ "serde", ] [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ucd-trie" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "unicode-bidi" version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "untrusted" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ "windows-targets 0.42.2", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-targets" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "yaml-rust" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" dependencies = [ "linked-hash-map", ] [[package]] name = "zmq" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aad98a7a617d608cd9e1127147f630d24af07c7cd95ba1533246d96cbdd76c66" dependencies = [ "bitflags 1.3.2", "libc", "log", "zmq-sys", ] [[package]] name = "zmq-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d33a2c51dde24d5b451a2ed4b488266df221a5eaee2ee519933dc46b9a9b3648" dependencies = [ "libc", "metadeps", ] pushpin-1.39.1/Cargo.toml000066400000000000000000000032721457610542000152330ustar00rootroot00000000000000[package] name = "pushpin" version = "1.39.0-dev" authors = ["Justin Karneges "] description = "Reverse proxy for realtime web services" repository = "https://github.com/fastly/pushpin" readme = "README.md" license = "Apache-2.0" edition = "2018" default-run = "pushpin" [profile.dev] panic = "abort" [profile.release] panic = "abort" [lib] crate-type = ["rlib", "staticlib"] [dependencies] arrayvec = "0.7" base64 = "0.13" clap = { version = "=4.2.1", features = ["cargo", "string", "wrap_help", "derive"] } config = "0.13.3" httparse = "1.7" ipnet = "2" jsonwebtoken = "8" libc = "0.2" log = "0.4" miniz_oxide = "0.6" mio = { version = "0.8", features = ["os-poll", "os-ext", "net"] } openssl = "0.10" paste = "1.0" rustls = "0.21" rustls-native-certs = "0.6" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha1 = "0.10" signal-hook = "0.3" slab = "0.4" socket2 = "0.4" thiserror = "1.0" time = { version = "=0.3.18", features = ["formatting", "local-offset", "macros"] } url = "2.3" zmq = "0.9" [dev-dependencies] criterion = "0.5" env_logger = { version = "0.9", default-features = false } test-log = "0.2" [build-dependencies] pkg-config = "0.3" time = { version = "=0.3.18", features = ["formatting", "local-offset", "macros"] } [[bench]] name = "server" harness = false [[bench]] name = "client" harness = false [[bin]] name = "pushpin-condure" test = false bench = false [[bin]] name = "m2adapter" test = false bench = false [[bin]] name = "pushpin-proxy" test = false bench = false [[bin]] name = "pushpin-handler" test = false bench = false [[bin]] name = "pushpin" test = false bench = false [[bin]] name = "pushpin-publish" test = false bench = false pushpin-1.39.1/LICENSE000066400000000000000000000261351457610542000143130ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. pushpin-1.39.1/Makefile000066400000000000000000000011531457610542000147370ustar00rootroot00000000000000ifdef RELEASE cargo_flags = --offline --release else cargo_flags = endif all: postbuild build: FORCE cargo build $(cargo_flags) cargo-test: FORCE cargo test $(cargo_flags) cargo-clean: FORCE cargo clean postbuild: build FORCE cd postbuild && $(MAKE) -f Makefile postbuild-install: FORCE cd postbuild && $(MAKE) -f Makefile install postbuild-clean: FORCE cd postbuild && $(MAKE) -f Makefile clean postbuild-distclean: FORCE cd postbuild && $(MAKE) -f Makefile distclean check: cargo-test install: postbuild-install clean: cargo-clean postbuild-clean distclean: cargo-clean postbuild-distclean FORCE: pushpin-1.39.1/README.md000066400000000000000000000316231457610542000145630ustar00rootroot00000000000000# Pushpin Website: https://pushpin.org/ Forum: https://community.fastly.com/c/pushpin/12 Pushpin is a reverse proxy server written in Rust & C++ that makes it easy to implement WebSocket, HTTP streaming, and HTTP long-polling services. The project is unique among realtime push solutions in that it is designed to address the needs of API creators. Pushpin is transparent to clients and integrates easily into an API stack. ## How it works Pushpin is placed in the network path between the backend and any clients:

pushpin-abstract

Pushpin communicates with backend web applications using regular, short-lived HTTP requests. This allows backend applications to be written in any language and use any webserver. There are two main integration points: 1. The backend must handle proxied requests. For HTTP, each incoming request is proxied to the backend. For WebSockets, the activity of each connection is translated into a series of HTTP requests[1](#proxy-modes) sent to the backend. Pushpin's behavior is determined by how the backend responds to these requests. 2. The backend must tell Pushpin to push data. Regardless of how clients are connected, data may be pushed to them by making an HTTP POST request to Pushpin's private control API (`http://localhost:5561/publish/` by default). Pushpin will inject this data into any client connections as necessary. To assist with integration, there are [libraries](https://pushpin.org/docs/usage/#libraries) for many backend languages and frameworks. Pushpin has no libraries on the client side because it is transparent to clients. ## Example To create an HTTP streaming connection, respond to a proxied request with special headers `Grip-Hold` and `Grip-Channel`[2](#grip): ```http HTTP/1.1 200 OK Content-Type: text/plain Content-Length: 22 Grip-Hold: stream Grip-Channel: test welcome to the stream ``` When Pushpin receives the above response from the backend, it will process it and send an initial response to the client that instead looks like this: ```http HTTP/1.1 200 OK Content-Type: text/plain Transfer-Encoding: chunked Connection: Transfer-Encoding welcome to the stream ``` Pushpin eats the special headers and switches to chunked encoding (notice there's no `Content-Length`). The request between Pushpin and the backend is now complete, but the request between the client and Pushpin remains held open. The request is subscribed to a channel called `test`. Data can then be pushed to the client by publishing data on the `test` channel: ```bash curl -d '{ "items": [ { "channel": "test", "formats": { "http-stream": \ { "content": "hello there\n" } } } ] }' \ http://localhost:5561/publish ``` The client would then see the line "hello there" appended to the response stream. Ta-da, transparent realtime push! For more details, see the [HTTP streaming](https://pushpin.org/docs/usage/#http-streaming) section of the documentation. Pushpin also supports [HTTP long-polling](https://pushpin.org/docs/usage/#http-long-polling) and [WebSockets](https://pushpin.org/docs/usage/#websockets). ## Example using a library Using a library on the backend makes integration even easier. Here's another HTTP streaming example, similar to the one shown above, except using Pushpin's [Django library](https://github.com/fanout/django-grip). Please note that Pushpin is not Python/Django-specific and there are backend libraries for [other languages/frameworks, too](https://pushpin.org/docs/usage/#libraries). The Django library requires configuration in `settings.py`: ```python MIDDLEWARE_CLASSES = ( 'django_grip.GripMiddleware', ... ) GRIP_PROXIES = [{'control_uri': 'http://localhost:5561'}] ``` Here's a simple view: ```python from django.http import HttpResponse from django_grip import set_hold_stream def myendpoint(request): if request.method == 'GET': # subscribe every incoming request to a channel in stream mode set_hold_stream(request, 'test') return HttpResponse('welcome to the stream\n', content_type='text/plain') ... ``` What happens here is the `set_hold_stream()` method flags the request as needing to turn into a stream, bound to channel `test`. The middleware will see this and add the necessary `Grip-Hold` and `Grip-Channel` headers to the response. Publishing data is easy: ```python from gripcontrol import HttpStreamFormat from django_grip import publish publish('test', HttpStreamFormat('hello there\n')) ``` ## Example using WebSockets Pushpin supports WebSockets by converting connection activity/messages into HTTP requests and sending them to the backend. For this example, we'll use Pushpin's [Express library](https://github.com/fanout/js-serve-grip). As before, please note that Pushpin is not Node/Express-specific and there are backend libraries for [other languages/frameworks, too](https://pushpin.org/docs/usage/#libraries). The Express library requires configuration and setting up a middleware handler: ```javascript const express = require('express'); const { ServeGrip } = require('@fanoutio/serve-grip'); var app = express(); // Instantiate the middleware and register it with Express const serveGrip = new ServeGrip({ grip: { 'control_uri': 'http://localhost:5561', 'key': 'changeme' } }); app.use(serveGrip); // Instantiate the publisher to use from your code to publish messages const publisher = serveGrip.getPublisher(); app.get('/hello', (req, res) => { res.send('hello world\n'); }); ``` With that structure in place, here's an example of a WebSocket endpoint: ```javascript const { WebSocketMessageFormat } = require( '@fanoutio/grip' ); app.post('/websocket', async (req, res) => { const { wsContext } = req.grip; // If this is a new connection, accept it and subscribe it to a channel if (wsContext.isOpening()) { wsContext.accept(); wsContext.subscribe('all'); } while (wsContext.canRecv()) { var message = wsContext.recv(); // If return value is null then connection is closed if (message == null) { wsContext.close(); break; } // broadcast the message to everyone connected await publisher.publishFormats('all', WebSocketMessageFormat(message)); } res.end(); }); ``` The above code binds all incoming connections to a channel called `all`. Any received messages are published out to all connected clients. What's particularly noteworthy is that the above endpoint is stateless. The app doesn't keep track of connections, and the handler code only runs whenever messages arrive. Restarting the app won't disconnect clients. The `while` loop is deceptive. It looks like it's looping for the lifetime of the WebSocket connection, but what it's really doing is looping through a batch of WebSocket messages that was just received via HTTP. Often this will be one message, and so the loop performs one iteration and then exits. Similarly, the `wsContext` object only exists for the duration of the handler invocation, rather than for the lifetime of the connection as you might expect. It may look like socket code, but it's all an illusion. :tophat: For details on the underlying protocol conversion, see the [WebSocket-Over-HTTP Protocol spec](https://pushpin.org/docs/protocols/websocket-over-http/). ## Example without a webserver Pushpin can also connect to backend servers via ZeroMQ instead of HTTP. This may be preferred for writing lower-level services where a real webserver isn't needed. The messages exchanged over the ZeroMQ connection contain the same information as HTTP, encoded as TNetStrings. To use a ZeroMQ backend, first make sure there's an appropriate route in Pushpin's `routes` file: ``` * zhttpreq/tcp://127.0.0.1:10000 ``` The above line tells Pushpin to bind a REQ-compatible socket on port 10000 that handlers can connect to. Activating an HTTP stream is as easy as responding on a REP socket: ```python import zmq import tnetstring zmq_context = zmq.Context() sock = zmq_context.socket(zmq.REP) sock.connect('tcp://127.0.0.1:10000') while True: req = tnetstring.loads(sock.recv()[1:]) resp = { 'id': req['id'], 'code': 200, 'reason': 'OK', 'headers': [ ['Grip-Hold', 'stream'], ['Grip-Channel', 'test'], ['Content-Type', 'text/plain'] ], 'body': 'welcome to the stream\n' } sock.send('T' + tnetstring.dumps(resp)) ``` ## Why another realtime solution? Pushpin is an ambitious project with two primary goals: * Make realtime API development easier. There are many other solutions out there that are excellent for building realtime apps, but few are useful within the context of *APIs*. For example, you can't use Socket.io to build Twitter's streaming API. A new kind of project is needed in this case. * Make realtime push behavior delegable. The reason there isn't a realtime push CDN yet is because the standards and practices necessary for delegating to a third party in a transparent way are not yet established. Pushpin is more than just another realtime push solution; it represents the next logical step in the evolution of realtime web architectures. To really understand Pushpin, you need to think of it as more like a gateway than a message queue. Pushpin does not persist data and it is agnostic to your application's data model. Your backend provides the mapping to whatever that data model is. Tools like Kafka and RabbitMQ are complementary. Pushpin is also agnostic to your API definition. Clients don't necessarily subscribe to "channels" or receive "messages". Clients make HTTP requests or send WebSocket frames, and your backend decides the meaning of those inputs. Pushpin could perhaps be awkwardly described as "a proxy server that enables web services to delegate the handling of realtime push primitives". On a practical level, there are many benefits to Pushpin that you don't see anywhere else: * The proxy design allows Pushpin to fit nicely within an API stack. This means it can inherit other facilities from your REST API, such as authentication, logging, throttling, etc. It can be combined with an API management system. * As your API scales, a multi-tiered architecture will become inevitable. With Pushpin you can easily do this from the start. * It works well with microservices. Each microservice can have its own Pushpin instance. No central bus needed. * Hot reload. Restarting the backend doesn't disconnect clients. * In the case of WebSocket messages being proxied out as HTTP requests, the messages may be handled statelessly by the backend. Messages from a single connection can even be load balanced across a set of backend instances. ## Install Check out the [the Install guide](https://pushpin.org/docs/install/), which covers how to install and run. There are packages available for Linux (Debian, Ubuntu, CentOS, Red Hat), Mac (Homebrew), or you can build from source. By default, Pushpin listens on port 7999 and requests are handled by its internal test handler. You can confirm the server is working by browsing to `http://localhost:7999/`. Next, you should modify the `routes` config file to route requests to your backend webserver. See [Configuration](https://pushpin.org/docs/configuration/). ## Scalability Pushpin is horizontally scalable. Instances don’t talk to each other, and sticky routing is not needed. Backends must publish data to all instances to ensure clients connected to any instance will receive the data. Most of the backend libraries support configuring more than one Pushpin instance, so that a single publish call will send data to multiple instances at once. Optionally, ZeroMQ PUB/SUB can be used to send data to Pushpin instead of using HTTP POST. When this method is used, subscription information is forwarded to each publisher, such that data will only be published to instances that have listeners. As for vertical scalability, Pushpin has been tested with up to [1 million concurrent connections](https://github.com/fanout/pushpin-c1m) running on a single DigitalOcean droplet with 8 CPU cores. In practice, you may want to plan for fewer connections per instance, depending on your throughput. The new connection accept rate is about 800/sec (though this also depends on the speed of your backend), and the message throughput is about 8,000/sec. The important thing is that Pushpin is horizontally scalable which is effectively limitless. ## What does the name mean? Pushpin means to "pin" connections open for "pushing". ## License Pushpin is offered under the Apache License, Version 2.0. See the LICENSE file. ## Footnotes 1: Pushpin can communicate WebSocket activity to the backend using either HTTP or WebSockets. Conversion to HTTP is generally recommended as it makes the backend easier to reason about. 2: GRIP (Generic Realtime Intermediary Protocol) is the name of Pushpin's backend protocol. More about that [here](https://pushpin.org/docs/protocols/grip/). pushpin-1.39.1/SECURITY.md000066400000000000000000000015741457610542000150770ustar00rootroot00000000000000## Report a security issue The project team welcomes security reports and is committed to providing prompt attention to security issues. Security issues should be reported privately via [Fastly’s security issue reporting process](https://www.fastly.com/security/report-security-issue). ## Security advisories Remediation of security vulnerabilities is prioritized by the project team. The project team endeavors to coordinate remediation with third-party stakeholders, and is committed to transparency in the disclosure process. The team announces security issues via [GitHub](https://github.com/fastly/pushpin/releases) as well as [RustSec](https://rustsec.org/advisories/) on a best-effort basis. Note that communications related to security issues in Fastly-maintained OSS as described here are distinct from [Fastly Security Advisories](https://www.fastly.com/security-advisories). pushpin-1.39.1/benches/000077500000000000000000000000001457610542000147065ustar00rootroot00000000000000pushpin-1.39.1/benches/client.rs000066400000000000000000000125401457610542000165340ustar00rootroot00000000000000/* * Copyright (C) 2023 Fanout, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use criterion::{criterion_group, criterion_main, Criterion}; use mio::net::TcpListener; use pushpin::channel; use pushpin::client::TestClient; use pushpin::executor::Executor; use pushpin::future::{AsyncReadExt, AsyncSender, AsyncTcpListener, AsyncTcpStream, AsyncWriteExt}; use pushpin::reactor::Reactor; use std::net::SocketAddr; use std::rc::Rc; use std::str; const REQS_PER_ITER: usize = 10; fn req(listener: TcpListener, start: F1, wait: F2) -> TcpListener where F1: Fn(SocketAddr) + 'static, F2: Fn() + 'static, { let executor = Executor::new(REQS_PER_ITER + 1); let addr = listener.local_addr().unwrap(); let (s, r) = channel::channel(1); for _ in 0..REQS_PER_ITER { start(addr); } let spawner = executor.spawner(); executor .spawn(async move { let s = AsyncSender::new(s); let listener = AsyncTcpListener::new(listener); for _ in 0..REQS_PER_ITER { let (stream, _) = listener.accept().await.unwrap(); let mut stream = AsyncTcpStream::new(stream); spawner .spawn(async move { let mut buf = Vec::new(); let mut req_end = 0; while req_end == 0 { let mut chunk = [0; 1024]; let size = stream.read(&mut chunk).await.unwrap(); buf.extend_from_slice(&chunk[..size]); for i in 0..(buf.len() - 3) { if &buf[i..(i + 4)] == b"\r\n\r\n" { req_end = i + 4; break; } } } let expected = format!( concat!("GET /path HTTP/1.1\r\n", "Host: {}\r\n", "\r\n"), addr ); assert_eq!(str::from_utf8(&buf[..req_end]).unwrap(), expected); stream .write( b"HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Type: text/plain\r\nContent-Length: 6\r\n\r\nhello\n", ).await .unwrap(); }) .unwrap(); } s.send(listener.into_inner()).await.unwrap(); }) .unwrap(); executor .run(|timeout| Reactor::current().unwrap().poll(timeout)) .unwrap(); for _ in 0..REQS_PER_ITER { wait(); } let listener = r.recv().unwrap(); listener } fn criterion_benchmark(c: &mut Criterion) { let mut req_listener = Some(TcpListener::bind("127.0.0.1:0".parse().unwrap()).unwrap()); let mut stream_listener = Some(TcpListener::bind("127.0.0.1:0".parse().unwrap()).unwrap()); let _reactor = Reactor::new(REQS_PER_ITER * 10); { let client = Rc::new(TestClient::new(1)); c.bench_function("req_client workers=1", |b| { b.iter(|| { let c1 = Rc::clone(&client); let c2 = Rc::clone(&client); req_listener = Some(req( req_listener.take().unwrap(), move |addr| c1.do_req(addr), move || c2.wait_req(), )) }) }); c.bench_function("stream_client workers=1", |b| { b.iter(|| { let c1 = Rc::clone(&client); let c2 = Rc::clone(&client); stream_listener = Some(req( stream_listener.take().unwrap(), move |addr| c1.do_stream_http(addr), move || c2.wait_stream(), )) }) }); } { let client = Rc::new(TestClient::new(2)); c.bench_function("req_client workers=2", |b| { b.iter(|| { let c1 = Rc::clone(&client); let c2 = Rc::clone(&client); req_listener = Some(req( req_listener.take().unwrap(), move |addr| c1.do_req(addr), move || c2.wait_req(), )) }) }); c.bench_function("stream_client workers=2", |b| { b.iter(|| { let c1 = Rc::clone(&client); let c2 = Rc::clone(&client); stream_listener = Some(req( stream_listener.take().unwrap(), move |addr| c1.do_stream_http(addr), move || c2.wait_stream(), )) }) }); } } criterion_group!(benches, criterion_benchmark); criterion_main!(benches); pushpin-1.39.1/benches/server.rs000066400000000000000000000112661457610542000165700ustar00rootroot00000000000000/* * Copyright (C) 2020 Fanout, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use criterion::{criterion_group, criterion_main, Criterion}; use pushpin::connection::testutil::{ BenchServerReqConnection, BenchServerReqHandler, BenchServerStreamConnection, BenchServerStreamHandler, }; use pushpin::executor::Executor; use pushpin::future::{AsyncReadExt, AsyncTcpStream, AsyncWriteExt}; use pushpin::reactor::Reactor; use pushpin::server::TestServer; use pushpin::websocket::testutil::{BenchRecvMessage, BenchSendMessage}; use std::io::{self, Write}; use std::net::SocketAddr; use std::str; const REQS_PER_ITER: usize = 10; fn req(addr: SocketAddr) { let reactor = Reactor::new(REQS_PER_ITER * 10); let executor = Executor::new(REQS_PER_ITER); for _ in 0..REQS_PER_ITER { executor .spawn(async move { let mut client = AsyncTcpStream::connect(&[addr]).await.unwrap(); client .write(b"GET /hello HTTP/1.0\r\nHost: example.com\r\n\r\n") .await .unwrap(); let mut resp = [0u8; 1024]; let mut resp = io::Cursor::new(&mut resp[..]); loop { let mut buf = [0; 1024]; let size = client.read(&mut buf).await.unwrap(); if size == 0 { break; } resp.write(&buf[..size]).unwrap(); } let size = resp.position() as usize; let resp = str::from_utf8(&resp.get_ref()[..size]).unwrap(); assert_eq!(resp, "HTTP/1.0 200 OK\r\nContent-Length: 6\r\n\r\nworld\n"); }) .unwrap(); } executor.run(|timeout| reactor.poll(timeout)).unwrap(); } fn criterion_benchmark(c: &mut Criterion) { { let t = BenchServerReqHandler::new(); c.bench_function("req_handler", |b| { b.iter_batched_ref(|| t.init(), |i| t.run(i), criterion::BatchSize::SmallInput) }); } { let t = BenchServerStreamHandler::new(); c.bench_function("stream_handler", |b| { b.iter_batched_ref(|| t.init(), |i| t.run(i), criterion::BatchSize::SmallInput) }); } { let t = BenchServerReqConnection::new(); c.bench_function("req_connection", |b| { b.iter_batched_ref(|| t.init(), |i| t.run(i), criterion::BatchSize::SmallInput) }); } { let t = BenchServerStreamConnection::new(); c.bench_function("stream_connection", |b| { b.iter_batched_ref(|| t.init(), |i| t.run(i), criterion::BatchSize::SmallInput) }); } { let t = BenchSendMessage::new(false); c.bench_function("ws_send", |b| { b.iter_batched_ref(|| t.init(), |i| t.run(i), criterion::BatchSize::SmallInput) }); } { let t = BenchSendMessage::new(true); c.bench_function("ws_send_deflate", |b| { b.iter_batched_ref(|| t.init(), |i| t.run(i), criterion::BatchSize::SmallInput) }); } { let t = BenchRecvMessage::new(false); c.bench_function("ws_recv", |b| { b.iter_batched_ref(|| t.init(), |i| t.run(i), criterion::BatchSize::SmallInput) }); } { let t = BenchRecvMessage::new(true); c.bench_function("ws_recv_deflate", |b| { b.iter_batched_ref(|| t.init(), |i| t.run(i), criterion::BatchSize::SmallInput) }); } { let server = TestServer::new(1); let req_addr = server.req_addr(); let stream_addr = server.stream_addr(); c.bench_function("req_server workers=1", |b| b.iter(|| req(req_addr))); c.bench_function("stream_server workers=1", |b| b.iter(|| req(stream_addr))); } { let server = TestServer::new(2); let req_addr = server.req_addr(); let stream_addr = server.stream_addr(); c.bench_function("req_server workers=2", |b| b.iter(|| req(req_addr))); c.bench_function("stream_server workers=2", |b| b.iter(|| req(stream_addr))); } } criterion_group!(benches, criterion_benchmark); criterion_main!(benches); pushpin-1.39.1/build.rs000066400000000000000000000361761457610542000147610ustar00rootroot00000000000000use std::collections::HashMap; use std::env; use std::error::Error; use std::ffi::OsStr; use std::fmt; use std::fs::{self, File}; use std::io::{self, BufRead, Write}; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use std::process::{Command, ExitStatus, Output, Stdio}; use std::str::FromStr; use time::macros::format_description; use time::OffsetDateTime; const DEFAULT_PREFIX: &str = "/usr/local"; fn get_version() -> String { let mut version = env!("CARGO_PKG_VERSION").to_string(); if version.ends_with("-dev") { let format = format_description!("[year][month][day]"); let date_str = OffsetDateTime::now_utc().format(&format).unwrap(); version.push_str(&format!("-{}", date_str)); } version } #[derive(Clone)] struct LibVersion { maj: u16, min: u16, orig: String, } #[derive(Debug)] struct ParseVersionError; impl fmt::Display for ParseVersionError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Ok(write!(f, "failed to parse version")?) } } impl Error for ParseVersionError {} impl FromStr for LibVersion { type Err = ParseVersionError; fn from_str(s: &str) -> Result { let parts: Vec<&str> = s.split('.').collect(); if parts.len() < 2 { return Err(ParseVersionError); } let (maj, min): (u16, u16) = match (parts[0].parse(), parts[1].parse()) { (Ok(maj), Ok(min)) => (maj, min), _ => return Err(ParseVersionError), }; Ok(LibVersion { maj, min, orig: s.to_string(), }) } } fn check_version( pkg: &str, found: LibVersion, expect_maj: u16, expect_min: u16, ) -> Result<(), Box> { if found.maj < expect_maj || (found.maj == expect_maj && found.min < expect_min) { return Err(format!( "{} version >={}.{} required, found: {}", pkg, expect_maj, expect_min, found.orig, ) .into()); } Ok(()) } fn prefixed_vars(prefix: &str) -> HashMap { let mut out = HashMap::new(); out.insert("BINDIR".into(), format!("{}/bin", prefix)); out.insert("CONFIGDIR".into(), format!("{}/etc", prefix)); out.insert("LIBDIR".into(), format!("{}/lib", prefix)); out.insert("LOGDIR".into(), "/var/log".into()); out.insert("RUNDIR".into(), "/var/run".into()); out } fn env_or_default(name: &str, defaults: &HashMap) -> String { match env::var(name) { Ok(s) => s, Err(_) => defaults.get(name).unwrap().to_string(), } } fn write_cpp_conf_pri( dest: &Path, release: bool, include_paths: &[&Path], deny_warnings: bool, ) -> Result<(), Box> { let mut f = fs::File::create(dest)?; writeln!(&mut f, "CONFIG -= debug_and_release")?; if release { writeln!(&mut f, "CONFIG += release")?; } else { writeln!(&mut f, "CONFIG += debug")?; } writeln!(&mut f)?; for path in include_paths { writeln!(&mut f, "INCLUDEPATH += {}", path.display())?; } writeln!(&mut f)?; if deny_warnings { writeln!(&mut f, "QMAKE_CXXFLAGS += \"-Werror\"")?; } Ok(()) } fn write_postbuild_conf_pri( dest: &Path, bin_dir: &str, lib_dir: &str, config_dir: &str, run_dir: &str, log_dir: &str, ) -> Result<(), Box> { let mut f = fs::File::create(dest)?; writeln!(&mut f, "BINDIR = {}", bin_dir)?; writeln!(&mut f, "LIBDIR = {}/pushpin", lib_dir)?; writeln!(&mut f, "CONFIGDIR = {}/pushpin", config_dir)?; writeln!(&mut f, "RUNDIR = {}/pushpin", run_dir)?; writeln!(&mut f, "LOGDIR = {}/pushpin", log_dir)?; Ok(()) } // returned vec size guaranteed >= 1 fn get_args_lossy(command: &mut Command) -> Vec { let mut args = vec![command.get_program().to_string_lossy().into_owned()]; for s in command.get_args() { args.push(s.to_string_lossy().into_owned()); } args } // convert Result to Result, separating stdout fn take_stdout(result: io::Result) -> (io::Result, Vec) { match result { Ok(output) => (Ok(output.status), output.stdout), Err(e) => (Err(e), Vec::new()), } } fn check_command_result( program: &str, result: io::Result, ) -> Result<(), Box> { let status = match result { Ok(status) => status, Err(e) => return Err(format!("{} failed: {}", program, e).into()), }; if !status.success() { return Err(format!("{} failed, {}", program, status).into()); } Ok(()) } fn check_command(command: &mut Command) -> Result<(), Box> { let args = get_args_lossy(command); println!("{}", args.join(" ")); check_command_result(&args[0], command.status()) } fn check_command_capture_stdout(command: &mut Command) -> Result, Box> { let args = get_args_lossy(command); println!("{}", args.join(" ")); // don't capture stderr let command = command.stderr(Stdio::inherit()); let (result, output) = take_stdout(command.output()); check_command_result(&args[0], result)?; Ok(output) } fn check_qmake(qmake_path: &Path) -> Result> { let version: LibVersion = { let output = check_command_capture_stdout(Command::new(qmake_path).args(["-query", "QT_VERSION"]))?; let s = String::from_utf8(output)?; let s = s.trim(); match s.parse() { Ok(v) => v, Err(_) => return Err(format!("unexpected qt version string: [{}]", s).into()), } }; check_version("qt", version.clone(), 5, 12)?; Ok(version) } fn find_in_path(name: &str) -> Option { for d in env::var("PATH").unwrap_or_default().split(':') { if d.is_empty() { continue; } let path = Path::new(d).join(name); if path.exists() { return Some(path); } } None } fn find_qmake() -> Result<(PathBuf, LibVersion), Box> { let mut errors = Vec::new(); // check for a usable qmake in PATH let names = &["qmake", "qmake6", "qmake5"]; for name in names { if let Some(p) = find_in_path(name) { match check_qmake(&p) { Ok(version) => return Ok((p, version)), Err(e) => errors.push(format!("skipping {}: {}", p.display(), e)), } } } if errors.is_empty() { errors.push(format!("none of ({}) found in PATH", names.join(", "))); } // check pkg-config let pkg = "Qt5Core"; match pkg_config::get_variable(pkg, "host_bins") { Ok(host_bins) if !host_bins.is_empty() => { let host_bins = PathBuf::from(host_bins); match fs::canonicalize(host_bins.join("qmake")) { Ok(p) => match check_qmake(&p) { Ok(version) => return Ok((p, version)), Err(e) => errors.push(format!("skipping {}: {}", p.display(), e)), }, Err(e) => errors.push(format!("qmake not found in {}: {}", host_bins.display(), e)), } } Ok(_) => errors.push(format!( "pkg-config variable host_bins does not exist for {}", pkg )), Err(e) => errors.push(format!("pkg-config error for {}: {}", pkg, e)), } Err(format!("unable to find a usable qmake: {}", errors.join(", ")).into()) } fn get_qmake() -> Result<(PathBuf, LibVersion), Box> { match env::var("QMAKE") { Ok(s) => { let path = PathBuf::from(s); let version = check_qmake(&path)?; Ok((path, version)) } Err(env::VarError::NotPresent) => find_qmake(), Err(env::VarError::NotUnicode(_)) => Err("QMAKE not unicode".into()), } } fn contains_file_prefix(dir: &Path, prefix: &str) -> Result { for entry in fs::read_dir(dir)? { let entry = entry?; if entry.file_name().as_bytes().starts_with(prefix.as_bytes()) { return Ok(true); } } Ok(false) } fn get_qt_lib_prefix(lib_dir: &Path, version_maj: u16) -> Result> { let prefixes = if cfg!(target_os = "macos") { [format!("Qt{}", version_maj), "Qt".to_string()] } else { [format!("libQt{}", version_maj), "libQt".to_string()] }; for prefix in &prefixes { if contains_file_prefix(lib_dir, prefix)? { return Ok(prefix.strip_prefix("lib").unwrap_or(prefix).to_string()); } } Err(format!( "no files in {} beginning with any of: {}", lib_dir.display(), prefixes.join(", ") ) .into()) } fn find_boost_include_dir() -> Result> { let paths = ["/usr/local/include", "/usr/include"]; let version_filename = "boost/version.hpp"; for path in paths { let path = PathBuf::from(path); let full_path = path.join(version_filename); if !full_path.exists() { continue; } let file = File::open(&full_path)?; let reader = io::BufReader::new(file); let mut version_line = None; for line in reader.lines() { match line { Ok(s) if s.contains("#define BOOST_LIB_VERSION") => version_line = Some(s), Ok(_) => continue, Err(e) => { return Err(format!("failed to read {}: {}", full_path.display(), e).into()) } } } let version_line = match version_line { Some(s) => s, None => return Err(format!("version line not found in {}", full_path.display()).into()), }; let parts: Vec<&str> = version_line.split('"').collect(); if parts.len() < 2 { return Err(format!("failed to parse version line in {}", full_path.display()).into()); } let version = parts[1].replace('_', "."); let version = match version.parse() { Ok(v) => v, Err(_) => return Err(format!("unexpected boost version string: {}", version).into()), }; check_version("boost", version, 1, 71)?; return Ok(path); } Err(format!( "{} not found in any of: {}", version_filename, paths.join(", ") ) .into()) } fn contains_subslice(haystack: &[T], needle: &[T]) -> bool { haystack.windows(needle.len()).any(|w| w == needle) } fn main() -> Result<(), Box> { let (qmake_path, qt_version) = get_qmake()?; let qt_install_libs = { let output = check_command_capture_stdout( Command::new(&qmake_path).args(["-query", "QT_INSTALL_LIBS"]), )?; let libs_dir = PathBuf::from(String::from_utf8(output)?.trim()); fs::canonicalize(&libs_dir) .map_err(|_| format!("QT_INSTALL_LIBS dir {} not found", libs_dir.display()))? }; let qt_lib_prefix = get_qt_lib_prefix(&qt_install_libs, qt_version.maj)?; let boost_include_dir = match env::var("BOOST_INCLUDE_DIR") { Ok(s) => PathBuf::from(s), Err(env::VarError::NotPresent) => find_boost_include_dir()?, Err(env::VarError::NotUnicode(_)) => return Err("BOOST_INCLUDE_DIR not unicode".into()), }; let default_vars = { let prefix = match env::var("PREFIX") { Ok(s) => Some(s), Err(env::VarError::NotPresent) => None, Err(env::VarError::NotUnicode(_)) => return Err("PREFIX not unicode".into()), }; if let Some(prefix) = prefix { prefixed_vars(&prefix) } else { prefixed_vars(DEFAULT_PREFIX) } }; let bin_dir = env_or_default("BINDIR", &default_vars); let config_dir = env_or_default("CONFIGDIR", &default_vars); let lib_dir = env_or_default("LIBDIR", &default_vars); let log_dir = env_or_default("LOGDIR", &default_vars); let run_dir = env_or_default("RUNDIR", &default_vars); let root_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); let out_dir = PathBuf::from(env::var("OUT_DIR")?); let profile = env::var("PROFILE")?; let cpp_src_dir = root_dir.join("src/cpp"); let cpp_tests_src_dir = root_dir.join("src/cpp/tests"); for dir in ["moc", "obj", "test-moc", "test-obj", "test-work"] { fs::create_dir_all(out_dir.join(dir))?; } let mut include_paths = Vec::new(); include_paths.push(out_dir.as_ref()); if boost_include_dir != Path::new("/usr/include") { include_paths.push(boost_include_dir.as_ref()); } let deny_warnings = match env::var("CARGO_ENCODED_RUSTFLAGS") { Ok(s) => { let flags: Vec<&str> = s.split('\x1f').collect(); contains_subslice(&flags, &["-D", "warnings"]) } Err(env::VarError::NotPresent) => false, Err(env::VarError::NotUnicode(_)) => { return Err("CARGO_ENCODED_RUSTFLAGS not unicode".into()) } }; write_cpp_conf_pri( &out_dir.join("conf.pri"), profile == "release", &include_paths, deny_warnings, )?; write_postbuild_conf_pri( &Path::new("postbuild").join("conf.pri"), &bin_dir, &lib_dir, &config_dir, &run_dir, &log_dir, )?; check_command(Command::new(&qmake_path).args([ OsStr::new("-o"), out_dir.join("Makefile").as_os_str(), cpp_src_dir.join("cpp.pro").as_os_str(), ]))?; check_command(Command::new(&qmake_path).args([ OsStr::new("-o"), out_dir.join("Makefile.test").as_os_str(), cpp_tests_src_dir.join("tests.pro").as_os_str(), ]))?; check_command( Command::new(&qmake_path) .args(["-o", "Makefile", "postbuild.pro"]) .current_dir("postbuild"), )?; check_command( Command::new("make") .env("MAKEFLAGS", env::var("CARGO_MAKEFLAGS")?) .args(["-f", "Makefile"]) .current_dir(&out_dir), )?; check_command( Command::new("make") .env("MAKEFLAGS", env::var("CARGO_MAKEFLAGS")?) .args(["-f", "Makefile.test"]) .current_dir(&out_dir), )?; println!("cargo:rustc-env=APP_VERSION={}", get_version()); println!("cargo:rustc-env=CONFIG_DIR={}/pushpin", config_dir); println!("cargo:rustc-env=LIB_DIR={}/pushpin", lib_dir); println!("cargo:rustc-cfg=qt_lib_prefix=\"{}\"", qt_lib_prefix); println!("cargo:rustc-link-search={}", out_dir.display()); if cfg!(target_os = "macos") { println!( "cargo:rustc-link-search=framework={}", qt_install_libs.display() ); } else { println!("cargo:rustc-link-search={}", qt_install_libs.display()); } println!("cargo:rerun-if-env-changed=RELEASE"); println!("cargo:rerun-if-env-changed=PREFIX"); println!("cargo:rerun-if-env-changed=BINDIR"); println!("cargo:rerun-if-env-changed=CONFIGDIR"); println!("cargo:rerun-if-env-changed=LIBDIR"); println!("cargo:rerun-if-env-changed=LOGDIR"); println!("cargo:rerun-if-env-changed=RUNDIR"); println!("cargo:rerun-if-changed=src"); Ok(()) } pushpin-1.39.1/examples/000077500000000000000000000000001457610542000151155ustar00rootroot00000000000000pushpin-1.39.1/examples/config/000077500000000000000000000000001457610542000163625ustar00rootroot00000000000000pushpin-1.39.1/examples/config/pushpin.conf000066400000000000000000000102251457610542000207170ustar00rootroot00000000000000[global] include={libdir}/internal.conf # directory to save runtime files rundir=run # prefix for zmq ipc specs ipc_prefix=pushpin- # port offset for zmq tcp specs and http control server port_offset=0 # TTL (seconds) for connection stats stats_connection_ttl=120 # whether to send individual connection stats stats_connection_send=true [runner] # services to start services=condure,pushpin-proxy,pushpin-handler # plain HTTP port to listen on for client connections http_port=7999 # list of HTTPS ports to listen on for client connections (you must have certs set) #https_ports=443 # list of unix socket paths to listen on for client connections #local_ports={rundir}/{ipc_prefix}server # directory to save log files logdir=log # logging level. 2 = info, >2 = verbose log_level=2 # client full request header must fit in this buffer client_buffer_size=8192 # maximum number of client connections client_maxconn=50000 # whether connections can use compression allow_compression=false # paths mongrel2_bin=mongrel2 m2sh_bin=m2sh zurl_bin=zurl [proxy] # routes config file (path relative to location of this file) routesfile=routes # enable debug mode to get informative error responses debug=false # whether to use automatic CORS and JSON-P wrapping auto_cross_origin=false # whether to accept x-forwarded-proto accept_x_forwarded_protocol=false # whether to assert x-forwarded-proto set_x_forwarded_protocol=proto-only # how to treat x-forwarded-for. example: "truncate:0,append" x_forwarded_for= # how to treat x-forwarded-for if grip-signed x_forwarded_for_trusted= # the following headers must be marked in order to qualify as orig orig_headers_need_mark= # whether to accept Pushpin-Route header accept_pushpin_route=false # value to append to the CDN-Loop header cdn_loop= # include client IP address in logs log_from=false # include client user agent in logs log_user_agent=false # for signing proxied requests sig_iss=pushpin # for signing proxied requests. use "base64:" prefix for binary key sig_key=changeme # use this to allow grip to be forwarded upstream (e.g. to fanout.io) upstream_key= # for the sockjs iframe transport sockjs_url=http://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js # updates check has three modes: # report: check for new pushpin version and report anonymous usage info to # the pushpin developers # check: check for new pushpin version only, don't report anything # off: don't do any reporting or checking # pushpin will output a log message when a new version is available. report # mode helps the pushpin project build credibility, so please enable it if you # enjoy this software :) updates_check=report # use this field to identify your organization in updates requests. if left # blank, updates requests will be anonymous organization_name= [handler] # ipc permissions (octal) #ipc_file_mode=777 # bind PULL for receiving publish commands push_in_spec=tcp://127.0.0.1:5560 # list of bind SUB for receiving published messages push_in_sub_specs=tcp://127.0.0.1:5562 # whether the above SUB socket should connect instead of bind push_in_sub_connect=false # addr/port to listen on for receiving publish commands via HTTP push_in_http_addr=127.0.0.1 push_in_http_port=5561 # maximum headers and body size in bytes when receiving publish commands via HTTP push_in_http_max_headers_size=10000 push_in_http_max_body_size=1000000 # bind PUB for sending stats (metrics, subscription info, etc) stats_spec=ipc://{rundir}/{ipc_prefix}stats # bind REP for responding to commands command_spec=tcp://127.0.0.1:5563 # max messages per second message_rate=2500 # max rate-limited messages message_hwm=25000 # set to report blocks counts in stats (content size / block size) #message_block_size= # max time (milliseconds) for out-of-order messages to wait message_wait=5000 # time (seconds) to cache message ids id_cache_ttl=60 # max subscriptions per connection connection_subscription_max=20 # time (seconds) to linger response mode subscriptions subscription_linger=60 # TTL (seconds) for subscription stats stats_subscription_ttl=60 # interval (seconds) to send report stats stats_report_interval=10 # stats output format stats_format=tnetstring pushpin-1.39.1/examples/config/routes000066400000000000000000000000071457610542000176230ustar00rootroot00000000000000* test pushpin-1.39.1/examples/config/runner/000077500000000000000000000000001457610542000176735ustar00rootroot00000000000000pushpin-1.39.1/examples/config/runner/certs/000077500000000000000000000000001457610542000210135ustar00rootroot00000000000000pushpin-1.39.1/examples/config/runner/certs/README000066400000000000000000000000211457610542000216640ustar00rootroot00000000000000Empty directory. pushpin-1.39.1/header.APACHE2000066400000000000000000000010611457610542000154520ustar00rootroot00000000000000 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * pushpin-1.39.1/package.sh000077500000000000000000000011671457610542000152360ustar00rootroot00000000000000#!/bin/sh set -e if [ $# -lt 1 ]; then echo "usage: $0 [version]" exit 1 fi VERSION=$1 DESTDIR=build/pushpin-$VERSION mkdir -p $DESTDIR cp -a .gitignore benches build.rs Cargo.lock Cargo.toml CHANGELOG.md examples LICENSE Makefile postbuild SECURITY.md README.md src tools $DESTDIR sed -i.orig -e "s/^version = .*/version = \"$VERSION\"/g" $DESTDIR/Cargo.toml rm $DESTDIR/Cargo.toml.orig cd $DESTDIR mkdir -p .cargo cat >.cargo/config.toml < pushpin.conf.inst pushpin_conf_inst.depends = ../examples/config/pushpin.conf conf.pri QMAKE_EXTRA_TARGETS += pushpin_conf_inst PRE_TARGETDEPS += pushpin.conf.inst # install bin files unix:!isEmpty(BINDIR) { binfiles.path = $$BINDIR binfiles.files = \ $$bin_dir/pushpin-condure \ $$bin_dir/m2adapter \ $$bin_dir/pushpin-proxy \ $$bin_dir/pushpin-handler \ $$root_dir/pushpin \ $$bin_dir/pushpin-publish binfiles.CONFIG += no_check_exist executable INSTALLS += binfiles } # install lib files libfiles.path = $$LIBDIR libfiles.files = $$PWD/../src/internal.conf runnerlibfiles.path = $$LIBDIR/runner runnerlibfiles.files = $$PWD/../src/runner/*.template # install config files runnerconfigfiles.path = $$CONFIGDIR/runner runnerconfigfiles.files = $$PWD/../examples/config/runner/certs routes.path = $$CONFIGDIR routes.extra = test -e $(INSTALL_ROOT)$$routes.path/routes || cp -f ../examples/config/routes $(INSTALL_ROOT)$$routes.path/routes pushpinconf.path = $$CONFIGDIR pushpinconf.extra = test -e $(INSTALL_ROOT)$$pushpinconf.path/pushpin.conf || cp -f pushpin.conf.inst $(INSTALL_ROOT)$$pushpinconf.path/pushpin.conf INSTALLS += libfiles runnerlibfiles runnerconfigfiles routes pushpinconf pushpin-1.39.1/src/000077500000000000000000000000001457610542000140665ustar00rootroot00000000000000pushpin-1.39.1/src/arena.rs000066400000000000000000000411761457610542000155330ustar00rootroot00000000000000/* * Copyright (C) 2020-2023 Fanout, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use slab::Slab; use std::cell::{RefCell, RefMut}; use std::mem; use std::ops::{Deref, DerefMut}; use std::sync::{Mutex, MutexGuard}; pub struct EntryGuard<'a, T> { entries: RefMut<'a, Slab>, entry: &'a mut T, key: usize, } impl EntryGuard<'_, T> { fn remove(mut self) { self.entries.remove(self.key); } } impl Deref for EntryGuard<'_, T> { type Target = T; fn deref(&self) -> &Self::Target { self.entry } } impl DerefMut for EntryGuard<'_, T> { fn deref_mut(&mut self) -> &mut Self::Target { self.entry } } // this is essentially a sharable slab for use within a single thread. // operations are protected by a RefCell. when an element is retrieved for // reading or modification, it is wrapped in a EntryGuard which keeps the // entire slab borrowed until the caller is done working with the element pub struct Memory { entries: RefCell>, } impl Memory { pub fn new(capacity: usize) -> Self { // allocate the slab with fixed capacity let s = Slab::with_capacity(capacity); Self { entries: RefCell::new(s), } } #[cfg(test)] pub fn len(&self) -> usize { let entries = self.entries.borrow(); entries.len() } fn insert(&self, e: T) -> Result { let mut entries = self.entries.borrow_mut(); // out of capacity. by preventing inserts beyond the capacity, we // ensure the underlying memory won't get moved due to a realloc if entries.len() == entries.capacity() { return Err(()); } Ok(entries.insert(e)) } fn get<'a>(&'a self, key: usize) -> Option> { let mut entries = self.entries.borrow_mut(); let entry = entries.get_mut(key)?; // slab element addresses are guaranteed to be stable once created, // and the only place we remove the element is in EntryGuard's // remove method which consumes itself, therefore it is safe to // assume the element will live at least as long as the EntryGuard // and we can extend the lifetime of the reference beyond the // RefMut let entry = unsafe { mem::transmute::<&mut T, &'a mut T>(entry) }; Some(EntryGuard { entries, entry, key, }) } // for tests, as a way to confirm the memory isn't moving. be careful // with this. the very first element inserted will be at index 0, but // if the slab has been used and cleared, then the next element // inserted may not be at index 0 and calling this method afterward // will panic #[cfg(test)] fn entry0_ptr(&self) -> *const T { let entries = self.entries.borrow(); entries.get(0).unwrap() as *const T } } pub struct SyncEntryGuard<'a, T> { entries: MutexGuard<'a, Slab>, entry: &'a mut T, key: usize, } impl SyncEntryGuard<'_, T> { fn remove(mut self) { self.entries.remove(self.key); } } impl Deref for SyncEntryGuard<'_, T> { type Target = T; fn deref(&self) -> &Self::Target { self.entry } } impl DerefMut for SyncEntryGuard<'_, T> { fn deref_mut(&mut self) -> &mut Self::Target { self.entry } } // this is essentially a thread-safe slab. operations are protected by a // mutex. when an element is retrieved for reading or modification, it is // wrapped in a EntryGuard which keeps the entire slab locked until the // caller is done working with the element pub struct SyncMemory { entries: Mutex>, } impl SyncMemory { pub fn new(capacity: usize) -> Self { // allocate the slab with fixed capacity let s = Slab::with_capacity(capacity); Self { entries: Mutex::new(s), } } #[cfg(test)] pub fn len(&self) -> usize { let entries = self.entries.lock().unwrap(); entries.len() } fn insert(&self, e: T) -> Result { let mut entries = self.entries.lock().unwrap(); // out of capacity. by preventing inserts beyond the capacity, we // ensure the underlying memory won't get moved due to a realloc if entries.len() == entries.capacity() { return Err(()); } Ok(entries.insert(e)) } fn get<'a>(&'a self, key: usize) -> Option> { let mut entries = self.entries.lock().unwrap(); let entry = entries.get_mut(key)?; // slab element addresses are guaranteed to be stable once created, // and the only place we remove the element is in SyncEntryGuard's // remove method which consumes itself, therefore it is safe to // assume the element will live at least as long as the SyncEntryGuard // and we can extend the lifetime of the reference beyond the // MutexGuard let entry = unsafe { mem::transmute::<&mut T, &'a mut T>(entry) }; Some(SyncEntryGuard { entries, entry, key, }) } // for tests, as a way to confirm the memory isn't moving. be careful // with this. the very first element inserted will be at index 0, but // if the slab has been used and cleared, then the next element // inserted may not be at index 0 and calling this method afterward // will panic #[cfg(test)] fn entry0_ptr(&self) -> *const T { let entries = self.entries.lock().unwrap(); entries.get(0).unwrap() as *const T } } pub struct ReusableValue { reusable: std::sync::Arc>, value: *mut T, key: usize, } impl ReusableValue { // vec element addresses are guaranteed to be stable once created, // and elements are only removed when the Reusable is dropped, and // the Arc'd Reusable is guaranteed to live as long as // ReusableValue, therefore it is safe to assume the element will // live at least as long as the ReusableValue fn get(&self) -> &T { unsafe { &*self.value } } fn get_mut(&mut self) -> &mut T { unsafe { &mut *self.value } } } impl Drop for ReusableValue { fn drop(&mut self) { let mut entries = self.reusable.entries.lock().unwrap(); entries.0.remove(self.key); } } impl Deref for ReusableValue { type Target = T; fn deref(&self) -> &Self::Target { self.get() } } impl DerefMut for ReusableValue { fn deref_mut(&mut self) -> &mut Self::Target { self.get_mut() } } // like Memory, but for preinitializing each value and reusing pub struct Reusable { entries: Mutex<(Slab<()>, Vec)>, } impl Reusable { pub fn new(capacity: usize, init_fn: F) -> Self where F: Fn() -> T, { let mut values = Vec::with_capacity(capacity); for _ in 0..capacity { values.push(init_fn()); } // allocate the slab with fixed capacity let s = Slab::with_capacity(capacity); Self { entries: Mutex::new((s, values)), } } #[cfg(test)] pub fn len(&self) -> usize { let entries = self.entries.lock().unwrap(); entries.0.len() } #[allow(clippy::result_unit_err)] pub fn reserve(self: &std::sync::Arc) -> Result, ()> { let mut entries = self.entries.lock().unwrap(); // out of capacity. the number of buffers is fixed if entries.0.len() == entries.0.capacity() { return Err(()); } let key = entries.0.insert(()); let value = &mut entries.1[key] as *mut T; Ok(ReusableValue { reusable: self.clone(), value, key, }) } } pub struct RcEntry { value: T, refs: usize, } pub type RcMemory = Memory>; pub struct Rc { memory: std::rc::Rc>, key: usize, } impl Rc { #[allow(clippy::result_unit_err)] pub fn new(v: T, memory: &std::rc::Rc>) -> Result { let key = memory.insert(RcEntry { value: v, refs: 1 })?; Ok(Self { memory: std::rc::Rc::clone(memory), key, }) } #[allow(clippy::should_implement_trait)] pub fn clone(rc: &Rc) -> Self { let mut e = rc.memory.get(rc.key).unwrap(); e.refs += 1; Self { memory: rc.memory.clone(), key: rc.key, } } pub fn get<'a>(&'a self) -> &'a T { let e = self.memory.get(self.key).unwrap(); // get a reference to the inner value let value = &e.value; // entry addresses are guaranteed to be stable once created, and the // entry managed by this Rc won't be dropped until this Rc drops, // therefore it is safe to assume the entry managed by this Rc will // live at least as long as this Rc, and we can extend the lifetime // of the reference beyond the EntryGuard unsafe { mem::transmute::<&T, &'a T>(value) } } } impl Drop for Rc { fn drop(&mut self) { let mut e = self.memory.get(self.key).unwrap(); if e.refs == 1 { e.remove(); return; } e.refs -= 1; } } pub type ArcMemory = SyncMemory>; pub struct Arc { memory: std::sync::Arc>, key: usize, } impl Arc { #[allow(clippy::result_unit_err)] pub fn new(v: T, memory: &std::sync::Arc>) -> Result { let key = memory.insert(RcEntry { value: v, refs: 1 })?; Ok(Self { memory: memory.clone(), key, }) } #[allow(clippy::should_implement_trait)] pub fn clone(rc: &Arc) -> Self { let mut e = rc.memory.get(rc.key).unwrap(); e.refs += 1; Self { memory: rc.memory.clone(), key: rc.key, } } pub fn get<'a>(&'a self) -> &'a T { let e = self.memory.get(self.key).unwrap(); // get a reference to the inner value let value = &e.value; // entry addresses are guaranteed to be stable once created, and the // entry managed by this Arc won't be dropped until this Arc drops, // therefore it is safe to assume the entry managed by this Arc will // live at least as long as this Arc, and we can extend the lifetime // of the reference beyond the SyncEntryGuard unsafe { mem::transmute::<&T, &'a T>(value) } } } impl Drop for Arc { fn drop(&mut self) { let mut e = self.memory.get(self.key).unwrap(); if e.refs == 1 { e.remove(); return; } e.refs -= 1; } } // adapted from https://github.com/rust-lang/rfcs/pull/2802 pub fn recycle_vec(mut v: Vec) -> Vec { assert_eq!(core::mem::size_of::(), core::mem::size_of::()); assert_eq!(core::mem::align_of::(), core::mem::align_of::()); v.clear(); let ptr = v.as_mut_ptr(); let capacity = v.capacity(); mem::forget(v); let ptr = ptr as *mut U; unsafe { Vec::from_raw_parts(ptr, 0, capacity) } } // ReusableVec inspired by recycle_vec pub struct ReusableVecHandle<'a, T> { vec: &'a mut Vec, } impl ReusableVecHandle<'_, T> { pub fn get_ref(&self) -> &Vec { self.vec } pub fn get_mut(&mut self) -> &mut Vec { self.vec } } impl Drop for ReusableVecHandle<'_, T> { fn drop(&mut self) { self.vec.clear(); } } impl Deref for ReusableVecHandle<'_, T> { type Target = Vec; fn deref(&self) -> &Self::Target { self.get_ref() } } impl DerefMut for ReusableVecHandle<'_, T> { fn deref_mut(&mut self) -> &mut Self::Target { self.get_mut() } } pub struct ReusableVec { vec: Vec, size: usize, align: usize, } impl ReusableVec { pub fn new(capacity: usize) -> Self { let size = mem::size_of::(); let align = mem::align_of::(); let vec: Vec = Vec::with_capacity(capacity); // safety: we must cast to Vec before using, where U has the same // size and alignment as T let vec: Vec = unsafe { mem::transmute(vec) }; Self { vec, size, align } } pub fn get_as_new(&mut self) -> ReusableVecHandle<'_, U> { let size = mem::size_of::(); let align = mem::align_of::(); // if these don't match, panic. it's up the user to ensure the type // is acceptable assert_eq!(self.size, size); assert_eq!(self.align, align); let vec: &mut Vec = &mut self.vec; // safety: U has the expected size and alignment let vec: &mut Vec = unsafe { mem::transmute(vec) }; // the vec starts empty, and is always cleared when the handle drops. // get_as_new() borrows self mutably, so it's not possible to create // a handle when one already exists assert!(vec.is_empty()); ReusableVecHandle { vec } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_reusable() { let reusable = std::sync::Arc::new(Reusable::new(2, || vec![0; 128])); assert_eq!(reusable.len(), 0); let mut buf1 = reusable.reserve().unwrap(); assert_eq!(reusable.len(), 1); let mut buf2 = reusable.reserve().unwrap(); assert_eq!(reusable.len(), 2); // no room assert!(reusable.reserve().is_err()); buf1[..5].copy_from_slice(b"hello"); buf2[..5].copy_from_slice(b"world"); assert_eq!(&buf1[..5], b"hello"); assert_eq!(&buf2[..5], b"world"); mem::drop(buf1); assert_eq!(reusable.len(), 1); mem::drop(buf2); assert_eq!(reusable.len(), 0); } #[test] fn test_rc() { let memory = std::rc::Rc::new(RcMemory::new(2)); assert_eq!(memory.len(), 0); let e0a = Rc::new(123 as i32, &memory).unwrap(); assert_eq!(memory.len(), 1); let p = memory.entry0_ptr(); let e0b = Rc::clone(&e0a); assert_eq!(memory.len(), 1); assert_eq!(memory.entry0_ptr(), p); let e1a = Rc::new(456 as i32, &memory).unwrap(); assert_eq!(memory.len(), 2); assert_eq!(memory.entry0_ptr(), p); // no room assert!(Rc::new(789 as i32, &memory).is_err()); assert_eq!(*e0a.get(), 123); assert_eq!(*e0b.get(), 123); assert_eq!(*e1a.get(), 456); mem::drop(e0b); assert_eq!(memory.len(), 2); assert_eq!(memory.entry0_ptr(), p); mem::drop(e0a); assert_eq!(memory.len(), 1); mem::drop(e1a); assert_eq!(memory.len(), 0); } #[test] fn test_arc() { let memory = std::sync::Arc::new(ArcMemory::new(2)); assert_eq!(memory.len(), 0); let e0a = Arc::new(123 as i32, &memory).unwrap(); assert_eq!(memory.len(), 1); let p = memory.entry0_ptr(); let e0b = Arc::clone(&e0a); assert_eq!(memory.len(), 1); assert_eq!(memory.entry0_ptr(), p); let e1a = Arc::new(456 as i32, &memory).unwrap(); assert_eq!(memory.len(), 2); assert_eq!(memory.entry0_ptr(), p); // no room assert!(Arc::new(789 as i32, &memory).is_err()); assert_eq!(*e0a.get(), 123); assert_eq!(*e0b.get(), 123); assert_eq!(*e1a.get(), 456); mem::drop(e0b); assert_eq!(memory.len(), 2); assert_eq!(memory.entry0_ptr(), p); mem::drop(e0a); assert_eq!(memory.len(), 1); mem::drop(e1a); assert_eq!(memory.len(), 0); } #[test] fn test_reusable_vec() { let mut vec_mem = ReusableVec::new::(100); let mut vec = vec_mem.get_as_new::(); assert_eq!(vec.capacity(), 100); assert_eq!(vec.len(), 0); vec.push(1); assert_eq!(vec.len(), 1); mem::drop(vec); let vec = vec_mem.get_as_new::(); assert_eq!(vec.capacity(), 100); assert_eq!(vec.len(), 0); } } pushpin-1.39.1/src/bin/000077500000000000000000000000001457610542000146365ustar00rootroot00000000000000pushpin-1.39.1/src/bin/m2adapter.rs000066400000000000000000000015751457610542000170730ustar00rootroot00000000000000/* * Copyright (C) 2023 Fastly, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use pushpin::{call_c_main, import_cpp}; use std::env; use std::process::ExitCode; import_cpp! { fn m2adapter_main(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; } fn main() -> ExitCode { unsafe { ExitCode::from(call_c_main(m2adapter_main, env::args_os())) } } pushpin-1.39.1/src/bin/pushpin-condure.rs000066400000000000000000000451241457610542000203350ustar00rootroot00000000000000/* * Copyright (C) 2020-2023 Fanout, Inc. * Copyright (C) 2023 Fastly, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use clap::{Arg, ArgAction, Command}; use log::{error, LevelFilter}; use pushpin::condure::{run, App, Config}; use pushpin::log::{get_simple_logger, local_offset_check}; use pushpin::version; use pushpin::{ListenConfig, ListenSpec}; use std::error::Error; use std::path::PathBuf; use std::process; use std::time::Duration; // safety values const WORKERS_MAX: usize = 1024; const CONNS_MAX: usize = 10_000_000; const PRIVATE_SUBNETS: &[&str] = &[ "127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "::1/128", "fc00::/7", "fe80::/10", ]; struct Args { id: String, workers: usize, req_maxconn: usize, stream_maxconn: usize, buffer_size: usize, body_buffer_size: usize, blocks_max: usize, connection_blocks_max: usize, messages_max: usize, req_timeout: usize, stream_timeout: usize, listen: Vec, zclient_req_specs: Vec, zclient_stream_specs: Vec, zclient_connect: bool, zserver_req_specs: Vec, zserver_stream_specs: Vec, zserver_connect: bool, ipc_file_mode: u32, tls_identities_dir: String, allow_compression: bool, deny_out_internal: bool, } fn process_args_and_run(args: Args) -> Result<(), Box> { if args.id.is_empty() || args.id.contains(' ') { return Err("failed to parse id: value cannot be empty or contain a space".into()); } if args.workers > WORKERS_MAX { return Err("failed to parse workers: value too large".into()); } if args.req_maxconn + args.stream_maxconn > CONNS_MAX { return Err("total maxconn is too large".into()); } if args.blocks_max < args.stream_maxconn * 2 { return Err("blocks-max is too small".into()); } if args.connection_blocks_max < 2 { return Err("connection-blocks-max is too small".into()); } let mut config = Config { instance_id: args.id, workers: args.workers, req_maxconn: args.req_maxconn, stream_maxconn: args.stream_maxconn, buffer_size: args.buffer_size, body_buffer_size: args.body_buffer_size, blocks_max: args.blocks_max, connection_blocks_max: args.connection_blocks_max, messages_max: args.messages_max, req_timeout: Duration::from_secs(args.req_timeout as u64), stream_timeout: Duration::from_secs(args.stream_timeout as u64), listen: Vec::new(), zclient_req: args.zclient_req_specs, zclient_stream: args.zclient_stream_specs, zclient_connect: args.zclient_connect, zserver_req: args.zserver_req_specs, zserver_stream: args.zserver_stream_specs, zserver_connect: args.zserver_connect, ipc_file_mode: args.ipc_file_mode, certs_dir: PathBuf::from(args.tls_identities_dir), allow_compression: args.allow_compression, deny: Vec::new(), }; for v in args.listen.iter() { let mut parts = v.split(','); // there's always a first part let part1 = parts.next().unwrap(); let mut stream = true; let mut tls = false; let mut default_cert = None; let mut local = false; let mut mode = None; let mut user = None; let mut group = None; for part in parts { let (k, v) = match part.find('=') { Some(pos) => (&part[..pos], &part[(pos + 1)..]), None => (part, ""), }; match k { "req" => stream = false, "stream" => stream = true, "tls" => tls = true, "default-cert" => default_cert = Some(String::from(v)), "local" => local = true, "mode" => match u32::from_str_radix(v, 8) { Ok(x) => mode = Some(x), Err(e) => return Err(format!("failed to parse mode: {}", e).into()), }, "user" => user = Some(String::from(v)), "group" => group = Some(String::from(v)), _ => return Err(format!("failed to parse listen: invalid param: {}", part).into()), } } let spec = if local { ListenSpec::Local { path: PathBuf::from(part1), mode, user, group, } } else { let port_pos = match part1.rfind(':') { Some(pos) => pos + 1, None => 0, }; let port = &part1[port_pos..]; if port.parse::().is_err() { return Err(format!("failed to parse listen: invalid port {}", port).into()); } let addr = if port_pos > 0 { String::from(part1) } else { format!("0.0.0.0:{}", part1) }; let addr = match addr.parse() { Ok(addr) => addr, Err(e) => { return Err(format!("failed to parse listen: {}", e).into()); } }; ListenSpec::Tcp { addr, tls, default_cert, } }; config.listen.push(ListenConfig { spec, stream }); } if args.deny_out_internal { for s in PRIVATE_SUBNETS.iter() { config.deny.push(s.parse().unwrap()); } } run(&config) } fn main() { let matches = Command::new("pushpin-condure") .version(version()) .about("HTTP/WebSocket connection manager") .arg( Arg::new("log-level") .long("log-level") .num_args(1) .value_name("N") .help("Log level") .default_value("2"), ) .arg( Arg::new("id") .long("id") .num_args(1) .value_name("ID") .help("Instance ID") .default_value("condure"), ) .arg( Arg::new("workers") .long("workers") .num_args(1) .value_name("N") .help("Number of worker threads") .default_value("2"), ) .arg( Arg::new("req-maxconn") .long("req-maxconn") .num_args(1) .value_name("N") .help("Maximum number of concurrent connections in req mode") .default_value("100"), ) .arg( Arg::new("stream-maxconn") .long("stream-maxconn") .num_args(1) .value_name("N") .help("Maximum number of concurrent connections in stream mode") .default_value("10000"), ) .arg( Arg::new("buffer-size") .long("buffer-size") .num_args(1) .value_name("N") .help("Connection buffer size (two buffers per connection)") .default_value("8192"), ) .arg( Arg::new("body-buffer-size") .long("body-buffer-size") .num_args(1) .value_name("N") .help("Body buffer size for connections in req mode") .default_value("100000"), ) .arg( Arg::new("blocks-max") .long("blocks-max") .num_args(1) .value_name("N") .help("Maximum number of buffer blocks in stream mode (minimum 2*maxconn)"), ) .arg( Arg::new("connection-blocks-max") .long("connection-blocks-max") .num_args(1) .value_name("N") .help("Maximum number of buffer blocks per connection in stream mode (minimum 2)") .default_value("2"), ) .arg( Arg::new("messages-max") .long("messages-max") .num_args(1) .value_name("N") .help("Maximum number of queued WebSocket messages per connection") .default_value("100"), ) .arg( Arg::new("req-timeout") .long("req-timeout") .num_args(1) .value_name("N") .help("Connection timeout in req mode (seconds)") .default_value("30"), ) .arg( Arg::new("stream-timeout") .long("stream-timeout") .num_args(1) .value_name("N") .help("Connection timeout in stream mode (seconds)") .default_value("1800"), ) .arg( Arg::new("listen") .long("listen") .num_args(1) .value_name("[addr:]port[,params...]") .action(ArgAction::Append) .help("Port to listen on"), ) .arg( Arg::new("zclient-req") .long("zclient-req") .num_args(1) .value_name("spec") .action(ArgAction::Append) .help("ZeroMQ client REQ spec") .default_value("ipc://client"), ) .arg( Arg::new("zclient-stream") .long("zclient-stream") .num_args(1) .value_name("spec-base") .action(ArgAction::Append) .help("ZeroMQ client PUSH/ROUTER/SUB spec base") .default_value("ipc://client"), ) .arg( Arg::new("zclient-connect") .long("zclient-connect") .action(ArgAction::SetTrue) .help("ZeroMQ client sockets should connect instead of bind"), ) .arg( Arg::new("zserver-req") .long("zserver-req") .num_args(1) .value_name("spec") .action(ArgAction::Append) .help("ZeroMQ server REQ spec"), ) .arg( Arg::new("zserver-stream") .long("zserver-stream") .num_args(1) .value_name("spec-base") .action(ArgAction::Append) .help("ZeroMQ server PULL/ROUTER/PUB spec base"), ) .arg( Arg::new("zserver-connect") .long("zserver-connect") .action(ArgAction::SetTrue) .help("ZeroMQ server sockets should connect instead of bind"), ) .arg( Arg::new("ipc-file-mode") .long("ipc-file-mode") .num_args(1) .value_name("octal") .help("Permissions for ZeroMQ IPC binds"), ) .arg( Arg::new("tls-identities-dir") .long("tls-identities-dir") .num_args(1) .value_name("directory") .help("Directory containing certificates and private keys") .default_value("."), ) .arg( Arg::new("compression") .long("compression") .action(ArgAction::SetTrue) .help("Allow compression to be used"), ) .arg( Arg::new("deny-out-internal") .long("deny-out-internal") .action(ArgAction::SetTrue) .help("Block outbound connections to local/internal IP address ranges"), ) .arg( Arg::new("sizes") .long("sizes") .action(ArgAction::SetTrue) .help("Prints sizes of tasks and other objects"), ) .get_matches(); log::set_logger(get_simple_logger()).unwrap(); log::set_max_level(LevelFilter::Info); let level = matches.get_one::("log-level").unwrap(); let level: usize = match level.parse() { Ok(x) => x, Err(e) => { error!("failed to parse log-level: {}", e); process::exit(1); } }; let level = match level { 0 => LevelFilter::Error, 1 => LevelFilter::Warn, 2 => LevelFilter::Info, 3 => LevelFilter::Debug, 4..=core::usize::MAX => LevelFilter::Trace, _ => unreachable!(), }; log::set_max_level(level); local_offset_check(); if *matches.get_one("sizes").unwrap() { for (name, size) in App::sizes() { println!("{}: {} bytes", name, size); } process::exit(0); } let id = matches.get_one::("id").unwrap(); let workers = matches.get_one::("workers").unwrap(); let workers: usize = match workers.parse() { Ok(x) => x, Err(e) => { error!("failed to parse workers: {}", e); process::exit(1); } }; let req_maxconn = matches.get_one::("req-maxconn").unwrap(); let req_maxconn: usize = match req_maxconn.parse() { Ok(x) => x, Err(e) => { error!("failed to parse req-maxconn: {}", e); process::exit(1); } }; let stream_maxconn = matches.get_one::("stream-maxconn").unwrap(); let stream_maxconn: usize = match stream_maxconn.parse() { Ok(x) => x, Err(e) => { error!("failed to parse stream-maxconn: {}", e); process::exit(1); } }; let buffer_size = matches.get_one::("buffer-size").unwrap(); let buffer_size: usize = match buffer_size.parse() { Ok(x) => x, Err(e) => { error!("failed to parse buffer-size: {}", e); process::exit(1); } }; let body_buffer_size = matches.get_one::("body-buffer-size").unwrap(); let body_buffer_size: usize = match body_buffer_size.parse() { Ok(x) => x, Err(e) => { error!("failed to parse body-buffer-size: {}", e); process::exit(1); } }; let blocks_max: usize = match matches.get_one::("blocks-max") { Some(v) => match v.parse() { Ok(x) => x, Err(e) => { error!("failed to parse blocks-max: {}", e); process::exit(1); } }, None => stream_maxconn * 2, }; let connection_blocks_max = matches.get_one::("connection-blocks-max").unwrap(); let connection_blocks_max: usize = match connection_blocks_max.parse() { Ok(x) => x, Err(e) => { error!("failed to parse connection-blocks-max: {}", e); process::exit(1); } }; let messages_max = matches.get_one::("messages-max").unwrap(); let messages_max: usize = match messages_max.parse() { Ok(x) => x, Err(e) => { error!("failed to parse messages-max: {}", e); process::exit(1); } }; let req_timeout = matches.get_one::("req-timeout").unwrap(); let req_timeout: usize = match req_timeout.parse() { Ok(x) => x, Err(e) => { error!("failed to parse req-timeout: {}", e); process::exit(1); } }; let stream_timeout = matches.get_one::("stream-timeout").unwrap(); let stream_timeout: usize = match stream_timeout.parse() { Ok(x) => x, Err(e) => { error!("failed to parse stream-timeout: {}", e); process::exit(1); } }; let mut listen: Vec = matches .get_many::("listen") .unwrap_or_default() .map(|v| v.to_owned()) .collect(); let zclient_req_specs: Vec = matches .get_many::("zclient-req") .unwrap() .map(|v| v.to_owned()) .collect(); let zclient_stream_specs: Vec = matches .get_many::("zclient-stream") .unwrap() .map(|v| v.to_owned()) .collect(); let zclient_connect = *matches.get_one("zclient-connect").unwrap(); let zserver_req_specs: Vec = matches .get_many::("zserver-req") .unwrap_or_default() .map(|v| v.to_owned()) .collect(); let zserver_stream_specs: Vec = matches .get_many::("zserver-stream") .unwrap_or_default() .map(|v| v.to_owned()) .collect(); let zserver_connect = *matches.get_one("zserver-connect").unwrap(); let ipc_file_mode = matches .get_one::("ipc-file-mode") .cloned() .unwrap_or_else(|| String::from("0")); let ipc_file_mode = match u32::from_str_radix(&ipc_file_mode, 8) { Ok(x) => x, Err(e) => { error!("failed to parse ipc-file-mode: {}", e); process::exit(1); } }; let tls_identities_dir = matches.get_one::("tls-identities-dir").unwrap(); let allow_compression = *matches.get_one("compression").unwrap(); let deny_out_internal = *matches.get_one("deny-out-internal").unwrap(); // if no zmq server specs are set (needed by client mode), specify // default listen configuration in order to enable server mode. this // means if zmq server specs are set, then server mode won't be enabled // by default if listen.is_empty() && zserver_req_specs.is_empty() && zserver_stream_specs.is_empty() { listen.push("0.0.0.0:8000,stream".to_string()); } let args = Args { id: id.to_string(), workers, req_maxconn, stream_maxconn, buffer_size, body_buffer_size, blocks_max, connection_blocks_max, messages_max, req_timeout, stream_timeout, listen, zclient_req_specs, zclient_stream_specs, zclient_connect, zserver_req_specs, zserver_stream_specs, zserver_connect, ipc_file_mode, tls_identities_dir: tls_identities_dir.to_string(), allow_compression, deny_out_internal, }; if let Err(e) = process_args_and_run(args) { error!("{}", e); process::exit(1); } } pushpin-1.39.1/src/bin/pushpin-handler.rs000066400000000000000000000015711457610542000203110ustar00rootroot00000000000000/* * Copyright (C) 2023 Fastly, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use pushpin::{call_c_main, import_cpp}; use std::env; use std::process::ExitCode; import_cpp! { fn handler_main(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; } fn main() -> ExitCode { unsafe { ExitCode::from(call_c_main(handler_main, env::args_os())) } } pushpin-1.39.1/src/bin/pushpin-proxy.rs000066400000000000000000000015651457610542000200600ustar00rootroot00000000000000/* * Copyright (C) 2023 Fastly, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use pushpin::{call_c_main, import_cpp}; use std::env; use std::process::ExitCode; import_cpp! { fn proxy_main(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; } fn main() -> ExitCode { unsafe { ExitCode::from(call_c_main(proxy_main, env::args_os())) } } pushpin-1.39.1/src/bin/pushpin-publish.rs000066400000000000000000000204351457610542000203420ustar00rootroot00000000000000/* * Copyright (C) 2021-2023 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ use clap::{Arg, ArgAction, Command}; use pushpin::publish_cli::{run, Action, Config, Content, Message}; use pushpin::version; use std::env; use std::error::Error; use std::process; const PROGRAM_NAME: &str = "pushpin-publish"; const DEFAULT_SPEC: &str = "http://localhost:5561"; struct Args { channel: String, content: Option, id: String, prev_id: String, sender: String, code: u16, headers: Vec, meta: Vec, hint: bool, close: bool, patch: bool, no_seq: bool, no_eol: bool, spec: String, user: Option, } fn process_args_and_run(args: Args) -> Result<(), Box> { let action = if args.hint { Action::Hint } else if args.close { Action::Close } else { if args.code > 999 { return Err("code must be an integer between 0 and 999".into()); } let content = match args.content { Some(s) => s, None => return Err("must specify content".into()), }; let content = if args.patch { let v: serde_json::Value = serde_json::from_str(&content)?; let arr = match v { serde_json::Value::Array(arr) => arr, _ => return Err("patch content must be a JSON array".into()), }; Content::Patch(arr) } else { Content::Value(content) }; Action::Send(Message { code: args.code, content, }) }; let mut headers = Vec::new(); for v in args.headers { let pos = match v.find(':') { Some(pos) => pos, None => return Err("header must be in the form \"name: value\"".into()), }; let name = &v[..pos]; let val = &v[(pos + 1)..].trim(); headers.push((name.to_string(), val.to_string())); } let mut meta = Vec::new(); for v in args.meta { let pos = match v.find('=') { Some(pos) => pos, None => return Err("meta must be in the form \"name=value\"".into()), }; let name = &v[..pos]; let val = &v[(pos + 1)..].trim(); meta.push((name.to_string(), val.to_string())); } let config = Config { spec: args.spec, basic_auth: args.user, channel: args.channel, id: args.id, prev_id: args.prev_id, sender: args.sender, action, headers, meta, no_seq: args.no_seq, eol: !args.no_eol, }; run(&config) } fn main() { let default_spec = match env::var("GRIP_URL") { Ok(s) => s, Err(_) => DEFAULT_SPEC.to_string(), }; let matches = Command::new(PROGRAM_NAME) .version(version()) .about("Publish messages to Pushpin") .arg( Arg::new("channel") .required(true) .num_args(1) .value_name("channel") .help("Channel to send to"), ) .arg( Arg::new("content") .num_args(1) .value_name("content") .help("Content to use for HTTP body and WebSocket message"), ) .arg( Arg::new("id") .long("id") .num_args(1) .value_name("id") .help("Payload ID"), ) .arg( Arg::new("prev-id") .long("prev-id") .num_args(1) .value_name("id") .help("Previous payload ID"), ) .arg( Arg::new("sender") .long("sender") .num_args(1) .value_name("sender") .help("Sender meta value"), ) .arg( Arg::new("code") .long("code") .num_args(1) .value_name("code") .help("HTTP response code to use") .default_value("200"), ) .arg( Arg::new("header") .short('H') .long("header") .num_args(1) .value_name("\"K: V\"") .action(ArgAction::Append) .help("Add HTTP response header"), ) .arg( Arg::new("meta") .short('M') .long("meta") .num_args(1) .value_name("\"K=V\"") .action(ArgAction::Append) .help("Add meta variable"), ) .arg( Arg::new("hint") .long("hint") .action(ArgAction::SetTrue) .help("Send hint instead of content"), ) .arg( Arg::new("close") .long("close") .action(ArgAction::SetTrue) .help("Close streaming and WebSocket connections"), ) .arg( Arg::new("patch") .long("patch") .action(ArgAction::SetTrue) .help("Content is JSON patch"), ) .arg( Arg::new("no-seq") .long("no-seq") .action(ArgAction::SetTrue) .help("Bypass sequencing buffer"), ) .arg( Arg::new("no-eol") .long("no-eol") .action(ArgAction::SetTrue) .help("Don't add newline to HTTP payloads"), ) .arg( Arg::new("spec") .long("spec") .num_args(1) .value_name("spec") .help("GRIP URL or ZeroMQ PUSH spec") .default_value(default_spec), ) .arg( Arg::new("user") .short('u') .long("user") .num_args(1) .value_name("user:pass") .help("Authenticate using basic auth"), ) .get_matches(); let channel = matches.get_one::("channel").unwrap().clone(); let content = matches.get_one::("content").cloned(); let id = matches.get_one::("id").cloned().unwrap_or_default(); let prev_id = matches .get_one::("prev-id") .cloned() .unwrap_or_default(); let sender = matches .get_one::("sender") .cloned() .unwrap_or_default(); let code = matches.get_one::("code").unwrap(); let code: u16 = match code.parse() { Ok(x) => x, Err(e) => { eprintln!("Error: failed to parse code: {}", e); process::exit(1); } }; let headers = matches .get_many::("header") .unwrap_or_default() .map(|v| v.to_owned()) .collect(); let meta = matches .get_many::("meta") .unwrap_or_default() .map(|v| v.to_owned()) .collect(); let hint = *matches.get_one("hint").unwrap(); let close = *matches.get_one("close").unwrap(); let patch = *matches.get_one("patch").unwrap(); let no_seq = *matches.get_one("no-seq").unwrap(); let no_eol = *matches.get_one("no-eol").unwrap(); let spec = matches.get_one::("spec").unwrap().clone(); let user = matches.get_one::("user").cloned(); let args = Args { channel, content, id, prev_id, sender, code, headers, meta, hint, close, patch, no_seq, no_eol, spec, user, }; if let Err(e) = process_args_and_run(args) { eprintln!("Error: {}", e); process::exit(1); } } pushpin-1.39.1/src/bin/pushpin.rs000066400000000000000000000015671457610542000167030ustar00rootroot00000000000000/* * Copyright (C) 2023 Fastly, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use pushpin::{call_c_main, import_cpp}; use std::env; use std::process::ExitCode; import_cpp! { fn runner_main(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; } fn main() -> ExitCode { unsafe { ExitCode::from(call_c_main(runner_main, env::args_os())) } } pushpin-1.39.1/src/buffer.rs000066400000000000000000001007451457610542000157140ustar00rootroot00000000000000/* * Copyright (C) 2020-2023 Fanout, Inc. * Copyright (C) 2023 Fastly, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use crate::future::{AsyncWrite, AsyncWriteExt}; use std::cell::RefCell; use std::cmp; use std::io; use std::io::Write; use std::mem::{self, MaybeUninit}; use std::rc::Rc; use std::slice; #[cfg(test)] use std::io::Read; pub const VECTORED_MAX: usize = 8; pub fn trim_for_display(s: &str, max: usize) -> String { // NOTE: O(n) let char_len = s.chars().count(); if char_len > max && max >= 7 { let dist = max / 2; let mut left_end = 0; let mut right_start = 0; // NOTE: O(n) for (i, (pos, _)) in s.char_indices().enumerate() { // dist guaranteed to be < char_len if i == dist { left_end = pos; } // (char_len - dist + 3) guaranteed to be < char_len if i == char_len - dist + 3 { right_start = pos; } } let left = &s[..left_end]; let right = &s[right_start..]; format!("{}...{}", left, right) } else { s.to_owned() } } fn init_array<'a, T, const N: usize>(arr: &'a mut MaybeUninit<[T; N]>, src: &mut [T]) -> &'a mut [T] where T: Default, { // SAFETY: T and MaybeUninit have the same layout let arr: &mut [MaybeUninit; N] = unsafe { mem::transmute(arr) }; let len = cmp::min(arr.len(), src.len()); for (d, s) in arr.iter_mut().zip(src) { d.write(mem::take(s)); } // SAFETY: the slice will contain only initialized elements unsafe { slice::from_raw_parts_mut(arr[0].as_mut_ptr(), len) } } pub trait Buffer { fn len(&self) -> usize; fn remaining_capacity(&self) -> usize; fn read_buf(&self) -> &[u8]; fn read_buf_mut(&mut self) -> &mut [u8]; fn read_commit(&mut self, amount: usize); fn write_buf(&mut self) -> &mut [u8]; fn write_commit(&mut self, amount: usize); fn is_empty(&self) -> bool { self.len() == 0 } fn read_bufs<'data, 'bufs>( &'data self, bufs: &'bufs mut [&'data [u8]], ) -> &'bufs mut [&'data [u8]] { if !bufs.is_empty() { bufs[0] = self.read_buf(); &mut bufs[..1] } else { &mut [] } } fn read_bufs_mut<'data, 'bufs, const N: usize>( &'data mut self, bufs: &'bufs mut MaybeUninit<[&'data mut [u8]; N]>, ) -> &'bufs mut [&'data mut [u8]] { init_array(bufs, &mut [self.read_buf_mut()]) } } // for reading only impl Buffer for io::Cursor<&mut [u8]> { fn len(&self) -> usize { Buffer::read_buf(self).len() } fn remaining_capacity(&self) -> usize { 0 } fn read_buf(&self) -> &[u8] { let pos = self.position() as usize; &self.get_ref()[pos..] } fn read_buf_mut(&mut self) -> &mut [u8] { let pos = self.position() as usize; &mut self.get_mut()[pos..] } fn read_commit(&mut self, amount: usize) { let pos = self.position(); self.set_position(pos + (amount as u64)); } fn write_buf(&mut self) -> &mut [u8] { &mut [] } fn write_commit(&mut self, amount: usize) { assert_eq!(amount, 0); } } pub fn write_vectored_offset( writer: &mut W, bufs: &[&[u8]], offset: usize, ) -> Result { if bufs.is_empty() { return Ok(0); } let mut offset = offset; let mut start = 0; while offset >= bufs[start].len() { // on the last buf? if start + 1 >= bufs.len() { // exceeding the last buf is an error if offset > bufs[start].len() { return Err(io::Error::from(io::ErrorKind::InvalidInput)); } return Ok(0); } offset -= bufs[start].len(); start += 1; } let mut arr = [io::IoSlice::new(&b""[..]); VECTORED_MAX]; let mut arr_len = 0; for (index, &buf) in bufs.iter().enumerate().skip(start) { let buf = if index == start { &buf[offset..] } else { buf }; arr[arr_len] = io::IoSlice::new(buf); arr_len += 1; } writer.write_vectored(&arr[..arr_len]) } pub async fn write_vectored_offset_async( writer: &mut W, bufs: &[&[u8]], offset: usize, ) -> Result { if bufs.is_empty() { return Ok(0); } let mut offset = offset; let mut start = 0; while offset >= bufs[start].len() { // on the last buf? if start + 1 >= bufs.len() { // exceeding the last buf is an error if offset > bufs[start].len() { return Err(io::Error::from(io::ErrorKind::InvalidInput)); } return Ok(0); } offset -= bufs[start].len(); start += 1; } let mut arr = [io::IoSlice::new(&b""[..]); VECTORED_MAX]; let mut arr_len = 0; for (index, &buf) in bufs.iter().enumerate().skip(start) { let buf = if index == start { &buf[offset..] } else { buf }; arr[arr_len] = io::IoSlice::new(buf); arr_len += 1; } writer.write_vectored(&arr[..arr_len]).await } struct LimitBufsRestore { index: usize, ptr: T, len: usize, } pub struct LimitBufsGuard<'a, 'b> { bufs: &'b mut [&'a [u8]], start: usize, end: usize, restore: Option>, } impl<'a: 'b, 'b> LimitBufsGuard<'a, 'b> { pub fn as_slice(&self) -> &[&'a [u8]] { &self.bufs[self.start..self.end] } } impl<'a: 'b, 'b> Drop for LimitBufsGuard<'a, 'b> { fn drop(&mut self) { if let Some(restore) = self.restore.take() { // SAFETY: ptr and len were collected earlier from the original // memory referred to by the slice at this index and they are // still valid. the only issue with reconstructing the slice is // that we currently have a different slice using the same memory // at this index. however, this is safe because we also replace // the slice at this index and the two slices don't coexist unsafe { self.bufs[restore.index] = slice::from_raw_parts(restore.ptr, restore.len); } } } } pub struct LimitBufsMutGuard<'a, 'b> { bufs: &'b mut [&'a mut [u8]], start: usize, end: usize, restore: Option>, } impl<'a: 'b, 'b> LimitBufsMutGuard<'a, 'b> { pub fn as_slice(&mut self) -> &mut [&'a mut [u8]] { &mut self.bufs[self.start..self.end] } } impl<'a: 'b, 'b> Drop for LimitBufsMutGuard<'a, 'b> { fn drop(&mut self) { if let Some(restore) = self.restore.take() { // SAFETY: ptr and len were collected earlier from the original // memory referred to by the slice at this index and they are // still valid. the only issue with reconstructing the slice is // that we currently have a different slice using the same memory // at this index. however, this is safe because we also replace // the slice at this index and the two slices don't coexist unsafe { self.bufs[restore.index] = slice::from_raw_parts_mut(restore.ptr, restore.len); } } } } pub trait LimitBufs<'a, 'b> { fn limit(&'b mut self, size: usize) -> LimitBufsGuard<'a, 'b>; } impl<'a: 'b, 'b> LimitBufs<'a, 'b> for [&'a [u8]] { fn limit(&'b mut self, size: usize) -> LimitBufsGuard<'a, 'b> { let mut end = self.len(); let mut restore = None; let mut want = size; for (index, item) in self.iter_mut().enumerate() { let buf: &[u8] = item; let buf_len = buf.len(); if buf_len >= want { let len = buf.len(); let ptr = buf.as_ptr(); restore = Some(LimitBufsRestore { index, ptr, len }); // SAFETY: ptr and len were obtained above and are still // valid. we just need to be careful about using them again // later on from the restore field unsafe { *item = &slice::from_raw_parts(ptr, len)[..want]; } end = index + 1; break; } want -= buf_len; } LimitBufsGuard { bufs: self, start: 0, end, restore, } } } pub trait LimitBufsMut<'a: 'b, 'b> { fn skip(&'b mut self, size: usize) -> LimitBufsMutGuard<'a, 'b>; fn limit(&'b mut self, size: usize) -> LimitBufsMutGuard<'a, 'b>; } impl<'a: 'b, 'b> LimitBufsMut<'a, 'b> for [&'a mut [u8]] { fn skip(&'b mut self, size: usize) -> LimitBufsMutGuard<'a, 'b> { let mut start = 0; let end = self.len(); let mut restore = None; let mut skip = size; for (index, item) in self.iter_mut().enumerate() { let buf: &mut [u8] = item; let buf_len = buf.len(); if buf_len >= skip { let len = buf.len(); let ptr = buf.as_mut_ptr(); restore = Some(LimitBufsRestore { index, ptr, len }); // SAFETY: ptr and len were obtained above and are still // valid. we just need to be careful about using them again // later on from the restore field unsafe { *item = &mut slice::from_raw_parts_mut(ptr, len)[skip..]; } start = index; break; } skip -= buf_len; } LimitBufsMutGuard { bufs: self, start, end, restore, } } fn limit(&'b mut self, size: usize) -> LimitBufsMutGuard<'a, 'b> { let mut end = self.len(); let mut restore = None; let mut want = size; for (index, item) in self.iter_mut().enumerate() { let buf: &mut [u8] = item; let buf_len = buf.len(); if buf_len >= want { let len = buf.len(); let ptr = buf.as_mut_ptr(); restore = Some(LimitBufsRestore { index, ptr, len }); // SAFETY: ptr and len were obtained above and are still // valid. we just need to be careful about using them again // later on from the restore field unsafe { *item = &mut slice::from_raw_parts_mut(ptr, len)[..want]; } end = index + 1; break; } want -= buf_len; } LimitBufsMutGuard { bufs: self, start: 0, end, restore, } } } pub struct ContiguousBuffer { buf: Vec, start: usize, end: usize, } #[allow(clippy::len_without_is_empty)] impl ContiguousBuffer { pub fn new(size: usize) -> Self { let buf = vec![0; size]; Self { buf, start: 0, end: 0, } } pub fn clear(&mut self) { self.start = 0; self.end = 0; } } impl Buffer for ContiguousBuffer { fn len(&self) -> usize { self.end - self.start } fn remaining_capacity(&self) -> usize { self.buf.len() - self.end } fn read_buf(&self) -> &[u8] { &self.buf[self.start..self.end] } fn read_buf_mut(&mut self) -> &mut [u8] { &mut self.buf[self.start..self.end] } fn read_commit(&mut self, amount: usize) { assert!(self.start + amount <= self.end); self.start += amount; } fn write_buf(&mut self) -> &mut [u8] { let len = self.buf.len(); &mut self.buf[self.end..len] } fn write_commit(&mut self, amount: usize) { assert!(self.end + amount <= self.buf.len()); self.end += amount; } } #[cfg(test)] impl Read for ContiguousBuffer { fn read(&mut self, buf: &mut [u8]) -> Result { // fully qualified to work around future method warning // https://github.com/rust-lang/rust/issues/48919 let src = Buffer::read_buf(self); let size = cmp::min(src.len(), buf.len()); buf[..size].copy_from_slice(&src[..size]); self.read_commit(size); Ok(size) } } impl Write for ContiguousBuffer { fn write(&mut self, buf: &[u8]) -> Result { if !buf.is_empty() && self.remaining_capacity() == 0 { return Err(io::Error::from(io::ErrorKind::WriteZero)); } let dest = self.write_buf(); let size = cmp::min(dest.len(), buf.len()); dest[..size].copy_from_slice(&buf[..size]); self.write_commit(size); Ok(size) } fn flush(&mut self) -> Result<(), io::Error> { Ok(()) } } pub struct TmpBuffer(RefCell>); #[allow(clippy::len_without_is_empty)] impl TmpBuffer { pub fn new(size: usize) -> Self { Self(RefCell::new(vec![0; size])) } pub fn len(&self) -> usize { self.0.borrow().len() } } // holds a Vec but only exposes the portion of it considered to be // readable ("filled"). any remaining bytes may be zeroed or uninitialized // and are not considered to be readable pub struct FilledBuf { data: Vec, filled: usize, } impl FilledBuf { // panics if filled is larger than data.len() pub fn new(data: Vec, filled: usize) -> Self { assert!(filled <= data.len()); Self { data, filled } } pub fn filled(&self) -> &[u8] { &self.data[..self.filled] } pub fn filled_len(&self) -> usize { self.filled } pub fn into_inner(self) -> Vec { self.data } } pub struct RingBuffer { buf: T, start: usize, end: usize, tmp: Rc, } #[allow(clippy::len_without_is_empty)] impl + AsMut<[u8]>> RingBuffer { pub fn capacity(&self) -> usize { self.buf.as_ref().len() } pub fn clear(&mut self) { self.start = 0; self.end = 0; } // return true if the readable bytes have not wrapped pub fn is_readable_contiguous(&self) -> bool { self.end <= self.buf.as_ref().len() } pub fn align(&mut self) -> usize { assert!(self.buf.as_ref().len() <= self.tmp.len()); if self.start == 0 { return 0; } let buf = self.buf.as_mut(); let size = self.end - self.start; if self.end <= buf.len() { // if the buffer hasn't wrapped, simply copy down buf.copy_within(self.start.., 0); } else if size <= self.start { // if the buffer has wrapped, but the wrapped part can be copied // without overlapping, then copy the wrapped part followed by // initial part let left_size = self.end - buf.len(); let right_size = buf.len() - self.start; buf.copy_within(..left_size, right_size); buf.copy_within(self.start..(self.start + right_size), 0); } else { // if the buffer has wrapped and the wrapped part can't be copied // without overlapping, then use a temporary buffer to // facilitate. smaller part is copied to the temp buffer, then // the larger and small parts (in that order) are copied into // their intended locations. in the worst case, up to 50% of // the buffer may be copied twice let left_size = self.end - buf.len(); let right_size = buf.len() - self.start; let (lsize, lsrc, ldest, hsize, hsrc, hdest); if left_size < right_size { lsize = left_size; hsize = right_size; lsrc = 0; ldest = hsize; hsrc = self.start; hdest = 0; } else { lsize = right_size; hsize = left_size; lsrc = self.start; ldest = 0; hsrc = 0; hdest = lsize; } let mut tmp = self.tmp.0.borrow_mut(); tmp[..lsize].copy_from_slice(&buf[lsrc..(lsrc + lsize)]); buf.copy_within(hsrc..(hsrc + hsize), hdest); buf[ldest..(ldest + lsize)].copy_from_slice(&tmp[..lsize]); } self.start = 0; self.end = size; size } pub fn get_tmp(&self) -> &Rc { &self.tmp } } #[cfg(test)] impl + AsMut<[u8]>> Read for RingBuffer { fn read(&mut self, buf: &mut [u8]) -> Result { let mut pos = 0; while pos < buf.len() && self.len() > 0 { // fully qualified to work around future method warning // https://github.com/rust-lang/rust/issues/48919 let src = Buffer::read_buf(self); let size = cmp::min(src.len(), buf.len() - pos); buf[pos..(pos + size)].copy_from_slice(&src[..size]); self.read_commit(size); pos += size; } Ok(pos) } } impl + AsMut<[u8]>> Write for RingBuffer { fn write(&mut self, buf: &[u8]) -> Result { if !buf.is_empty() && self.remaining_capacity() == 0 { return Err(io::Error::from(io::ErrorKind::WriteZero)); } let mut pos = 0; while pos < buf.len() && self.remaining_capacity() > 0 { let dest = self.write_buf(); let size = cmp::min(dest.len(), buf.len() - pos); dest[..size].copy_from_slice(&buf[pos..(pos + size)]); self.write_commit(size); pos += size; } Ok(pos) } fn flush(&mut self) -> Result<(), io::Error> { Ok(()) } } impl + AsMut<[u8]>> Buffer for RingBuffer { fn len(&self) -> usize { self.end - self.start } fn remaining_capacity(&self) -> usize { self.buf.as_ref().len() - (self.end - self.start) } fn read_buf(&self) -> &[u8] { let buf = self.buf.as_ref(); let end = cmp::min(self.end, buf.len()); &buf[self.start..end] } fn read_buf_mut(&mut self) -> &mut [u8] { let buf = self.buf.as_mut(); let end = cmp::min(self.end, buf.len()); &mut buf[self.start..end] } fn read_commit(&mut self, amount: usize) { assert!(self.start + amount <= self.end); let buf = self.buf.as_ref(); self.start += amount; if self.start == self.end { self.start = 0; self.end = 0; } else if self.start >= buf.len() { self.start -= buf.len(); self.end -= buf.len(); } } fn write_buf(&mut self) -> &mut [u8] { let buf = self.buf.as_mut(); let (start, end) = if self.end < buf.len() { (self.end, buf.len()) } else { (self.end - buf.len(), self.start) }; &mut buf[start..end] } fn write_commit(&mut self, amount: usize) { assert!((self.end - self.start) + amount <= self.buf.as_ref().len()); self.end += amount; } fn read_bufs<'data, 'bufs>( &'data self, bufs: &'bufs mut [&'data [u8]], ) -> &'bufs mut [&'data [u8]] { assert!(!bufs.is_empty()); let buf = self.buf.as_ref(); let buf_len = buf.len(); if self.end > buf_len && bufs.len() >= 2 { let (part1, part2) = buf.split_at(self.start); bufs[0] = part2; bufs[1] = &part1[..(self.end - buf_len)]; &mut bufs[..2] } else { bufs[0] = &buf[self.start..self.end]; &mut bufs[..1] } } fn read_bufs_mut<'data, 'bufs, const N: usize>( &'data mut self, bufs: &'bufs mut MaybeUninit<[&'data mut [u8]; N]>, ) -> &'bufs mut [&'data mut [u8]] { let buf = self.buf.as_mut(); let buf_len = buf.len(); if self.end > buf_len { let (part1, part2) = buf.split_at_mut(self.start); init_array(bufs, &mut [part2, &mut part1[..(self.end - buf_len)]]) } else { init_array(bufs, &mut [&mut buf[self.start..self.end]]) } } } impl RingBuffer> { pub fn new(size: usize, tmp: &Rc) -> Self { assert!(size <= tmp.len()); let buf = vec![0; size]; Self { buf, start: 0, end: 0, tmp: Rc::clone(tmp), } } // extract inner buffer, aligning it first if necessary, and replace it // with an empty buffer. this should be cheap if the inner buffer is // already aligned. afterwards, the ringbuffer will have a capacity of // zero and will be essentially unusable until set_inner is called with a // non-empty buffer pub fn take_inner(&mut self) -> FilledBuf { self.align(); let data = mem::take(&mut self.buf); let filled = self.end; self.end = 0; FilledBuf::new(data, filled) } // replace the inner buffer. this should be cheap if the original inner // buffer is empty, which is the case if take_inner was called earlier. // panics if the new buffer is larger than the tmp buffer pub fn set_inner(&mut self, buf: FilledBuf) { let filled = buf.filled_len(); let data = buf.into_inner(); assert!(data.len() <= self.tmp.len()); self.buf = data; self.start = 0; self.end = filled; } pub fn swap_inner(&mut self, other: &mut Self) { let buf = self.take_inner(); self.set_inner(other.take_inner()); other.set_inner(buf); } pub fn resize(&mut self, size: usize) { if size == self.buf.len() { return; } self.align(); self.buf.resize(size, 0); self.buf.shrink_to_fit(); self.end = cmp::min(self.end, size); } } impl<'a> RingBuffer<&'a mut [u8]> { pub fn new(buf: &'a mut [u8], tmp: &Rc) -> Self { assert!(buf.len() <= tmp.len()); Self { buf, start: 0, end: 0, tmp: Rc::clone(tmp), } } } pub type VecRingBuffer = RingBuffer>; pub type SliceRingBuffer<'a> = RingBuffer<&'a mut [u8]>; #[cfg(test)] mod tests { use super::*; use std::io::{Read, Write}; #[test] fn test_write_vectored_offset() { struct MyWriter { bufs: Vec, } impl MyWriter { fn new() -> Self { Self { bufs: Vec::new() } } } impl Write for MyWriter { fn write(&mut self, buf: &[u8]) -> Result { self.bufs.push(String::from_utf8(buf.to_vec()).unwrap()); Ok(buf.len()) } fn write_vectored(&mut self, bufs: &[io::IoSlice]) -> Result { let mut total = 0; for buf in bufs { total += buf.len(); self.bufs.push(String::from_utf8(buf.to_vec()).unwrap()); } Ok(total) } fn flush(&mut self) -> Result<(), io::Error> { Ok(()) } } // empty let mut w = MyWriter::new(); let r = write_vectored_offset(&mut w, &[], 0); assert_eq!(r.unwrap(), 0); // offset too large let mut w = MyWriter::new(); let r = write_vectored_offset(&mut w, &[b"apple"], 6); assert!(r.is_err()); // offset too large let mut w = MyWriter::new(); let r = write_vectored_offset(&mut w, &[b"apple", b"banana"], 12); assert!(r.is_err()); // nothing to write let mut w = MyWriter::new(); let r = write_vectored_offset(&mut w, &[b"apple"], 5); assert_eq!(r.unwrap(), 0); let mut w = MyWriter::new(); let r = write_vectored_offset(&mut w, &[b"apple"], 0); assert_eq!(r.unwrap(), 5); assert_eq!(w.bufs.len(), 1); assert_eq!(w.bufs[0], "apple"); let mut w = MyWriter::new(); let r = write_vectored_offset(&mut w, &[b"apple"], 3); assert_eq!(r.unwrap(), 2); assert_eq!(w.bufs.len(), 1); assert_eq!(w.bufs[0], "le"); let mut w = MyWriter::new(); let r = write_vectored_offset(&mut w, &[b"apple", b"banana"], 3); assert_eq!(r.unwrap(), 8); assert_eq!(w.bufs.len(), 2); assert_eq!(w.bufs[0], "le"); assert_eq!(w.bufs[1], "banana"); let mut w = MyWriter::new(); let r = write_vectored_offset(&mut w, &[b"apple", b"banana"], 5); assert_eq!(r.unwrap(), 6); assert_eq!(w.bufs.len(), 1); assert_eq!(w.bufs[0], "banana"); let mut w = MyWriter::new(); let r = write_vectored_offset(&mut w, &[b"apple", b"banana"], 6); assert_eq!(r.unwrap(), 5); assert_eq!(w.bufs.len(), 1); assert_eq!(w.bufs[0], "anana"); } #[test] fn test_buffer() { let mut b = ContiguousBuffer::new(8); assert_eq!(b.len(), 0); assert_eq!(b.remaining_capacity(), 8); let size = b.write(b"hello").unwrap(); assert_eq!(size, 5); assert_eq!(b.len(), 5); assert_eq!(b.remaining_capacity(), 3); let size = b.write(b"world").unwrap(); assert_eq!(size, 3); assert_eq!(b.len(), 8); assert_eq!(b.remaining_capacity(), 0); let mut tmp = [0; 16]; let size = b.read(&mut tmp).unwrap(); assert_eq!(&tmp[..size], b"hellowor"); b.clear(); assert_eq!(b.len(), 0); assert_eq!(b.remaining_capacity(), 8); } #[test] fn test_ringbuffer() { let mut buf = [0u8; 8]; let tmp = Rc::new(TmpBuffer::new(8)); let mut r = VecRingBuffer::new(8, &tmp); assert_eq!(r.len(), 0); assert_eq!(r.remaining_capacity(), 8); r.write(b"12345").unwrap(); assert_eq!(r.len(), 5); assert_eq!(r.remaining_capacity(), 3); r.write(b"678").unwrap(); let mut bufs_arr = [&b""[..]; VECTORED_MAX]; let bufs = r.read_bufs(&mut bufs_arr); assert_eq!(r.len(), 8); assert_eq!(r.remaining_capacity(), 0); assert_eq!(r.read_buf(), b"12345678"); assert_eq!(bufs.len(), 1); assert_eq!(bufs[0], b"12345678"); r.read(&mut buf[..5]).unwrap(); assert_eq!(r.len(), 3); assert_eq!(r.remaining_capacity(), 5); assert_eq!(r.write_buf().len(), 5); r.write(b"9abcd").unwrap(); assert_eq!(r.len(), 8); assert_eq!(r.remaining_capacity(), 0); r.read(&mut buf[5..]).unwrap(); assert_eq!(r.len(), 5); assert_eq!(r.remaining_capacity(), 3); r.read(&mut buf[..5]).unwrap(); assert_eq!(r.len(), 0); assert_eq!(r.remaining_capacity(), 8); assert_eq!(&buf, b"9abcd678"); r.write(b"12345").unwrap(); r.read(&mut buf[..2]).unwrap(); let mut bufs_arr = [&b""[..]; VECTORED_MAX]; let bufs = r.read_bufs(&mut bufs_arr); assert_eq!(r.len(), 3); assert_eq!(r.read_buf(), b"345"); assert_eq!(bufs.len(), 1); assert_eq!(bufs[0], b"345"); assert_eq!(r.remaining_capacity(), 5); assert_eq!(r.write_buf().len(), 3); r.align(); assert_eq!(r.len(), 3); assert_eq!(r.read_buf(), b"345"); assert_eq!(r.remaining_capacity(), 5); assert_eq!(r.write_buf().len(), 5); r.write(b"6789a").unwrap(); r.read(&mut buf[..2]).unwrap(); r.write(b"bc").unwrap(); let mut bufs_arr = [&b""[..]; VECTORED_MAX]; let bufs = r.read_bufs(&mut bufs_arr); assert_eq!(r.len(), 8); assert_eq!(r.read_buf(), b"56789a"); assert_eq!(bufs.len(), 2); assert_eq!(bufs[0], b"56789a"); assert_eq!(bufs[1], b"bc"); assert_eq!(r.remaining_capacity(), 0); r.align(); assert_eq!(r.len(), 8); assert_eq!(r.read_buf(), b"56789abc"); assert_eq!(r.remaining_capacity(), 0); r.read(&mut buf[..6]).unwrap(); r.write(b"def123").unwrap(); let mut bufs_arr = [&b""[..]; VECTORED_MAX]; let bufs = r.read_bufs(&mut bufs_arr); assert_eq!(r.len(), 8); assert_eq!(r.read_buf(), b"bc"); assert_eq!(bufs.len(), 2); assert_eq!(bufs[0], b"bc"); assert_eq!(bufs[1], b"def123"); assert_eq!(r.remaining_capacity(), 0); r.align(); let mut bufs_arr = [&b""[..]; VECTORED_MAX]; let bufs = r.read_bufs(&mut bufs_arr); assert_eq!(r.len(), 8); assert_eq!(r.read_buf(), b"bcdef123"); assert_eq!(bufs.len(), 1); assert_eq!(bufs[0], b"bcdef123"); assert_eq!(r.remaining_capacity(), 0); r.clear(); r.write(b"12345678").unwrap(); r.read(&mut buf[..6]).unwrap(); r.write(b"9abc").unwrap(); assert_eq!(r.len(), 6); assert_eq!(r.read_buf().len(), 2); r.align(); assert_eq!(r.len(), 6); assert_eq!(r.read_buf().len(), 6); } #[test] fn test_slice_ringbuffer() { let mut buf = [0; 8]; let mut backing_buf = [0; 8]; let tmp = Rc::new(TmpBuffer::new(8)); let mut r = SliceRingBuffer::new(&mut backing_buf, &tmp); r.write(b"12345678").unwrap(); let size = r.read(&mut buf[..4]).unwrap(); assert_eq!(&buf[..size], b"1234"); r.write(b"90ab").unwrap(); let size = r.read(&mut buf).unwrap(); assert_eq!(&buf[..size], b"567890ab"); } #[test] fn test_limitbufs() { let mut buf1 = [b'1', b'2', b'3', b'4']; let mut buf2 = [b'5', b'6', b'7', b'8']; let mut buf3 = [b'9', b'0', b'a', b'b']; let mut bufs = [buf1.as_slice(), buf2.as_slice(), buf3.as_slice()]; { let limited = bufs.limit(7); let limited = limited.as_slice(); assert_eq!(limited.len(), 2); assert_eq!(&limited[0], b"1234"); assert_eq!(&limited[1], b"567"); } assert_eq!(bufs.len(), 3); assert_eq!(&bufs[0], b"1234"); assert_eq!(&bufs[1], b"5678"); assert_eq!(&bufs[2], b"90ab"); let mut bufs = [ buf1.as_mut_slice(), buf2.as_mut_slice(), buf3.as_mut_slice(), ]; { let mut limited = bufs.limit(7); let limited = limited.as_slice(); assert_eq!(limited.len(), 2); assert_eq!(&limited[0], b"1234"); assert_eq!(&limited[1], b"567"); } { let mut limited = bufs.skip(7); let limited = limited.as_slice(); assert_eq!(limited.len(), 2); assert_eq!(&limited[0], b"8"); assert_eq!(&limited[1], b"90ab"); } assert_eq!(bufs.len(), 3); assert_eq!(&bufs[0], b"1234"); assert_eq!(&bufs[1], b"5678"); assert_eq!(&bufs[2], b"90ab"); } #[test] fn test_resize() { let tmp = Rc::new(TmpBuffer::new(16)); let mut r = VecRingBuffer::new(8, &tmp); assert_eq!(r.capacity(), 8); let size = r.write(b"12345678").unwrap(); assert_eq!(size, 8); let mut buf = [0; 4]; let size = r.read(&mut buf).unwrap(); assert_eq!(size, 4); assert_eq!(&buf[..size], b"1234"); let size = r.write(b"90ab").unwrap(); assert_eq!(size, 4); assert!(r.write(b"cdef").is_err()); r.resize(12); assert_eq!(r.capacity(), 12); let size = r.write(b"cdef").unwrap(); assert_eq!(size, 4); let mut buf = [0; 12]; let size = r.read(&mut buf).unwrap(); assert_eq!(size, 12); assert_eq!(&buf[..size], b"567890abcdef"); let size = r.write(b"1234567890").unwrap(); assert_eq!(size, 10); r.resize(8); assert_eq!(r.capacity(), 8); let mut buf = [0; 12]; let size = r.read(&mut buf).unwrap(); assert_eq!(size, 8); assert_eq!(&buf[..size], b"12345678"); } } pushpin-1.39.1/src/channel.rs000066400000000000000000000577571457610542000160710ustar00rootroot00000000000000/* * Copyright (C) 2020-2023 Fanout, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use crate::arena; use crate::event; use crate::list; use slab::Slab; use std::cell::RefCell; use std::collections::VecDeque; use std::mem; use std::rc::Rc; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc; use std::sync::Arc; pub struct Sender { sender: Option>, read_set_readiness: event::SetReadiness, write_registration: event::Registration, cts: Option>, } impl Sender { // NOTE: only makes sense for rendezvous channels pub fn can_send(&self) -> bool { match &self.cts { Some(cts) => cts.load(Ordering::Relaxed), None => true, } } pub fn get_write_registration(&self) -> &event::Registration { &self.write_registration } pub fn try_send(&self, t: T) -> Result<(), mpsc::TrySendError> { if let Some(cts) = &self.cts { if cts .compare_exchange(true, false, Ordering::Relaxed, Ordering::Relaxed) .is_err() { return Err(mpsc::TrySendError::Full(t)); } // cts will only be true if a read was performed while the queue // was empty, and this function is the only place where the queue // is written to. this means the try_send call below will only // fail if the receiver disconnected } match self.sender.as_ref().unwrap().try_send(t) { Ok(_) => { self.read_set_readiness .set_readiness(mio::Interest::READABLE) .unwrap(); Ok(()) } Err(e) => Err(e), } } pub fn send(&self, t: T) -> Result<(), mpsc::SendError> { if self.cts.is_some() { panic!("blocking send with rendezvous channel not supported") } match self.sender.as_ref().unwrap().send(t) { Ok(_) => { self.read_set_readiness .set_readiness(mio::Interest::READABLE) .unwrap(); Ok(()) } Err(e) => Err(e), } } } impl Drop for Sender { fn drop(&mut self) { mem::drop(self.sender.take().unwrap()); self.read_set_readiness .set_readiness(mio::Interest::READABLE) .unwrap(); } } pub struct Receiver { receiver: mpsc::Receiver, read_registration: event::Registration, write_set_readiness: event::SetReadiness, cts: Option>, } impl Receiver { pub fn get_read_registration(&self) -> &event::Registration { &self.read_registration } pub fn try_recv(&self) -> Result { match self.receiver.try_recv() { Ok(t) => { if self.cts.is_none() { self.write_set_readiness .set_readiness(mio::Interest::WRITABLE) .unwrap(); } Ok(t) } Err(mpsc::TryRecvError::Empty) if self.cts.is_some() => { let cts = self.cts.as_ref().unwrap(); if cts .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) .is_ok() { self.write_set_readiness .set_readiness(mio::Interest::WRITABLE) .unwrap(); } Err(mpsc::TryRecvError::Empty) } Err(e) => Err(e), } } pub fn recv(&self) -> Result { let t = self.receiver.recv()?; if self.cts.is_none() { self.write_set_readiness .set_readiness(mio::Interest::WRITABLE) .unwrap(); } Ok(t) } } pub fn channel(bound: usize) -> (Sender, Receiver) { let (read_reg, read_sr) = event::Registration::new(); let (write_reg, write_sr) = event::Registration::new(); // rendezvous channel if bound == 0 { let (s, r) = mpsc::sync_channel::(1); let cts = Arc::new(AtomicBool::new(false)); let sender = Sender { sender: Some(s), read_set_readiness: read_sr, write_registration: write_reg, cts: Some(Arc::clone(&cts)), }; let receiver = Receiver { receiver: r, read_registration: read_reg, write_set_readiness: write_sr, cts: Some(Arc::clone(&cts)), }; (sender, receiver) } else { let (s, r) = mpsc::sync_channel::(bound); let sender = Sender { sender: Some(s), read_set_readiness: read_sr, write_registration: write_reg, cts: None, }; let receiver = Receiver { receiver: r, read_registration: read_reg, write_set_readiness: write_sr, cts: None, }; // channel is immediately writable receiver .write_set_readiness .set_readiness(mio::Interest::WRITABLE) .unwrap(); (sender, receiver) } } struct LocalSenderData { notified: bool, write_set_readiness: event::LocalSetReadiness, } struct LocalSenders { nodes: Slab>, waiting: list::List, } struct LocalChannel { queue: RefCell>, senders: RefCell, read_set_readiness: RefCell>, } impl LocalChannel { fn senders_is_empty(&self) -> bool { self.senders.borrow().nodes.is_empty() } fn add_sender(&self, write_sr: event::LocalSetReadiness) -> Result { let mut senders = self.senders.borrow_mut(); if senders.nodes.len() == senders.nodes.capacity() { return Err(()); } let key = senders.nodes.insert(list::Node::new(LocalSenderData { notified: false, write_set_readiness: write_sr, })); Ok(key) } fn remove_sender(&self, key: usize) { let senders = &mut *self.senders.borrow_mut(); senders.waiting.remove(&mut senders.nodes, key); senders.nodes.remove(key); if senders.nodes.is_empty() { if let Some(read_sr) = &*self.read_set_readiness.borrow() { // notify for disconnect read_sr.set_readiness(mio::Interest::READABLE).unwrap(); } } } fn set_sender_waiting(&self, key: usize) { let senders = &mut *self.senders.borrow_mut(); // add if not already present if senders.nodes[key].prev.is_none() && senders.waiting.head != Some(key) { senders.waiting.push_back(&mut senders.nodes, key); } } fn notify_one_sender(&self) { let senders = &mut *self.senders.borrow_mut(); // notify next waiting sender, if any if let Some(key) = senders.waiting.pop_front(&mut senders.nodes) { let sender = &mut senders.nodes[key].value; sender.notified = true; sender .write_set_readiness .set_readiness(mio::Interest::WRITABLE) .unwrap(); } } fn sender_is_notified(&self, key: usize) -> bool { self.senders.borrow().nodes[key].value.notified } fn clear_sender_notified(&self, key: usize) { self.senders.borrow_mut().nodes[key].value.notified = false; } } pub struct LocalSender { channel: Rc>, key: usize, write_registration: event::LocalRegistration, } impl LocalSender { pub fn get_write_registration(&self) -> &event::LocalRegistration { &self.write_registration } // if this returns true, then the next call to try_send() by any sender // is guaranteed to not return TrySendError::Full. // if this returns false, the sender is added to the wait list pub fn check_send(&self) -> bool { let queue = self.channel.queue.borrow(); let can_send = queue.len() < queue.capacity(); if !can_send { self.channel.set_sender_waiting(self.key); } can_send } pub fn try_send(&self, t: T) -> Result<(), mpsc::TrySendError> { // we are acting, so clear the notified flag self.channel.clear_sender_notified(self.key); let read_sr = &*self.channel.read_set_readiness.borrow(); let read_sr = match read_sr { Some(sr) => sr, None => { // receiver is disconnected return Err(mpsc::TrySendError::Disconnected(t)); } }; let mut queue = self.channel.queue.borrow_mut(); if queue.len() < queue.capacity() { queue.push_back(t); read_sr.set_readiness(mio::Interest::READABLE).unwrap(); Ok(()) } else { self.channel.set_sender_waiting(self.key); Err(mpsc::TrySendError::Full(t)) } } pub fn cancel(&self) { // if we were notified but never acted on it, notify the next waiting sender, if any if self.channel.sender_is_notified(self.key) { self.channel.clear_sender_notified(self.key); self.channel.notify_one_sender(); } } // NOTE: if the receiver is dropped while there are multiple senders, // only one of the senders will be notified of the disconnect #[allow(clippy::result_unit_err)] pub fn try_clone( &self, memory: &Rc>, ) -> Result { let (write_reg, write_sr) = event::LocalRegistration::new(memory); let key = self.channel.add_sender(write_sr)?; Ok(Self { channel: self.channel.clone(), key, write_registration: write_reg, }) } // returns error if a receiver already exists #[allow(clippy::result_unit_err)] pub fn make_receiver( &self, memory: &Rc>, ) -> Result, ()> { if self.channel.read_set_readiness.borrow().is_some() { return Err(()); } let (read_reg, read_sr) = event::LocalRegistration::new(memory); *self.channel.read_set_readiness.borrow_mut() = Some(read_sr); Ok(LocalReceiver { channel: self.channel.clone(), read_registration: read_reg, }) } } impl Drop for LocalSender { fn drop(&mut self) { self.cancel(); self.channel.remove_sender(self.key); } } pub struct LocalReceiver { channel: Rc>, read_registration: event::LocalRegistration, } impl LocalReceiver { pub fn get_read_registration(&self) -> &event::LocalRegistration { &self.read_registration } pub fn try_recv(&self) -> Result { let mut queue = self.channel.queue.borrow_mut(); if queue.is_empty() { if self.channel.senders_is_empty() { return Err(mpsc::TryRecvError::Disconnected); } return Err(mpsc::TryRecvError::Empty); } let value = queue.pop_front().unwrap(); self.channel.notify_one_sender(); Ok(value) } pub fn clear(&self) { // loop over try_recv() in order to notify senders while self.try_recv().is_ok() {} } } impl Drop for LocalReceiver { fn drop(&mut self) { *self.channel.read_set_readiness.borrow_mut() = None; self.channel.notify_one_sender(); } } pub fn local_channel( bound: usize, max_senders: usize, memory: &Rc>, ) -> (LocalSender, LocalReceiver) { let (read_reg, read_sr) = event::LocalRegistration::new(memory); let (write_reg, write_sr) = event::LocalRegistration::new(memory); // no support for rendezvous channels assert!(bound > 0); // need to support at least one sender assert!(max_senders > 0); let channel = Rc::new(LocalChannel { queue: RefCell::new(VecDeque::with_capacity(bound)), senders: RefCell::new(LocalSenders { nodes: Slab::with_capacity(max_senders), waiting: list::List::default(), }), read_set_readiness: RefCell::new(Some(read_sr)), }); let key = channel.add_sender(write_sr).unwrap(); let sender = LocalSender { channel: channel.clone(), key, write_registration: write_reg, }; let receiver = LocalReceiver { channel, read_registration: read_reg, }; (sender, receiver) } #[cfg(test)] mod tests { use super::*; use std::time; #[test] fn test_send_recv_bound0() { let (sender, receiver) = channel(0); assert_eq!(sender.can_send(), false); let result = sender.try_send(42); assert_eq!(result.is_err(), true); assert_eq!(result.unwrap_err(), mpsc::TrySendError::Full(42)); let result = receiver.try_recv(); assert_eq!(result.is_err(), true); assert_eq!(result.unwrap_err(), mpsc::TryRecvError::Empty); assert_eq!(sender.can_send(), true); let result = sender.try_send(42); assert_eq!(result.is_ok(), true); assert_eq!(sender.can_send(), false); let result = receiver.try_recv(); assert_eq!(result.is_ok(), true); let v = result.unwrap(); assert_eq!(v, 42); let result = receiver.try_recv(); assert_eq!(result.is_err(), true); assert_eq!(result.unwrap_err(), mpsc::TryRecvError::Empty); mem::drop(sender); let result = receiver.try_recv(); assert_eq!(result.is_err(), true); assert_eq!(result.unwrap_err(), mpsc::TryRecvError::Disconnected); } #[test] fn test_send_recv_bound1() { let (sender, receiver) = channel(1); let result = receiver.try_recv(); assert_eq!(result.is_err(), true); assert_eq!(result.unwrap_err(), mpsc::TryRecvError::Empty); let result = sender.try_send(42); assert_eq!(result.is_ok(), true); let result = sender.try_send(42); assert_eq!(result.is_err(), true); assert_eq!(result.unwrap_err(), mpsc::TrySendError::Full(42)); let result = receiver.try_recv(); assert_eq!(result.is_ok(), true); let v = result.unwrap(); assert_eq!(v, 42); let result = receiver.try_recv(); assert_eq!(result.is_err(), true); assert_eq!(result.unwrap_err(), mpsc::TryRecvError::Empty); mem::drop(sender); let result = receiver.try_recv(); assert_eq!(result.is_err(), true); assert_eq!(result.unwrap_err(), mpsc::TryRecvError::Disconnected); } #[test] fn test_notify_bound0() { let (sender, receiver) = channel(0); let mut poller = event::Poller::new(2).unwrap(); poller .register_custom( sender.get_write_registration(), mio::Token(1), mio::Interest::WRITABLE, ) .unwrap(); poller .register_custom( receiver.get_read_registration(), mio::Token(2), mio::Interest::READABLE, ) .unwrap(); assert_eq!(sender.can_send(), false); poller.poll(Some(time::Duration::from_millis(0))).unwrap(); assert_eq!(poller.iter_events().next(), None); let result = receiver.try_recv(); assert_eq!(result.is_err(), true); assert_eq!(result.unwrap_err(), mpsc::TryRecvError::Empty); poller.poll(None).unwrap(); let mut it = poller.iter_events(); let event = it.next().unwrap(); assert_eq!(event.token(), mio::Token(1)); assert_eq!(event.is_writable(), true); assert_eq!(it.next(), None); assert_eq!(sender.can_send(), true); sender.try_send(42).unwrap(); poller.poll(None).unwrap(); let mut it = poller.iter_events(); let event = it.next().unwrap(); assert_eq!(event.token(), mio::Token(2)); assert_eq!(event.is_readable(), true); assert_eq!(it.next(), None); let v = receiver.try_recv().unwrap(); assert_eq!(v, 42); mem::drop(sender); poller.poll(None).unwrap(); let mut it = poller.iter_events(); let event = it.next().unwrap(); assert_eq!(event.token(), mio::Token(2)); assert_eq!(event.is_readable(), true); assert_eq!(it.next(), None); let e = receiver.try_recv().unwrap_err(); assert_eq!(e, mpsc::TryRecvError::Disconnected); } #[test] fn test_notify_bound1() { let (sender, receiver) = channel(1); let mut poller = event::Poller::new(2).unwrap(); poller .register_custom( sender.get_write_registration(), mio::Token(1), mio::Interest::WRITABLE, ) .unwrap(); poller .register_custom( receiver.get_read_registration(), mio::Token(2), mio::Interest::READABLE, ) .unwrap(); poller.poll(Some(time::Duration::from_millis(0))).unwrap(); let mut it = poller.iter_events(); let event = it.next().unwrap(); assert_eq!(event.token(), mio::Token(1)); assert_eq!(event.is_writable(), true); assert_eq!(it.next(), None); sender.try_send(42).unwrap(); poller.poll(None).unwrap(); let mut it = poller.iter_events(); let event = it.next().unwrap(); assert_eq!(event.token(), mio::Token(2)); assert_eq!(event.is_readable(), true); assert_eq!(it.next(), None); let v = receiver.try_recv().unwrap(); assert_eq!(v, 42); mem::drop(sender); poller.poll(None).unwrap(); let mut it = poller.iter_events(); let event = it.next().unwrap(); assert_eq!(event.token(), mio::Token(2)); assert_eq!(event.is_readable(), true); assert_eq!(it.next(), None); let e = receiver.try_recv().unwrap_err(); assert_eq!(e, mpsc::TryRecvError::Disconnected); } #[test] fn test_local_send_recv() { let poller = event::Poller::new(6).unwrap(); let (sender1, receiver) = local_channel(1, 2, poller.local_registration_memory()); assert_eq!(receiver.try_recv(), Err(mpsc::TryRecvError::Empty)); assert_eq!(sender1.try_send(1), Ok(())); assert_eq!(receiver.try_recv(), Ok(1)); let sender2 = sender1 .try_clone(poller.local_registration_memory()) .unwrap(); assert_eq!(sender1.try_send(2), Ok(())); let channel = sender2.channel.clone(); assert_eq!(channel.senders.borrow().waiting.is_empty(), true); assert_eq!( channel.senders.borrow().nodes[sender2.key].value.notified, false ); assert_eq!(sender2.try_send(3), Err(mpsc::TrySendError::Full(3))); assert_eq!(channel.senders.borrow().waiting.is_empty(), false); assert_eq!( channel.senders.borrow().nodes[sender2.key].value.notified, false ); assert_eq!(receiver.try_recv(), Ok(2)); assert_eq!(channel.senders.borrow().waiting.is_empty(), true); assert_eq!( channel.senders.borrow().nodes[sender2.key].value.notified, true ); assert_eq!(sender2.try_send(3), Ok(())); assert_eq!( channel.senders.borrow().nodes[sender2.key].value.notified, false ); assert_eq!(receiver.try_recv(), Ok(3)); mem::drop(sender1); mem::drop(sender2); assert_eq!(receiver.try_recv(), Err(mpsc::TryRecvError::Disconnected)); } #[test] fn test_local_send_disc() { let poller = event::Poller::new(4).unwrap(); let (sender, receiver) = local_channel(1, 1, poller.local_registration_memory()); mem::drop(receiver); assert_eq!(sender.try_send(1), Err(mpsc::TrySendError::Disconnected(1))); } #[test] fn test_local_cancel() { let poller = event::Poller::new(6).unwrap(); let (sender1, receiver) = local_channel(1, 2, poller.local_registration_memory()); let sender2 = sender1 .try_clone(poller.local_registration_memory()) .unwrap(); let channel = sender2.channel.clone(); assert_eq!(sender1.try_send(1), Ok(())); assert_eq!(sender2.try_send(2), Err(mpsc::TrySendError::Full(2))); assert_eq!(sender1.try_send(3), Err(mpsc::TrySendError::Full(3))); assert_eq!(channel.senders.borrow().waiting.is_empty(), false); assert_eq!( channel.senders.borrow().nodes[sender1.key].value.notified, false ); assert_eq!( channel.senders.borrow().nodes[sender2.key].value.notified, false ); assert_eq!(receiver.try_recv(), Ok(1)); assert_eq!(channel.senders.borrow().waiting.is_empty(), false); assert_eq!( channel.senders.borrow().nodes[sender1.key].value.notified, false ); assert_eq!( channel.senders.borrow().nodes[sender2.key].value.notified, true ); sender2.cancel(); assert_eq!(channel.senders.borrow().waiting.is_empty(), true); assert_eq!( channel.senders.borrow().nodes[sender1.key].value.notified, true ); assert_eq!( channel.senders.borrow().nodes[sender2.key].value.notified, false ); assert_eq!(sender1.try_send(3), Ok(())); assert_eq!( channel.senders.borrow().nodes[sender1.key].value.notified, false ); assert_eq!(receiver.try_recv(), Ok(3)); } #[test] fn test_local_check_send() { let poller = event::Poller::new(4).unwrap(); let (sender, receiver) = local_channel(1, 1, poller.local_registration_memory()); assert_eq!(receiver.try_recv(), Err(mpsc::TryRecvError::Empty)); let channel = sender.channel.clone(); assert_eq!(sender.check_send(), true); assert_eq!(channel.senders.borrow().waiting.is_empty(), true); assert_eq!( channel.senders.borrow().nodes[sender.key].value.notified, false ); assert_eq!(sender.try_send(1), Ok(())); assert_eq!(channel.senders.borrow().waiting.is_empty(), true); assert_eq!( channel.senders.borrow().nodes[sender.key].value.notified, false ); assert_eq!(sender.check_send(), false); assert_eq!(channel.senders.borrow().waiting.is_empty(), false); assert_eq!( channel.senders.borrow().nodes[sender.key].value.notified, false ); assert_eq!(receiver.try_recv(), Ok(1)); assert_eq!(channel.senders.borrow().waiting.is_empty(), true); assert_eq!( channel.senders.borrow().nodes[sender.key].value.notified, true ); assert_eq!(sender.try_send(2), Ok(())); assert_eq!(channel.senders.borrow().waiting.is_empty(), true); assert_eq!( channel.senders.borrow().nodes[sender.key].value.notified, false ); assert_eq!(receiver.try_recv(), Ok(2)); } } pushpin-1.39.1/src/client.rs000066400000000000000000002766051457610542000157320ustar00rootroot00000000000000/* * Copyright (C) 2023 Fanout, Inc. * Copyright (C) 2023 Fastly, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use crate::arena; use crate::buffer::TmpBuffer; use crate::can_move_mio_sockets_between_threads; use crate::channel; use crate::connection::{ client_req_connection, client_stream_connection, ConnectionPool, StreamSharedData, }; use crate::counter::Counter; use crate::event; use crate::executor::{Executor, Spawner}; use crate::future::{ event_wait, select_2, select_5, select_6, select_option, yield_to_local_events, AsyncLocalReceiver, AsyncLocalSender, AsyncReceiver, CancellationSender, CancellationToken, Select2, Select5, Select6, Timeout, }; use crate::list; use crate::pin; use crate::reactor::Reactor; use crate::resolver::Resolver; use crate::tnetstring; use crate::zhttppacket; use crate::zhttpsocket::{self, SessionKey, FROM_MAX, REQ_ID_MAX}; use crate::zmq::{MultipartHeader, SpecInfo}; use arrayvec::ArrayVec; use ipnet::IpNet; use log::{debug, error, info, warn}; use mio::unix::SourceFd; use slab::Slab; use std::cell::Cell; use std::cell::RefCell; use std::collections::{HashMap, VecDeque}; use std::convert::TryFrom; use std::io::{self, Write}; use std::mem; use std::rc::Rc; use std::str; use std::sync::{mpsc, Arc}; use std::thread; use std::time::Duration; const REQ_SENDER_BOUND: usize = 1; // we read and process each request message one at a time, wrapping it in an // rc, and sending it to connections via channels. on the other side of each // channel, the message is received and processed immediately, except for the // first message. this means the max number of messages retained per // connection is the channel bound per connection plus one pub const MSG_RETAINED_PER_CONNECTION_MAX: usize = REQ_SENDER_BOUND + 1; // the max number of messages retained outside of connections is one per // handle we read from (req and stream), in preparation for sending to any // connections pub const MSG_RETAINED_PER_WORKER_MAX: usize = 2; // run x1 // req_handle_task x1 // stream_handle_task x1 // keep_alives_task x1 const WORKER_NON_CONNECTION_TASKS_MAX: usize = 10; // this is meant to be an average max of registrations per task, in order // to determine the total number of registrations sufficient for all tasks, // however it is not enforced per task const REGISTRATIONS_PER_TASK_MAX: usize = 32; const REACTOR_BUDGET: u32 = 100; const KEEP_ALIVE_TIMEOUT_MS: usize = 45_000; const KEEP_ALIVE_BATCH_MS: usize = 100; const KEEP_ALIVE_INTERVAL: Duration = Duration::from_millis(KEEP_ALIVE_BATCH_MS as u64); const KEEP_ALIVE_BATCHES: usize = KEEP_ALIVE_TIMEOUT_MS / KEEP_ALIVE_BATCH_MS; const BULK_PACKET_SIZE_MAX: usize = 65_000; const SHUTDOWN_TIMEOUT: Duration = Duration::from_millis(10_000); const RESOLVER_THREADS: usize = 10; fn local_channel( bound: usize, max_senders: usize, ) -> (channel::LocalSender, channel::LocalReceiver) { let (s, r) = channel::local_channel( bound, max_senders, &Reactor::current().unwrap().local_registration_memory(), ); (s, r) } fn async_local_channel( bound: usize, max_senders: usize, ) -> (AsyncLocalSender, AsyncLocalReceiver) { let (s, r) = local_channel(bound, max_senders); let s = AsyncLocalSender::new(s); let r = AsyncLocalReceiver::new(r); (s, r) } struct BatchKey { addr_index: usize, nkey: usize, } struct BatchGroup<'a, 'b> { addr: &'b [u8], ids: arena::ReusableVecHandle<'b, zhttppacket::Id<'a>>, } impl<'a> BatchGroup<'a, '_> { fn addr(&self) -> &[u8] { self.addr } fn ids(&self) -> &[zhttppacket::Id<'a>] { &self.ids } } struct Batch { nodes: Slab>, addrs: Vec<(ArrayVec, list::List)>, addr_index: usize, group_ids: arena::ReusableVec, last_group_ckeys: Vec, } impl Batch { fn new(capacity: usize) -> Self { Self { nodes: Slab::with_capacity(capacity), addrs: Vec::with_capacity(capacity), addr_index: 0, group_ids: arena::ReusableVec::new::(capacity), last_group_ckeys: Vec::with_capacity(capacity), } } fn len(&self) -> usize { self.nodes.len() } fn capacity(&self) -> usize { self.nodes.capacity() } fn is_empty(&self) -> bool { self.nodes.is_empty() } fn clear(&mut self) { self.addrs.clear(); self.nodes.clear(); self.addr_index = 0; } fn add(&mut self, to_addr: &[u8], ckey: usize) -> Result { let mut pos = self.addrs.len(); for (i, a) in self.addrs.iter().enumerate() { if a.0.as_ref() == to_addr { pos = i; } } if pos == self.addrs.len() { // connection limits to_addr to FROM_MAX so this is guaranteed to succeed let a = ArrayVec::try_from(to_addr).unwrap(); self.addrs.push((a, list::List::default())); } if self.nodes.len() == self.nodes.capacity() { return Err(()); } let nkey = self.nodes.insert(list::Node::new(ckey)); self.addrs[pos].1.push_back(&mut self.nodes, nkey); Ok(BatchKey { addr_index: pos, nkey, }) } fn remove(&mut self, key: BatchKey) { self.addrs[key.addr_index] .1 .remove(&mut self.nodes, key.nkey); self.nodes.remove(key.nkey); } fn take_group<'a, 'b: 'a, F>(&'a mut self, get_ids: F) -> Option where F: Fn(usize) -> (&'b [u8], u32), { // find the next addr with items while self.addr_index < self.addrs.len() && self.addrs[self.addr_index].1.is_empty() { self.addr_index += 1; } // if all are empty, we're done if self.addr_index == self.addrs.len() { return None; } let (addr, keys) = &mut self.addrs[self.addr_index]; self.last_group_ckeys.clear(); let mut ids = self.group_ids.get_as_new(); // get ids/seqs while ids.len() < zhttppacket::IDS_MAX { let nkey = match keys.pop_front(&mut self.nodes) { Some(nkey) => nkey, None => break, }; let ckey = self.nodes[nkey].value; self.nodes.remove(nkey); let (id, seq) = get_ids(ckey); self.last_group_ckeys.push(ckey); ids.push(zhttppacket::Id { id, seq: Some(seq) }); } Some(BatchGroup { addr, ids }) } fn last_group_ckeys(&self) -> &[usize] { &self.last_group_ckeys } } enum BatchType { KeepAlive, Cancel, } struct ChannelPool { items: RefCell, channel::LocalReceiver)>>, } impl ChannelPool { fn new(capacity: usize) -> Self { Self { items: RefCell::new(VecDeque::with_capacity(capacity)), } } fn take(&self) -> Option<(channel::LocalSender, channel::LocalReceiver)> { let p = &mut *self.items.borrow_mut(); p.pop_back() } fn push(&self, pair: (channel::LocalSender, channel::LocalReceiver)) { let p = &mut *self.items.borrow_mut(); p.push_back(pair); } } struct ConnectionDone { ckey: usize, } struct ConnectionItem { id: Option, stop: Option, zreceiver_sender: Option, usize)>>, shared: Option>, batch_key: Option, } struct ConnectionItems { nodes: Slab>, nodes_by_id: HashMap, batch: Batch, } impl ConnectionItems { fn new(capacity: usize, batch: Batch) -> Self { Self { nodes: Slab::with_capacity(capacity), nodes_by_id: HashMap::with_capacity(capacity), batch, } } } struct ConnectionsInner { active: list::List, count: usize, max: usize, } struct Connections { items: Rc>, inner: RefCell, } impl Connections { fn new(items: Rc>, max: usize) -> Self { Self { items, inner: RefCell::new(ConnectionsInner { active: list::List::default(), count: 0, max, }), } } fn count(&self) -> usize { self.inner.borrow().count } fn max(&self) -> usize { self.inner.borrow().max } fn add( &self, stop: CancellationSender, zreceiver_sender: Option< channel::LocalSender<(arena::Rc, usize)>, >, shared: Option>, ) -> Result { let items = &mut *self.items.borrow_mut(); let c = &mut *self.inner.borrow_mut(); if items.nodes.len() == items.nodes.capacity() { return Err(()); } let nkey = items.nodes.insert(list::Node::new(ConnectionItem { id: None, stop: Some(stop), zreceiver_sender, shared, batch_key: None, })); c.active.push_back(&mut items.nodes, nkey); c.count += 1; Ok(nkey) } // return zreceiver_sender fn remove( &self, ckey: usize, ) -> Option, usize)>> { let nkey = ckey; let items = &mut *self.items.borrow_mut(); let c = &mut *self.inner.borrow_mut(); let ci = &mut items.nodes[nkey].value; // clear active keep alive if let Some(bkey) = ci.batch_key.take() { items.batch.remove(bkey); } c.active.remove(&mut items.nodes, nkey); c.count -= 1; let ci = items.nodes.remove(nkey).value; if let Some(id) = &ci.id { items.nodes_by_id.remove(id); } ci.zreceiver_sender } fn set_id(&self, ckey: usize, id: Option<&SessionKey>) { let nkey = ckey; let items = &mut *self.items.borrow_mut(); let ci = &mut items.nodes[nkey].value; // unset current id, if any if let Some(cur_id) = &ci.id { items.nodes_by_id.remove(cur_id); ci.id = None; } if let Some(id) = id.cloned() { ci.id = Some(id.clone()); items.nodes_by_id.insert(id, nkey); } else { // clear active keep alive if let Some(bkey) = ci.batch_key.take() { items.batch.remove(bkey); } } } fn find_key(&self, id: &SessionKey) -> Option { let items = &*self.items.borrow(); items.nodes_by_id.get(id).copied() } fn try_send( &self, ckey: usize, value: (arena::Rc, usize), ) -> Result<(), mpsc::TrySendError<(arena::Rc, usize)>> { let nkey = ckey; let items = &*self.items.borrow(); let ci = &items.nodes[nkey].value; let sender = match &ci.zreceiver_sender { Some(s) => s, None => return Err(mpsc::TrySendError::Disconnected(value)), }; sender.try_send(value) } fn stop_all(&self, about_to_stop: F) where F: Fn(usize), { let items = &mut *self.items.borrow_mut(); let cinner = &*self.inner.borrow_mut(); let mut next = cinner.active.head; while let Some(nkey) = next { let n = &mut items.nodes[nkey]; let ci = &mut n.value; about_to_stop(nkey); ci.stop = None; next = n.next; } } fn items_capacity(&self) -> usize { self.items.borrow().nodes.capacity() } fn can_stream(&self, ckey: usize) -> bool { let items = &*self.items.borrow(); match items.nodes.get(ckey) { Some(n) => { let ci = &n.value; // is stream mode with an id ci.shared.is_some() && ci.id.is_some() } None => false, } } fn batch_is_empty(&self) -> bool { let items = &*self.items.borrow(); items.batch.is_empty() } fn batch_len(&self) -> usize { let items = &*self.items.borrow(); items.batch.len() } fn batch_capacity(&self) -> usize { let items = &*self.items.borrow(); items.batch.capacity() } fn batch_clear(&self) { let items = &mut *self.items.borrow_mut(); items.batch.clear(); } fn batch_add(&self, ckey: usize) -> Result<(), ()> { let items = &mut *self.items.borrow_mut(); let ci = &mut items.nodes[ckey].value; let cshared = ci.shared.as_ref().unwrap().get(); // only batch connections with known handler addresses let addr_ref = cshared.to_addr(); let addr = match addr_ref.get() { Some(addr) => addr, None => return Err(()), }; let bkey = items.batch.add(addr, ckey)?; ci.batch_key = Some(bkey); Ok(()) } fn next_batch_message(&self, from: &str, btype: BatchType) -> Option<(usize, zmq::Message)> { let items = &mut *self.items.borrow_mut(); let nodes = &mut items.nodes; let batch = &mut items.batch; while !batch.is_empty() { let group = batch .take_group(|ckey| { let ci = &nodes[ckey].value; let cshared = ci.shared.as_ref().unwrap().get(); // item is guaranteed to have an id. only items with an // id are added to a batch, and if an item's id is // removed then the item is removed from the batch let id = ci.id.as_ref().unwrap(); (&id.1, cshared.out_seq()) }) .unwrap(); let count = group.ids().len(); assert!(count <= zhttppacket::IDS_MAX); let zreq = zhttppacket::Request { from: from.as_bytes(), ids: group.ids(), multi: true, ptype: match btype { BatchType::KeepAlive => zhttppacket::RequestPacket::KeepAlive, BatchType::Cancel => zhttppacket::RequestPacket::Cancel, }, ptype_str: "", }; let mut data = [0; BULK_PACKET_SIZE_MAX]; let size = match zreq.serialize(&mut data) { Ok(size) => size, Err(e) => { error!( "failed to serialize keep-alive packet with {} ids: {}", zreq.ids.len(), e ); continue; } }; let data = &data[..size]; let addr = group.addr(); let msg = { let mut v = vec![0; addr.len() + 1 + data.len()]; v[..addr.len()].copy_from_slice(addr); v[addr.len()] = b' '; let pos = addr.len() + 1; v[pos..(pos + data.len())].copy_from_slice(data); // this takes over the vec's memory without copying zmq::Message::from(v) }; drop(group); for &ckey in batch.last_group_ckeys() { let ci = &mut nodes[ckey].value; let cshared = ci.shared.as_ref().unwrap().get(); cshared.inc_out_seq(); ci.batch_key = None; } return Some((count, msg)); } None } } #[derive(Clone)] struct ConnectionOpts { instance_id: Rc, buffer_size: usize, timeout: Duration, rb_tmp: Rc, packet_buf: Rc>>, tmp_buf: Rc>>, } struct ConnectionReqOpts { body_buffer_size: usize, sender: channel::LocalSender<(MultipartHeader, zmq::Message)>, } struct ConnectionStreamOpts { blocks_max: usize, blocks_avail: Arc, messages_max: usize, allow_compression: bool, sender: channel::LocalSender, } struct Worker { thread: Option>, stop: Option>, } impl Worker { #[allow(clippy::too_many_arguments)] fn new( instance_id: &str, id: usize, req_maxconn: usize, stream_maxconn: usize, buffer_size: usize, body_buffer_size: usize, connection_blocks_max: usize, blocks_avail: &Arc, messages_max: usize, req_timeout: Duration, stream_timeout: Duration, allow_compression: bool, deny: &[IpNet], resolver: &Arc, pool: &Arc, zsockman: &Arc, handle_bound: usize, ) -> Self { debug!("client worker {}: starting", id); let (stop, r_stop) = channel::channel(1); let (s_ready, ready) = channel::channel(1); let instance_id = String::from(instance_id); let blocks_avail = Arc::clone(blocks_avail); let deny = deny.to_vec(); let resolver = Arc::clone(resolver); let pool = Arc::clone(pool); let zsockman = Arc::clone(zsockman); let thread = thread::Builder::new() .name(format!("client-worker-{}", id)) .spawn(move || { let maxconn = req_maxconn + stream_maxconn; // 1 task per connection, plus a handful of supporting tasks let tasks_max = maxconn + WORKER_NON_CONNECTION_TASKS_MAX; let registrations_max = REGISTRATIONS_PER_TASK_MAX * tasks_max; let reactor = Reactor::new(registrations_max); let executor = Executor::new(tasks_max); { let reactor = reactor.clone(); executor.set_pre_poll(move || { reactor.set_budget(Some(REACTOR_BUDGET)); }); } executor .spawn(Self::run( r_stop, s_ready, instance_id, id, req_maxconn, stream_maxconn, buffer_size, body_buffer_size, connection_blocks_max, blocks_avail, messages_max, req_timeout, stream_timeout, allow_compression, deny, resolver, pool, zsockman, handle_bound, )) .unwrap(); executor.run(|timeout| reactor.poll(timeout)).unwrap(); debug!("client worker {}: stopped", id); }) .unwrap(); ready.recv().unwrap(); Self { thread: Some(thread), stop: Some(stop), } } fn stop(&mut self) { self.stop = None; } #[allow(clippy::too_many_arguments)] async fn run( stop: channel::Receiver<()>, ready: channel::Sender<()>, instance_id: String, id: usize, req_maxconn: usize, stream_maxconn: usize, buffer_size: usize, body_buffer_size: usize, connection_blocks_max: usize, blocks_avail: Arc, messages_max: usize, req_timeout: Duration, stream_timeout: Duration, allow_compression: bool, deny: Vec, resolver: Arc, pool: Arc, zsockman: Arc, handle_bound: usize, ) { let executor = Executor::current().unwrap(); let reactor = Reactor::current().unwrap(); let stop = AsyncReceiver::new(stop); debug!("client-worker {}: allocating buffers", id); let rb_tmp = Rc::new(TmpBuffer::new(buffer_size * connection_blocks_max)); // large enough to fit anything let packet_buf = Rc::new(RefCell::new(vec![0; buffer_size + body_buffer_size + 4096])); // same size as working buffers let tmp_buf = Rc::new(RefCell::new(vec![0; buffer_size])); let instance_id = Rc::new(instance_id); let ka_batch = (stream_maxconn + (KEEP_ALIVE_BATCHES - 1)) / KEEP_ALIVE_BATCHES; let batch = Batch::new(ka_batch); let maxconn = req_maxconn + stream_maxconn; let conn_items = Rc::new(RefCell::new(ConnectionItems::new(maxconn, batch))); let req_conns = Rc::new(Connections::new(conn_items.clone(), req_maxconn)); let stream_conns = Rc::new(Connections::new(conn_items.clone(), stream_maxconn)); let (req_handle_stop, r_req_handle_stop) = async_local_channel(1, 1); let (stream_handle_stop, r_stream_handle_stop) = async_local_channel(1, 1); let (keep_alives_stop, r_keep_alives_stop) = async_local_channel(1, 1); let (s_req_handle_done, req_handle_done) = async_local_channel(1, 1); let (s_stream_handle_done, stream_handle_done) = async_local_channel(1, 1); let (s_keep_alives_done, keep_alives_done) = async_local_channel(1, 1); // max_senders is 1 per connection + 1 for the handle task + 1 for the keep alive task let (zstream_out_sender, zstream_out_receiver) = local_channel(handle_bound, stream_maxconn + 2); let zstream_out_receiver = AsyncLocalReceiver::new(zstream_out_receiver); let req_handle = zhttpsocket::AsyncServerReqHandle::new(zsockman.server_req_handle()); let stream_handle = zhttpsocket::AsyncServerStreamHandle::new(zsockman.server_stream_handle()); let deny = Rc::new(deny); executor .spawn(Self::req_handle_task( id, r_req_handle_stop, s_req_handle_done, executor.spawner(), Arc::clone(&resolver), Arc::clone(&pool), req_handle, req_maxconn, req_conns, body_buffer_size, Rc::clone(&deny), handle_bound, ConnectionOpts { instance_id: instance_id.clone(), buffer_size, timeout: req_timeout, rb_tmp: rb_tmp.clone(), packet_buf: packet_buf.clone(), tmp_buf: tmp_buf.clone(), }, )) .unwrap(); { let zstream_out_sender = zstream_out_sender .try_clone(&reactor.local_registration_memory()) .unwrap(); executor .spawn(Self::stream_handle_task( id, r_stream_handle_stop, s_stream_handle_done, zstream_out_receiver, zstream_out_sender, executor.spawner(), Arc::clone(&resolver), Arc::clone(&pool), stream_handle, stream_maxconn, stream_conns.clone(), connection_blocks_max, blocks_avail, messages_max, allow_compression, Rc::clone(&deny), ConnectionOpts { instance_id: instance_id.clone(), buffer_size, timeout: stream_timeout, rb_tmp: rb_tmp.clone(), packet_buf: packet_buf.clone(), tmp_buf: tmp_buf.clone(), }, )) .unwrap(); } executor .spawn(Self::keep_alives_task( id, r_keep_alives_stop, s_keep_alives_done, instance_id.clone(), zstream_out_sender, stream_conns.clone(), )) .unwrap(); debug!("client-worker {}: started", id); ready.send(()).unwrap(); drop(ready); // wait for stop let _ = stop.recv().await; // stop keep alives drop(keep_alives_stop); let _ = keep_alives_done.recv().await; // stop remaining tasks drop(req_handle_stop); drop(stream_handle_stop); let _ = req_handle_done.recv().await; let stream_handle = stream_handle_done.recv().await.unwrap(); // send cancels stream_conns.batch_clear(); let now = reactor.now(); let shutdown_timeout = Timeout::new(now + SHUTDOWN_TIMEOUT); let mut next_cancel_index = 0; 'outer: while next_cancel_index < stream_conns.items_capacity() { while stream_conns.batch_len() < stream_conns.batch_capacity() && next_cancel_index < stream_conns.items_capacity() { let key = next_cancel_index; next_cancel_index += 1; if stream_conns.can_stream(key) { // ignore errors let _ = stream_conns.batch_add(key); } } while let Some((count, msg)) = stream_conns.next_batch_message(&instance_id, BatchType::Cancel) { debug!( "client-worker {}: sending cancels for {} sessions", id, count ); match select_2(pin!(stream_handle.send(msg)), shutdown_timeout.elapsed()).await { Select2::R1(r) => r.unwrap(), Select2::R2(_) => break 'outer, } } stream_conns.batch_clear(); } } #[allow(clippy::too_many_arguments)] async fn req_handle_task( id: usize, stop: AsyncLocalReceiver<()>, _done: AsyncLocalSender<()>, spawner: Spawner, resolver: Arc, conn_pool: Arc, req_handle: zhttpsocket::AsyncServerReqHandle, req_maxconn: usize, conns: Rc, body_buffer_size: usize, deny: Rc>, handle_bound: usize, opts: ConnectionOpts, ) { let reactor = Reactor::current().unwrap(); let msg_retained_max = 1 + (MSG_RETAINED_PER_CONNECTION_MAX * req_maxconn); let req_scratch_mem = Rc::new(arena::RcMemory::new(msg_retained_max)); let req_req_mem = Rc::new(arena::RcMemory::new(msg_retained_max)); // max_senders is 1 per connection + 1 for this task let (zreq_sender, zreq_receiver) = local_channel(handle_bound, req_maxconn + 1); let zreq_receiver = AsyncLocalReceiver::new(zreq_receiver); // bound is 1 per connection, so all connections can indicate done at once // max_senders is 1 per connection + 1 for this task let (s_cdone, r_cdone) = channel::local_channel::( conns.max(), conns.max() + 1, &reactor.local_registration_memory(), ); let r_cdone = AsyncLocalReceiver::new(r_cdone); debug!("client-worker {}: task started: req_handle", id); let mut handle_send = pin!(None); loop { let receiver_recv = if handle_send.is_none() { Some(zreq_receiver.recv()) } else { None }; let req_handle_recv = if conns.count() < conns.max() { Some(req_handle.recv()) } else { None }; match select_5( stop.recv(), select_option(receiver_recv), select_option(handle_send.as_mut().as_pin_mut()), r_cdone.recv(), select_option(pin!(req_handle_recv).as_pin_mut()), ) .await { // stop.recv Select5::R1(_) => break, // receiver_recv Select5::R2(result) => match result { Ok((header, msg)) => handle_send.set(Some(req_handle.send(header, msg))), Err(e) => panic!("zreq_receiver channel error: {}", e), }, // handle_send Select5::R3(result) => { handle_send.set(None); if let Err(e) = result { error!("req send error: {}", e); } } // r_cdone.recv Select5::R4(result) => match result { Ok(done) => { let ret = conns.remove(done.ckey); // req mode doesn't have a sender assert!(ret.is_none()); } Err(e) => panic!("r_cdone channel error: {}", e), }, // req_handle_recv Select5::R5(result) => match result { Ok((header, msg)) => { let scratch = arena::Rc::new( RefCell::new(zhttppacket::ParseScratch::new()), &req_scratch_mem, ) .unwrap(); let zreq = match zhttppacket::OwnedRequest::parse(msg, 0, scratch) { Ok(zreq) => zreq, Err(e) => { warn!("client-worker {}: zhttp parse error: {}", id, e); continue; } }; let zreq_ref = zreq.get(); let ids = zreq_ref.ids; if ids.len() > 1 { warn!( "client-worker {}: request contained more than one id, skipping", id ); continue; } let from: ArrayVec = match ArrayVec::try_from(zreq_ref.from) { Ok(v) => v, Err(_) => { warn!("client-worker {}: from address too long, skipping", id); continue; } }; let cid: Option> = if !ids.is_empty() { match ArrayVec::try_from(ids[0].id) { Ok(v) => Some(v), Err(_) => { warn!("client-worker {}: request id too long, skipping", id); continue; } } } else { None }; let zreq = arena::Rc::new(zreq, &req_req_mem).unwrap(); let (cstop, r_cstop) = CancellationToken::new(&reactor.local_registration_memory()); let s_cdone = s_cdone .try_clone(&reactor.local_registration_memory()) .unwrap(); let zreq_sender = zreq_sender .try_clone(&reactor.local_registration_memory()) .unwrap(); let ckey = conns.add(cstop, None, None).unwrap(); if let Some(cid) = &cid { let cid = (from, cid.clone()); conns.set_id(ckey, Some(&cid)); } debug!( "client-worker {}: req conn starting {} {}/{}", id, ckey, conns.count(), conns.max(), ); if spawner .spawn(Self::req_connection_task( r_cstop, s_cdone, id, ckey, cid, (header, zreq), Arc::clone(&resolver), Arc::clone(&conn_pool), Rc::clone(&deny), opts.clone(), ConnectionReqOpts { body_buffer_size, sender: zreq_sender, }, )) .is_err() { // this should never happen. we only read a message // if we know we can spawn panic!("failed to spawn req_connection_task"); } } Err(e) => panic!("client-worker {}: handle read error {}", id, e), }, } } drop(s_cdone); conns.stop_all(|ckey| debug!("client-worker {}: stopping {}", id, ckey)); while r_cdone.recv().await.is_ok() {} debug!("client-worker {}: task stopped: req_handle", id); } #[allow(clippy::too_many_arguments)] async fn stream_handle_task( id: usize, stop: AsyncLocalReceiver<()>, done: AsyncLocalSender, zstream_out_receiver: AsyncLocalReceiver, zstream_out_sender: channel::LocalSender, spawner: Spawner, resolver: Arc, conn_pool: Arc, stream_handle: zhttpsocket::AsyncServerStreamHandle, stream_maxconn: usize, conns: Rc, connection_blocks_max: usize, blocks_avail: Arc, messages_max: usize, allow_compression: bool, deny: Rc>, opts: ConnectionOpts, ) { let reactor = Reactor::current().unwrap(); let stream_shared_mem = Rc::new(arena::RcMemory::new(stream_maxconn)); let zreceiver_pool = Rc::new(ChannelPool::new(stream_maxconn)); for _ in 0..stream_maxconn { zreceiver_pool.push(local_channel(REQ_SENDER_BOUND, 1)); } let msg_retained_max = 1 + (MSG_RETAINED_PER_CONNECTION_MAX * stream_maxconn); let stream_scratch_mem = Rc::new(arena::RcMemory::new(msg_retained_max)); let stream_req_mem = Rc::new(arena::RcMemory::new(msg_retained_max)); // bound is 1 per connection, so all connections can indicate done at once // max_senders is 1 per connection + 1 for this task let (s_cdone, r_cdone) = channel::local_channel::( conns.max(), conns.max() + 1, &reactor.local_registration_memory(), ); let r_cdone = AsyncLocalReceiver::new(r_cdone); debug!("client-worker {}: task started: stream_handle", id); { let mut handle_send = pin!(None); loop { let receiver_recv = if handle_send.is_none() { Some(zstream_out_receiver.recv()) } else { None }; let stream_handle_recv_from_any = if conns.count() < conns.max() { Some(stream_handle.recv_from_any()) } else { None }; match select_6( stop.recv(), select_option(receiver_recv), select_option(handle_send.as_mut().as_pin_mut()), r_cdone.recv(), select_option(pin!(stream_handle_recv_from_any).as_pin_mut()), pin!(stream_handle.recv_directed()), ) .await { // stop.recv Select6::R1(_) => break, // receiver_recv Select6::R2(result) => match result { Ok(msg) => handle_send.set(Some(stream_handle.send(msg))), Err(e) => panic!("zstream_out_receiver channel error: {}", e), }, // handle_send Select6::R3(result) => { handle_send.set(None); if let Err(e) = result { error!("stream send error: {}", e); } } // r_cdone.recv Select6::R4(result) => match result { Ok(done) => { let zreceiver_sender = conns.remove(done.ckey).unwrap(); let zreceiver = zreceiver_sender .make_receiver(&reactor.local_registration_memory()) .unwrap(); zreceiver.clear(); zreceiver_pool.push((zreceiver_sender, zreceiver)); } Err(e) => panic!("r_cdone channel error: {}", e), }, // stream_handle_recv_from_any Select6::R5(result) => match result { Ok(ret) => { let (msg, session) = ret; let scratch = arena::Rc::new( RefCell::new(zhttppacket::ParseScratch::new()), &stream_scratch_mem, ) .unwrap(); let zreq = match zhttppacket::OwnedRequest::parse(msg, 0, scratch) { Ok(zreq) => zreq, Err(e) => { warn!("client-worker {}: zhttp parse error: {}", id, e); continue; } }; let zreq_ref = zreq.get(); let ids = zreq_ref.ids; if ids.len() != 1 { warn!("client-worker {}: packet did not contain exactly one id, skipping", id); continue; } if ids[0].seq != Some(0) { warn!("client-worker {}: received message with seq != 0 as first message, skipping", id); continue; } if !zreq_ref.ptype_str.is_empty() { warn!("client-worker {}: received non-data message as first message, skipping", id); continue; } if zreq_ref.from.len() > FROM_MAX { warn!("client-worker {}: from address too long, skipping", id); continue; } let cid: ArrayVec = match ArrayVec::try_from(ids[0].id) { Ok(v) => v, Err(_) => { warn!("client-worker {}: request id too long, skipping", id); continue; } }; let zreq = arena::Rc::new(zreq, &stream_req_mem).unwrap(); let (cstop, r_cstop) = CancellationToken::new(&reactor.local_registration_memory()); let s_cdone = s_cdone .try_clone(&reactor.local_registration_memory()) .unwrap(); let zstream_out_sender = zstream_out_sender .try_clone(&reactor.local_registration_memory()) .unwrap(); let (zstream_receiver_sender, zstream_receiver) = zreceiver_pool.take().unwrap(); let shared = arena::Rc::new(StreamSharedData::new(), &stream_shared_mem) .unwrap(); let ckey = conns .add( cstop, Some(zstream_receiver_sender), Some(arena::Rc::clone(&shared)), ) .unwrap(); debug!( "client-worker {}: stream conn starting {} {}/{}", id, ckey, conns.count(), conns.max(), ); if spawner .spawn(Self::stream_connection_task( r_cstop, s_cdone, id, ckey, cid, arena::Rc::clone(&zreq), Arc::clone(&resolver), Arc::clone(&conn_pool), zstream_receiver, Rc::clone(&deny), Rc::clone(&conns), opts.clone(), ConnectionStreamOpts { blocks_max: connection_blocks_max, blocks_avail: Arc::clone(&blocks_avail), messages_max, allow_compression, sender: zstream_out_sender, }, shared, Some(session), )) .is_err() { // this should never happen. we only read a message // if we know we can spawn panic!("failed to spawn stream_connection_task"); } } Err(e) => panic!("client-worker {}: handle read error {}", id, e), }, // stream_handle.recv_directed Select6::R6(result) => match result { Ok(msg) => { let scratch = arena::Rc::new( RefCell::new(zhttppacket::ParseScratch::new()), &stream_scratch_mem, ) .unwrap(); let zreq = match zhttppacket::OwnedRequest::parse(msg, 0, scratch) { Ok(zreq) => zreq, Err(e) => { warn!("client-worker {}: zhttp parse error: {}", id, e); continue; } }; let zreq = arena::Rc::new(zreq, &stream_req_mem).unwrap(); let zreq_ref = zreq.get().get(); let ids = zreq_ref.ids; if ids.is_empty() { warn!("client-worker {}: packet contained no ids, skipping", id); continue; } let from: ArrayVec = match ArrayVec::try_from(zreq_ref.from) { Ok(v) => v, Err(_) => { warn!( "client-worker {}: from address too long, skipping", id ); continue; } }; let mut count = 0; for (i, rid) in ids.iter().enumerate() { let cid: ArrayVec = match ArrayVec::try_from(rid.id) { Ok(v) => v, Err(_) => { warn!( "client-worker {}: request id too long, skipping", id ); continue; } }; let cid = (from.clone(), cid); let key = match conns.find_key(&cid) { Some(key) => key, None => continue, }; // this should always succeed, since afterwards we yield // to let the connection receive the message match conns.try_send(key, (arena::Rc::clone(&zreq), i)) { Ok(()) => count += 1, Err(mpsc::TrySendError::Full(_)) => error!( "client-worker {}: connection-{} cannot receive message", id, key ), Err(mpsc::TrySendError::Disconnected(_)) => {} // conn task ended } } debug!( "client-worker {}: queued zmq message for {} conns", id, count ); if count > 0 { yield_to_local_events().await; } } Err(e) => panic!("client-worker {}: handle read error {}", id, e), }, } } } drop(s_cdone); conns.stop_all(|ckey| debug!("client-worker {}: stopping {}", id, ckey)); while r_cdone.recv().await.is_ok() {} // give the handle back done.send(stream_handle).await.unwrap(); debug!("client-worker {}: task stopped: stream_handle", id); } #[allow(clippy::too_many_arguments)] async fn req_connection_task( token: CancellationToken, done: channel::LocalSender, worker_id: usize, ckey: usize, cid: Option>, zreq: (MultipartHeader, arena::Rc), resolver: Arc, pool: Arc, deny: Rc>, opts: ConnectionOpts, req_opts: ConnectionReqOpts, ) { let done = AsyncLocalSender::new(done); debug!( "client-worker {}: task started: connection-{}", worker_id, ckey ); let log_id = if let Some(cid) = &cid { // zhttp ids are pretty much always valid strings, but we'll // do a lossy conversion just in case let cid_str = String::from_utf8_lossy(cid); format!("{}-{}-{}", worker_id, ckey, cid_str) } else { format!("{}-{}", worker_id, ckey) }; client_req_connection( token, &log_id, cid.as_deref(), zreq, opts.buffer_size, req_opts.body_buffer_size, &opts.rb_tmp, opts.packet_buf, opts.timeout, &deny, &resolver, &pool, AsyncLocalSender::new(req_opts.sender), ) .await; done.send(ConnectionDone { ckey }).await.unwrap(); debug!( "client-worker {}: task stopped: connection-{}", worker_id, ckey ); } #[allow(clippy::too_many_arguments)] async fn stream_connection_task( token: CancellationToken, done: channel::LocalSender, worker_id: usize, ckey: usize, cid: ArrayVec, zreq: arena::Rc, resolver: Arc, pool: Arc, zreceiver: channel::LocalReceiver<(arena::Rc, usize)>, deny: Rc>, conns: Rc, opts: ConnectionOpts, stream_opts: ConnectionStreamOpts, shared: arena::Rc, session: Option, ) { let done = AsyncLocalSender::new(done); let zreceiver = AsyncLocalReceiver::new(zreceiver); debug!( "client-worker {}: task started: connection-{}", worker_id, ckey ); let log_id = { // zhttp ids are pretty much always valid strings, but we'll // do a lossy conversion just in case let cid_str = String::from_utf8_lossy(&cid); format!("{}-{}-{}", worker_id, ckey, cid_str) }; client_stream_connection( token, &log_id, &cid, arena::Rc::clone(&zreq), opts.buffer_size, stream_opts.blocks_max, &stream_opts.blocks_avail, stream_opts.messages_max, &opts.rb_tmp, opts.packet_buf, opts.tmp_buf, opts.timeout, stream_opts.allow_compression, &deny, &opts.instance_id, &resolver, &pool, zreceiver, AsyncLocalSender::new(stream_opts.sender), shared, &|| { // handle task limits addr to FROM_MAX so this is guaranteed to succeed let from: ArrayVec = ArrayVec::try_from(zreq.get().get().from).unwrap(); let cid = (from, cid.clone()); conns.set_id(ckey, Some(&cid)) }, ) .await; drop(session); done.send(ConnectionDone { ckey }).await.unwrap(); debug!( "client-worker {}: task stopped: connection-{}", worker_id, ckey ); } async fn keep_alives_task( id: usize, stop: AsyncLocalReceiver<()>, _done: AsyncLocalSender<()>, instance_id: Rc, sender: channel::LocalSender, conns: Rc, ) { debug!("client-worker {}: task started: keep_alives", id); let reactor = Reactor::current().unwrap(); let mut keep_alive_count = 0; let mut next_keep_alive_time = reactor.now() + KEEP_ALIVE_INTERVAL; let next_keep_alive_timeout = Timeout::new(next_keep_alive_time); let mut next_keep_alive_index = 0; let sender_registration = reactor .register_custom_local(sender.get_write_registration(), mio::Interest::WRITABLE) .unwrap(); sender_registration.set_readiness(Some(mio::Interest::WRITABLE)); 'main: loop { while conns.batch_is_empty() { // wait for next keep alive time match select_2(stop.recv(), next_keep_alive_timeout.elapsed()).await { Select2::R1(_) => break 'main, Select2::R2(_) => {} } for _ in 0..conns.batch_capacity() { if next_keep_alive_index >= conns.items_capacity() { break; } let key = next_keep_alive_index; next_keep_alive_index += 1; if conns.can_stream(key) { // ignore errors let _ = conns.batch_add(key); } } keep_alive_count += 1; if keep_alive_count >= KEEP_ALIVE_BATCHES { keep_alive_count = 0; next_keep_alive_index = 0; } // keep steady pace next_keep_alive_time += KEEP_ALIVE_INTERVAL; next_keep_alive_timeout.set_deadline(next_keep_alive_time); } match select_2( stop.recv(), pin!(event_wait(&sender_registration, mio::Interest::WRITABLE)), ) .await { Select2::R1(_) => break, Select2::R2(_) => {} } if !sender.check_send() { // if check_send returns false, we'll be on the waitlist for a notification sender_registration.clear_readiness(mio::Interest::WRITABLE); continue; } // if check_send returns true, we are guaranteed to be able to send match conns.next_batch_message(&instance_id, BatchType::KeepAlive) { Some((count, msg)) => { debug!( "client-worker {}: sending keep alives for {} sessions", id, count ); if let Err(e) = sender.try_send(msg) { error!("zhttp write error: {}", e); } } None => { // this could happen if message construction failed sender.cancel(); } } if conns.batch_is_empty() { conns.batch_clear(); let now = reactor.now(); if now >= next_keep_alive_time + KEEP_ALIVE_INTERVAL { // got really behind somehow. just skip ahead next_keep_alive_time = now + KEEP_ALIVE_INTERVAL; next_keep_alive_timeout.set_deadline(next_keep_alive_time); } } } debug!("client-worker {}: task stopped: keep_alives", id); } } impl Drop for Worker { fn drop(&mut self) { self.stop(); let thread = self.thread.take().unwrap(); thread.join().unwrap(); } } pub struct Client { workers: Vec, } impl Client { #[allow(clippy::too_many_arguments)] pub fn new( instance_id: &str, worker_count: usize, req_maxconn: usize, stream_maxconn: usize, buffer_size: usize, body_buffer_size: usize, blocks_max: usize, connection_blocks_max: usize, messages_max: usize, req_timeout: Duration, stream_timeout: Duration, allow_compression: bool, deny: &[IpNet], zsockman: Arc, handle_bound: usize, ) -> Result { assert!(blocks_max >= stream_maxconn * 2); // 1 active query per connection let queries_max = req_maxconn + stream_maxconn; let resolver = Arc::new(Resolver::new(RESOLVER_THREADS, queries_max)); let pool_max = if can_move_mio_sockets_between_threads() { (req_maxconn + stream_maxconn) / 10 } else { // disable persistent connections 0 }; let pool = Arc::new(ConnectionPool::new(pool_max)); if !deny.is_empty() { info!("default policy: block outgoing connections to {:?}", deny); } let blocks_avail = Arc::new(Counter::new(blocks_max - (stream_maxconn * 2))); let mut workers = Vec::new(); for i in 0..worker_count { let w = Worker::new( instance_id, i, req_maxconn / worker_count, stream_maxconn / worker_count, buffer_size, body_buffer_size, connection_blocks_max, &blocks_avail, messages_max, req_timeout, stream_timeout, allow_compression, deny, &resolver, &pool, &zsockman, handle_bound, ); workers.push(w); } Ok(Self { workers }) } pub fn task_sizes() -> Vec<(String, usize)> { let req_task_size = { let reactor = Reactor::new(10); let (_, stop) = CancellationToken::new(&reactor.local_registration_memory()); let (done, _) = local_channel(1, 1); let (sender, _) = local_channel(1, 1); let req_scratch_mem = Rc::new(arena::RcMemory::new(1)); let req_req_mem = Rc::new(arena::RcMemory::new(1)); let msg_mem = Arc::new(arena::ArcMemory::new(1)); let scratch = arena::Rc::new( RefCell::new(zhttppacket::ParseScratch::new()), &req_scratch_mem, ) .unwrap(); let msg = concat!( "T161:4:from,6:client,2:id,1:1,3:seq,1:0#6:method,4:POST,3:uri", ",23:http://example.com/path,7:headers,34:30:12:Content-Type,1", "0:text/plain,]]4:body,5:hello,4:more,4:true!}", ); let msg = arena::Arc::new(zmq::Message::from(msg.as_bytes()), &msg_mem).unwrap(); let zreq = zhttppacket::OwnedRequest::parse(msg, 0, scratch).unwrap(); let zreq = arena::Rc::new(zreq, &req_req_mem).unwrap(); let resolver = Arc::new(Resolver::new(1, 1)); let pool = Arc::new(ConnectionPool::new(0)); let fut = Worker::req_connection_task( stop, done, 0, 0, None, (MultipartHeader::new(), zreq), resolver, pool, Rc::new(Vec::new()), ConnectionOpts { instance_id: Rc::new("".to_string()), buffer_size: 0, timeout: Duration::from_millis(0), rb_tmp: Rc::new(TmpBuffer::new(1)), packet_buf: Rc::new(RefCell::new(Vec::new())), tmp_buf: Rc::new(RefCell::new(Vec::new())), }, ConnectionReqOpts { body_buffer_size: 0, sender, }, ); mem::size_of_val(&fut) }; let stream_task_size = { let reactor = Reactor::new(10); let (_, stop) = CancellationToken::new(&reactor.local_registration_memory()); let (done, _) = local_channel(1, 1); let (_, zreceiver) = local_channel(1, 1); let (sender, _) = local_channel(1, 1); let batch = Batch::new(1); let conn_items = Rc::new(RefCell::new(ConnectionItems::new(1, batch))); let conns = Rc::new(Connections::new(conn_items, 1)); let req_scratch_mem = Rc::new(arena::RcMemory::new(1)); let req_req_mem = Rc::new(arena::RcMemory::new(1)); let msg_mem = Arc::new(arena::ArcMemory::new(1)); let scratch = arena::Rc::new( RefCell::new(zhttppacket::ParseScratch::new()), &req_scratch_mem, ) .unwrap(); let msg = concat!( "T161:4:from,6:client,2:id,1:1,3:seq,1:0#6:method,4:POST,3:uri", ",23:http://example.com/path,7:headers,34:30:12:Content-Type,1", "0:text/plain,]]4:body,5:hello,4:more,4:true!}", ); let msg = arena::Arc::new(zmq::Message::from(msg.as_bytes()), &msg_mem).unwrap(); let zreq = zhttppacket::OwnedRequest::parse(msg, 0, scratch).unwrap(); let zreq = arena::Rc::new(zreq, &req_req_mem).unwrap(); let resolver = Arc::new(Resolver::new(1, 1)); let pool = Arc::new(ConnectionPool::new(0)); let stream_shared_mem = Rc::new(arena::RcMemory::new(1)); let shared = arena::Rc::new(StreamSharedData::new(), &stream_shared_mem).unwrap(); let fut = Worker::stream_connection_task( stop, done, 0, 0, ArrayVec::new(), zreq, resolver, pool, zreceiver, Rc::new(Vec::new()), conns, ConnectionOpts { instance_id: Rc::new("".to_string()), buffer_size: 0, timeout: Duration::from_millis(0), rb_tmp: Rc::new(TmpBuffer::new(1)), packet_buf: Rc::new(RefCell::new(Vec::new())), tmp_buf: Rc::new(RefCell::new(Vec::new())), }, ConnectionStreamOpts { blocks_max: 2, blocks_avail: Arc::new(Counter::new(0)), messages_max: 0, allow_compression: false, sender, }, shared, None, ); mem::size_of_val(&fut) }; vec![ ("client_req_connection_task".to_string(), req_task_size), ( "client_stream_connection_task".to_string(), stream_task_size, ), ] } } impl Drop for Client { fn drop(&mut self) { for w in self.workers.iter_mut() { w.stop(); } } } #[derive(Debug, Eq, PartialEq)] enum StatusMessage { Started, ReqFinished, StreamFinished, } enum ControlMessage { Stop, Req(zmq::Message), Stream(zmq::Message), } pub struct TestClient { _client: Client, thread: Option>, status: channel::Receiver, control: channel::Sender, next_id: Cell, } impl TestClient { pub fn new(workers: usize) -> Self { let zmq_context = Arc::new(zmq::Context::new()); let req_maxconn = 100; let stream_maxconn = 100; let maxconn = req_maxconn + stream_maxconn; let mut zsockman = zhttpsocket::ServerSocketManager::new( Arc::clone(&zmq_context), "test", (MSG_RETAINED_PER_CONNECTION_MAX * maxconn) + (MSG_RETAINED_PER_WORKER_MAX * workers), 100, 100, 100, stream_maxconn, ); zsockman .set_server_req_specs(&[SpecInfo { spec: String::from("inproc://client-test"), bind: true, ipc_file_mode: 0, }]) .unwrap(); let zsockman = Arc::new(zsockman); let client = Client::new( "test", workers, req_maxconn, stream_maxconn, 1024, 1024, stream_maxconn * 2, 2, 10, Duration::from_secs(5), Duration::from_secs(5), false, &[], zsockman.clone(), 100, ) .unwrap(); zsockman .set_server_stream_specs( &[SpecInfo { spec: String::from("inproc://client-test-out"), bind: true, ipc_file_mode: 0, }], &[SpecInfo { spec: String::from("inproc://client-test-out-stream"), bind: true, ipc_file_mode: 0, }], &[SpecInfo { spec: String::from("inproc://client-test-in"), bind: true, ipc_file_mode: 0, }], ) .unwrap(); let (status_s, status_r) = channel::channel(1000); let (control_s, control_r) = channel::channel(1000); let thread = thread::spawn(move || { Self::run(status_s, control_r, zmq_context); }); // wait for handler thread to start assert_eq!(status_r.recv().unwrap(), StatusMessage::Started); Self { _client: client, thread: Some(thread), status: status_r, control: control_s, next_id: Cell::new(0), } } pub fn do_req(&self, addr: std::net::SocketAddr) { let msg = self.make_req_message(addr).unwrap(); self.control.send(ControlMessage::Req(msg)).unwrap(); } pub fn do_stream_http(&self, addr: std::net::SocketAddr) { let msg = self.make_stream_message(addr, false).unwrap(); self.control.send(ControlMessage::Stream(msg)).unwrap(); } pub fn do_stream_ws(&self, addr: std::net::SocketAddr) { let msg = self.make_stream_message(addr, true).unwrap(); self.control.send(ControlMessage::Stream(msg)).unwrap(); } pub fn wait_req(&self) { assert_eq!(self.status.recv().unwrap(), StatusMessage::ReqFinished); } pub fn wait_stream(&self) { assert_eq!(self.status.recv().unwrap(), StatusMessage::StreamFinished); } fn make_req_message(&self, addr: std::net::SocketAddr) -> Result { let mut dest = [0; 1024]; let mut cursor = io::Cursor::new(&mut dest[..]); cursor.write_all(b"T")?; let mut w = tnetstring::Writer::new(&mut cursor); w.start_map()?; let mut tmp = [0u8; 1024]; let id = { let id = self.next_id.get(); self.next_id.set(id + 1); let mut cursor = io::Cursor::new(&mut tmp[..]); write!(&mut cursor, "{}", id)?; let pos = cursor.position() as usize; &tmp[..pos] }; w.write_string(b"id")?; w.write_string(id)?; w.write_string(b"method")?; w.write_string(b"GET")?; let mut tmp = [0u8; 1024]; let uri = { let mut cursor = io::Cursor::new(&mut tmp[..]); write!(&mut cursor, "http://{}/path", addr)?; let pos = cursor.position() as usize; &tmp[..pos] }; w.write_string(b"uri")?; w.write_string(uri)?; w.end_map()?; w.flush()?; let size = cursor.position() as usize; Ok(zmq::Message::from(&dest[..size])) } fn make_stream_message( &self, addr: std::net::SocketAddr, ws: bool, ) -> Result { let mut dest = [0; 1024]; let mut cursor = io::Cursor::new(&mut dest[..]); cursor.write_all(b"T")?; let mut w = tnetstring::Writer::new(&mut cursor); w.start_map()?; w.write_string(b"from")?; w.write_string(b"handler")?; let mut tmp = [0u8; 1024]; let id = { let id = self.next_id.get(); self.next_id.set(id + 1); let mut cursor = io::Cursor::new(&mut tmp[..]); write!(&mut cursor, "{}", id)?; let pos = cursor.position() as usize; &tmp[..pos] }; w.write_string(b"id")?; w.write_string(id)?; w.write_string(b"seq")?; w.write_int(0)?; let mut tmp = [0u8; 1024]; let uri = if ws { let mut cursor = io::Cursor::new(&mut tmp[..]); write!(&mut cursor, "ws://{}/path", addr)?; let pos = cursor.position() as usize; &tmp[..pos] } else { w.write_string(b"method")?; w.write_string(b"GET")?; let mut cursor = io::Cursor::new(&mut tmp[..]); write!(&mut cursor, "http://{}/path", addr)?; let pos = cursor.position() as usize; &tmp[..pos] }; w.write_string(b"uri")?; w.write_string(uri)?; w.write_string(b"credits")?; w.write_int(1024)?; w.end_map()?; w.flush()?; let size = cursor.position() as usize; Ok(zmq::Message::from(&dest[..size])) } fn respond_msg( id: &[u8], seq: u32, ptype: &str, content_type: &str, body: &[u8], code: Option, ) -> Result { let mut dest = [0; 1024]; let mut cursor = io::Cursor::new(&mut dest[..]); cursor.write_all(b"T")?; let mut w = tnetstring::Writer::new(&mut cursor); w.start_map()?; w.write_string(b"from")?; w.write_string(b"handler")?; w.write_string(b"id")?; w.write_string(id)?; w.write_string(b"seq")?; w.write_int(seq as isize)?; if ptype.is_empty() { w.write_string(b"content-type")?; w.write_string(content_type.as_bytes())?; } else { w.write_string(b"type")?; w.write_string(ptype.as_bytes())?; } if let Some(x) = code { w.write_string(b"code")?; w.write_int(x as isize)?; } w.write_string(b"body")?; w.write_string(body)?; w.end_map()?; w.flush()?; let size = cursor.position() as usize; Ok(zmq::Message::from(&dest[..size])) } fn run( status: channel::Sender, control: channel::Receiver, zmq_context: Arc, ) { let req_sock = zmq_context.socket(zmq::DEALER).unwrap(); req_sock.connect("inproc://client-test").unwrap(); let out_sock = zmq_context.socket(zmq::PUSH).unwrap(); out_sock.connect("inproc://client-test-out").unwrap(); let out_stream_sock = zmq_context.socket(zmq::ROUTER).unwrap(); out_stream_sock .connect("inproc://client-test-out-stream") .unwrap(); let in_sock = zmq_context.socket(zmq::SUB).unwrap(); in_sock.set_subscribe(b"handler ").unwrap(); in_sock.connect("inproc://client-test-in").unwrap(); // ensure zsockman is subscribed thread::sleep(Duration::from_millis(100)); status.send(StatusMessage::Started).unwrap(); let mut poller = event::Poller::new(1).unwrap(); poller .register_custom( control.get_read_registration(), mio::Token(1), mio::Interest::READABLE, ) .unwrap(); poller .register( &mut SourceFd(&req_sock.get_fd().unwrap()), mio::Token(2), mio::Interest::READABLE, ) .unwrap(); poller .register( &mut SourceFd(&in_sock.get_fd().unwrap()), mio::Token(3), mio::Interest::READABLE, ) .unwrap(); let mut req_events = req_sock.get_events().unwrap(); let mut in_events = in_sock.get_events().unwrap(); 'main: loop { while req_events.contains(zmq::POLLIN) { let parts = match req_sock.recv_multipart(zmq::DONTWAIT) { Ok(parts) => parts, Err(zmq::Error::EAGAIN) => { req_events = req_sock.get_events().unwrap(); break; } Err(e) => panic!("recv error: {:?}", e), }; req_events = req_sock.get_events().unwrap(); assert_eq!(parts.len(), 2); let msg = &parts[1]; assert_eq!(msg[0], b'T'); let mut ptype = ""; let mut code: u16 = 0; let mut reason = ""; let mut body = b"".as_slice(); for f in tnetstring::parse_map(&msg[1..]).unwrap() { let f = f.unwrap(); match f.key { "type" => { let s = tnetstring::parse_string(f.data).unwrap(); ptype = str::from_utf8(s).unwrap(); } "code" => { let x = tnetstring::parse_int(f.data).unwrap(); code = x as u16; } "reason" => { let s = tnetstring::parse_string(f.data).unwrap(); reason = str::from_utf8(s).unwrap(); } "body" => { let s = tnetstring::parse_string(f.data).unwrap(); body = s; } _ => {} } } assert_eq!(ptype, ""); assert_eq!(code, 200); assert_eq!(reason, "OK"); assert_eq!(str::from_utf8(body).unwrap(), "hello\n"); status.send(StatusMessage::ReqFinished).unwrap(); } while in_events.contains(zmq::POLLIN) { let parts = match in_sock.recv_multipart(zmq::DONTWAIT) { Ok(parts) => parts, Err(zmq::Error::EAGAIN) => { in_events = in_sock.get_events().unwrap(); break; } Err(e) => panic!("recv error: {:?}", e), }; in_events = in_sock.get_events().unwrap(); assert_eq!(parts.len(), 1); let buf = &parts[0]; let mut pos = None; for (i, b) in buf.iter().enumerate() { if *b == b' ' { pos = Some(i); break; } } let pos = pos.unwrap(); let msg = &buf[(pos + 1)..]; assert_eq!(msg[0], b'T'); let mut id = ""; let mut seq = None; let mut ptype = ""; let mut code = None; let mut reason = ""; let mut content_type = ""; let mut body = &b""[..]; let mut more = false; for f in tnetstring::parse_map(&msg[1..]).unwrap() { let f = f.unwrap(); match f.key { "id" => { let s = tnetstring::parse_string(f.data).unwrap(); id = str::from_utf8(s).unwrap(); } "seq" => { let x = tnetstring::parse_int(f.data).unwrap(); seq = Some(x as u32); } "type" => { let s = tnetstring::parse_string(f.data).unwrap(); ptype = str::from_utf8(s).unwrap(); } "code" => { let x = tnetstring::parse_int(f.data).unwrap(); code = Some(x as u16); } "reason" => { let s = tnetstring::parse_string(f.data).unwrap(); reason = str::from_utf8(s).unwrap(); } "content-type" => { let s = tnetstring::parse_string(f.data).unwrap(); content_type = str::from_utf8(s).unwrap(); } "body" => { let s = tnetstring::parse_string(f.data).unwrap(); body = s; } "more" => { let b = tnetstring::parse_bool(f.data).unwrap(); more = b; } _ => {} } } let seq = seq.unwrap() + 1; // as a hack to make the test server stateless, respond to every message // using the received sequence number. for messages we don't care about, // respond with keep-alive in order to keep the sequencing going if ptype.is_empty() || ptype == "ping" || ptype == "pong" || ptype == "close" { if ptype.is_empty() && content_type.is_empty() { // assume http/ws accept, or http body if !reason.is_empty() { // http/ws accept let code = code.unwrap(); assert!(code == 200 || code == 101); if code == 200 { assert_eq!(reason, "OK"); assert_eq!(body.len(), 0); assert!(more); } else { // 101 assert_eq!(reason, "Switching Protocols"); assert_eq!(body.len(), 0); assert!(!more); } let msg = Self::respond_msg(id.as_bytes(), seq, "keep-alive", "", b"", None) .unwrap(); out_stream_sock .send_multipart( [ zmq::Message::from(b"test".as_slice()), zmq::Message::new(), msg, ], 0, ) .unwrap(); } else { // http body assert_eq!(str::from_utf8(body).unwrap(), "hello\n"); assert!(!more); status.send(StatusMessage::StreamFinished).unwrap(); } } else { // assume ws message if ptype == "ping" { ptype = "pong"; } // echo let msg = Self::respond_msg(id.as_bytes(), seq, ptype, content_type, body, code) .unwrap(); out_stream_sock .send_multipart( [ zmq::Message::from(b"test".as_slice()), zmq::Message::new(), msg, ], 0, ) .unwrap(); if ptype == "close" { status.send(StatusMessage::StreamFinished).unwrap(); } } } else { let msg = Self::respond_msg(id.as_bytes(), seq, "keep-alive", "", b"", None).unwrap(); out_stream_sock .send_multipart( [ zmq::Message::from(b"test".as_slice()), zmq::Message::new(), msg, ], 0, ) .unwrap(); } } poller.poll(None).unwrap(); for event in poller.iter_events() { match event.token() { mio::Token(1) => { while let Ok(msg) = control.try_recv() { match msg { ControlMessage::Stop => break 'main, ControlMessage::Req(msg) => { req_sock .send_multipart([zmq::Message::new(), msg], 0) .unwrap(); req_events = req_sock.get_events().unwrap(); } ControlMessage::Stream(msg) => out_sock.send(msg, 0).unwrap(), } } } mio::Token(2) => req_events = req_sock.get_events().unwrap(), mio::Token(3) => in_events = in_sock.get_events().unwrap(), _ => unreachable!(), } } } } } impl Drop for TestClient { fn drop(&mut self) { self.control.try_send(ControlMessage::Stop).unwrap(); let thread = self.thread.take().unwrap(); thread.join().unwrap(); } } #[cfg(test)] pub mod tests { use super::*; use crate::connection::calculate_ws_accept; use crate::websocket; use std::io::Read; use test_log::test; fn recv_frame( stream: &mut R, buf: &mut Vec, ) -> Result<(bool, u8, Vec), io::Error> { loop { let fi = match websocket::read_header(buf) { Ok(fi) => fi, Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => { let mut chunk = [0; 1024]; let size = stream.read(&mut chunk)?; if size == 0 { return Err(io::Error::from(io::ErrorKind::UnexpectedEof)); } buf.extend_from_slice(&chunk[..size]); continue; } Err(e) => return Err(e), }; while buf.len() < fi.payload_offset + fi.payload_size { let mut chunk = [0; 1024]; let size = stream.read(&mut chunk)?; if size == 0 { return Err(io::Error::from(io::ErrorKind::UnexpectedEof)); } buf.extend_from_slice(&chunk[..size]); } let mut content = Vec::from(&buf[fi.payload_offset..(fi.payload_offset + fi.payload_size)]); if let Some(mask) = fi.mask { websocket::apply_mask(&mut content, mask, 0); } *buf = buf.split_off(fi.payload_offset + fi.payload_size); return Ok((fi.fin, fi.opcode, content)); } } #[test] fn test_batch() { let mut batch = Batch::new(3); assert_eq!(batch.capacity(), 3); assert_eq!(batch.len(), 0); assert!(batch.last_group_ckeys().is_empty()); assert!(batch.add(b"addr-a", 1).is_ok()); assert!(batch.add(b"addr-a", 2).is_ok()); assert!(batch.add(b"addr-b", 3).is_ok()); assert_eq!(batch.len(), 3); assert!(batch.add(b"addr-c", 4).is_err()); assert_eq!(batch.len(), 3); assert_eq!(batch.is_empty(), false); let ids = ["id-1", "id-2", "id-3"]; let group = batch .take_group(|ckey| (ids[ckey - 1].as_bytes(), 0)) .unwrap(); assert_eq!(group.ids().len(), 2); assert_eq!(group.ids()[0].id, b"id-1"); assert_eq!(group.ids()[0].seq, Some(0)); assert_eq!(group.ids()[1].id, b"id-2"); assert_eq!(group.ids()[1].seq, Some(0)); assert_eq!(group.addr(), b"addr-a"); drop(group); assert_eq!(batch.is_empty(), false); assert_eq!(batch.last_group_ckeys(), &[1, 2]); let group = batch .take_group(|ckey| (ids[ckey - 1].as_bytes(), 0)) .unwrap(); assert_eq!(group.ids().len(), 1); assert_eq!(group.ids()[0].id, b"id-3"); assert_eq!(group.ids()[0].seq, Some(0)); assert_eq!(group.addr(), b"addr-b"); drop(group); assert_eq!(batch.is_empty(), true); assert_eq!(batch.last_group_ckeys(), &[3]); assert!(batch .take_group(|ckey| { (ids[ckey - 1].as_bytes(), 0) }) .is_none()); assert_eq!(batch.last_group_ckeys(), &[3]); } #[test] fn test_client() { let client = TestClient::new(1); let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); let addr = listener.local_addr().unwrap(); // req client.do_req(addr); let (mut stream, _) = listener.accept().unwrap(); let mut buf = Vec::new(); let mut req_end = 0; while req_end == 0 { let mut chunk = [0; 1024]; let size = stream.read(&mut chunk).unwrap(); buf.extend_from_slice(&chunk[..size]); for i in 0..(buf.len() - 3) { if &buf[i..(i + 4)] == b"\r\n\r\n" { req_end = i + 4; break; } } } let expected = format!( concat!("GET /path HTTP/1.1\r\n", "Host: {}\r\n", "\r\n"), addr ); assert_eq!(str::from_utf8(&buf[..req_end]).unwrap(), expected); stream .write( b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 6\r\n\r\nhello\n", ) .unwrap(); drop(stream); client.wait_req(); // stream (http) client.do_stream_http(addr); let (mut stream, _) = listener.accept().unwrap(); let mut buf = Vec::new(); let mut req_end = 0; while req_end == 0 { let mut chunk = [0; 1024]; let size = stream.read(&mut chunk).unwrap(); buf.extend_from_slice(&chunk[..size]); for i in 0..(buf.len() - 3) { if &buf[i..(i + 4)] == b"\r\n\r\n" { req_end = i + 4; break; } } } let expected = format!( concat!("GET /path HTTP/1.1\r\n", "Host: {}\r\n", "\r\n"), addr ); assert_eq!(str::from_utf8(&buf[..req_end]).unwrap(), expected); stream .write( b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 6\r\n\r\nhello\n", ) .unwrap(); drop(stream); client.wait_stream(); // stream (ws) client.do_stream_ws(addr); let (mut stream, _) = listener.accept().unwrap(); let mut buf = Vec::new(); let mut req_end = 0; while req_end == 0 { let mut chunk = [0; 1024]; let size = stream.read(&mut chunk).unwrap(); buf.extend_from_slice(&chunk[..size]); for i in 0..(buf.len() - 3) { if &buf[i..(i + 4)] == b"\r\n\r\n" { req_end = i + 4; break; } } } let req_buf = &buf[..req_end]; // use httparse to fish out Sec-WebSocket-Key let ws_key = { let mut headers = [httparse::EMPTY_HEADER; 32]; let mut req = httparse::Request::new(&mut headers); match req.parse(req_buf) { Ok(httparse::Status::Complete(_)) => {} _ => panic!("unexpected parse status"), } let mut ws_key = String::new(); for h in req.headers { if h.name.eq_ignore_ascii_case("Sec-WebSocket-Key") { ws_key = String::from_utf8(h.value.to_vec()).unwrap(); } } ws_key }; let expected = format!( concat!( "GET /path HTTP/1.1\r\n", "Host: {}\r\n", "Upgrade: websocket\r\n", "Connection: Upgrade\r\n", "Sec-WebSocket-Version: 13\r\n", "Sec-WebSocket-Key: {}\r\n", "\r\n" ), addr, ws_key, ); assert_eq!(str::from_utf8(&buf[..req_end]).unwrap(), expected); buf = buf.split_off(req_end); let ws_accept = calculate_ws_accept(ws_key.as_bytes()).unwrap(); let resp_data = format!( concat!( "HTTP/1.1 101 Switching Protocols\r\n", "Upgrade: websocket\r\n", "Connection: Upgrade\r\n", "Sec-WebSocket-Accept: {}\r\n", "\r\n", ), ws_accept ); stream.write(resp_data.as_bytes()).unwrap(); // send message let mut data = vec![0; 1024]; let body = &b"hello"[..]; let size = websocket::write_header( true, false, websocket::OPCODE_TEXT, body.len(), None, &mut data, ) .unwrap(); data[size..(size + body.len())].copy_from_slice(body); stream.write(&data[..(size + body.len())]).unwrap(); // recv message let (fin, opcode, content) = recv_frame(&mut stream, &mut buf).unwrap(); assert_eq!(fin, true); assert_eq!(opcode, websocket::OPCODE_TEXT); assert_eq!(str::from_utf8(&content).unwrap(), "hello"); } #[test] fn test_ws() { let client = TestClient::new(1); let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); let addr = listener.local_addr().unwrap(); client.do_stream_ws(addr); let (mut stream, _) = listener.accept().unwrap(); let mut buf = Vec::new(); let mut req_end = 0; while req_end == 0 { let mut chunk = [0; 1024]; let size = stream.read(&mut chunk).unwrap(); buf.extend_from_slice(&chunk[..size]); for i in 0..(buf.len() - 3) { if &buf[i..(i + 4)] == b"\r\n\r\n" { req_end = i + 4; break; } } } let req_buf = &buf[..req_end]; // use httparse to fish out Sec-WebSocket-Key let ws_key = { let mut headers = [httparse::EMPTY_HEADER; 32]; let mut req = httparse::Request::new(&mut headers); match req.parse(req_buf) { Ok(httparse::Status::Complete(_)) => {} _ => panic!("unexpected parse status"), } let mut ws_key = String::new(); for h in req.headers { if h.name.eq_ignore_ascii_case("Sec-WebSocket-Key") { ws_key = String::from_utf8(h.value.to_vec()).unwrap(); } } ws_key }; let expected = format!( concat!( "GET /path HTTP/1.1\r\n", "Host: {}\r\n", "Upgrade: websocket\r\n", "Connection: Upgrade\r\n", "Sec-WebSocket-Version: 13\r\n", "Sec-WebSocket-Key: {}\r\n", "\r\n" ), addr, ws_key, ); assert_eq!(str::from_utf8(&buf[..req_end]).unwrap(), expected); buf = buf.split_off(req_end); let ws_accept = calculate_ws_accept(ws_key.as_bytes()).unwrap(); let resp_data = format!( concat!( "HTTP/1.1 101 Switching Protocols\r\n", "Upgrade: websocket\r\n", "Connection: Upgrade\r\n", "Sec-WebSocket-Accept: {}\r\n", "\r\n", ), ws_accept ); stream.write(resp_data.as_bytes()).unwrap(); // send binary let mut data = vec![0; 1024]; let body = &[1, 2, 3][..]; let size = websocket::write_header( true, false, websocket::OPCODE_BINARY, body.len(), None, &mut data, ) .unwrap(); data[size..(size + body.len())].copy_from_slice(body); stream.write(&data[..(size + body.len())]).unwrap(); // recv binary let (fin, opcode, content) = recv_frame(&mut stream, &mut buf).unwrap(); assert_eq!(fin, true); assert_eq!(opcode, websocket::OPCODE_BINARY); assert_eq!(content, &[1, 2, 3][..]); buf.clear(); // send ping let mut data = vec![0; 1024]; let body = &b""[..]; let size = websocket::write_header( true, false, websocket::OPCODE_PING, body.len(), None, &mut data, ) .unwrap(); stream.write(&data[..size]).unwrap(); // recv pong let (fin, opcode, content) = recv_frame(&mut stream, &mut buf).unwrap(); assert_eq!(fin, true); assert_eq!(opcode, websocket::OPCODE_PONG); assert_eq!(str::from_utf8(&content).unwrap(), ""); buf.clear(); // send close let mut data = vec![0; 1024]; let body = &b"\x03\xf0gone"[..]; let size = websocket::write_header( true, false, websocket::OPCODE_CLOSE, body.len(), None, &mut data, ) .unwrap(); data[size..(size + body.len())].copy_from_slice(body); stream.write(&data[..(size + body.len())]).unwrap(); // recv close let (fin, opcode, content) = recv_frame(&mut stream, &mut buf).unwrap(); assert_eq!(fin, true); assert_eq!(opcode, websocket::OPCODE_CLOSE); assert_eq!(&content, &b"\x03\xf0gone"[..]); // expect tcp close let mut chunk = [0; 1024]; let size = stream.read(&mut chunk).unwrap(); assert_eq!(size, 0); client.wait_stream(); } #[cfg(target_arch = "x86_64")] #[cfg(debug_assertions)] #[test] fn test_task_sizes() { // sizes in debug mode at commit c0e4d161997e5c2880ba3409efe13afa3ec26fd7 const REQ_TASK_SIZE_BASE: usize = 6888; const STREAM_TASK_SIZE_BASE: usize = 12152; // cause tests to fail if sizes grow too much const GROWTH_LIMIT: usize = 1000; const REQ_TASK_SIZE_MAX: usize = REQ_TASK_SIZE_BASE + GROWTH_LIMIT; const STREAM_TASK_SIZE_MAX: usize = STREAM_TASK_SIZE_BASE + GROWTH_LIMIT; let sizes = Client::task_sizes(); assert_eq!(sizes[0].0, "client_req_connection_task"); assert!(sizes[0].1 <= REQ_TASK_SIZE_MAX); assert_eq!(sizes[1].0, "client_stream_connection_task"); assert!(sizes[1].1 <= STREAM_TASK_SIZE_MAX); } } pushpin-1.39.1/src/condure.rs000066400000000000000000000315071457610542000161010ustar00rootroot00000000000000/* * Copyright (C) 2020-2023 Fanout, Inc. * Copyright (C) 2023 Fastly, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use crate::client::Client; use crate::server::{Server, MSG_RETAINED_PER_CONNECTION_MAX, MSG_RETAINED_PER_WORKER_MAX}; use crate::websocket; use crate::zhttpsocket; use crate::zmq::SpecInfo; use crate::ListenConfig; use ipnet::IpNet; use log::info; use signal_hook; use signal_hook::consts::TERM_SIGNALS; use signal_hook::iterator::Signals; use std::cmp; use std::error::Error; use std::path::PathBuf; use std::sync::atomic::AtomicBool; use std::sync::Arc; use std::time::Duration; const INIT_HWM: usize = 128; fn make_specs(base: &str, is_server: bool) -> Result<(String, String, String), String> { if base.starts_with("ipc:") { if is_server { Ok(( format!("{}-{}", base, "in"), format!("{}-{}", base, "in-stream"), format!("{}-{}", base, "out"), )) } else { Ok(( format!("{}-{}", base, "out"), format!("{}-{}", base, "out-stream"), format!("{}-{}", base, "in"), )) } } else if base.starts_with("tcp:") { match base.rfind(':') { Some(pos) => match base[(pos + 1)..base.len()].parse::() { Ok(port) => Ok(( format!("{}:{}", &base[..pos], port), format!("{}:{}", &base[..pos], port + 1), format!("{}:{}", &base[..pos], port + 2), )), Err(e) => Err(format!("error parsing tcp port in base spec: {}", e)), }, None => Err("tcp base spec must specify port".into()), } } else { Err("base spec must be ipc or tcp".into()) } } pub struct Config { pub instance_id: String, pub workers: usize, pub req_maxconn: usize, pub stream_maxconn: usize, pub buffer_size: usize, pub body_buffer_size: usize, pub blocks_max: usize, pub connection_blocks_max: usize, pub messages_max: usize, pub req_timeout: Duration, pub stream_timeout: Duration, pub listen: Vec, pub zclient_req: Vec, pub zclient_stream: Vec, pub zclient_connect: bool, pub zserver_req: Vec, pub zserver_stream: Vec, pub zserver_connect: bool, pub ipc_file_mode: u32, pub certs_dir: PathBuf, pub allow_compression: bool, pub deny: Vec, } pub struct App { _server: Option, _client: Option, } impl App { pub fn new(config: &Config) -> Result { if config.req_maxconn < config.workers { return Err("req maxconn must be >= workers".into()); } if config.stream_maxconn < config.workers { return Err("stream maxconn must be >= workers".into()); } let zmq_context = Arc::new(zmq::Context::new()); // set hwm to 5% of maxconn let other_hwm = cmp::max((config.req_maxconn + config.stream_maxconn) / 20, 1); let handle_bound = cmp::max(other_hwm / config.workers, 1); let maxconn = config.req_maxconn + config.stream_maxconn; let server = if !config.listen.is_empty() { let mut any_req = false; let mut any_stream = false; for lc in config.listen.iter() { if lc.stream { any_stream = true; } else { any_req = true; } } let mut zsockman = zhttpsocket::ClientSocketManager::new( Arc::clone(&zmq_context), &config.instance_id, (MSG_RETAINED_PER_CONNECTION_MAX * maxconn) + (MSG_RETAINED_PER_WORKER_MAX * config.workers), INIT_HWM, other_hwm, handle_bound, ); if any_req { let mut specs = Vec::new(); for spec in config.zclient_req.iter() { if config.zclient_connect { info!("zhttp client connect {}", spec); } else { info!("zhttp client bind {}", spec); } specs.push(SpecInfo { spec: spec.clone(), bind: !config.zclient_connect, ipc_file_mode: config.ipc_file_mode, }); } if let Err(e) = zsockman.set_client_req_specs(&specs) { return Err(format!("failed to set zhttp client req specs: {}", e)); } } if any_stream { let mut out_specs = Vec::new(); let mut out_stream_specs = Vec::new(); let mut in_specs = Vec::new(); for spec in config.zclient_stream.iter() { let (out_spec, out_stream_spec, in_spec) = make_specs(spec, false)?; if config.zclient_connect { info!( "zhttp client connect {} {} {}", out_spec, out_stream_spec, in_spec ); } else { info!( "zhttp client bind {} {} {}", out_spec, out_stream_spec, in_spec ); } out_specs.push(SpecInfo { spec: out_spec, bind: !config.zclient_connect, ipc_file_mode: config.ipc_file_mode, }); out_stream_specs.push(SpecInfo { spec: out_stream_spec, bind: !config.zclient_connect, ipc_file_mode: config.ipc_file_mode, }); in_specs.push(SpecInfo { spec: in_spec, bind: !config.zclient_connect, ipc_file_mode: config.ipc_file_mode, }); } if let Err(e) = zsockman.set_client_stream_specs(&out_specs, &out_stream_specs, &in_specs) { return Err(format!("failed to set zhttp client stream specs: {}", e)); } } Some(Server::new( &config.instance_id, config.workers, config.req_maxconn, config.stream_maxconn, config.buffer_size, config.body_buffer_size, config.blocks_max, config.connection_blocks_max, config.messages_max, config.req_timeout, config.stream_timeout, &config.listen, config.certs_dir.as_path(), config.allow_compression, zsockman, handle_bound, )?) } else { None }; let client = if !config.zserver_req.is_empty() || !config.zserver_stream.is_empty() { let mut zsockman = zhttpsocket::ServerSocketManager::new( Arc::clone(&zmq_context), &config.instance_id, (MSG_RETAINED_PER_CONNECTION_MAX * maxconn) + (MSG_RETAINED_PER_WORKER_MAX * config.workers), INIT_HWM, other_hwm, handle_bound, config.stream_maxconn, ); if !config.zserver_req.is_empty() { let mut specs = Vec::new(); for spec in config.zserver_req.iter() { if config.zserver_connect { info!("zhttp server connect {}", spec); } else { info!("zhttp server bind {}", spec); } specs.push(SpecInfo { spec: spec.clone(), bind: !config.zserver_connect, ipc_file_mode: config.ipc_file_mode, }); } if let Err(e) = zsockman.set_server_req_specs(&specs) { return Err(format!("failed to set zhttp server req specs: {}", e)); } } let zsockman = Arc::new(zsockman); let client = Client::new( &config.instance_id, config.workers, config.req_maxconn, config.stream_maxconn, config.buffer_size, config.body_buffer_size, config.blocks_max, config.connection_blocks_max, config.messages_max, config.req_timeout, config.stream_timeout, config.allow_compression, &config.deny, zsockman.clone(), handle_bound, )?; // stream specs must only be applied after client is initialized if !config.zserver_stream.is_empty() { let mut in_specs = Vec::new(); let mut in_stream_specs = Vec::new(); let mut out_specs = Vec::new(); for spec in config.zserver_stream.iter() { let (in_spec, in_stream_spec, out_spec) = make_specs(spec, true)?; if config.zserver_connect { info!( "zhttp server connect {} {} {}", in_spec, in_stream_spec, out_spec ); } else { info!( "zhttp server bind {} {} {}", in_spec, in_stream_spec, out_spec ); } in_specs.push(SpecInfo { spec: in_spec, bind: !config.zserver_connect, ipc_file_mode: config.ipc_file_mode, }); in_stream_specs.push(SpecInfo { spec: in_stream_spec, bind: !config.zserver_connect, ipc_file_mode: config.ipc_file_mode, }); out_specs.push(SpecInfo { spec: out_spec, bind: !config.zserver_connect, ipc_file_mode: config.ipc_file_mode, }); } if let Err(e) = zsockman.set_server_stream_specs(&in_specs, &in_stream_specs, &out_specs) { return Err(format!("failed to set zhttp server stream specs: {}", e)); } } Some(client) } else { None }; Ok(Self { _server: server, _client: client, }) } pub fn wait_for_term(&self) { let mut signals = Signals::new(TERM_SIGNALS).unwrap(); let term_now = Arc::new(AtomicBool::new(false)); // ensure two term signals in a row causes the app to immediately exit for signal_type in TERM_SIGNALS { signal_hook::flag::register_conditional_shutdown( *signal_type, 1, // exit code Arc::clone(&term_now), ) .unwrap(); signal_hook::flag::register(*signal_type, Arc::clone(&term_now)).unwrap(); } // wait for termination let signal_type = signals.into_iter().next().unwrap(); assert!(TERM_SIGNALS.contains(&signal_type)); } pub fn sizes() -> Vec<(String, usize)> { let mut out = Vec::new(); out.extend(Server::task_sizes()); out.extend(Client::task_sizes()); out.push(( "deflate_codec_state".to_string(), websocket::deflate_codec_state_size(), )); out } } pub fn run(config: &Config) -> Result<(), Box> { info!("starting..."); { let a = match App::new(config) { Ok(a) => a, Err(e) => { return Err(e.into()); } }; info!("started"); a.wait_for_term(); info!("stopping..."); } info!("stopped"); Ok(()) } pushpin-1.39.1/src/config.rs000066400000000000000000000452271457610542000157130ustar00rootroot00000000000000/* * Copyright (C) 2023 Fastly, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use config::{Config, ConfigError}; use serde::Deserialize; use std::env; use std::error::Error; use std::path::{Path, PathBuf}; #[cfg(not(test))] use config::File; #[derive(Debug, Deserialize, Default)] pub struct Global { pub include: String, pub rundir: String, pub libdir: String, pub ipc_prefix: String, pub port_offset: i32, pub stats_connection_ttl: i32, pub stats_connection_send: bool, } impl From for config::ValueKind { fn from(global: Global) -> Self { let mut properties = std::collections::HashMap::new(); properties.insert("include".to_string(), config::Value::from(global.include)); properties.insert("rundir".to_string(), config::Value::from(global.rundir)); properties.insert("libdir".to_string(), config::Value::from(global.libdir)); properties.insert( "ipc_prefix".to_string(), config::Value::from(global.ipc_prefix), ); properties.insert( "port_offset".to_string(), config::Value::from(global.port_offset), ); properties.insert( "stats_connection_ttl".to_string(), config::Value::from(global.stats_connection_ttl), ); properties.insert( "stats_connection_send".to_string(), config::Value::from(global.stats_connection_send), ); Self::Table(properties) } } #[derive(serde::Deserialize, Eq, PartialEq, Debug, Default)] pub struct Runner { //runner rundir is deprecated pub rundir: String, pub services: String, pub http_port: String, pub https_ports: String, pub local_ports: String, pub logdir: String, pub log_level: String, pub client_buffer_size: i32, pub client_maxconn: i32, pub allow_compression: bool, pub condure_bin: String, } impl From for config::ValueKind { fn from(runner: Runner) -> Self { let mut properties = std::collections::HashMap::new(); properties.insert("rundir".to_string(), config::Value::from(runner.rundir)); properties.insert("services".to_string(), config::Value::from(runner.services)); properties.insert( "http_port".to_string(), config::Value::from(runner.http_port), ); properties.insert( "https_ports".to_string(), config::Value::from(runner.https_ports), ); properties.insert( "local_ports".to_string(), config::Value::from(runner.local_ports), ); properties.insert("logdir".to_string(), config::Value::from(runner.logdir)); properties.insert( "log_level".to_string(), config::Value::from(runner.log_level), ); properties.insert("client_buffer_size".to_string(), config::Value::from(8192)); properties.insert("client_maxconn".to_string(), config::Value::from(50000)); properties.insert( "allow_compression".to_string(), config::Value::from(runner.allow_compression), ); properties.insert( "condure_bin".to_string(), config::Value::from(runner.condure_bin), ); Self::Table(properties) } } #[derive(Debug, Deserialize, Default)] pub struct Proxy { pub routesfile: String, pub debug: bool, pub auto_cross_origin: bool, pub accept_x_forwarded_protocol: bool, pub set_x_forwarded_protocol: String, pub x_forwarded_for: String, pub x_forwarded_for_trusted: String, pub orig_headers_need_mark: String, pub accept_pushpin_route: bool, pub cdn_loop: String, pub log_from: bool, pub log_user_agent: bool, pub sig_iss: String, pub sig_key: String, pub upstream_key: String, pub sockjs_url: String, pub updates_check: String, pub organization_name: String, } impl From for config::ValueKind { fn from(proxy: Proxy) -> Self { let mut properties = std::collections::HashMap::new(); properties.insert( "routesfile".to_string(), config::Value::from(proxy.routesfile), ); properties.insert("debug".to_string(), config::Value::from(proxy.debug)); properties.insert( "auto_cross_origin".to_string(), config::Value::from(proxy.auto_cross_origin), ); properties.insert( "accept_x_forwarded_protocol".to_string(), config::Value::from(proxy.accept_x_forwarded_protocol), ); properties.insert( "set_x_forwarded_protocol".to_string(), config::Value::from(proxy.set_x_forwarded_protocol), ); properties.insert( "x_forwarded_for".to_string(), config::Value::from(proxy.x_forwarded_for), ); properties.insert( "x_forwarded_for_trusted".to_string(), config::Value::from(proxy.x_forwarded_for_trusted), ); properties.insert( "orig_headers_need_mark".to_string(), config::Value::from(proxy.orig_headers_need_mark), ); properties.insert( "accept_pushpin_route".to_string(), config::Value::from(proxy.accept_pushpin_route), ); properties.insert("cdn_loop".to_string(), config::Value::from(proxy.cdn_loop)); properties.insert("log_from".to_string(), config::Value::from(proxy.log_from)); properties.insert( "log_user_agent".to_string(), config::Value::from(proxy.log_user_agent), ); properties.insert("sig_iss".to_string(), config::Value::from(proxy.sig_iss)); properties.insert("sig_key".to_string(), config::Value::from(proxy.sig_key)); properties.insert( "upstream_key".to_string(), config::Value::from(proxy.upstream_key), ); properties.insert( "sockjs_url".to_string(), config::Value::from(proxy.sockjs_url), ); properties.insert( "updates_check".to_string(), config::Value::from(proxy.updates_check), ); properties.insert( "organization_name".to_string(), config::Value::from(proxy.organization_name), ); Self::Table(properties) } } #[derive(Debug, Deserialize, Default)] pub struct Handler { pub ipc_file_mode: u16, pub push_in_spec: String, pub push_in_sub_specs: String, pub push_in_sub_connect: bool, pub push_in_http_addr: String, pub push_in_http_port: u16, pub push_in_http_max_headers_size: u32, pub push_in_http_max_body_size: u32, pub stats_spec: String, pub command_spec: String, pub message_rate: u32, pub message_hwm: u32, pub message_block_size: u32, pub message_wait: u32, pub id_cache_ttl: u32, pub connection_subscription_max: u32, pub subscription_linger: u32, pub stats_subscription_ttl: u32, pub stats_report_interval: u32, pub stats_format: String, pub prometheus_port: String, pub prometheus_prefix: String, } impl From for config::ValueKind { fn from(handler: Handler) -> Self { let mut properties = std::collections::HashMap::new(); properties.insert( "ipc_file_mode".to_string(), config::Value::from(handler.ipc_file_mode), ); properties.insert( "push_in_spec".to_string(), config::Value::from(handler.push_in_spec), ); properties.insert( "push_in_sub_specs".to_string(), config::Value::from(handler.push_in_sub_specs), ); properties.insert( "push_in_sub_connect".to_string(), config::Value::from(handler.push_in_sub_connect), ); properties.insert( "push_in_http_addr".to_string(), config::Value::from(handler.push_in_http_addr), ); properties.insert( "push_in_http_port".to_string(), config::Value::from(handler.push_in_http_port), ); properties.insert( "push_in_http_max_headers_size".to_string(), config::Value::from(handler.push_in_http_max_headers_size), ); properties.insert( "push_in_http_max_body_size".to_string(), config::Value::from(handler.push_in_http_max_body_size), ); properties.insert( "stats_spec".to_string(), config::Value::from(handler.stats_spec), ); properties.insert( "command_spec".to_string(), config::Value::from(handler.command_spec), ); properties.insert( "message_rate".to_string(), config::Value::from(handler.message_rate), ); properties.insert( "message_hwm".to_string(), config::Value::from(handler.message_hwm), ); properties.insert( "message_block_size".to_string(), config::Value::from(handler.message_block_size), ); properties.insert( "message_wait".to_string(), config::Value::from(handler.message_wait), ); properties.insert( "id_cache_ttl".to_string(), config::Value::from(handler.id_cache_ttl), ); properties.insert( "connection_subscription_max".to_string(), config::Value::from(handler.connection_subscription_max), ); properties.insert( "subscription_linger".to_string(), config::Value::from(handler.subscription_linger), ); properties.insert( "stats_subscription_ttl".to_string(), config::Value::from(handler.stats_subscription_ttl), ); properties.insert( "stats_report_interval".to_string(), config::Value::from(handler.stats_report_interval), ); properties.insert( "stats_format".to_string(), config::Value::from(handler.stats_format), ); properties.insert( "prometheus_port".to_string(), config::Value::from(handler.prometheus_port), ); properties.insert( "prometheus_prefix".to_string(), config::Value::from(handler.prometheus_prefix), ); Self::Table(properties) } } #[derive(Debug, Deserialize, Default)] pub struct CustomConfig { pub global: Global, pub runner: Runner, pub proxy: Proxy, pub handler: Handler, } impl CustomConfig { #[cfg(not(test))] pub fn new(config_file: &str) -> Result { let config = Config::builder() .add_source(File::with_name(config_file).format(config::FileFormat::Ini)) .set_default("global", Global::default())? .set_default("runner", Runner::default())? .set_default("proxy", Proxy::default())? .set_default("handler", Handler::default())? .build()?; config.try_deserialize() } #[cfg(test)] pub fn new(_config_file: &str) -> Result { let config = Config::builder() .set_default( "global", Global { include: String::from("{libdir}/internal.conf"), rundir: String::from("run"), ipc_prefix: String::from("pushpin-"), port_offset: 0, stats_connection_ttl: 120, stats_connection_send: true, libdir: String::new(), }, )? .set_default( "runner", Runner { rundir: String::new(), services: String::from("condure,pushpin-proxy,pushpin-handler"), http_port: String::from("7999"), https_ports: String::from("443"), local_ports: String::from("{rundir}/{ipc_prefix}server"), logdir: String::from("log"), log_level: String::from("2"), client_buffer_size: 8192, client_maxconn: 50000, allow_compression: false, condure_bin: String::from("condure"), }, )? .set_default( "proxy", Proxy { routesfile: String::from("routes"), debug: false, auto_cross_origin: false, accept_x_forwarded_protocol: false, set_x_forwarded_protocol: String::from("proto-only"), x_forwarded_for: String::new(), x_forwarded_for_trusted: String::new(), orig_headers_need_mark: String::new(), accept_pushpin_route: false, cdn_loop: String::new(), log_from: false, log_user_agent: false, sig_iss: String::from("pushpin"), sig_key: String::from("changeme"), upstream_key: String::new(), sockjs_url: String::from("http://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js"), updates_check: String::from("report"), organization_name: String::new(), }, )? .set_default( "handler", Handler { ipc_file_mode: 777, push_in_spec: String::from("tcp://127.0.0.1:5560"), push_in_sub_specs: String::from("tcp://127.0.0.1:5562"), push_in_sub_connect: false, push_in_http_addr: String::from("127.0.0.1"), push_in_http_port: 5561, push_in_http_max_headers_size: 10000, push_in_http_max_body_size: 1000000, stats_spec: String::from("ipc://{rundir}/{ipc_prefix}stats"), command_spec: String::from("tcp://127.0.0.1:5563"), message_rate: 2500, message_hwm: 25000, message_block_size: 0, message_wait: 5000, id_cache_ttl: 60, connection_subscription_max: 20, subscription_linger: 60, stats_subscription_ttl: 60, stats_report_interval: 10, stats_format: String::from("tnetstring"), prometheus_port: String::new(), prometheus_prefix: String::new(), }, )? .build()?; config.try_deserialize() } } pub fn get_config_file( work_dir: &Path, arg_config: Option, ) -> Result> { let mut config_files: Vec = vec![]; match arg_config { Some(x) => config_files.push(x), None => { // ./config config_files.push(work_dir.join("config").join("pushpin.conf")); // same dir as executable (NOTE: deprecated) config_files.push(work_dir.join("pushpin.conf")); // ./examples/config config_files.push( work_dir .join("examples") .join("config") .join("pushpin.conf"), ); // default config_files.push(PathBuf::from(format!( "{}/pushpin.conf", env!("CONFIG_DIR") ))); } } let mut config_file = ""; for cf in config_files.iter() { if cf.is_file() { config_file = cf.to_str().unwrap_or(""); break; } } if config_file.is_empty() { return Err(format!( "no configuration file found. Tried: {}", config_files .iter() .map(|path_buf| path_buf.display().to_string()) .collect::>() .join(" ") ) .into()); } match Path::new(config_file).try_exists() { Ok(true) => {} Ok(false) => { return Err(format!("failed to open {}", config_file).into()); } Err(e) => { return Err(format!("failed to open {}, with error: {:?}", config_file, e).into()); } } Ok(config_file.into()) } #[cfg(test)] mod tests { use super::*; use crate::{ensure_example_config, test_dir}; use std::error::Error; use std::path::PathBuf; struct TestArgs { name: &'static str, work_dir: PathBuf, input: Option, output: Result>, } #[test] fn it_works() { let test_dir = test_dir(); ensure_example_config(&test_dir); let test_args: Vec = vec![TestArgs { name: "no input", work_dir: test_dir.clone(), input: None, output: Ok(test_dir .join("examples") .join("config") .join("pushpin.conf")), }]; for test_arg in test_args.iter() { assert_eq!( get_config_file(&test_arg.work_dir, test_arg.input.clone()).unwrap(), test_arg.output.as_deref().unwrap(), "{}", test_arg.name ); } } #[test] fn it_fails() { let test_args: Vec = vec![TestArgs { name: "invalid config file", work_dir: test_dir(), input: Some(PathBuf::from("no/such/file")), output: Err("no configuration file found. Tried: no/such/file".into()), }]; for test_arg in test_args.iter() { match get_config_file(&test_arg.work_dir, test_arg.input.clone()) { Ok(x) => panic!( "Test case {} should fail, but its passing with this output {:?}", test_arg.name, x ), Err(e) => { assert_eq!( e.to_string(), test_arg.output.as_deref().unwrap_err().to_string() ); } } } } } pushpin-1.39.1/src/connection.rs000066400000000000000000012305041457610542000166000ustar00rootroot00000000000000/* * Copyright (C) 2020-2023 Fanout, Inc. * Copyright (C) 2023 Fastly, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Note: Always Be Receiving (ABR) // // Connection handlers are expected to read ZHTTP messages as fast as // possible. If they don't, the whole thread could stall. This is by design, // to limit the number of to-be-processed messages in memory. They either // need to do something immediately with the messages, or discard them. // // Every await point must ensure messages keep getting read/processed, by // doing one of: // // - Directly awaiting a message. // - Awaiting a select that is awaiting a message. // - Wrapping other activity with discard_while(). // - Calling handle_other(), which itself will read messages. // - Awaiting something known to not block. #![allow(clippy::collapsible_if)] #![allow(clippy::collapsible_else_if)] use crate::arena; use crate::buffer::{ Buffer, ContiguousBuffer, LimitBufsMut, TmpBuffer, VecRingBuffer, VECTORED_MAX, }; use crate::counter::Counter; use crate::future::{ io_split, poll_async, select_2, select_3, select_4, select_option, AsyncLocalReceiver, AsyncLocalSender, AsyncRead, AsyncReadExt, AsyncResolver, AsyncTcpStream, AsyncTlsStream, AsyncWrite, AsyncWriteExt, CancellationToken, ReadHalf, Select2, Select3, Select4, StdWriteWrapper, Timeout, TlsWaker, WriteHalf, }; use crate::http1; use crate::net::SocketAddr; use crate::pool::Pool; use crate::reactor::Reactor; use crate::resolver; use crate::shuffle::random; use crate::tls::{TlsStream, VerifyMode}; use crate::track::{ self, track_future, Track, TrackFlag, TrackedAsyncLocalReceiver, ValueActiveError, }; use crate::waker::RefWakerData; use crate::websocket; use crate::zhttppacket; use crate::zmq::MultipartHeader; use crate::{pin, Defer}; use arrayvec::{ArrayString, ArrayVec}; use ipnet::IpNet; use log::{debug, log, warn, Level}; use sha1::{Digest, Sha1}; use std::cell::{Ref, RefCell}; use std::cmp; use std::collections::VecDeque; use std::convert::TryFrom; use std::future::Future; use std::io::{self, Read, Write}; use std::mem; use std::net::IpAddr; use std::pin::Pin; use std::rc::Rc; use std::str; use std::str::FromStr; use std::sync::{mpsc, Arc, Mutex}; use std::task::Context; use std::task::Poll; use std::thread; use std::time::{Duration, Instant}; const URI_SIZE_MAX: usize = 4096; const HEADERS_MAX: usize = 64; const WS_HASH_INPUT_MAX: usize = 256; const WS_KEY_MAX: usize = 24; // base64_encode([16 bytes]) = 24 bytes const WS_ACCEPT_MAX: usize = 28; // base64_encode(sha1_hash) = 28 bytes const REDIRECTS_MAX: usize = 8; const ZHTTP_SESSION_TIMEOUT: Duration = Duration::from_secs(60); const CONNECTION_POOL_TTL: Duration = Duration::from_secs(55); pub trait CidProvider { fn get_new_assigned_cid(&mut self) -> ArrayString<32>; } pub trait Identify { fn set_id(&mut self, id: &str); } #[derive(PartialEq)] enum Mode { HttpReq, HttpStream, WebSocket, } fn get_host<'a>(headers: &'a [httparse::Header]) -> &'a str { for h in headers.iter() { if h.name.eq_ignore_ascii_case("Host") { match str::from_utf8(h.value) { Ok(s) => return s, Err(_) => break, } } } "localhost" } fn gen_ws_key() -> ArrayString { let mut nonce = [0; 16]; for b in nonce.iter_mut() { *b = (random() % 256) as u8; } let mut output = [0; WS_KEY_MAX]; let size = base64::encode_config_slice(nonce, base64::STANDARD, &mut output); let output = str::from_utf8(&output[..size]).unwrap(); ArrayString::from_str(output).unwrap() } #[allow(clippy::result_unit_err)] pub fn calculate_ws_accept(key: &[u8]) -> Result, ()> { let input_len = key.len() + websocket::WS_GUID.len(); if input_len > WS_HASH_INPUT_MAX { return Err(()); } let mut input = [0; WS_HASH_INPUT_MAX]; input[..key.len()].copy_from_slice(key); input[key.len()..input_len].copy_from_slice(websocket::WS_GUID.as_bytes()); let input = &input[..input_len]; let mut hasher = Sha1::new(); hasher.update(input); let digest = hasher.finalize(); let mut output = [0; WS_ACCEPT_MAX]; let size = base64::encode_config_slice(digest, base64::STANDARD, &mut output); let output = match str::from_utf8(&output[..size]) { Ok(s) => s, Err(_) => return Err(()), }; Ok(ArrayString::from_str(output).unwrap()) } fn validate_ws_request( req: &http1::Request, ws_version: Option<&[u8]>, ws_key: Option<&[u8]>, ) -> Result, ()> { // a websocket request must not have a body. // some clients send "Content-Length: 0", which we'll allow. // chunked encoding will be rejected. if req.method == "GET" && (req.body_size == http1::BodySize::NoBody || req.body_size == http1::BodySize::Known(0)) && ws_version == Some(b"13") { if let Some(ws_key) = ws_key { return calculate_ws_accept(ws_key); } } Err(()) } fn validate_ws_response(ws_key: &[u8], ws_accept: Option<&[u8]>) -> Result<(), ()> { if let Some(ws_accept) = ws_accept { if calculate_ws_accept(ws_key)?.as_bytes() == ws_accept { return Ok(()); } } Err(()) } fn gen_mask() -> [u8; 4] { let mut out = [0; 4]; for b in out.iter_mut() { *b = (random() % 256) as u8; } out } fn write_ws_ext_header_value( config: &websocket::PerMessageDeflateConfig, dest: &mut W, ) -> Result<(), io::Error> { write!(dest, "permessage-deflate")?; config.serialize(dest) } #[allow(clippy::too_many_arguments)] fn make_zhttp_request( instance: &str, ids: &[zhttppacket::Id], method: &str, path: &str, headers: &[httparse::Header], body: &[u8], more: bool, mode: Mode, credits: u32, peer_addr: Option<&SocketAddr>, secure: bool, packet_buf: &mut [u8], ) -> Result { let mut data = zhttppacket::RequestData::new(); data.method = method; let host = get_host(headers); let mut zheaders = [zhttppacket::EMPTY_HEADER; HEADERS_MAX]; let mut zheaders_len = 0; for h in headers.iter() { zheaders[zheaders_len] = zhttppacket::Header { name: h.name, value: h.value, }; zheaders_len += 1; } data.headers = &zheaders[..zheaders_len]; let scheme = match mode { Mode::HttpReq | Mode::HttpStream => { if secure { "https" } else { "http" } } Mode::WebSocket => { if secure { "wss" } else { "ws" } } }; let mut uri = [0; URI_SIZE_MAX]; let mut c = io::Cursor::new(&mut uri[..]); write!(&mut c, "{}://{}{}", scheme, host, path)?; let size = c.position() as usize; data.uri = match str::from_utf8(&uri[..size]) { Ok(s) => s, Err(_) => return Err(io::Error::from(io::ErrorKind::InvalidData)), }; data.body = body; data.more = more; if mode == Mode::HttpStream { data.stream = true; } data.credits = credits; let mut addr = [0; 128]; if let Some(SocketAddr::Ip(peer_addr)) = peer_addr { let mut c = io::Cursor::new(&mut addr[..]); write!(&mut c, "{}", peer_addr.ip()).unwrap(); let size = c.position() as usize; data.peer_address = str::from_utf8(&addr[..size]).unwrap(); data.peer_port = peer_addr.port(); } let mut zreq = zhttppacket::Request::new_data(instance.as_bytes(), ids, data); zreq.multi = true; let size = zreq.serialize(packet_buf)?; Ok(zmq::Message::from(&packet_buf[..size])) } // return the capacity increase fn resize_write_buffer_if_full( buf: &mut VecRingBuffer, block_size: usize, blocks_max: usize, blocks_avail: &Counter, ) -> usize { assert!(blocks_max >= 2); // all but one block can be used for writing let allowed = blocks_max - 1; if buf.remaining_capacity() == 0 && buf.capacity() < block_size * allowed && blocks_avail.dec(1).is_ok() { buf.resize(buf.capacity() + block_size); block_size } else { 0 } } #[derive(Debug)] enum Error { Io(io::Error), Utf8(str::Utf8Error), Http(http1::Error), WebSocket(websocket::Error), InvalidWebSocketRequest, InvalidWebSocketResponse, Compression, BadMessage, Handler, HandlerCancel, BufferExceeded, Unusable, BadFrame, BadRequest, Tls, PolicyViolation, TooManyRedirects, ValueActive, StreamTimeout, SessionTimeout, Stopped, } impl Error { fn to_condition(&self) -> &'static str { match self { Error::Io(e) if e.kind() == io::ErrorKind::ConnectionRefused => { "remote-connection-failed" } Error::Io(e) if e.kind() == io::ErrorKind::TimedOut => "connection-timeout", Error::BadRequest => "bad-request", Error::StreamTimeout => "connection-timeout", Error::Tls => "tls-error", Error::PolicyViolation => "policy-violation", Error::TooManyRedirects => "too-many-redirects", _ => "undefined-condition", } } } impl From for Error { fn from(e: io::Error) -> Self { Self::Io(e) } } impl From for Error { fn from(e: str::Utf8Error) -> Self { Self::Utf8(e) } } impl From> for Error { fn from(_e: mpsc::SendError) -> Self { Self::Io(io::Error::from(io::ErrorKind::BrokenPipe)) } } impl From> for Error { fn from(e: mpsc::TrySendError) -> Self { let kind = match e { mpsc::TrySendError::Full(_) => io::ErrorKind::WriteZero, mpsc::TrySendError::Disconnected(_) => io::ErrorKind::BrokenPipe, }; Self::Io(io::Error::from(kind)) } } impl From for Error { fn from(e: http1::Error) -> Self { Self::Http(e) } } impl From for Error { fn from(e: websocket::Error) -> Self { Self::WebSocket(e) } } impl From for Error { fn from(_e: ValueActiveError) -> Self { Self::ValueActive } } impl From for Error { fn from(e: track::RecvError) -> Self { match e { track::RecvError::Disconnected => { Self::Io(io::Error::from(io::ErrorKind::UnexpectedEof)) } track::RecvError::ValueActive => Self::ValueActive, } } } #[derive(Clone, Copy)] struct MessageItem { mtype: u8, avail: usize, } struct MessageTracker { items: VecDeque, last_partial: bool, } impl MessageTracker { fn new(max_messages: usize) -> Self { Self { items: VecDeque::with_capacity(max_messages), last_partial: false, } } fn in_progress(&self) -> bool { self.last_partial } fn start(&mut self, mtype: u8) -> Result<(), ()> { if self.last_partial || self.items.len() == self.items.capacity() { return Err(()); } self.items.push_back(MessageItem { mtype, avail: 0 }); self.last_partial = true; Ok(()) } fn extend(&mut self, amt: usize) { assert!(self.last_partial); self.items.back_mut().unwrap().avail += amt; } fn done(&mut self) { self.last_partial = false; } // type, avail, done fn current(&self) -> Option<(u8, usize, bool)> { #[allow(clippy::comparison_chain)] if self.items.len() > 1 { let m = self.items.front().unwrap(); Some((m.mtype, m.avail, true)) } else if self.items.len() == 1 { let m = self.items.front().unwrap(); Some((m.mtype, m.avail, !self.last_partial)) } else { None } } fn consumed(&mut self, amt: usize, done: bool) { assert!(amt <= self.items[0].avail); self.items[0].avail -= amt; if done { assert_eq!(self.items[0].avail, 0); self.items.pop_front().unwrap(); } } } pub struct AddrRef<'a> { s: Ref<'a, Option>>, } impl<'a> AddrRef<'a> { pub fn get(&self) -> Option<&[u8]> { match &*self.s { Some(s) => Some(s.as_slice()), None => None, } } } struct StreamSharedDataInner { to_addr: Option>, out_seq: u32, } pub struct StreamSharedData { inner: RefCell, } #[allow(clippy::new_without_default)] impl StreamSharedData { pub fn new() -> Self { Self { inner: RefCell::new(StreamSharedDataInner { to_addr: None, out_seq: 0, }), } } fn reset(&self) { let s = &mut *self.inner.borrow_mut(); s.to_addr = None; s.out_seq = 0; } fn set_to_addr(&self, addr: Option>) { let s = &mut *self.inner.borrow_mut(); s.to_addr = addr; } pub fn to_addr(&self) -> AddrRef { AddrRef { s: Ref::map(self.inner.borrow(), |s| &s.to_addr), } } pub fn out_seq(&self) -> u32 { self.inner.borrow().out_seq } pub fn inc_out_seq(&self) { let s = &mut *self.inner.borrow_mut(); s.out_seq += 1; } } fn make_zhttp_req_response( id: Option<&[u8]>, ptype: zhttppacket::ResponsePacket, scratch: &mut [u8], ) -> Result { let mut ids_mem = [zhttppacket::Id { id: b"", seq: None }]; let ids = if let Some(id) = id { ids_mem[0].id = id; ids_mem.as_slice() } else { &[] }; let zresp = zhttppacket::Response { from: b"", ids, multi: false, ptype, ptype_str: "", }; let size = zresp.serialize(scratch)?; let payload = &scratch[..size]; Ok(zmq::Message::from(payload)) } fn make_zhttp_response( addr: &[u8], zresp: zhttppacket::Response, scratch: &mut [u8], ) -> Result { let size = zresp.serialize(scratch)?; let payload = &scratch[..size]; let mut v = vec![0; addr.len() + 1 + payload.len()]; v[..addr.len()].copy_from_slice(addr); v[addr.len()] = b' '; let pos = addr.len() + 1; v[pos..(pos + payload.len())].copy_from_slice(payload); // this takes over the vec's memory without copying Ok(zmq::Message::from(v)) } async fn recv_nonzero(r: &mut R, buf: &mut VecRingBuffer) -> Result<(), io::Error> { if buf.remaining_capacity() == 0 { return Err(io::Error::from(io::ErrorKind::WriteZero)); } let size = match r.read(buf.write_buf()).await { Ok(size) => size, Err(e) => return Err(e), }; buf.write_commit(size); if size == 0 { return Err(io::Error::from(io::ErrorKind::UnexpectedEof)); } Ok(()) } struct LimitedRingBuffer<'a> { inner: &'a mut VecRingBuffer, limit: usize, } impl AsRef<[u8]> for LimitedRingBuffer<'_> { fn as_ref(&self) -> &[u8] { let buf = Buffer::read_buf(self.inner); let limit = cmp::min(buf.len(), self.limit); &buf[..limit] } } struct HttpRead<'a, R: AsyncRead> { stream: ReadHalf<'a, R>, buf1: &'a mut VecRingBuffer, buf2: &'a mut VecRingBuffer, } struct HttpWrite<'a, W: AsyncWrite> { stream: WriteHalf<'a, W>, } struct RequestHandler<'a, R: AsyncRead, W: AsyncWrite> { r: HttpRead<'a, R>, w: HttpWrite<'a, W>, } impl<'a, R: AsyncRead, W: AsyncWrite> RequestHandler<'a, R, W> { fn new( stream: (ReadHalf<'a, R>, WriteHalf<'a, W>), buf1: &'a mut VecRingBuffer, buf2: &'a mut VecRingBuffer, ) -> Self { buf1.align(); buf2.clear(); Self { r: HttpRead { stream: stream.0, buf1, buf2, }, w: HttpWrite { stream: stream.1 }, } } // read from stream into buf, and parse buf as a request header async fn recv_request<'b: 'c, 'c, const N: usize>( mut self, mut scratch: &'b mut http1::ParseScratch, req_mem: &'c mut Option>, ) -> Result, Error> { let mut protocol = http1::ServerProtocol::new(); assert_eq!(protocol.state(), http1::ServerState::ReceivingRequest); loop { { let hbuf = self.r.buf1.take_inner(); match protocol.recv_request_owned(hbuf, scratch) { http1::ParseStatus::Complete(req) => { assert!([ http1::ServerState::ReceivingBody, http1::ServerState::AwaitingResponse ] .contains(&protocol.state())); *req_mem = Some(req); break Ok(RequestHeader { r: self.r, w: self.w, protocol, req_mem, }); } http1::ParseStatus::Incomplete((), hbuf, ret_scratch) => { // NOTE: after polonius it may not be necessary for // scratch to be returned scratch = ret_scratch; self.r.buf1.set_inner(hbuf); } http1::ParseStatus::Error(e, hbuf, _) => { self.r.buf1.set_inner(hbuf); return Err(e.into()); } } } if let Err(e) = recv_nonzero(&mut self.r.stream, self.r.buf1).await { if e.kind() == io::ErrorKind::WriteZero { return Err(Error::BufferExceeded); } return Err(e.into()); } } } } struct RequestHeader<'a, 'b, 'c, R: AsyncRead, W: AsyncWrite, const N: usize> { r: HttpRead<'a, R>, w: HttpWrite<'a, W>, protocol: http1::ServerProtocol, req_mem: &'c mut Option>, } impl<'a, 'b, 'c, R: AsyncRead, W: AsyncWrite, const N: usize> RequestHeader<'a, 'b, 'c, R, W, N> { fn request(&self) -> http1::Request { self.req_mem.as_ref().unwrap().get() } async fn start_recv_body(mut self) -> Result, Error> { self.handle_expect().await?; // restore the read ringbuffer self.discard_request(); Ok(self.into_recv_body().0) } async fn start_recv_body_and_keep_header( mut self, ) -> Result, Error> { self.handle_expect().await?; // we're keeping the request, so put any remaining bytes into buf2 // and swap the inner buffers. those bytes will then become readable // from buf1. we'll plan to give the request's inner buffer to buf2 // after the request is no longer needed let req = self.req_mem.as_ref().unwrap(); self.r.buf2.write_all(req.remaining_bytes())?; self.r.buf1.swap_inner(self.r.buf2); let (recv_body, req_mem) = self.into_recv_body(); Ok(RequestRecvBodyKeepHeader { inner: recv_body, req_mem, }) } fn recv_done(mut self) -> Result, Error> { // restore the read ringbuffer self.discard_request(); Ok(RequestStartResponse::new(self.r, self.w, self.protocol)) } // this method requires the request to exist async fn handle_expect(&mut self) -> Result<(), Error> { if !self.request().expect_100 { return Ok(()); } let mut cont = [0; 32]; let cont = { let mut c = io::Cursor::new(&mut cont[..]); if let Err(e) = self.protocol.send_100_continue(&mut c) { return Err(e.into()); } let size = c.position() as usize; &cont[..size] }; let mut left = cont.len(); while left > 0 { let pos = cont.len() - left; let size = match self.w.stream.write(&cont[pos..]).await { Ok(size) => size, Err(e) => return Err(e.into()), }; left -= size; } Ok(()) } // consumes request and gives the inner buffer back to buf1 fn discard_request(&mut self) { let req = self.req_mem.take().unwrap(); let remaining_len = req.remaining_bytes().len(); let inner_buf = req.into_buf(); let hsize = inner_buf.filled_len() - remaining_len; self.r.buf1.set_inner(inner_buf); self.r.buf1.read_commit(hsize); } fn into_recv_body( self, ) -> ( RequestRecvBody<'a, R, W>, &'c mut Option>, ) { ( RequestRecvBody { r: RefCell::new(RecvBodyRead { stream: self.r.stream, buf: self.r.buf1, }), wstream: self.w.stream, buf2: self.r.buf2, protocol: RefCell::new(self.protocol), }, self.req_mem, ) } } struct RecvBodyRead<'a, R: AsyncRead> { stream: ReadHalf<'a, R>, buf: &'a mut VecRingBuffer, } struct RequestRecvBody<'a, R: AsyncRead, W: AsyncWrite> { r: RefCell>, wstream: WriteHalf<'a, W>, buf2: &'a mut VecRingBuffer, protocol: RefCell, } impl<'a, R: AsyncRead, W: AsyncWrite> RequestRecvBody<'a, R, W> { fn more(&self) -> bool { self.protocol.borrow().state() == http1::ServerState::ReceivingBody } #[allow(clippy::await_holding_refcell_ref)] async fn add_to_recv_buffer(&self) -> Result<(), Error> { let r = &mut *self.r.borrow_mut(); if let Err(e) = recv_nonzero(&mut r.stream, r.buf).await { if e.kind() == io::ErrorKind::WriteZero { return Err(Error::BufferExceeded); } return Err(e.into()); } Ok(()) } fn try_recv_body(&self, dest: &mut [u8]) -> Option> { let r = &mut *self.r.borrow_mut(); let protocol = &mut *self.protocol.borrow_mut(); if protocol.state() == http1::ServerState::ReceivingBody { loop { let (size, read_size) = { let mut buf = io::Cursor::new(Buffer::read_buf(r.buf)); let mut headers = [httparse::EMPTY_HEADER; HEADERS_MAX]; let (size, _) = match protocol.recv_body(&mut buf, dest, &mut headers) { Ok(ret) => ret, Err(e) => return Some(Err(e.into())), }; let read_size = buf.position() as usize; (size, read_size) }; if protocol.state() == http1::ServerState::ReceivingBody && read_size == 0 { if !r.buf.is_readable_contiguous() { r.buf.align(); continue; } return None; } r.buf.read_commit(read_size); return Some(Ok(size)); } } assert_eq!(protocol.state(), http1::ServerState::AwaitingResponse); Some(Ok(0)) } async fn recv_body(&self, dest: &mut [u8]) -> Result { loop { if let Some(ret) = self.try_recv_body(dest) { return ret; } self.add_to_recv_buffer().await?; } } fn recv_done(self) -> RequestStartResponse<'a, R, W> { let r = self.r.into_inner(); RequestStartResponse::new( HttpRead { stream: r.stream, buf1: r.buf, buf2: self.buf2, }, HttpWrite { stream: self.wstream, }, self.protocol.into_inner(), ) } } struct RequestRecvBodyKeepHeader<'a, 'b, 'c, R: AsyncRead, W: AsyncWrite, const N: usize> { inner: RequestRecvBody<'a, R, W>, req_mem: &'c mut Option>, } impl<'a, 'b, 'c, R: AsyncRead, W: AsyncWrite, const N: usize> RequestRecvBodyKeepHeader<'a, 'b, 'c, R, W, N> { fn request(&self) -> http1::Request { self.req_mem.as_ref().unwrap().get() } async fn recv_body(&self, dest: &mut [u8]) -> Result { self.inner.recv_body(dest).await } fn recv_done(self) -> RequestStartResponse<'a, R, W> { // the request is no longer needed, so give its inner buffer to buf2 // and clear it let buf = self.req_mem.take().unwrap().into_buf(); self.inner.buf2.set_inner(buf); self.inner.buf2.clear(); self.inner.recv_done() } } struct RequestStartResponse<'a, R: AsyncRead, W: AsyncWrite> { r: HttpRead<'a, R>, w: HttpWrite<'a, W>, protocol: http1::ServerProtocol, } impl<'a, R: AsyncRead, W: AsyncWrite> RequestStartResponse<'a, R, W> { fn new(r: HttpRead<'a, R>, w: HttpWrite<'a, W>, protocol: http1::ServerProtocol) -> Self { Self { r, w, protocol } } async fn fill_recv_buffer(&mut self) -> Error { loop { if let Err(e) = recv_nonzero(&mut self.r.stream, self.r.buf1).await { if e.kind() == io::ErrorKind::WriteZero { // if there's no more space, suspend forever std::future::pending::<()>().await; } return e.into(); } } } fn prepare_response( mut self, code: u16, reason: &str, headers: &[http1::Header<'_>], body_size: http1::BodySize, ) -> Result, Error> { self.r.buf2.clear(); let mut hbuf = io::Cursor::new(self.r.buf2.write_buf()); if let Err(e) = self .protocol .send_response(&mut hbuf, code, reason, headers, body_size) { return Err(e.into()); } let size = hbuf.position() as usize; self.r.buf2.write_commit(size); let (stream, buf1, buf2) = ((self.r.stream, self.w.stream), self.r.buf1, self.r.buf2); Ok(RequestSendHeader::new( stream, buf1, buf2, self.protocol, size, )) } } struct SendHeaderRead<'a, R: AsyncRead> { stream: ReadHalf<'a, R>, buf: &'a mut VecRingBuffer, } struct EarlyBody { overflow: Option, done: bool, } struct RequestSendHeader<'a, R: AsyncRead, W: AsyncWrite> { r: RefCell>, wstream: RefCell>, wbuf: RefCell>, protocol: http1::ServerProtocol, early_body: RefCell, } impl<'a, R: AsyncRead, W: AsyncWrite> RequestSendHeader<'a, R, W> { fn new( stream: (ReadHalf<'a, R>, WriteHalf<'a, W>), buf1: &'a mut VecRingBuffer, buf2: &'a mut VecRingBuffer, protocol: http1::ServerProtocol, header_size: usize, ) -> Self { Self { r: RefCell::new(SendHeaderRead { stream: stream.0, buf: buf1, }), wstream: RefCell::new(stream.1), wbuf: RefCell::new(LimitedRingBuffer { inner: buf2, limit: header_size, }), protocol, early_body: RefCell::new(EarlyBody { overflow: None, done: false, }), } } #[allow(clippy::await_holding_refcell_ref)] async fn send_header(&self) -> Result<(), Error> { let mut stream = self.wstream.borrow_mut(); // limit = header bytes left while self.wbuf.borrow().limit > 0 { let size = stream.write_shared(&self.wbuf).await?; let mut wbuf = self.wbuf.borrow_mut(); wbuf.inner.read_commit(size); wbuf.limit -= size; } let mut wbuf = self.wbuf.borrow_mut(); let mut early_body = self.early_body.borrow_mut(); if let Some(overflow) = &mut early_body.overflow { wbuf.inner.write_all(Buffer::read_buf(overflow))?; early_body.overflow = None; } Ok(()) } fn append_body(&self, body: &[u8], more: bool, id: &str) -> Result<(), Error> { let mut wbuf = self.wbuf.borrow_mut(); let mut early_body = self.early_body.borrow_mut(); // limit = header bytes left if wbuf.limit > 0 { // if there are still header bytes in the buffer, then we may // need to overflow into a separate buffer if there's not enough // room let accepted = if early_body.overflow.is_none() { wbuf.inner.write(body)? } else { 0 }; if accepted < body.len() { debug!( "server-conn {}: overflowing {} bytes", id, body.len() - accepted ); if early_body.overflow.is_none() { // only allow overflowing as much as there are header // bytes left early_body.overflow = Some(ContiguousBuffer::new(wbuf.limit)); } let overflow = early_body.overflow.as_mut().unwrap(); overflow.write_all(&body[accepted..])?; } } else { // if the header has been fully cleared from the buffer, then // always write directly to the buffer wbuf.inner.write_all(body)?; } early_body.done = !more; Ok(()) } fn send_header_done(self) -> RequestSendBody<'a, R, W> { let r = self.r.into_inner(); let wstream = self.wstream.into_inner(); let wbuf = self.wbuf.into_inner(); let early_body = self.early_body.borrow(); assert_eq!(wbuf.limit, 0); assert!(early_body.overflow.is_none()); let (stream, buf1, buf2) = { ((r.stream, wstream), r.buf, wbuf.inner) }; let block_size = buf2.capacity(); RequestSendBody { r: RefCell::new(HttpSendBodyRead { stream: stream.0, buf: buf1, }), w: RefCell::new(HttpSendBodyWrite { stream: stream.1, buf: buf2, body_done: early_body.done, block_size, }), protocol: RefCell::new(self.protocol), } } } struct HttpSendBodyRead<'a, R: AsyncRead> { stream: ReadHalf<'a, R>, buf: &'a mut VecRingBuffer, } struct HttpSendBodyWrite<'a, W: AsyncWrite> { stream: WriteHalf<'a, W>, buf: &'a mut VecRingBuffer, body_done: bool, block_size: usize, } struct SendBodyFuture<'a, 'b, W: AsyncWrite> { w: &'a RefCell>, protocol: &'a RefCell, } impl<'a, 'b, W: AsyncWrite> Future for SendBodyFuture<'a, 'b, W> { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { let f = &*self; let w = &mut *f.w.borrow_mut(); let stream = &mut w.stream; if !stream.is_writable() { return Poll::Pending; } let protocol = &mut *f.protocol.borrow_mut(); let mut buf_arr = [&b""[..]; VECTORED_MAX - 2]; let bufs = w.buf.read_bufs(&mut buf_arr); match protocol.send_body( &mut StdWriteWrapper::new(Pin::new(&mut w.stream), cx), bufs, w.body_done, None, ) { Ok(size) => Poll::Ready(Ok(size)), Err(http1::Error::Io(e)) if e.kind() == io::ErrorKind::WouldBlock => Poll::Pending, Err(e) => Poll::Ready(Err(e.into())), } } } impl Drop for SendBodyFuture<'_, '_, W> { fn drop(&mut self) { self.w.borrow_mut().stream.cancel(); } } struct RequestSendBody<'a, R: AsyncRead, W: AsyncWrite> { r: RefCell>, w: RefCell>, protocol: RefCell, } impl<'a, R: AsyncRead, W: AsyncWrite> RequestSendBody<'a, R, W> { fn append_body(&self, body: &[u8], more: bool) -> Result<(), Error> { let w = &mut *self.w.borrow_mut(); w.buf.write_all(body)?; w.body_done = !more; Ok(()) } fn expand_write_buffer(&self, blocks_max: usize, blocks_avail: &Counter) -> usize { let w = &mut *self.w.borrow_mut(); resize_write_buffer_if_full(w.buf, w.block_size, blocks_max, blocks_avail) } fn can_flush(&self) -> bool { let w = &*self.w.borrow(); w.buf.len() > 0 || w.body_done } async fn flush_body(&self) -> Result<(usize, bool), Error> { { let protocol = &*self.protocol.borrow(); assert_eq!(protocol.state(), http1::ServerState::SendingBody); let w = &*self.w.borrow(); if w.buf.len() == 0 && !w.body_done { return Ok((0, false)); } } let size = SendBodyFuture { w: &self.w, protocol: &self.protocol, } .await?; let w = &mut *self.w.borrow_mut(); let protocol = &*self.protocol.borrow(); w.buf.read_commit(size); if w.buf.len() > 0 || !w.body_done || protocol.state() == http1::ServerState::SendingBody { return Ok((size, false)); } assert_eq!(protocol.state(), http1::ServerState::Finished); Ok((size, true)) } #[allow(clippy::await_holding_refcell_ref)] async fn send_body(&self, body: &[u8], more: bool) -> Result { let w = &mut *self.w.borrow_mut(); let protocol = &mut *self.protocol.borrow_mut(); assert_eq!(protocol.state(), http1::ServerState::SendingBody); Ok(protocol .send_body_async(&mut w.stream, &[body], !more, None) .await?) } #[allow(clippy::await_holding_refcell_ref)] async fn fill_recv_buffer(&self) -> Error { let r = &mut *self.r.borrow_mut(); loop { if let Err(e) = recv_nonzero(&mut r.stream, r.buf).await { if e.kind() == io::ErrorKind::WriteZero { // if there's no more space, suspend forever std::future::pending::<()>().await; } return e.into(); } } } fn finish(self) -> bool { self.protocol.borrow().is_persistent() } } struct WebSocketRead<'a, R: AsyncRead> { stream: ReadHalf<'a, R>, buf: &'a mut VecRingBuffer, } struct WebSocketWrite<'a, W: AsyncWrite> { stream: WriteHalf<'a, W>, buf: &'a mut VecRingBuffer, block_size: usize, } struct SendMessageContentFuture<'a, 'b, W: AsyncWrite, M> { w: &'a RefCell>, protocol: &'a websocket::Protocol, avail: usize, done: bool, } impl<'a, 'b, W: AsyncWrite, M: AsRef<[u8]> + AsMut<[u8]>> Future for SendMessageContentFuture<'a, 'b, W, M> { type Output = Result<(usize, bool), Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { let f = &*self; let w = &mut *f.w.borrow_mut(); let stream = &mut w.stream; if !stream.is_writable() { return Poll::Pending; } // protocol.send_message_content may add 1 element to vector let mut buf_arr = mem::MaybeUninit::<[&mut [u8]; VECTORED_MAX - 1]>::uninit(); let mut bufs = w.buf.read_bufs_mut(&mut buf_arr).limit(f.avail); match f.protocol.send_message_content( &mut StdWriteWrapper::new(Pin::new(&mut w.stream), cx), bufs.as_slice(), f.done, ) { Ok(ret) => Poll::Ready(Ok(ret)), Err(websocket::Error::Io(e)) if e.kind() == io::ErrorKind::WouldBlock => Poll::Pending, Err(e) => Poll::Ready(Err(e.into())), } } } impl Drop for SendMessageContentFuture<'_, '_, W, M> { fn drop(&mut self) { self.w.borrow_mut().stream.cancel(); } } struct WebSocketHandler<'a, R: AsyncRead, W: AsyncWrite> { r: RefCell>, w: RefCell>, protocol: websocket::Protocol>, } impl<'a, R: AsyncRead, W: AsyncWrite> WebSocketHandler<'a, R, W> { fn new( stream: (ReadHalf<'a, R>, WriteHalf<'a, W>), buf1: &'a mut VecRingBuffer, buf2: &'a mut VecRingBuffer, deflate_config: Option<(bool, VecRingBuffer)>, ) -> Self { buf2.clear(); let block_size = buf2.capacity(); Self { r: RefCell::new(WebSocketRead { stream: stream.0, buf: buf1, }), w: RefCell::new(WebSocketWrite { stream: stream.1, buf: buf2, block_size, }), protocol: websocket::Protocol::new(deflate_config), } } fn state(&self) -> websocket::State { self.protocol.state() } #[allow(clippy::await_holding_refcell_ref)] async fn add_to_recv_buffer(&self) -> Result<(), Error> { let r = &mut *self.r.borrow_mut(); if let Err(e) = recv_nonzero(&mut r.stream, r.buf).await { if e.kind() == io::ErrorKind::WriteZero { return Err(Error::BufferExceeded); } return Err(e.into()); } Ok(()) } fn try_recv_message_content( &self, dest: &mut [u8], ) -> Option> { let r = &mut *self.r.borrow_mut(); loop { match self.protocol.recv_message_content(r.buf, dest) { Some(Ok(ret)) => return Some(Ok(ret)), Some(Err(e)) => return Some(Err(e.into())), None => { if !r.buf.is_readable_contiguous() { r.buf.align(); continue; } return None; } } } } fn accept_avail(&self) -> usize { self.w.borrow().buf.remaining_capacity() } fn accept_body(&self, body: &[u8]) -> Result<(), Error> { let w = &mut *self.w.borrow_mut(); w.buf.write_all(body)?; Ok(()) } fn expand_write_buffer(&self, blocks_max: usize, blocks_avail: &Counter) -> usize { let w = &mut *self.w.borrow_mut(); resize_write_buffer_if_full(w.buf, w.block_size, blocks_max, blocks_avail) } fn is_sending_message(&self) -> bool { self.protocol.is_sending_message() } fn send_message_start(&self, opcode: u8, mask: Option<[u8; 4]>) { self.protocol.send_message_start(opcode, mask); } async fn send_message_content( &self, avail: usize, done: bool, bytes_sent: &F, ) -> Result<(usize, bool), Error> where F: Fn(), { loop { let (size, done) = SendMessageContentFuture { w: &self.w, protocol: &self.protocol, avail, done, } .await?; let w = &mut *self.w.borrow_mut(); if size == 0 && !done { continue; } w.buf.read_commit(size); bytes_sent(); return Ok((size, done)); } } } struct ZhttpStreamSessionOut<'a> { instance_id: &'a str, id: &'a str, packet_buf: &'a RefCell>, sender_stream: &'a AsyncLocalSender<(ArrayVec, zmq::Message)>, shared: &'a StreamSharedData, } impl<'a> ZhttpStreamSessionOut<'a> { fn new( instance_id: &'a str, id: &'a str, packet_buf: &'a RefCell>, sender_stream: &'a AsyncLocalSender<(ArrayVec, zmq::Message)>, shared: &'a StreamSharedData, ) -> Self { Self { instance_id, id, packet_buf, sender_stream, shared, } } async fn check_send(&self) { self.sender_stream.check_send().await } fn cancel_send(&self) { self.sender_stream.cancel(); } // this method is non-blocking, in order to increment the sequence number // and send the message in one shot, without concurrent activity // interfering with the sequencing. to send asynchronously, first await // on check_send and then call this method fn try_send_msg(&self, zreq: zhttppacket::Request) -> Result<(), Error> { let msg = { let mut zreq = zreq; let ids = [zhttppacket::Id { id: self.id.as_bytes(), seq: Some(self.shared.out_seq()), }]; zreq.from = self.instance_id.as_bytes(); zreq.ids = &ids; zreq.multi = true; let packet_buf = &mut *self.packet_buf.borrow_mut(); let size = zreq.serialize(packet_buf)?; zmq::Message::from(&packet_buf[..size]) }; let mut addr = ArrayVec::new(); if addr .try_extend_from_slice(self.shared.to_addr().get().unwrap()) .is_err() { return Err(io::Error::from(io::ErrorKind::InvalidInput).into()); } self.sender_stream.try_send((addr, msg))?; self.shared.inc_out_seq(); Ok(()) } } struct ZhttpServerStreamSessionOut<'a> { instance_id: &'a str, id: &'a [u8], packet_buf: &'a RefCell>, sender: &'a AsyncLocalSender, shared: &'a StreamSharedData, } impl<'a> ZhttpServerStreamSessionOut<'a> { fn new( instance_id: &'a str, id: &'a [u8], packet_buf: &'a RefCell>, sender: &'a AsyncLocalSender, shared: &'a StreamSharedData, ) -> Self { Self { instance_id, id, packet_buf, sender, shared, } } async fn check_send(&self) { self.sender.check_send().await } fn cancel_send(&self) { self.sender.cancel(); } // this method is non-blocking, in order to increment the sequence number // and send the message in one shot, without concurrent activity // interfering with the sequencing. to send asynchronously, first await // on check_send and then call this method fn try_send_msg(&self, zresp: zhttppacket::Response) -> Result<(), Error> { let msg = { let mut zresp = zresp; let ids = [zhttppacket::Id { id: self.id, seq: Some(self.shared.out_seq()), }]; zresp.from = self.instance_id.as_bytes(); zresp.ids = &ids; zresp.multi = true; let addr = self.shared.to_addr(); let addr = addr.get().unwrap(); let packet_buf = &mut *self.packet_buf.borrow_mut(); make_zhttp_response(addr, zresp, packet_buf)? }; self.sender.try_send(msg)?; self.shared.inc_out_seq(); Ok(()) } } struct ZhttpStreamSessionIn<'a, 'b, R> { id: &'a str, send_buf_size: usize, websocket: bool, receiver: &'a TrackedAsyncLocalReceiver<'b, (arena::Rc, usize)>, shared: &'a StreamSharedData, msg_read: &'a R, next: Option<(Track<'b, arena::Rc>, usize)>, seq: u32, credits: u32, first_data: bool, } impl<'a, 'b: 'a, R> ZhttpStreamSessionIn<'a, 'b, R> where R: Fn(), { fn new( id: &'a str, send_buf_size: usize, websocket: bool, receiver: &'a TrackedAsyncLocalReceiver<'b, (arena::Rc, usize)>, shared: &'a StreamSharedData, msg_read: &'a R, ) -> Self { Self { id, send_buf_size, websocket, receiver, shared, msg_read, next: None, seq: 0, credits: 0, first_data: true, } } fn credits(&self) -> u32 { self.credits } fn subtract_credits(&mut self, amount: u32) { self.credits -= amount; } async fn peek_msg(&mut self) -> Result<&arena::Rc, Error> { if self.next.is_none() { let (r, id_index) = loop { let (r, id_index) = Track::map_first(self.receiver.recv().await?); let zresp = r.get().get(); if zresp.ids[id_index].id != self.id.as_bytes() { // skip messages addressed to old ids continue; } break (r, id_index); }; let zresp = r.get().get(); if !zresp.ptype_str.is_empty() { debug!( "server-conn {}: handle packet: {}", self.id, zresp.ptype_str ); } else { debug!("server-conn {}: handle packet: (data)", self.id); } if zresp.ids.is_empty() { return Err(Error::BadMessage); } if let Some(seq) = zresp.ids[id_index].seq { if seq != self.seq { debug!( "server-conn {}: bad seq (expected {}, got {}), skipping", self.id, self.seq, seq ); return Err(Error::BadMessage); } self.seq += 1; } let mut addr = ArrayVec::new(); if addr.try_extend_from_slice(zresp.from).is_err() { return Err(Error::BadMessage); } self.shared.set_to_addr(Some(addr)); (self.msg_read)(); match &zresp.ptype { zhttppacket::ResponsePacket::Data(rdata) => { let mut credits = rdata.credits; if self.first_data { self.first_data = false; if self.websocket && credits == 0 { // workaround for pushpin-proxy, which doesn't // send credits on websocket accept credits = self.send_buf_size as u32; debug!( "server-conn {}: no credits in websocket accept, assuming {}", self.id, credits ); } } self.credits += credits; } zhttppacket::ResponsePacket::Error(edata) => { debug!( "server-conn {}: zhttp error condition={}", self.id, edata.condition ); } zhttppacket::ResponsePacket::Credit(cdata) => { self.credits += cdata.credits; } zhttppacket::ResponsePacket::Ping(pdata) => { self.credits += pdata.credits; } zhttppacket::ResponsePacket::Pong(pdata) => { self.credits += pdata.credits; } _ => {} } self.next = Some((r, id_index)); } Ok(&self.next.as_ref().unwrap().0) } async fn recv_msg( &mut self, ) -> Result>, Error> { self.peek_msg().await?; Ok(self.next.take().unwrap().0) } } struct ZhttpServerStreamSessionIn<'a, 'b, R> { log_id: &'a str, id: &'a [u8], receiver: &'a TrackedAsyncLocalReceiver<'b, (arena::Rc, usize)>, shared: &'a StreamSharedData, msg_read: &'a R, next: Option<(Track<'b, arena::Rc>, usize)>, seq: u32, credits: u32, } impl<'a, 'b: 'a, R> ZhttpServerStreamSessionIn<'a, 'b, R> where R: Fn(), { fn new( log_id: &'a str, id: &'a [u8], credits: u32, receiver: &'a TrackedAsyncLocalReceiver<'b, (arena::Rc, usize)>, shared: &'a StreamSharedData, msg_read: &'a R, ) -> Self { Self { log_id, id, receiver, shared, msg_read, next: None, seq: 1, credits, } } fn credits(&self) -> u32 { self.credits } fn subtract_credits(&mut self, amount: u32) { self.credits -= amount; } async fn peek_msg(&mut self) -> Result<&arena::Rc, Error> { if self.next.is_none() { let (r, id_index) = loop { let (r, id_index) = Track::map_first(self.receiver.recv().await?); let zreq = r.get().get(); if zreq.ids[id_index].id != self.id { // skip messages addressed to old ids continue; } break (r, id_index); }; let zreq = r.get().get(); if !zreq.ptype_str.is_empty() { debug!( "client-conn {}: handle packet: {}", self.log_id, zreq.ptype_str ); } else { debug!("client-conn {}: handle packet: (data)", self.log_id); } if zreq.ids.is_empty() { return Err(Error::BadMessage); } if let Some(seq) = zreq.ids[id_index].seq { if seq != self.seq { debug!( "client-conn {}: bad seq (expected {}, got {}), skipping", self.log_id, self.seq, seq ); return Err(Error::BadMessage); } self.seq += 1; } let mut addr = ArrayVec::new(); if addr.try_extend_from_slice(zreq.from).is_err() { return Err(Error::BadMessage); } self.shared.set_to_addr(Some(addr)); (self.msg_read)(); match &zreq.ptype { zhttppacket::RequestPacket::Data(rdata) => { self.credits += rdata.credits; } zhttppacket::RequestPacket::Error(edata) => { debug!( "client-conn {}: zhttp error condition={}", self.log_id, edata.condition ); } zhttppacket::RequestPacket::Credit(cdata) => { self.credits += cdata.credits; } zhttppacket::RequestPacket::Ping(pdata) => { self.credits += pdata.credits; } zhttppacket::RequestPacket::Pong(pdata) => { self.credits += pdata.credits; } _ => {} } self.next = Some((r, id_index)); } Ok(&self.next.as_ref().unwrap().0) } async fn recv_msg(&mut self) -> Result>, Error> { self.peek_msg().await?; Ok(self.next.take().unwrap().0) } } async fn send_msg(sender: &AsyncLocalSender, msg: zmq::Message) -> Result<(), Error> { Ok(sender.send(msg).await?) } async fn discard_while( receiver: &TrackedAsyncLocalReceiver<'_, (arena::Rc, usize)>, fut: F, ) -> F::Output where F: Future> + Unpin, { match select_2(fut, pin!(receiver.recv())).await { Select2::R1(v) => v, Select2::R2(ret) => { ret?; // unexpected message in current state Err(Error::BadMessage) } } } async fn server_discard_while( receiver: &TrackedAsyncLocalReceiver<'_, (arena::Rc, usize)>, fut: F, ) -> F::Output where F: Future> + Unpin, { match select_2(fut, pin!(receiver.recv())).await { Select2::R1(v) => v, Select2::R2(_) => Err(Error::BadMessage), // unexpected message in current state } } // return true if persistent #[allow(clippy::too_many_arguments)] async fn server_req_handler( id: &str, stream: &mut S, peer_addr: Option<&SocketAddr>, secure: bool, buf1: &mut VecRingBuffer, buf2: &mut VecRingBuffer, body_buf: &mut ContiguousBuffer, packet_buf: &RefCell>, zsender: &AsyncLocalSender, zreceiver: &TrackedAsyncLocalReceiver<'_, (arena::Rc, usize)>, ) -> Result { let stream = RefCell::new(stream); let handler = RequestHandler::new(io_split(&stream), buf1, buf2); let mut scratch = http1::ParseScratch::::new(); let mut req_mem = None; // receive request header // ABR: discard_while let handler = match discard_while( zreceiver, pin!(handler.recv_request(&mut scratch, &mut req_mem)), ) .await { Ok(handler) => handler, Err(Error::Io(e)) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(false), Err(e) => return Err(e), }; // log request { let req = handler.request(); let host = get_host(req.headers); let scheme = if secure { "https" } else { "http" }; debug!( "server-conn {}: request: {} {}://{}{}", id, req.method, scheme, host, req.uri ); } // receive request body // ABR: discard_while let handler = discard_while(zreceiver, pin!(handler.start_recv_body_and_keep_header())).await?; loop { // ABR: discard_while let size = discard_while(zreceiver, pin!(handler.recv_body(body_buf.write_buf()))).await?; if size == 0 { break; } body_buf.write_commit(size); } // determine how to respond let msg = { let req = handler.request(); let mut websocket = false; for h in req.headers.iter() { if h.name.eq_ignore_ascii_case("Upgrade") && h.value == b"websocket" { websocket = true; break; } } if websocket { // websocket requests are not supported in req mode // toss the request body body_buf.clear(); None } else { // regular http requests we can handle // prepare zmq message let ids = [zhttppacket::Id { id: id.as_bytes(), seq: None, }]; let msg = make_zhttp_request( "", &ids, req.method, req.uri, req.headers, Buffer::read_buf(body_buf), false, Mode::HttpReq, 0, peer_addr, secure, &mut packet_buf.borrow_mut(), )?; // body consumed body_buf.clear(); Some(msg) } }; let (handler, websocket) = if let Some(msg) = msg { // handle as http let handler = handler.recv_done(); // send message // ABR: discard_while discard_while(zreceiver, pin!(send_msg(zsender, msg))).await?; // receive message let zresp = loop { // ABR: direct read let (zresp, id_index) = Track::map_first(zreceiver.recv().await?); let zresp_ref = zresp.get().get(); if zresp_ref.ids[id_index].id != id.as_bytes() { // skip messages addressed to old ids continue; } if !zresp_ref.ptype_str.is_empty() { debug!("server-conn {}: handle packet: {}", id, zresp_ref.ptype_str); } else { debug!("server-conn {}: handle packet: (data)", id); } // skip non-data messages match &zresp_ref.ptype { zhttppacket::ResponsePacket::Data(_) => break zresp, _ => debug!( "server-conn {}: unexpected packet in req mode: {}", id, zresp_ref.ptype_str ), } }; let handler = { let zresp = zresp.get().get(); let rdata = match &zresp.ptype { zhttppacket::ResponsePacket::Data(rdata) => rdata, _ => unreachable!(), // we confirmed the type above }; // send response header let mut headers = [http1::EMPTY_HEADER; HEADERS_MAX]; let mut headers_len = 0; for h in rdata.headers.iter() { if headers_len >= headers.len() { return Err(Error::BadMessage); } headers[headers_len] = http1::Header { name: h.name, value: h.value, }; headers_len += 1; } let headers = &headers[..headers_len]; let handler = handler.prepare_response( rdata.code, rdata.reason, headers, http1::BodySize::Known(rdata.body.len()), )?; body_buf.write_all(rdata.body)?; handler }; drop(zresp); // ABR: discard_while discard_while(zreceiver, pin!(handler.send_header())).await?; (handler.send_header_done(), false) } else { // handle as websocket // send response header let headers = &[http1::Header { name: "Content-Type", value: b"text/plain", }]; let body = "WebSockets not supported on req mode interface.\n"; let handler = handler.recv_done(); let handler = handler.prepare_response( 400, "Bad Request", headers, http1::BodySize::Known(body.len()), )?; // ABR: discard_while discard_while(zreceiver, pin!(handler.send_header())).await?; let handler = handler.send_header_done(); body_buf.write_all(body.as_bytes())?; (handler, true) }; // send response body while body_buf.len() > 0 { // ABR: discard_while let size = discard_while( zreceiver, pin!(handler.send_body(Buffer::read_buf(body_buf), false)), ) .await?; body_buf.read_commit(size); } let persistent = handler.finish(); if websocket { return Ok(false); } Ok(persistent) } #[allow(clippy::too_many_arguments)] async fn server_req_connection_inner( token: CancellationToken, cid: &mut ArrayString<32>, cid_provider: &mut P, mut stream: S, peer_addr: Option<&SocketAddr>, secure: bool, buffer_size: usize, body_buffer_size: usize, rb_tmp: &Rc, packet_buf: Rc>>, timeout: Duration, zsender: AsyncLocalSender, zreceiver: &TrackedAsyncLocalReceiver<'_, (arena::Rc, usize)>, ) -> Result<(), Error> { let reactor = Reactor::current().unwrap(); let mut buf1 = VecRingBuffer::new(buffer_size, rb_tmp); let mut buf2 = VecRingBuffer::new(buffer_size, rb_tmp); let mut body_buf = ContiguousBuffer::new(body_buffer_size); loop { stream.set_id(cid); // this was originally logged when starting the non-async state // machine, so we'll keep doing that debug!("server-conn {}: assigning id", cid); let reuse = { let handler = server_req_handler( cid.as_ref(), &mut stream, peer_addr, secure, &mut buf1, &mut buf2, &mut body_buf, &packet_buf, &zsender, zreceiver, ); let timeout = Timeout::new(reactor.now() + timeout); match select_3(pin!(handler), timeout.elapsed(), token.cancelled()).await { Select3::R1(ret) => ret?, Select3::R2(_) => return Err(Error::StreamTimeout), Select3::R3(_) => return Err(Error::Stopped), } }; if !reuse { break; } // note: buf1 is not cleared as there may be data to read buf2.clear(); body_buf.clear(); *cid = cid_provider.get_new_assigned_cid(); } // ABR: discard_while discard_while(zreceiver, pin!(async { Ok(stream.close().await?) })).await?; Ok(()) } #[allow(clippy::too_many_arguments)] pub async fn server_req_connection( token: CancellationToken, mut cid: ArrayString<32>, cid_provider: &mut P, stream: S, peer_addr: Option<&SocketAddr>, secure: bool, buffer_size: usize, body_buffer_size: usize, rb_tmp: &Rc, packet_buf: Rc>>, timeout: Duration, zsender: AsyncLocalSender, zreceiver: AsyncLocalReceiver<(arena::Rc, usize)>, ) { let value_active = TrackFlag::default(); let zreceiver = TrackedAsyncLocalReceiver::new(zreceiver, &value_active); match track_future( server_req_connection_inner( token, &mut cid, cid_provider, stream, peer_addr, secure, buffer_size, body_buffer_size, rb_tmp, packet_buf, timeout, zsender, &zreceiver, ), &value_active, ) .await { Ok(()) => debug!("server-conn {}: finished", cid), Err(e) => { let level = match e { Error::ValueActive => Level::Error, _ => Level::Debug, }; log!(level, "server-conn {}: process error: {:?}", cid, e); } } } async fn accept_handoff( zsess_in: &mut ZhttpStreamSessionIn<'_, '_, R>, zsess_out: &ZhttpStreamSessionOut<'_>, ) -> Result<(), Error> where R: Fn(), { // discarding here is fine. the sender should cease sending // messages until we've replied with proceed discard_while( zsess_in.receiver, pin!(async { zsess_out.check_send().await; Ok(()) }), ) .await?; let zreq = zhttppacket::Request::new_handoff_proceed(b"", &[]); // check_send just finished, so this should succeed zsess_out.try_send_msg(zreq)?; // pause until we get a msg zsess_in.peek_msg().await?; Ok(()) } async fn server_accept_handoff( zsess_in: &mut ZhttpServerStreamSessionIn<'_, '_, R>, zsess_out: &ZhttpServerStreamSessionOut<'_>, ) -> Result<(), Error> where R: Fn(), { // discarding here is fine. the sender should cease sending // messages until we've replied with proceed server_discard_while( zsess_in.receiver, pin!(async { zsess_out.check_send().await; Ok(()) }), ) .await?; let zresp = zhttppacket::Response::new_handoff_proceed(b"", &[]); // check_send just finished, so this should succeed zsess_out.try_send_msg(zresp)?; // pause until we get a msg zsess_in.peek_msg().await?; Ok(()) } // this function will either return immediately or await messages async fn handle_other( zresp: Track<'_, arena::Rc>, zsess_in: &mut ZhttpStreamSessionIn<'_, '_, R>, zsess_out: &ZhttpStreamSessionOut<'_>, ) -> Result<(), Error> where R: Fn(), { match &zresp.get().get().ptype { zhttppacket::ResponsePacket::KeepAlive => Ok(()), zhttppacket::ResponsePacket::Credit(_) => Ok(()), zhttppacket::ResponsePacket::HandoffStart => { drop(zresp); accept_handoff(zsess_in, zsess_out).await?; Ok(()) } zhttppacket::ResponsePacket::Error(_) => Err(Error::Handler), zhttppacket::ResponsePacket::Cancel => Err(Error::HandlerCancel), _ => Err(Error::BadMessage), // unexpected type } } // this function will either return immediately or await messages async fn server_handle_other( zreq: Track<'_, arena::Rc>, zsess_in: &mut ZhttpServerStreamSessionIn<'_, '_, R>, zsess_out: &ZhttpServerStreamSessionOut<'_>, ) -> Result<(), Error> where R: Fn(), { match &zreq.get().get().ptype { zhttppacket::RequestPacket::KeepAlive => Ok(()), zhttppacket::RequestPacket::Credit(_) => Ok(()), zhttppacket::RequestPacket::HandoffStart => { drop(zreq); server_accept_handoff(zsess_in, zsess_out).await?; Ok(()) } zhttppacket::RequestPacket::Error(_) => Err(Error::Handler), zhttppacket::RequestPacket::Cancel => Err(Error::HandlerCancel), _ => Err(Error::BadMessage), // unexpected type } } async fn stream_recv_body<'a, 'b, 'c, R1, R2, R, W, const N: usize>( tmp_buf: &RefCell>, bytes_read: &R1, handler: RequestHeader<'a, 'b, 'c, R, W, N>, zsess_in: &mut ZhttpStreamSessionIn<'_, '_, R2>, zsess_out: &ZhttpStreamSessionOut<'_>, ) -> Result, Error> where R1: Fn(), R2: Fn(), R: AsyncRead, W: AsyncWrite, { let handler = { let mut start_recv_body = pin!(handler.start_recv_body()); // ABR: poll_async doesn't block match poll_async(start_recv_body.as_mut()).await { Poll::Ready(ret) => ret?, Poll::Pending => { // if we get here, then the send buffer with the client is full // keep trying to process while reading messages loop { // ABR: select contains read let ret = select_2(start_recv_body.as_mut(), pin!(zsess_in.recv_msg())).await; match ret { Select2::R1(ret) => break ret?, Select2::R2(ret) => { let zresp = ret?; // note: if we get a data message, handle_other will // error out. technically a data message should be // allowed here, but we're not in a position to do // anything with it, so we error. // // fortunately, the conditions to hit this are unusual: // * we need to receive a subsequent request over // a persistent connection // * that request needs to be one for which a body // would be expected, and the request needs to // include an expect header // * the send buffer to that connection needs to be // full // * the handler needs to provide an early response // before receiving the request body // // in other words, a client needs to send a large // pipelined POST over a reused connection, before it // has read the previous response, and the handler // needs to reject the request // ABR: handle_other handle_other(zresp, zsess_in, zsess_out).await?; } } } } } }; { let mut check_send = pin!(None); let mut add_to_recv_buffer = pin!(None); loop { if zsess_in.credits() > 0 && add_to_recv_buffer.is_none() && check_send.is_none() { check_send.set(Some(zsess_out.check_send())); } // ABR: select contains read let ret = select_3( select_option(check_send.as_mut().as_pin_mut()), select_option(add_to_recv_buffer.as_mut().as_pin_mut()), pin!(zsess_in.peek_msg()), ) .await; match ret { Select3::R1(()) => { check_send.set(None); let _defer = Defer::new(|| zsess_out.cancel_send()); assert!(zsess_in.credits() > 0); assert!(add_to_recv_buffer.is_none()); let tmp_buf = &mut *tmp_buf.borrow_mut(); let max_read = cmp::min(tmp_buf.len(), zsess_in.credits() as usize); let size = match handler.try_recv_body(&mut tmp_buf[..max_read]) { Some(ret) => ret?, None => { add_to_recv_buffer.set(Some(handler.add_to_recv_buffer())); continue; } }; bytes_read(); let body = &tmp_buf[..size]; zsess_in.subtract_credits(size as u32); let mut rdata = zhttppacket::RequestData::new(); rdata.body = body; rdata.more = handler.more(); let zreq = zhttppacket::Request::new_data(b"", &[], rdata); // check_send just finished, so this should succeed zsess_out.try_send_msg(zreq)?; if !handler.more() { break; } } Select3::R2(ret) => { ret?; add_to_recv_buffer.set(None); } Select3::R3(ret) => { let r = ret?; let zresp_ref = r.get().get(); match &zresp_ref.ptype { zhttppacket::ResponsePacket::Data(_) => break, _ => { // ABR: direct read let zresp = zsess_in.recv_msg().await?; // ABR: handle_other handle_other(zresp, zsess_in, zsess_out).await?; } } } } } } Ok(handler.recv_done()) } async fn server_stream_recv_body<'a, R1, R2, R>( tmp_buf: &RefCell>, bytes_read: &R1, resp_body: ClientResponseBody<'a, R>, zsess_in: &mut ZhttpServerStreamSessionIn<'_, '_, R2>, zsess_out: &ZhttpServerStreamSessionOut<'_>, ) -> Result where R1: Fn(), R2: Fn(), R: AsyncRead, { let mut check_send = pin!(None); let mut add_to_buffer = pin!(None); loop { if zsess_in.credits() > 0 && add_to_buffer.is_none() && check_send.is_none() { check_send.set(Some(zsess_out.check_send())); } // ABR: select contains read let ret = select_3( select_option(check_send.as_mut().as_pin_mut()), select_option(add_to_buffer.as_mut().as_pin_mut()), pin!(zsess_in.recv_msg()), ) .await; match ret { Select3::R1(()) => { check_send.set(None); let _defer = Defer::new(|| zsess_out.cancel_send()); assert!(zsess_in.credits() > 0); assert!(add_to_buffer.is_none()); let tmp_buf = &mut *tmp_buf.borrow_mut(); let max_read = cmp::min(tmp_buf.len(), zsess_in.credits() as usize); let (size, mut finished) = match resp_body.try_recv(&mut tmp_buf[..max_read])? { RecvStatus::Complete(finished, written) => (written, Some(finished)), RecvStatus::Read((), written) => { if written == 0 { add_to_buffer.set(Some(resp_body.add_to_buffer())); continue; } (written, None) } }; bytes_read(); let body = &tmp_buf[..size]; zsess_in.subtract_credits(size as u32); let mut rdata = zhttppacket::ResponseData::new(); rdata.body = body; rdata.more = finished.is_none(); let zresp = zhttppacket::Response::new_data(b"", &[], rdata); // check_send just finished, so this should succeed zsess_out.try_send_msg(zresp)?; if let Some(finished) = finished.take() { return Ok(finished); } } Select3::R2(ret) => { ret?; add_to_buffer.set(None); } Select3::R3(ret) => { let zreq = ret?; // ABR: handle_other server_handle_other(zreq, zsess_in, zsess_out).await?; } } } } async fn stream_send_body<'a, R1, R2, R, W>( bytes_read: &R1, handler: &RequestSendBody<'a, R, W>, zsess_in: &mut ZhttpStreamSessionIn<'_, '_, R2>, zsess_out: &ZhttpStreamSessionOut<'_>, blocks_max: usize, blocks_avail: &Counter, ) -> Result<(), Error> where R1: Fn(), R2: Fn(), R: AsyncRead, W: AsyncWrite, { let mut out_credits = 0; let mut flush_body = pin!(None); let mut check_send = pin!(None); 'main: loop { let ret = { if flush_body.is_none() && handler.can_flush() { flush_body.set(Some(handler.flush_body())); } if out_credits > 0 && check_send.is_none() { check_send.set(Some(zsess_out.check_send())); } // ABR: select contains read select_4( select_option(flush_body.as_mut().as_pin_mut()), select_option(check_send.as_mut().as_pin_mut()), pin!(zsess_in.recv_msg()), pin!(handler.fill_recv_buffer()), ) .await }; match ret { Select4::R1(ret) => { flush_body.set(None); let (size, done) = ret?; if done { break; } out_credits += size as u32; if size > 0 { bytes_read(); } } Select4::R2(()) => { check_send.set(None); let zreq = zhttppacket::Request::new_credit(b"", &[], out_credits); out_credits = 0; // check_send just finished, so this should succeed zsess_out.try_send_msg(zreq)?; } Select4::R3(ret) => { let zresp = ret?; match &zresp.get().get().ptype { zhttppacket::ResponsePacket::Data(rdata) => { handler.append_body(rdata.body, rdata.more)?; out_credits += handler.expand_write_buffer(blocks_max, blocks_avail) as u32; } zhttppacket::ResponsePacket::HandoffStart => { drop(zresp); // if handoff requested, flush what we can before accepting // so that the data is not delayed while we wait if flush_body.is_none() && handler.can_flush() { flush_body.set(Some(handler.flush_body())); } while let Some(fut) = flush_body.as_mut().as_pin_mut() { // ABR: poll_async doesn't block let ret = match poll_async(fut).await { Poll::Ready(ret) => ret, Poll::Pending => break, }; flush_body.set(None); let (size, done) = ret?; if done { break 'main; } out_credits += size as u32; if size > 0 { bytes_read(); } if handler.can_flush() { flush_body.set(Some(handler.flush_body())); } } // ABR: function contains read accept_handoff(zsess_in, zsess_out).await?; } _ => { // ABR: handle_other handle_other(zresp, zsess_in, zsess_out).await?; } } } Select4::R4(e) => return Err(e), } } Ok(()) } struct Overflow { buf: ContiguousBuffer, end: bool, } #[allow(clippy::too_many_arguments)] async fn server_stream_send_body<'a, R1, R2, R, W>( bytes_read: &R1, req_body: ClientRequestBody<'a, R, W>, mut overflow: Option, recv_buf_size: usize, zsess_in: &mut ZhttpServerStreamSessionIn<'_, '_, R2>, zsess_out: &ZhttpServerStreamSessionOut<'_>, blocks_max: usize, blocks_avail: &Counter, ) -> Result, Error> where R1: Fn(), R2: Fn(), R: AsyncRead, W: AsyncWrite, { // send initial body, including overflow, before offering credits let mut send = pin!(None); while send.is_some() || req_body.can_send() { if send.is_none() { send.set(Some(req_body.send())); } // ABR: select contains read let result = select_2( select_option(send.as_mut().as_pin_mut()), pin!(zsess_in.recv_msg()), ) .await; match result { Select2::R1(ret) => { send.set(None); match ret { SendStatus::Complete(resp) => return Ok(resp), SendStatus::EarlyResponse(resp) => return Ok(resp), SendStatus::Partial((), _) => { if !req_body.can_send() { if let Some(overflow) = &mut overflow { let size = req_body.prepare(overflow.buf.read_buf(), overflow.end)?; overflow.buf.read_commit(size); } } } SendStatus::Error((), e) => return Err(e), } } Select2::R2(ret) => { let zreq = ret?; // ABR: handle_other server_handle_other(zreq, zsess_in, zsess_out).await?; } } } assert!(!req_body.can_send()); let mut out_credits = recv_buf_size as u32; let mut send = pin!(None); let mut check_send = pin!(None); let mut prepare_done = false; let resp = 'main: loop { let ret = { if send.is_none() && req_body.can_send() { send.set(Some(req_body.send())); } if !prepare_done && out_credits > 0 && check_send.is_none() { check_send.set(Some(zsess_out.check_send())); } // ABR: select contains read select_3( select_option(send.as_mut().as_pin_mut()), select_option(check_send.as_mut().as_pin_mut()), pin!(zsess_in.recv_msg()), ) .await }; match ret { Select3::R1(ret) => { send.set(None); match ret { SendStatus::Complete(resp) => break resp, SendStatus::EarlyResponse(resp) => break resp, SendStatus::Partial((), size) => { out_credits += size as u32; if size > 0 { bytes_read(); } } SendStatus::Error(_, e) => return Err(e), } } Select3::R2(()) => { check_send.set(None); let zresp = zhttppacket::Response::new_credit(b"", &[], out_credits); out_credits = 0; // check_send just finished, so this should succeed zsess_out.try_send_msg(zresp)?; } Select3::R3(ret) => { let zreq = ret?; match &zreq.get().get().ptype { zhttppacket::RequestPacket::Data(rdata) => { let size = req_body.prepare(rdata.body, !rdata.more)?; if size < rdata.body.len() { return Err(Error::BufferExceeded); } if rdata.more { out_credits += req_body.expand_write_buffer(blocks_max, blocks_avail)? as u32; } else { prepare_done = true; } } zhttppacket::RequestPacket::HandoffStart => { drop(zreq); // if handoff requested, flush what we can before accepting // so that the data is not delayed while we wait if send.is_none() && req_body.can_send() { send.set(Some(req_body.send())); } while let Some(fut) = send.as_mut().as_pin_mut() { // ABR: poll_async doesn't block let ret = match poll_async(fut).await { Poll::Ready(ret) => ret, Poll::Pending => break, }; send.set(None); match ret { SendStatus::Complete(resp) => break 'main resp, SendStatus::EarlyResponse(resp) => break 'main resp, SendStatus::Partial((), size) => { out_credits += size as u32; if size > 0 { bytes_read(); } } SendStatus::Error((), e) => return Err(e), } if req_body.can_send() { send.set(Some(req_body.send())); } } // ABR: function contains read server_accept_handoff(zsess_in, zsess_out).await?; } _ => { // ABR: handle_other server_handle_other(zreq, zsess_in, zsess_out).await?; } } } } }; Ok(resp) } #[allow(clippy::too_many_arguments)] async fn stream_websocket( log_id: &str, stream: RefCell<&mut S>, buf1: &mut VecRingBuffer, buf2: &mut VecRingBuffer, blocks_max: usize, blocks_avail: &Counter, messages_max: usize, tmp_buf: &RefCell>, bytes_read: &R1, deflate_config: Option<(websocket::PerMessageDeflateConfig, usize)>, zsess_in: &mut ZhttpStreamSessionIn<'_, '_, R2>, zsess_out: &ZhttpStreamSessionOut<'_>, ) -> Result<(), Error> where S: AsyncRead + AsyncWrite, R1: Fn(), R2: Fn(), { let deflate_config = match deflate_config { Some((config, enc_buf_size)) => { let ebuf = VecRingBuffer::new(enc_buf_size, buf2.get_tmp()); Some((!config.server_no_context_takeover, ebuf)) } None => None, }; let handler = WebSocketHandler::new(io_split(&stream), buf1, buf2, deflate_config); let mut ws_in_tracker = MessageTracker::new(messages_max); let mut out_credits = 0; let mut check_send = pin!(None); let mut add_to_recv_buffer = pin!(None); let mut send_content = pin!(None); loop { let (do_send, do_recv) = match handler.state() { websocket::State::Connected => (true, true), websocket::State::PeerClosed => (true, false), websocket::State::Closing => (false, true), websocket::State::Finished => break, }; if out_credits > 0 || (do_recv && zsess_in.credits() > 0 && add_to_recv_buffer.is_none()) && check_send.is_none() { check_send.set(Some(zsess_out.check_send())); } if do_send && send_content.is_none() { if let Some((mtype, avail, done)) = ws_in_tracker.current() { if !handler.is_sending_message() { handler.send_message_start(mtype, None); } if avail > 0 || done { send_content.set(Some(handler.send_message_content(avail, done, bytes_read))); } } } // ABR: select contains read let ret = select_4( select_option(check_send.as_mut().as_pin_mut()), select_option(add_to_recv_buffer.as_mut().as_pin_mut()), select_option(send_content.as_mut().as_pin_mut()), pin!(zsess_in.recv_msg()), ) .await; match ret { Select4::R1(()) => { check_send.set(None); let _defer = Defer::new(|| zsess_out.cancel_send()); if out_credits > 0 { let zreq = zhttppacket::Request::new_credit(b"", &[], out_credits); out_credits = 0; // check_send just finished, so this should succeed zsess_out.try_send_msg(zreq)?; continue; } assert!(zsess_in.credits() > 0); assert!(add_to_recv_buffer.is_none()); let tmp_buf = &mut *tmp_buf.borrow_mut(); let max_read = cmp::min(tmp_buf.len(), zsess_in.credits() as usize); let (opcode, size, end) = match handler.try_recv_message_content(&mut tmp_buf[..max_read]) { Some(ret) => ret?, None => { add_to_recv_buffer.set(Some(handler.add_to_recv_buffer())); continue; } }; bytes_read(); let body = &tmp_buf[..size]; let zreq = match opcode { websocket::OPCODE_TEXT | websocket::OPCODE_BINARY => { if body.is_empty() && !end { // don't bother sending empty message continue; } let mut data = zhttppacket::RequestData::new(); data.body = body; data.content_type = if opcode == websocket::OPCODE_TEXT { Some(zhttppacket::ContentType::Text) } else { Some(zhttppacket::ContentType::Binary) }; data.more = !end; zhttppacket::Request::new_data(b"", &[], data) } websocket::OPCODE_CLOSE => { let status = if body.len() >= 2 { let mut arr = [0; 2]; arr[..].copy_from_slice(&body[..2]); let code = u16::from_be_bytes(arr); let reason = match str::from_utf8(&body[2..]) { Ok(reason) => reason, Err(e) => return Err(e.into()), }; Some((code, reason)) } else { None }; zhttppacket::Request::new_close(b"", &[], status) } websocket::OPCODE_PING => zhttppacket::Request::new_ping(b"", &[], body), websocket::OPCODE_PONG => zhttppacket::Request::new_pong(b"", &[], body), opcode => { debug!( "server-conn {}: unsupported websocket opcode: {}", log_id, opcode ); return Err(Error::BadFrame); } }; zsess_in.subtract_credits(size as u32); // check_send just finished, so this should succeed zsess_out.try_send_msg(zreq)?; } Select4::R2(ret) => { ret?; add_to_recv_buffer.set(None); } Select4::R3(ret) => { send_content.set(None); let (size, done) = ret?; ws_in_tracker.consumed(size, done); if handler.state() == websocket::State::Connected || handler.state() == websocket::State::PeerClosed { out_credits += size as u32; } } Select4::R4(ret) => { let zresp = ret?; match &zresp.get().get().ptype { zhttppacket::ResponsePacket::Data(rdata) => match handler.state() { websocket::State::Connected | websocket::State::PeerClosed => { let avail = handler.accept_avail(); if let Err(e) = handler.accept_body(rdata.body) { warn!( "received too much data from handler (size={}, credits={})", rdata.body.len(), avail, ); return Err(e); } out_credits += handler.expand_write_buffer(blocks_max, blocks_avail) as u32; let opcode = match &rdata.content_type { Some(zhttppacket::ContentType::Binary) => websocket::OPCODE_BINARY, _ => websocket::OPCODE_TEXT, }; if !ws_in_tracker.in_progress() { if ws_in_tracker.start(opcode).is_err() { return Err(Error::BufferExceeded); } } ws_in_tracker.extend(rdata.body.len()); if !rdata.more { ws_in_tracker.done(); } } _ => {} }, zhttppacket::ResponsePacket::Close(cdata) => match handler.state() { websocket::State::Connected | websocket::State::PeerClosed => { let (code, reason) = cdata.status.unwrap_or((1000, "")); let arr: [u8; 2] = code.to_be_bytes(); // close content isn't limited by credits. if we // don't have space for it, just error out handler.accept_body(&arr)?; handler.accept_body(reason.as_bytes())?; if ws_in_tracker.start(websocket::OPCODE_CLOSE).is_err() { return Err(Error::BadFrame); } ws_in_tracker.extend(arr.len() + reason.len()); ws_in_tracker.done(); } _ => {} }, zhttppacket::ResponsePacket::Ping(pdata) => match handler.state() { websocket::State::Connected | websocket::State::PeerClosed => { let avail = handler.accept_avail(); if let Err(e) = handler.accept_body(pdata.body) { warn!( "received too much data from handler (size={}, credits={})", pdata.body.len(), avail, ); return Err(e); } if ws_in_tracker.start(websocket::OPCODE_PING).is_err() { return Err(Error::BadFrame); } ws_in_tracker.extend(pdata.body.len()); ws_in_tracker.done(); } _ => {} }, zhttppacket::ResponsePacket::Pong(pdata) => match handler.state() { websocket::State::Connected | websocket::State::PeerClosed => { let avail = handler.accept_avail(); if let Err(e) = handler.accept_body(pdata.body) { warn!( "received too much data from handler (size={}, credits={})", pdata.body.len(), avail, ); return Err(e); } if ws_in_tracker.start(websocket::OPCODE_PONG).is_err() { return Err(Error::BadFrame); } ws_in_tracker.extend(pdata.body.len()); ws_in_tracker.done(); } _ => {} }, zhttppacket::ResponsePacket::HandoffStart => { drop(zresp); // if handoff requested, flush what we can before accepting // so that the data is not delayed while we wait loop { if send_content.is_none() { if let Some((mtype, avail, done)) = ws_in_tracker.current() { if !handler.is_sending_message() { handler.send_message_start(mtype, None); } if avail > 0 || done { send_content.set(Some( handler.send_message_content(avail, done, bytes_read), )); } } } if let Some(fut) = send_content.as_mut().as_pin_mut() { // ABR: poll_async doesn't block let ret = match poll_async(fut).await { Poll::Ready(ret) => ret, Poll::Pending => break, }; send_content.set(None); let (size, done) = ret?; ws_in_tracker.consumed(size, done); if handler.state() == websocket::State::Connected || handler.state() == websocket::State::PeerClosed { out_credits += size as u32; } } else { break; } } // ABR: function contains read accept_handoff(zsess_in, zsess_out).await?; } _ => { // ABR: handle_other handle_other(zresp, zsess_in, zsess_out).await?; } } } } } Ok(()) } #[allow(clippy::too_many_arguments)] async fn server_stream_websocket( log_id: &str, stream: RefCell<&mut S>, buf1: &mut VecRingBuffer, buf2: &mut VecRingBuffer, blocks_max: usize, blocks_avail: &Counter, messages_max: usize, tmp_buf: &RefCell>, bytes_read: &R1, deflate_config: Option<(websocket::PerMessageDeflateConfig, usize)>, zsess_in: &mut ZhttpServerStreamSessionIn<'_, '_, R2>, zsess_out: &ZhttpServerStreamSessionOut<'_>, ) -> Result<(), Error> where S: AsyncRead + AsyncWrite, R1: Fn(), R2: Fn(), { let deflate_config = match deflate_config { Some((config, enc_buf_size)) => { let ebuf = VecRingBuffer::new(enc_buf_size, buf2.get_tmp()); Some((!config.client_no_context_takeover, ebuf)) } None => None, }; let handler = WebSocketHandler::new(io_split(&stream), buf1, buf2, deflate_config); let mut ws_in_tracker = MessageTracker::new(messages_max); let mut out_credits = 0; let mut check_send = pin!(None); let mut add_to_recv_buffer = pin!(None); let mut send_content = pin!(None); loop { let (do_send, do_recv) = match handler.state() { websocket::State::Connected => (true, true), websocket::State::PeerClosed => (true, false), websocket::State::Closing => (false, true), websocket::State::Finished => break, }; if out_credits > 0 || (do_recv && zsess_in.credits() > 0 && add_to_recv_buffer.is_none()) && check_send.is_none() { check_send.set(Some(zsess_out.check_send())); } if do_send && send_content.is_none() { if let Some((mtype, avail, done)) = ws_in_tracker.current() { if !handler.is_sending_message() { handler.send_message_start(mtype, Some(gen_mask())); } if avail > 0 || done { send_content.set(Some(handler.send_message_content(avail, done, bytes_read))); } } } // ABR: select contains read let ret = select_4( select_option(check_send.as_mut().as_pin_mut()), select_option(add_to_recv_buffer.as_mut().as_pin_mut()), select_option(send_content.as_mut().as_pin_mut()), pin!(zsess_in.recv_msg()), ) .await; match ret { Select4::R1(()) => { check_send.set(None); let _defer = Defer::new(|| zsess_out.cancel_send()); if out_credits > 0 { let zresp = zhttppacket::Response::new_credit(b"", &[], out_credits); out_credits = 0; // check_send just finished, so this should succeed zsess_out.try_send_msg(zresp)?; continue; } assert!(zsess_in.credits() > 0); assert!(add_to_recv_buffer.is_none()); let tmp_buf = &mut *tmp_buf.borrow_mut(); let max_read = cmp::min(tmp_buf.len(), zsess_in.credits() as usize); let (opcode, size, end) = match handler.try_recv_message_content(&mut tmp_buf[..max_read]) { Some(ret) => ret?, None => { add_to_recv_buffer.set(Some(handler.add_to_recv_buffer())); continue; } }; bytes_read(); let body = &tmp_buf[..size]; let zresp = match opcode { websocket::OPCODE_TEXT | websocket::OPCODE_BINARY => { if body.is_empty() && !end { // don't bother sending empty message continue; } let mut data = zhttppacket::ResponseData::new(); data.body = body; data.content_type = if opcode == websocket::OPCODE_TEXT { Some(zhttppacket::ContentType::Text) } else { Some(zhttppacket::ContentType::Binary) }; data.more = !end; zhttppacket::Response::new_data(b"", &[], data) } websocket::OPCODE_CLOSE => { let status = if body.len() >= 2 { let mut arr = [0; 2]; arr[..].copy_from_slice(&body[..2]); let code = u16::from_be_bytes(arr); let reason = match str::from_utf8(&body[2..]) { Ok(reason) => reason, Err(e) => return Err(e.into()), }; Some((code, reason)) } else { None }; zhttppacket::Response::new_close(b"", &[], status) } websocket::OPCODE_PING => zhttppacket::Response::new_ping(b"", &[], body), websocket::OPCODE_PONG => zhttppacket::Response::new_pong(b"", &[], body), opcode => { debug!( "client-conn {}: unsupported websocket opcode: {}", log_id, opcode ); return Err(Error::BadFrame); } }; zsess_in.subtract_credits(size as u32); // check_send just finished, so this should succeed zsess_out.try_send_msg(zresp)?; } Select4::R2(ret) => { ret?; add_to_recv_buffer.set(None); } Select4::R3(ret) => { send_content.set(None); let (size, done) = ret?; ws_in_tracker.consumed(size, done); if handler.state() == websocket::State::Connected || handler.state() == websocket::State::PeerClosed { out_credits += size as u32; } } Select4::R4(ret) => { let zreq = ret?; match &zreq.get().get().ptype { zhttppacket::RequestPacket::Data(rdata) => match handler.state() { websocket::State::Connected | websocket::State::PeerClosed => { let avail = handler.accept_avail(); if let Err(e) = handler.accept_body(rdata.body) { warn!( "received too much data from handler (size={}, credits={})", rdata.body.len(), avail, ); return Err(e); } out_credits += handler.expand_write_buffer(blocks_max, blocks_avail) as u32; let opcode = match &rdata.content_type { Some(zhttppacket::ContentType::Binary) => websocket::OPCODE_BINARY, _ => websocket::OPCODE_TEXT, }; if !ws_in_tracker.in_progress() { if ws_in_tracker.start(opcode).is_err() { return Err(Error::BufferExceeded); } } ws_in_tracker.extend(rdata.body.len()); if !rdata.more { ws_in_tracker.done(); } } _ => {} }, zhttppacket::RequestPacket::Close(cdata) => match handler.state() { websocket::State::Connected | websocket::State::PeerClosed => { let (code, reason) = cdata.status.unwrap_or((1000, "")); let arr: [u8; 2] = code.to_be_bytes(); // close content isn't limited by credits. if we // don't have space for it, just error out handler.accept_body(&arr)?; handler.accept_body(reason.as_bytes())?; if ws_in_tracker.start(websocket::OPCODE_CLOSE).is_err() { return Err(Error::BadFrame); } ws_in_tracker.extend(arr.len() + reason.len()); ws_in_tracker.done(); } _ => {} }, zhttppacket::RequestPacket::Ping(pdata) => match handler.state() { websocket::State::Connected | websocket::State::PeerClosed => { let avail = handler.accept_avail(); if let Err(e) = handler.accept_body(pdata.body) { warn!( "received too much data from handler (size={}, credits={})", pdata.body.len(), avail, ); return Err(e); } if ws_in_tracker.start(websocket::OPCODE_PING).is_err() { return Err(Error::BadFrame); } ws_in_tracker.extend(pdata.body.len()); ws_in_tracker.done(); } _ => {} }, zhttppacket::RequestPacket::Pong(pdata) => match handler.state() { websocket::State::Connected | websocket::State::PeerClosed => { let avail = handler.accept_avail(); if let Err(e) = handler.accept_body(pdata.body) { warn!( "received too much data from handler (size={}, credits={})", pdata.body.len(), avail, ); return Err(e); } if ws_in_tracker.start(websocket::OPCODE_PONG).is_err() { return Err(Error::BadFrame); } ws_in_tracker.extend(pdata.body.len()); ws_in_tracker.done(); } _ => {} }, zhttppacket::RequestPacket::HandoffStart => { drop(zreq); // if handoff requested, flush what we can before accepting // so that the data is not delayed while we wait loop { if send_content.is_none() { if let Some((mtype, avail, done)) = ws_in_tracker.current() { if !handler.is_sending_message() { handler.send_message_start(mtype, Some(gen_mask())); } if avail > 0 || done { send_content.set(Some( handler.send_message_content(avail, done, bytes_read), )); } } } if let Some(fut) = send_content.as_mut().as_pin_mut() { // ABR: poll_async doesn't block let ret = match poll_async(fut).await { Poll::Ready(ret) => ret, Poll::Pending => break, }; send_content.set(None); let (size, done) = ret?; ws_in_tracker.consumed(size, done); if handler.state() == websocket::State::Connected || handler.state() == websocket::State::PeerClosed { out_credits += size as u32; } } else { break; } } // ABR: function contains read server_accept_handoff(zsess_in, zsess_out).await?; } _ => { // ABR: handle_other server_handle_other(zreq, zsess_in, zsess_out).await?; } } } } } Ok(()) } // return true if persistent #[allow(clippy::too_many_arguments)] async fn server_stream_handler( id: &str, stream: &mut S, peer_addr: Option<&SocketAddr>, secure: bool, buf1: &mut VecRingBuffer, buf2: &mut VecRingBuffer, blocks_max: usize, blocks_avail: &Counter, messages_max: usize, allow_compression: bool, packet_buf: &RefCell>, tmp_buf: &RefCell>, instance_id: &str, zsender: &AsyncLocalSender, zsender_stream: &AsyncLocalSender<(ArrayVec, zmq::Message)>, zreceiver: &TrackedAsyncLocalReceiver<'_, (arena::Rc, usize)>, shared: &StreamSharedData, refresh_stream_timeout: &R1, refresh_session_timeout: &R2, ) -> Result where S: AsyncRead + AsyncWrite, R1: Fn(), R2: Fn(), { let stream = RefCell::new(stream); let send_buf_size = buf1.capacity(); // for sending to handler let recv_buf_size = buf2.capacity(); // for receiving from handler let handler = RequestHandler::new(io_split(&stream), buf1, buf2); let mut scratch = http1::ParseScratch::::new(); let mut req_mem = None; let zsess_out = ZhttpStreamSessionOut::new(instance_id, id, packet_buf, zsender_stream, shared); // receive request header // ABR: discard_while let handler = match discard_while( zreceiver, pin!(handler.recv_request(&mut scratch, &mut req_mem)), ) .await { Ok(handler) => handler, Err(Error::Io(e)) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(false), Err(e) => return Err(e), }; refresh_stream_timeout(); let (body_size, ws_config, msg) = { let req = handler.request(); let mut websocket = false; let mut ws_version = None; let mut ws_key = None; let mut ws_deflate_config = None; for h in req.headers.iter() { if h.name.eq_ignore_ascii_case("Upgrade") && h.value == b"websocket" { websocket = true; } if h.name.eq_ignore_ascii_case("Sec-WebSocket-Version") { ws_version = Some(h.value); } if h.name.eq_ignore_ascii_case("Sec-WebSocket-Key") { ws_key = Some(h.value); } if h.name.eq_ignore_ascii_case("Sec-WebSocket-Extensions") { for value in http1::parse_header_value(h.value) { let (name, params) = match value { Ok(v) => v, Err(_) => return Err(Error::InvalidWebSocketRequest), }; match name { "permessage-deflate" => { // the client can present multiple offers. take // the first that works. if none work, it's not // an error. we'll just not use compression if allow_compression && ws_deflate_config.is_none() { if let Ok(config) = websocket::PerMessageDeflateConfig::from_params(params) { if let Ok(resp_config) = config.create_response() { // set the encoded buffer to be 25% the size of the // recv buffer let enc_buf_size = recv_buf_size / 4; ws_deflate_config = Some((resp_config, enc_buf_size)); } } } } name => { debug!("ignoring unsupported websocket extension: {}", name); continue; } } } } } // log request let host = get_host(req.headers); let scheme = if websocket { if secure { "wss" } else { "ws" } } else { if secure { "https" } else { "http" } }; debug!( "server-conn {}: request: {} {}://{}{}", id, req.method, scheme, host, req.uri ); let ws_config: Option<( ArrayString, Option<(websocket::PerMessageDeflateConfig, usize)>, )> = if websocket { let accept = match validate_ws_request(&req, ws_version, ws_key) { Ok(s) => s, Err(_) => return Err(Error::InvalidWebSocketRequest), }; Some((accept, ws_deflate_config)) } else { None }; let ids = [zhttppacket::Id { id: id.as_bytes(), seq: Some(shared.out_seq()), }]; let (mode, more) = if websocket { (Mode::WebSocket, false) } else { let more = match req.body_size { http1::BodySize::NoBody => false, http1::BodySize::Known(x) => x > 0, http1::BodySize::Unknown => true, }; (Mode::HttpStream, more) }; let msg = make_zhttp_request( instance_id, &ids, req.method, req.uri, req.headers, b"", more, mode, recv_buf_size as u32, peer_addr, secure, &mut packet_buf.borrow_mut(), )?; shared.inc_out_seq(); (req.body_size, ws_config, msg) }; // send request message // ABR: discard_while discard_while(zreceiver, pin!(send_msg(zsender, msg))).await?; let mut zsess_in = ZhttpStreamSessionIn::new( id, send_buf_size, ws_config.is_some(), zreceiver, shared, refresh_session_timeout, ); // receive any message, in order to get a handler address // ABR: direct read zsess_in.peek_msg().await?; let mut handler = if body_size != http1::BodySize::NoBody { // receive request body and send to handler // ABR: function contains read stream_recv_body( tmp_buf, refresh_stream_timeout, handler, &mut zsess_in, &zsess_out, ) .await? } else { handler.recv_done()? }; // receive response message let zresp = loop { // ABR: select contains read let ret = select_2(pin!(zsess_in.recv_msg()), pin!(handler.fill_recv_buffer())).await; match ret { Select2::R1(ret) => { let zresp = ret?; match zresp.get().get().ptype { zhttppacket::ResponsePacket::Data(_) | zhttppacket::ResponsePacket::Error(_) => break zresp, _ => { // ABR: handle_other handle_other(zresp, &mut zsess_in, &zsess_out).await?; } } } Select2::R2(e) => return Err(e), } }; // determine how to respond let (handler, ws_config) = { let rdata = match &zresp.get().get().ptype { zhttppacket::ResponsePacket::Data(rdata) => rdata, zhttppacket::ResponsePacket::Error(edata) => { if ws_config.is_some() && edata.condition == "rejected" { // send websocket rejection let rdata = edata.rejected_info.as_ref().unwrap(); let handler = { let mut headers = [http1::EMPTY_HEADER; HEADERS_MAX]; let mut headers_len = 0; for h in rdata.headers.iter() { // don't send these headers if h.name.eq_ignore_ascii_case("Upgrade") || h.name.eq_ignore_ascii_case("Connection") || h.name.eq_ignore_ascii_case("Sec-WebSocket-Accept") || h.name.eq_ignore_ascii_case("Sec-WebSocket-Extensions") { continue; } if headers_len >= headers.len() { return Err(Error::BadMessage); } headers[headers_len] = http1::Header { name: h.name, value: h.value, }; headers_len += 1; } let headers = &headers[..headers_len]; handler.prepare_response( rdata.code, rdata.reason, headers, http1::BodySize::Known(rdata.body.len()), )? }; handler.append_body(rdata.body, false, id)?; drop(zresp); // ABR: discard_while discard_while(zreceiver, pin!(handler.send_header())).await?; let handler = handler.send_header_done(); loop { // ABR: discard_while let (_, done) = discard_while(zreceiver, pin!(handler.flush_body())).await?; if done { break; } } return Ok(false); } else { // ABR: handle_other return Err(handle_other(zresp, &mut zsess_in, &zsess_out) .await .unwrap_err()); } } _ => unreachable!(), // we confirmed the type above }; // send response header let handler = { let mut headers = [http1::EMPTY_HEADER; HEADERS_MAX]; let mut headers_len = 0; let mut body_size = http1::BodySize::Unknown; for h in rdata.headers.iter() { if ws_config.is_some() { // don't send these headers if h.name.eq_ignore_ascii_case("Upgrade") || h.name.eq_ignore_ascii_case("Connection") || h.name.eq_ignore_ascii_case("Sec-WebSocket-Accept") || h.name.eq_ignore_ascii_case("Sec-WebSocket-Extensions") { continue; } } else { if h.name.eq_ignore_ascii_case("Content-Length") { let s = str::from_utf8(h.value)?; let clen: usize = match s.parse() { Ok(clen) => clen, Err(_) => { return Err(io::Error::from(io::ErrorKind::InvalidInput).into()) } }; body_size = http1::BodySize::Known(clen); } } if headers_len >= headers.len() { return Err(Error::BadMessage); } headers[headers_len] = http1::Header { name: h.name, value: h.value, }; headers_len += 1; } if body_size == http1::BodySize::Unknown && !rdata.more { body_size = http1::BodySize::Known(rdata.body.len()); } let mut ws_ext = ArrayVec::::new(); if let Some(ws_config) = &ws_config { let accept_data = &ws_config.0; if headers_len + 4 > headers.len() { return Err(Error::BadMessage); } headers[headers_len] = http1::Header { name: "Upgrade", value: b"websocket", }; headers_len += 1; headers[headers_len] = http1::Header { name: "Connection", value: b"Upgrade", }; headers_len += 1; headers[headers_len] = http1::Header { name: "Sec-WebSocket-Accept", value: accept_data.as_bytes(), }; headers_len += 1; if let Some((config, _)) = &ws_config.1 { if write_ws_ext_header_value(config, &mut ws_ext).is_err() { return Err(Error::Compression); } headers[headers_len] = http1::Header { name: "Sec-WebSocket-Extensions", value: ws_ext.as_ref(), }; headers_len += 1; } } let headers = &headers[..headers_len]; handler.prepare_response(rdata.code, rdata.reason, headers, body_size)? }; handler.append_body(rdata.body, rdata.more, id)?; drop(zresp); { let mut send_header = pin!(handler.send_header()); loop { // ABR: select contains read let ret = select_2(send_header.as_mut(), pin!(zsess_in.recv_msg())).await; match ret { Select2::R1(ret) => { ret?; break; } Select2::R2(ret) => { let zresp = ret?; match &zresp.get().get().ptype { zhttppacket::ResponsePacket::Data(rdata) => { handler.append_body(rdata.body, rdata.more, id)?; } _ => { // ABR: handle_other handle_other(zresp, &mut zsess_in, &zsess_out).await?; } } } } } } let handler = handler.send_header_done(); refresh_stream_timeout(); let ws_config = if let Some((_, ws_deflate_config)) = ws_config { Some(ws_deflate_config) } else { None }; (handler, ws_config) }; if let Some(deflate_config) = ws_config { // reduce size of future #[allow(clippy::drop_non_drop)] drop(handler); // handle as websocket connection // ABR: function contains read stream_websocket( id, stream, buf1, buf2, blocks_max, blocks_avail, messages_max, tmp_buf, refresh_stream_timeout, deflate_config, &mut zsess_in, &zsess_out, ) .await?; Ok(false) } else { // send response body // ABR: function contains read stream_send_body( refresh_stream_timeout, &handler, &mut zsess_in, &zsess_out, blocks_max, blocks_avail, ) .await?; let persistent = handler.finish(); Ok(persistent) } } #[allow(clippy::too_many_arguments)] async fn server_stream_connection_inner( token: CancellationToken, cid: &mut ArrayString<32>, cid_provider: &mut P, mut stream: S, peer_addr: Option<&SocketAddr>, secure: bool, buffer_size: usize, blocks_max: usize, blocks_avail: &Counter, messages_max: usize, rb_tmp: &Rc, packet_buf: Rc>>, tmp_buf: Rc>>, stream_timeout_duration: Duration, allow_compression: bool, instance_id: &str, zsender: AsyncLocalSender, zsender_stream: AsyncLocalSender<(ArrayVec, zmq::Message)>, zreceiver: &TrackedAsyncLocalReceiver<'_, (arena::Rc, usize)>, shared: arena::Rc, ) -> Result<(), Error> { let reactor = Reactor::current().unwrap(); let mut buf1 = VecRingBuffer::new(buffer_size, rb_tmp); let mut buf2 = VecRingBuffer::new(buffer_size, rb_tmp); loop { stream.set_id(cid); // this was originally logged when starting the non-async state // machine, so we'll keep doing that debug!("server-conn {}: assigning id", cid); let reuse = { let stream_timeout = Timeout::new(reactor.now() + stream_timeout_duration); let session_timeout = Timeout::new(reactor.now() + ZHTTP_SESSION_TIMEOUT); let refresh_stream_timeout = || { stream_timeout.set_deadline(reactor.now() + stream_timeout_duration); }; let refresh_session_timeout = || { session_timeout.set_deadline(reactor.now() + ZHTTP_SESSION_TIMEOUT); }; let handler = pin!(server_stream_handler( cid.as_ref(), &mut stream, peer_addr, secure, &mut buf1, &mut buf2, blocks_max, blocks_avail, messages_max, allow_compression, &packet_buf, &tmp_buf, instance_id, &zsender, &zsender_stream, zreceiver, shared.get(), &refresh_stream_timeout, &refresh_session_timeout, )); let ret = match select_4( handler, stream_timeout.elapsed(), session_timeout.elapsed(), token.cancelled(), ) .await { Select4::R1(ret) => ret, Select4::R2(_) => Err(Error::StreamTimeout), Select4::R3(_) => return Err(Error::SessionTimeout), Select4::R4(_) => return Err(Error::Stopped), }; match ret { Ok(reuse) => reuse, Err(e) => { let handler_caused = matches!( &e, Error::BadMessage | Error::Handler | Error::HandlerCancel ); if !handler_caused { let shared = shared.get(); let msg = if let Some(addr) = shared.to_addr().get() { let id = cid.as_ref(); let mut zreq = zhttppacket::Request::new_cancel(b"", &[]); let ids = [zhttppacket::Id { id: id.as_bytes(), seq: Some(shared.out_seq()), }]; zreq.from = instance_id.as_bytes(); zreq.ids = &ids; zreq.multi = true; let packet_buf = &mut *packet_buf.borrow_mut(); let size = zreq.serialize(packet_buf)?; let msg = zmq::Message::from(&packet_buf[..size]); let addr = match ArrayVec::try_from(addr) { Ok(v) => v, Err(_) => { return Err(io::Error::from(io::ErrorKind::InvalidInput).into()) } }; Some((addr, msg)) } else { None }; if let Some((addr, msg)) = msg { // best effort let _ = zsender_stream.try_send((addr, msg)); shared.inc_out_seq(); } } return Err(e); } } }; if !reuse { break; } // note: buf1 is not cleared as there may be data to read let additional_blocks = (buf2.capacity() / buffer_size) - 1; buf2.clear(); buf2.resize(buffer_size); shared.get().reset(); blocks_avail.inc(additional_blocks).unwrap(); *cid = cid_provider.get_new_assigned_cid(); } // ABR: discard_while discard_while(zreceiver, pin!(async { Ok(stream.close().await?) })).await?; Ok(()) } #[allow(clippy::too_many_arguments)] pub async fn server_stream_connection( token: CancellationToken, mut cid: ArrayString<32>, cid_provider: &mut P, stream: S, peer_addr: Option<&SocketAddr>, secure: bool, buffer_size: usize, blocks_max: usize, blocks_avail: &Counter, messages_max: usize, rb_tmp: &Rc, packet_buf: Rc>>, tmp_buf: Rc>>, timeout: Duration, allow_compression: bool, instance_id: &str, zsender: AsyncLocalSender, zsender_stream: AsyncLocalSender<(ArrayVec, zmq::Message)>, zreceiver: AsyncLocalReceiver<(arena::Rc, usize)>, shared: arena::Rc, ) { let value_active = TrackFlag::default(); let zreceiver = TrackedAsyncLocalReceiver::new(zreceiver, &value_active); match track_future( server_stream_connection_inner( token, &mut cid, cid_provider, stream, peer_addr, secure, buffer_size, blocks_max, blocks_avail, messages_max, rb_tmp, packet_buf, tmp_buf, timeout, allow_compression, instance_id, zsender, zsender_stream, &zreceiver, shared, ), &value_active, ) .await { Ok(()) => debug!("server-conn {}: finished", cid), Err(e) => { let level = match e { Error::ValueActive => Level::Error, _ => Level::Debug, }; log!(level, "server-conn {}: process error: {:?}", cid, e); } } } struct AsyncOperation where C: FnMut(), { op_fn: O, cancel_fn: C, } impl AsyncOperation where O: FnMut(&mut Context) -> Option, C: FnMut(), { fn new(op_fn: O, cancel_fn: C) -> Self { Self { op_fn, cancel_fn } } } impl Future for AsyncOperation where O: FnMut(&mut Context) -> Option + Unpin, C: FnMut() + Unpin, { type Output = R; fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { let s = Pin::into_inner(self); match (s.op_fn)(cx) { Some(ret) => Poll::Ready(ret), None => Poll::Pending, } } } impl Drop for AsyncOperation where C: FnMut(), { fn drop(&mut self) { (self.cancel_fn)(); } } pub enum SendStatus { Complete(T), EarlyResponse(T), Partial(P, usize), Error(P, E), } pub enum RecvStatus { Read(T, usize), Complete(C, usize), } struct ClientRequest<'a, R: AsyncRead, W: AsyncWrite> { r: ReadHalf<'a, R>, w: WriteHalf<'a, W>, buf1: &'a mut VecRingBuffer, buf2: &'a mut VecRingBuffer, } impl<'a, R: AsyncRead, W: AsyncWrite> ClientRequest<'a, R, W> { fn new( stream: (ReadHalf<'a, R>, WriteHalf<'a, W>), buf1: &'a mut VecRingBuffer, buf2: &'a mut VecRingBuffer, ) -> Self { Self { r: stream.0, w: stream.1, buf1, buf2, } } #[allow(clippy::too_many_arguments)] fn prepare_header( self, method: &str, uri: &str, headers: &[http1::Header<'_>], body_size: http1::BodySize, websocket: bool, initial_body: &[u8], end: bool, ) -> Result, Error> { let req = http1::ClientRequest::new(); let req_body = match req.send_header(self.buf1, method, uri, headers, body_size, websocket) { Ok(ret) => ret, Err(_) => return Err(Error::BufferExceeded), }; if self.buf2.write_all(initial_body).is_err() { return Err(Error::BufferExceeded); } Ok(ClientRequestHeader { r: self.r, w: self.w, buf1: self.buf1, buf2: self.buf2, req_body, end, }) } } struct ClientRequestHeader<'a, R: AsyncRead, W: AsyncWrite> { r: ReadHalf<'a, R>, w: WriteHalf<'a, W>, buf1: &'a mut VecRingBuffer, buf2: &'a mut VecRingBuffer, req_body: http1::ClientRequestBody, end: bool, } impl<'a, R: AsyncRead, W: AsyncWrite> ClientRequestHeader<'a, R, W> { async fn send(mut self) -> Result, Error> { while self.buf1.len() > 0 { let size = self.w.write(Buffer::read_buf(self.buf1)).await?; self.buf1.read_commit(size); } let block_size = self.buf2.capacity(); Ok(ClientRequestBody { inner: RefCell::new(Some(ClientRequestBodyInner { r: RefCell::new(ClientRequestBodyRead { stream: self.r, buf: self.buf1, }), w: RefCell::new(ClientRequestBodyWrite { stream: self.w, buf: self.buf2, req_body: Some(self.req_body), end: self.end, block_size, }), })), }) } } struct ClientRequestBodyRead<'a, R: AsyncRead> { stream: ReadHalf<'a, R>, buf: &'a mut VecRingBuffer, } struct ClientRequestBodyWrite<'a, W: AsyncWrite> { stream: WriteHalf<'a, W>, buf: &'a mut VecRingBuffer, req_body: Option, end: bool, block_size: usize, } struct ClientRequestBodyInner<'a, R: AsyncRead, W: AsyncWrite> { r: RefCell>, w: RefCell>, } struct ClientRequestBody<'a, R: AsyncRead, W: AsyncWrite> { inner: RefCell>>, } impl<'a, R: AsyncRead, W: AsyncWrite> ClientRequestBody<'a, R, W> { fn prepare(&self, src: &[u8], end: bool) -> Result { if let Some(inner) = &*self.inner.borrow() { let w = &mut *inner.w.borrow_mut(); // call not allowed if the end has already been indicated if w.end { return Err(Error::Io(io::Error::from(io::ErrorKind::InvalidInput))); } let size = w.buf.write(src)?; assert!(size <= src.len()); if size == src.len() && end { w.end = true; } Ok(size) } else { Err(Error::Unusable) } } fn expand_write_buffer( &self, blocks_max: usize, blocks_avail: &Counter, ) -> Result { if let Some(inner) = &*self.inner.borrow() { let w = &mut *inner.w.borrow_mut(); Ok(resize_write_buffer_if_full( w.buf, w.block_size, blocks_max, blocks_avail, )) } else { Err(Error::Unusable) } } fn can_send(&self) -> bool { if let Some(inner) = &*self.inner.borrow() { let w = &*inner.w.borrow(); w.buf.len() > 0 || w.end } else { false } } async fn send(&self) -> SendStatus, (), Error> { if self.inner.borrow().is_none() { return SendStatus::Error((), Error::Unusable); } let status = loop { if let Some(inner) = self.take_inner_if_early_response() { let r = inner.r.into_inner(); let w = inner.w.into_inner(); let resp = w.req_body.unwrap().into_early_response(); w.buf.clear(); return SendStatus::EarlyResponse(ClientResponse { r: r.stream, buf1: r.buf, buf2: w.buf, inner: resp, }); } match self.process().await { Some(Ok(status)) => break status, Some(Err(e)) => return SendStatus::Error((), e), None => {} // received data. loop and check for early response } }; let mut inner = self.inner.borrow_mut(); assert!(inner.is_some()); match status { http1::SendStatus::Complete(resp, size) => { let inner = inner.take().unwrap(); let r = inner.r.into_inner(); let w = inner.w.into_inner(); w.buf.read_commit(size); assert_eq!(w.buf.len(), 0); SendStatus::Complete(ClientResponse { r: r.stream, buf1: r.buf, buf2: w.buf, inner: resp, }) } http1::SendStatus::Partial(req_body, size) => { let inner = inner.as_ref().unwrap(); let mut w = inner.w.borrow_mut(); w.req_body = Some(req_body); w.buf.read_commit(size); SendStatus::Partial((), size) } http1::SendStatus::Error(req_body, e) => { let inner = inner.as_ref().unwrap(); inner.w.borrow_mut().req_body = Some(req_body); SendStatus::Error((), e.into()) } } } // assumes self.inner is Some #[allow(clippy::await_holding_refcell_ref)] async fn process( &self, ) -> Option< Result< http1::SendStatus, Error, >, > { let inner = self.inner.borrow(); let inner = inner.as_ref().unwrap(); let mut r = inner.r.borrow_mut(); let result = select_2( AsyncOperation::new( |cx| { let w = &mut *inner.w.borrow_mut(); if !w.stream.is_writable() { return None; } let req_body = w.req_body.take().unwrap(); let mut buf_arr = [&b""[..]; VECTORED_MAX - 2]; let bufs = w.buf.read_bufs(&mut buf_arr); match req_body.send( &mut StdWriteWrapper::new(Pin::new(&mut w.stream), cx), bufs, w.end, None, ) { http1::SendStatus::Error(req_body, http1::Error::Io(e)) if e.kind() == io::ErrorKind::WouldBlock => { w.req_body = Some(req_body); None } ret => Some(ret), } }, || inner.w.borrow_mut().stream.cancel(), ), pin!(async { let r = &mut *r; if let Err(e) = recv_nonzero(&mut r.stream, r.buf).await { if e.kind() == io::ErrorKind::WriteZero { // if there's no more space, suspend forever std::future::pending::<()>().await; } return Err(Error::from(e)); } Ok(()) }), ) .await; match result { Select2::R1(ret) => match ret { http1::SendStatus::Error(req_body, http1::Error::Io(e)) if e.kind() == io::ErrorKind::BrokenPipe => { // if we get an error when trying to send, it could be // due to the server closing the connection after sending // an early response. here we'll check if the server left // us any data to read let w = &mut *inner.w.borrow_mut(); w.req_body = Some(req_body); if r.buf.len() == 0 { let r = &mut *r; match recv_nonzero(&mut r.stream, r.buf).await { Ok(()) => None, // received data Err(e) => Some(Err(e.into())), // error while receiving data } } else { None // we already received data } } ret => Some(Ok(ret)), }, Select2::R2(ret) => match ret { Ok(()) => None, // received data Err(e) => Some(Err(e)), // error while receiving data }, } } // assumes self.inner is Some fn take_inner_if_early_response(&self) -> Option> { let mut inner = self.inner.borrow_mut(); let inner_mut = inner.as_mut().unwrap(); if inner_mut.r.borrow().buf.len() > 0 { Some(inner.take().unwrap()) } else { None } } } struct ClientResponse<'a, R: AsyncRead> { r: ReadHalf<'a, R>, buf1: &'a mut VecRingBuffer, buf2: &'a mut VecRingBuffer, inner: http1::ClientResponse, } impl<'a, R: AsyncRead> ClientResponse<'a, R> { async fn recv_header<'b, const N: usize>( mut self, mut scratch: &'b mut http1::ParseScratch, ) -> Result< ( http1::OwnedResponse<'b, N>, ClientResponseBodyKeepHeader<'a, R>, ), Error, > { let mut resp = self.inner; let (resp, resp_body) = loop { { let hbuf = self.buf1.take_inner(); resp = match resp.recv_header(hbuf, scratch) { http1::ParseStatus::Complete(ret) => break ret, http1::ParseStatus::Incomplete(resp, hbuf, ret_scratch) => { // NOTE: after polonius it may not be necessary for // scratch to be returned scratch = ret_scratch; self.buf1.set_inner(hbuf); resp } http1::ParseStatus::Error(e, hbuf, _) => { self.buf1.set_inner(hbuf); return Err(e.into()); } } } if !self.buf1.is_readable_contiguous() { self.buf1.align(); continue; } if let Err(e) = recv_nonzero(&mut self.r, self.buf1).await { if e.kind() == io::ErrorKind::WriteZero { return Err(Error::BufferExceeded); } return Err(e.into()); } }; // at this point, resp has taken buf1's inner buffer, such that // buf1 has no inner buffer // put remaining readable bytes in buf2 self.buf2.write_all(resp.remaining_bytes())?; // swap inner buffers, such that buf1 now contains the remaining // readable bytes, and buf2 is now the one with no inner buffer self.buf1.swap_inner(self.buf2); Ok(( resp, ClientResponseBodyKeepHeader { inner: ClientResponseBody { inner: RefCell::new(Some(ClientResponseBodyInner { r: self.r, closed: false, buf1: self.buf1, resp_body, })), }, buf2: RefCell::new(Some(self.buf2)), }, )) } } struct ClientResponseBodyInner<'a, R: AsyncRead> { r: ReadHalf<'a, R>, closed: bool, buf1: &'a mut VecRingBuffer, resp_body: http1::ClientResponseBody, } struct ClientResponseBody<'a, R: AsyncRead> { inner: RefCell>>, } impl<'a, R: AsyncRead> ClientResponseBody<'a, R> { // on EOF and any subsequent calls, return success #[allow(clippy::await_holding_refcell_ref)] async fn add_to_buffer(&self) -> Result<(), Error> { if let Some(inner) = &mut *self.inner.borrow_mut() { if !inner.closed { match recv_nonzero(&mut inner.r, inner.buf1).await { Ok(()) => {} Err(e) if e.kind() == io::ErrorKind::WriteZero => { return Err(Error::BufferExceeded) } Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => inner.closed = true, Err(e) => return Err(e.into()), } } Ok(()) } else { Err(Error::Unusable) } } fn try_recv(&self, dest: &mut [u8]) -> Result, Error> { loop { let mut b_inner = self.inner.borrow_mut(); if let Some(inner) = b_inner.take() { let mut scratch = mem::MaybeUninit::<[httparse::Header; HEADERS_MAX]>::uninit(); let src = Buffer::read_buf(inner.buf1); let end = src.len() == inner.buf1.len() && inner.closed; match inner.resp_body.recv(src, dest, end, &mut scratch)? { http1::RecvStatus::Complete(finished, read, written) => { inner.buf1.read_commit(read); *b_inner = None; break Ok(RecvStatus::Complete( ClientFinished { inner: finished }, written, )); } http1::RecvStatus::Read(resp_body, read, written) => { *b_inner = Some(ClientResponseBodyInner { r: inner.r, closed: inner.closed, buf1: inner.buf1, resp_body, }); let inner = b_inner.as_mut().unwrap(); if read == 0 && written == 0 { if !inner.buf1.is_readable_contiguous() { inner.buf1.align(); continue; } } inner.buf1.read_commit(read); return Ok(RecvStatus::Read((), written)); } } } else { return Err(Error::Unusable); } } } } struct ClientResponseBodyKeepHeader<'a, R: AsyncRead> { inner: ClientResponseBody<'a, R>, buf2: RefCell>, } impl<'a, R: AsyncRead> ClientResponseBodyKeepHeader<'a, R> { fn discard_header( self, resp: http1::OwnedResponse, ) -> Result, Error> { if let Some(buf2) = self.buf2.borrow_mut().take() { buf2.set_inner(resp.into_buf()); buf2.clear(); Ok(self.inner) } else { Err(Error::Unusable) } } async fn add_to_buffer(&self) -> Result<(), Error> { self.inner.add_to_buffer().await } fn try_recv( &self, dest: &mut [u8], ) -> Result>, Error> { if !self.buf2.borrow().is_some() { return Err(Error::Unusable); } match self.inner.try_recv(dest)? { RecvStatus::Complete(finished, written) => Ok(RecvStatus::Complete( ClientFinishedKeepHeader { inner: finished, buf2: self.buf2.borrow_mut().take().unwrap(), }, written, )), RecvStatus::Read((), written) => Ok(RecvStatus::Read((), written)), } } } struct ClientFinished { inner: http1::ClientFinished, } struct ClientFinishedKeepHeader<'a> { inner: ClientFinished, buf2: &'a mut VecRingBuffer, } impl<'a> ClientFinishedKeepHeader<'a> { fn discard_header(self, resp: http1::OwnedResponse) -> ClientFinished { self.buf2.set_inner(resp.into_buf()); self.buf2.clear(); self.inner } } enum Stream { Plain(std::net::TcpStream), Tls(TlsStream), } impl Read for Stream { fn read(&mut self, buf: &mut [u8]) -> Result { match self { Self::Plain(stream) => stream.read(buf), Self::Tls(stream) => stream.read(buf), } } } enum AsyncStream<'a> { Plain(AsyncTcpStream), Tls(AsyncTlsStream<'a>), } impl<'a> AsyncStream<'a> { fn into_inner(self) -> Stream { match self { Self::Plain(stream) => Stream::Plain(stream.into_std()), Self::Tls(stream) => Stream::Tls(stream.into_std()), } } } #[derive(Clone, Eq, Hash, PartialEq)] struct ConnectionPoolKey { addr: std::net::SocketAddr, tls: bool, host: String, } impl ConnectionPoolKey { fn new(addr: std::net::SocketAddr, tls: bool, host: String) -> Self { Self { addr, tls, host } } } pub struct ConnectionPool { inner: Arc>>, thread: Option>, done: Option>, } impl ConnectionPool { pub fn new(capacity: usize) -> Self { let inner = Arc::new(Mutex::new(Pool::::new(capacity))); let (s, r) = mpsc::sync_channel(1); let thread = { let inner = Arc::clone(&inner); thread::Builder::new() .name("connection-pool".into()) .spawn(move || { while let Err(mpsc::RecvTimeoutError::Timeout) = r.recv_timeout(Duration::from_secs(1)) { let now = Instant::now(); while let Some((key, _)) = inner.lock().unwrap().expire(now) { debug!("closing idle connection to {:?} for {}", key.addr, key.host); } } }) .unwrap() }; Self { inner, thread: Some(thread), done: Some(s), } } #[allow(clippy::result_large_err)] fn push( &self, addr: std::net::SocketAddr, tls: bool, host: String, stream: Stream, ttl: Duration, ) -> Result<(), Stream> { self.inner.lock().unwrap().add( ConnectionPoolKey::new(addr, tls, host), stream, Instant::now() + ttl, ) } fn take(&self, addr: std::net::SocketAddr, tls: bool, host: &str) -> Option { let key = ConnectionPoolKey::new(addr, tls, host.to_string()); // take the first connection that returns WouldBlock when attempting a read. // anything else is considered an error and the connection is discarded while let Some(mut stream) = self.inner.lock().unwrap().take(&key) { match stream.read(&mut [0]) { Err(e) if e.kind() == io::ErrorKind::WouldBlock => return Some(stream), _ => {} } debug!( "discarding broken connection to {:?} for {}", key.addr, key.host ); } None } } impl Drop for ConnectionPool { fn drop(&mut self) { self.done = None; let thread = self.thread.take().unwrap(); thread.join().unwrap(); } } fn is_allowed(addr: &IpAddr, deny: &[IpNet]) -> bool { for net in deny { if net.contains(addr) { return false; } } true } async fn client_connect<'a>( log_id: &str, rdata: &zhttppacket::RequestData<'_, '_>, uri: &url::Url, resolver: &resolver::Resolver, deny: &[IpNet], pool: &ConnectionPool, tls_waker_data: &'a RefWakerData, ) -> Result<(std::net::SocketAddr, bool, AsyncStream<'a>), Error> { let use_tls = ["https", "wss"].contains(&uri.scheme()); let uri_host = match uri.host_str() { Some(s) => s, None => return Err(Error::BadRequest), }; let default_port = if use_tls { 443 } else { 80 }; let (connect_host, connect_port) = if !rdata.connect_host.is_empty() { (rdata.connect_host, rdata.connect_port) } else { (uri_host, uri.port().unwrap_or(default_port)) }; let resolver = AsyncResolver::new(resolver); debug!("client-conn {}: resolving: [{}]", log_id, connect_host); let resolver_results = resolver.resolve(connect_host).await?; let mut addrs = ArrayVec::::new(); let mut denied = false; let mut reuse_stream = None; for addr in resolver_results { if !is_allowed(&addr, deny) { denied = true; continue; } let addr = std::net::SocketAddr::new(addr, connect_port); if let Some(stream) = pool.take(addr, use_tls, uri_host) { reuse_stream = Some((addr, stream)); break; } addrs.push(addr); } let (peer_addr, mut stream, is_new) = if let Some((peer_addr, stream)) = reuse_stream { debug!( "client-conn {}: reusing connection to {:?}", log_id, peer_addr, ); let stream = match stream { Stream::Plain(stream) => AsyncStream::Plain(AsyncTcpStream::from_std(stream)), Stream::Tls(stream) => { AsyncStream::Tls(AsyncTlsStream::from_std(stream, tls_waker_data)) } }; (peer_addr, stream, false) } else { if addrs.is_empty() && denied { return Err(Error::PolicyViolation); } debug!("client-conn {}: connecting to one of {:?}", log_id, addrs); let stream = AsyncTcpStream::connect(&addrs).await?; let peer_addr = stream.peer_addr()?; debug!("client-conn {}: connected to {}", log_id, peer_addr); let stream = if use_tls { let host = if rdata.trust_connect_host { connect_host } else { uri_host }; let verify_mode = if rdata.ignore_tls_errors { VerifyMode::None } else { VerifyMode::Full }; let stream = match AsyncTlsStream::connect(host, stream, verify_mode, tls_waker_data) { Ok(stream) => stream, Err(e) => { debug!("client-conn {}: tls connect error: {}", log_id, e); return Err(Error::Tls); } }; AsyncStream::Tls(stream) } else { AsyncStream::Plain(stream) }; (peer_addr, stream, true) }; if let AsyncStream::Tls(stream) = &mut stream { if stream.inner().set_id(log_id).is_err() { warn!("client-conn {}: log id too long for TlsStream", log_id); return Err(Error::BadRequest); } if is_new { if let Err(e) = stream.ensure_handshake().await { debug!("client-conn {}: tls handshake error: {:?}", log_id, e); return Err(Error::Tls); } } } Ok((peer_addr, use_tls, stream)) } // return Some if fully valid redirect response, else return None. fn check_redirect( method: &str, base_url: &url::Url, resp: &http1::Response, schemes: &[&str], ) -> Option<(url::Url, bool)> { if resp.code >= 300 && resp.code <= 399 { let mut location = None; for h in resp.headers.iter() { if h.name.eq_ignore_ascii_case("Location") { location = Some(h.value); break; } } // must have location header if let Some(s) = location { // must be UTF-8 if let Ok(s) = str::from_utf8(s) { // must be valid URL if let Ok(url) = base_url.join(s) { // must have an acceptable scheme if schemes.contains(&url.scheme()) { let use_get = resp.code >= 301 && resp.code <= 303 && method == "POST"; // all is well! return Some((url, use_get)); } } } } } None } enum ClientHandlerDone { Complete(T, bool), Redirect(bool, url::Url, bool), // rare alloc } impl ClientHandlerDone { fn is_persistent(&self) -> bool { match self { ClientHandlerDone::Complete(_, persistent) => *persistent, ClientHandlerDone::Redirect(persistent, _, _) => *persistent, } } } // return (_, true) if persistent #[allow(clippy::too_many_arguments)] async fn client_req_handler( log_id: &str, id: Option<&[u8]>, stream: &mut S, zreq: &zhttppacket::Request<'_, '_, '_>, method: &str, url: &url::Url, include_body: bool, follow_redirects: bool, buf1: &mut VecRingBuffer, buf2: &mut VecRingBuffer, body_buf: &mut ContiguousBuffer, packet_buf: &RefCell>, ) -> Result, Error> where S: AsyncRead + AsyncWrite, { let stream = RefCell::new(stream); let req = ClientRequest::new(io_split(&stream), buf1, buf2); let req_header = { let rdata = match &zreq.ptype { zhttppacket::RequestPacket::Data(data) => data, _ => return Err(Error::BadRequest), }; let host_port = &url[url::Position::BeforeHost..url::Position::AfterPort]; let mut headers = ArrayVec::::new(); headers.push(http1::Header { name: "Host", value: host_port.as_bytes(), }); for h in rdata.headers.iter() { if headers.remaining_capacity() == 0 { return Err(Error::BadRequest); } // host comes from the uri if h.name.eq_ignore_ascii_case("Host") { continue; } headers.push(http1::Header { name: h.name, value: h.value, }); } let path = &url[url::Position::BeforePath..]; let body_size = if include_body { body_buf.write_all(rdata.body)?; http1::BodySize::Known(rdata.body.len()) } else { http1::BodySize::NoBody }; req.prepare_header(method, path, &headers, body_size, false, &[], false)? }; let resp = { // send request header let req_body = req_header.send().await?; // send request body loop { // fill the buffer as much as possible let size = req_body.prepare(Buffer::read_buf(body_buf), true)?; body_buf.read_commit(size); // send the buffer match req_body.send().await { SendStatus::Complete(resp) => break resp, SendStatus::EarlyResponse(resp) => { body_buf.clear(); break resp; } SendStatus::Partial((), _) => {} SendStatus::Error((), e) => return Err(e), } } }; assert_eq!(body_buf.len(), 0); // receive response header let mut scratch = http1::ParseScratch::::new(); let (resp, resp_body) = resp.recv_header(&mut scratch).await?; let (zresp, finished) = { let resp_ref = resp.get(); debug!( "client-conn {}: response: {} {}", log_id, resp_ref.code, resp_ref.reason ); // receive response body let finished = { loop { match resp_body.try_recv(body_buf.write_buf())? { RecvStatus::Complete(finished, written) => { body_buf.write_commit(written); break finished; } RecvStatus::Read((), written) => { body_buf.write_commit(written); if written == 0 { resp_body.add_to_buffer().await?; } } } } }; if follow_redirects { if let Some((url, use_get)) = check_redirect(method, url, &resp_ref, &["http", "https"]) { let finished = finished.discard_header(resp); debug!("client-conn {}: redirecting to {}", log_id, url); return Ok(ClientHandlerDone::Redirect( finished.inner.persistent, url, use_get, )); } } let mut zheaders = ArrayVec::::new(); for h in resp_ref.headers { zheaders.push(zhttppacket::Header { name: h.name, value: h.value, }); } let rdata = zhttppacket::ResponseData { credits: 0, more: false, code: resp_ref.code, reason: resp_ref.reason, headers: &zheaders, content_type: None, body: Buffer::read_buf(body_buf), }; let zresp = make_zhttp_req_response( id, zhttppacket::ResponsePacket::Data(rdata), &mut packet_buf.borrow_mut(), )?; (zresp, finished) }; let finished = finished.discard_header(resp); Ok(ClientHandlerDone::Complete( zresp, finished.inner.persistent, )) } #[allow(clippy::too_many_arguments)] async fn client_req_connect( log_id: &str, id: Option<&[u8]>, zreq: arena::Rc, buf1: &mut VecRingBuffer, buf2: &mut VecRingBuffer, body_buf: &mut ContiguousBuffer, packet_buf: &RefCell>, deny: &[IpNet], resolver: &resolver::Resolver, pool: &ConnectionPool, ) -> Result { let zreq = zreq.get().get(); let rdata = match &zreq.ptype { zhttppacket::RequestPacket::Data(data) => data, _ => return Err(Error::BadRequest), }; let initial_url = match url::Url::parse(rdata.uri) { Ok(url) => url, Err(_) => return Err(Error::BadRequest), }; // must be an http url if !["http", "https"].contains(&initial_url.scheme()) { return Err(Error::BadRequest); } // must have a method if rdata.method.is_empty() { return Err(Error::BadRequest); } debug!( "client-conn {}: request: {} {}", log_id, rdata.method, rdata.uri, ); let deny = if rdata.ignore_policies { &[] } else { deny }; let mut last_redirect: Option<(url::Url, bool)> = None; let mut redirect_count = 0; let zresp = loop { let (method, url, include_body) = match &last_redirect { Some((url, use_get)) => { let (method, include_body) = if *use_get { ("GET", false) } else { (rdata.method, true) }; (method, url, include_body) } None => (rdata.method, &initial_url, true), }; let url_host = match url.host_str() { Some(s) => s, None => return Err(Error::BadRequest), }; let tls_waker_data = RefWakerData::new(TlsWaker::new()); let (peer_addr, using_tls, mut stream) = client_connect(log_id, rdata, url, resolver, deny, pool, &tls_waker_data).await?; let done = match &mut stream { AsyncStream::Plain(stream) => { client_req_handler( log_id, id, stream, zreq, method, url, include_body, rdata.follow_redirects, buf1, buf2, body_buf, packet_buf, ) .await? } AsyncStream::Tls(stream) => { client_req_handler( log_id, id, stream, zreq, method, url, include_body, rdata.follow_redirects, buf1, buf2, body_buf, packet_buf, ) .await? } }; if done.is_persistent() { if pool .push( peer_addr, using_tls, url_host.to_string(), stream.into_inner(), CONNECTION_POOL_TTL, ) .is_ok() { debug!("client-conn {}: leaving connection intact", log_id); } } match done { ClientHandlerDone::Complete(zresp, _) => break zresp, ClientHandlerDone::Redirect(_, url, mut use_get) => { if redirect_count >= REDIRECTS_MAX { return Err(Error::TooManyRedirects); } redirect_count += 1; if let Some((_, b)) = &last_redirect { use_get = use_get || *b; } last_redirect = Some((url, use_get)); } } }; Ok(zresp) } #[allow(clippy::too_many_arguments)] async fn client_req_connection_inner( token: CancellationToken, log_id: &str, id: Option<&[u8]>, zreq: (MultipartHeader, arena::Rc), buffer_size: usize, body_buffer_size: usize, rb_tmp: &Rc, packet_buf: Rc>>, timeout: Duration, deny: &[IpNet], resolver: &resolver::Resolver, pool: &ConnectionPool, zsender: AsyncLocalSender<(MultipartHeader, zmq::Message)>, ) -> Result<(), Error> { let reactor = Reactor::current().unwrap(); let (zheader, zreq) = zreq; let mut buf1 = VecRingBuffer::new(buffer_size, rb_tmp); let mut buf2 = VecRingBuffer::new(buffer_size, rb_tmp); let mut body_buf = ContiguousBuffer::new(body_buffer_size); let handler = client_req_connect( log_id, id, zreq, &mut buf1, &mut buf2, &mut body_buf, &packet_buf, deny, resolver, pool, ); let timeout = Timeout::new(reactor.now() + timeout); let ret = match select_3(pin!(handler), timeout.elapsed(), token.cancelled()).await { Select3::R1(ret) => ret, Select3::R2(_) => Err(Error::StreamTimeout), Select3::R3(_) => return Err(Error::Stopped), }; match ret { Ok(zresp) => zsender.send((zheader, zresp)).await?, Err(e) => { let zresp = make_zhttp_req_response( id, zhttppacket::ResponsePacket::Error(zhttppacket::ResponseErrorData { condition: e.to_condition(), rejected_info: None, }), &mut packet_buf.borrow_mut(), )?; zsender.send((zheader, zresp)).await?; return Err(e); } } Ok(()) } #[allow(clippy::too_many_arguments)] pub async fn client_req_connection( token: CancellationToken, log_id: &str, id: Option<&[u8]>, zreq: (MultipartHeader, arena::Rc), buffer_size: usize, body_buffer_size: usize, rb_tmp: &Rc, packet_buf: Rc>>, timeout: Duration, deny: &[IpNet], resolver: &resolver::Resolver, pool: &ConnectionPool, zsender: AsyncLocalSender<(MultipartHeader, zmq::Message)>, ) { match client_req_connection_inner( token, log_id, id, zreq, buffer_size, body_buffer_size, rb_tmp, packet_buf, timeout, deny, resolver, pool, zsender, ) .await { Ok(()) => debug!("client-conn {}: finished", log_id), Err(e) => { let level = match e { Error::ValueActive => Level::Error, _ => Level::Debug, }; log!(level, "client-conn {}: process error: {:?}", log_id, e); } } } // return true if persistent #[allow(clippy::too_many_arguments)] async fn client_stream_handler( log_id: &str, stream: &mut S, zreq: &zhttppacket::Request<'_, '_, '_>, method: &str, url: &url::Url, include_body: bool, mut follow_redirects: bool, buf1: &mut VecRingBuffer, buf2: &mut VecRingBuffer, blocks_max: usize, blocks_avail: &Counter, messages_max: usize, allow_compression: bool, tmp_buf: &RefCell>, zsess_in: &mut ZhttpServerStreamSessionIn<'_, '_, R2>, zsess_out: &ZhttpServerStreamSessionOut<'_>, response_received: &mut bool, refresh_stream_timeout: &R1, ) -> Result, Error> where S: AsyncRead + AsyncWrite, R1: Fn(), R2: Fn(), { let stream = RefCell::new(stream); let send_buf_size = buf1.capacity(); // for sending to handler let recv_buf_size = buf2.capacity(); // for receiving from handler let req = ClientRequest::new(io_split(&stream), buf1, buf2); let (req_header, ws_key, overflow) = { let rdata = match &zreq.ptype { zhttppacket::RequestPacket::Data(data) => data, _ => return Err(Error::BadRequest), }; let websocket = ["wss", "ws"].contains(&url.scheme()); let host_port = &url[url::Position::BeforeHost..url::Position::AfterPort]; let ws_key = if websocket { Some(gen_ws_key()) } else { None }; if !websocket && rdata.more { follow_redirects = false; } let mut ws_ext = ArrayVec::::new(); let mut headers = ArrayVec::::new(); headers.push(http1::Header { name: "Host", value: host_port.as_bytes(), }); if let Some(ws_key) = &ws_key { headers.push(http1::Header { name: "Upgrade", value: b"websocket", }); headers.push(http1::Header { name: "Connection", value: b"Upgrade", }); headers.push(http1::Header { name: "Sec-WebSocket-Version", value: b"13", }); headers.push(http1::Header { name: "Sec-WebSocket-Key", value: ws_key.as_bytes(), }); if allow_compression { if write_ws_ext_header_value( &websocket::PerMessageDeflateConfig::default(), &mut ws_ext, ) .is_err() { return Err(Error::Compression); } headers.push(http1::Header { name: "Sec-WebSocket-Extensions", value: ws_ext.as_slice(), }); } } let mut body_size = if websocket || !include_body { http1::BodySize::NoBody } else { http1::BodySize::Unknown }; for h in rdata.headers.iter() { // host comes from the uri if h.name.eq_ignore_ascii_case("Host") { continue; } if websocket { // don't send these headers if h.name.eq_ignore_ascii_case("Connection") || h.name.eq_ignore_ascii_case("Upgrade") || h.name.eq_ignore_ascii_case("Sec-WebSocket-Version") || h.name.eq_ignore_ascii_case("Sec-WebSocket-Key") { continue; } } else { if h.name.eq_ignore_ascii_case("Content-Length") { let s = str::from_utf8(h.value)?; let clen: usize = match s.parse() { Ok(clen) => clen, Err(_) => return Err(io::Error::from(io::ErrorKind::InvalidInput).into()), }; body_size = http1::BodySize::Known(clen); } } if headers.remaining_capacity() == 0 { return Err(Error::BadRequest); } headers.push(http1::Header { name: h.name, value: h.value, }); } let method = if websocket { "GET" } else { method }; let path = &url[url::Position::BeforePath..]; if body_size == http1::BodySize::Unknown && !rdata.more { body_size = http1::BodySize::Known(rdata.body.len()); } let mut overflow = None; let req_header = if websocket { req.prepare_header(method, path, &headers, body_size, true, &[], true)? } else { let (initial_body, end) = if include_body { if rdata.body.len() > recv_buf_size { let body = &rdata.body[..recv_buf_size]; let mut remainder = ContiguousBuffer::new(rdata.body.len() - body.len()); remainder.write_all(&rdata.body[body.len()..])?; debug!( "initial={} overflow={} end={}", body.len(), remainder.len(), !rdata.more ); overflow = Some(Overflow { buf: remainder, end: !rdata.more, }); (body, false) } else { (rdata.body, !rdata.more) } } else { (&[][..], true) }; req.prepare_header(method, path, &headers, body_size, false, initial_body, end)? }; (req_header, ws_key, overflow) }; // send request header let req_body = { let mut send_header = pin!(req_header.send()); loop { // ABR: select contains read let result = select_2(send_header.as_mut(), pin!(zsess_in.recv_msg())).await; match result { Select2::R1(ret) => break ret?, Select2::R2(ret) => { let zreq = ret?; // ABR: handle_other server_handle_other(zreq, zsess_in, zsess_out).await?; } } } }; refresh_stream_timeout(); // send request body // ABR: function contains read let resp = server_stream_send_body( refresh_stream_timeout, req_body, overflow, recv_buf_size, zsess_in, zsess_out, blocks_max, blocks_avail, ) .await?; // receive response header let (resp_body, ws_config) = { let mut scratch = http1::ParseScratch::::new(); let mut recv_header = pin!(resp.recv_header(&mut scratch)); let (resp, resp_body) = loop { // ABR: select contains read let result = select_2(recv_header.as_mut(), pin!(zsess_in.recv_msg())).await; match result { Select2::R1(ret) => break ret?, Select2::R2(ret) => { let zreq = ret?; // ABR: handle_other server_handle_other(zreq, zsess_in, zsess_out).await?; } } }; let ws_config = { let resp_ref = resp.get(); debug!( "client-conn {}: response: {} {}", log_id, resp_ref.code, resp_ref.reason ); loop { // ABR: select contains read let result = select_2(pin!(zsess_out.check_send()), pin!(zsess_in.recv_msg())).await; match result { Select2::R1(()) => break, Select2::R2(ret) => { let zreq = ret?; // ABR: handle_other server_handle_other(zreq, zsess_in, zsess_out).await?; } } } if follow_redirects { let schemes = if ws_key.is_some() { ["ws", "wss"] } else { ["http", "https"] }; if let Some((url, use_get)) = check_redirect(method, url, &resp_ref, &schemes) { // eat response body let finished = loop { let ret = { let mut buf = [0; 4_096]; resp_body.try_recv(&mut buf)? }; match ret { RecvStatus::Complete(finished, _) => break finished, RecvStatus::Read((), written) => { if written == 0 { let mut add_to_buffer = pin!(resp_body.add_to_buffer()); loop { // ABR: select contains read let result = select_2( add_to_buffer.as_mut(), pin!(zsess_in.recv_msg()), ) .await; match result { Select2::R1(ret) => { ret?; break; } Select2::R2(ret) => { let zreq = ret?; // ABR: handle_other server_handle_other(zreq, zsess_in, zsess_out) .await?; } } } } } } }; let finished = finished.discard_header(resp); debug!("client-conn {}: redirecting to {}", log_id, url); return Ok(ClientHandlerDone::Redirect( finished.inner.persistent, url, use_get, )); } } let mut zheaders = ArrayVec::::new(); let mut ws_accept = None; let mut ws_deflate_config = None; for h in resp_ref.headers { if ws_key.is_some() { if h.name.eq_ignore_ascii_case("Sec-WebSocket-Accept") { ws_accept = Some(h.value); } if h.name.eq_ignore_ascii_case("Sec-WebSocket-Extensions") { for value in http1::parse_header_value(h.value) { let (name, params) = match value { Ok(v) => v, Err(_) => return Err(Error::InvalidWebSocketResponse), }; match name { "permessage-deflate" => { // we must have offered, and server must // provide one response at most if !allow_compression || ws_deflate_config.is_some() { return Err(Error::InvalidWebSocketResponse); } if let Ok(config) = websocket::PerMessageDeflateConfig::from_params(params) { if config.check_response().is_ok() { // set the encoded buffer to be 25% the size of the // recv buffer let enc_buf_size = recv_buf_size / 4; ws_deflate_config = Some((config, enc_buf_size)); } } } name => { debug!("ignoring unsupported websocket extension: {}", name); continue; } } } } } zheaders.push(zhttppacket::Header { name: h.name, value: h.value, }); } if let Some(ws_key) = &ws_key { if resp_ref.code == 101 { if validate_ws_response(ws_key.as_bytes(), ws_accept).is_err() { return Err(Error::InvalidWebSocketResponse); } } else { // websocket request rejected // we need to allocate to collect the response body, // since buf1 holds bytes read from the socket, and // resp is using buf2's inner buffer let mut body_buf = ContiguousBuffer::new(send_buf_size); // receive response body let finished = loop { match resp_body.try_recv(body_buf.write_buf())? { RecvStatus::Complete(finished, written) => { body_buf.write_commit(written); break finished; } RecvStatus::Read((), written) => { body_buf.write_commit(written); if written == 0 { let mut add_to_buffer = pin!(resp_body.add_to_buffer()); loop { // ABR: select contains read let result = select_2( add_to_buffer.as_mut(), pin!(zsess_in.recv_msg()), ) .await; match result { Select2::R1(ret) => { ret?; break; } Select2::R2(ret) => { let zreq = ret?; // ABR: handle_other server_handle_other(zreq, zsess_in, zsess_out) .await?; } } } } } } }; let edata = zhttppacket::ResponseErrorData { condition: "rejected", rejected_info: Some(zhttppacket::RejectedInfo { code: resp_ref.code, reason: resp_ref.reason, headers: &zheaders, body: body_buf.read_buf(), }), }; let zresp = zhttppacket::Response::new_error(b"", &[], edata); // check_send just finished, so this should succeed zsess_out.try_send_msg(zresp)?; drop(zheaders); let finished = finished.discard_header(resp); return Ok(ClientHandlerDone::Complete((), finished.inner.persistent)); } } let credits = if ws_key.is_some() { // for websockets, provide credits when sending response to handler recv_buf_size as u32 } else { // for http, it is not necessary to provide credits when responding 0 }; let rdata = zhttppacket::ResponseData { credits, more: ws_key.is_none(), code: resp_ref.code, reason: resp_ref.reason, headers: &zheaders, content_type: None, body: b"", }; let zresp = zhttppacket::Response::new_data(b"", &[], rdata); // check_send just finished, so this should succeed zsess_out.try_send_msg(zresp)?; if ws_key.is_some() { Some(ws_deflate_config) } else { None } }; let resp_body = resp_body.discard_header(resp)?; (resp_body, ws_config) }; *response_received = true; if let Some(deflate_config) = ws_config { // handle as websocket connection // ABR: function contains read server_stream_websocket( log_id, stream, buf1, buf2, blocks_max, blocks_avail, messages_max, tmp_buf, refresh_stream_timeout, deflate_config, zsess_in, zsess_out, ) .await?; Ok(ClientHandlerDone::Complete((), false)) } else { // receive response body // ABR: function contains read let finished = server_stream_recv_body( tmp_buf, refresh_stream_timeout, resp_body, zsess_in, zsess_out, ) .await?; Ok(ClientHandlerDone::Complete((), finished.inner.persistent)) } } #[allow(clippy::too_many_arguments)] async fn client_stream_connect( log_id: &str, id: &[u8], zreq: arena::Rc, buf1: &mut VecRingBuffer, buf2: &mut VecRingBuffer, buffer_size: usize, blocks_max: usize, blocks_avail: &Counter, messages_max: usize, allow_compression: bool, packet_buf: &RefCell>, tmp_buf: &RefCell>, deny: &[IpNet], instance_id: &str, resolver: &resolver::Resolver, pool: &ConnectionPool, zreceiver: &TrackedAsyncLocalReceiver<'_, (arena::Rc, usize)>, zsender: &AsyncLocalSender, shared: &StreamSharedData, enable_routing: &E, response_received: &mut bool, refresh_stream_timeout: &R1, refresh_session_timeout: &R2, ) -> Result<(), Error> where E: Fn(), R1: Fn(), R2: Fn(), { let zreq = zreq.get().get(); // assign address so we can send replies let addr: ArrayVec = match ArrayVec::try_from(zreq.from) { Ok(v) => v, Err(_) => return Err(Error::BadRequest), }; shared.set_to_addr(Some(addr)); let rdata = match &zreq.ptype { zhttppacket::RequestPacket::Data(data) => data, _ => return Err(Error::BadRequest), }; let initial_url = match url::Url::parse(rdata.uri) { Ok(url) => url, Err(_) => return Err(Error::BadRequest), }; // must be an http or websocket url if !["http", "https", "ws", "wss"].contains(&initial_url.scheme()) { return Err(Error::BadRequest); } // http requests must have a method if ["http", "https"].contains(&initial_url.scheme()) && rdata.method.is_empty() { return Err(Error::BadRequest); } let method = if !rdata.method.is_empty() { rdata.method } else { "_" }; debug!("client-conn {}: request: {} {}", log_id, method, rdata.uri); let zsess_out = ZhttpServerStreamSessionOut::new(instance_id, id, packet_buf, zsender, shared); // ack request // ABR: discard_while server_discard_while( zreceiver, pin!(async { zsess_out.check_send().await; Ok(()) }), ) .await?; zsess_out.try_send_msg(zhttppacket::Response::new_keep_alive(b"", &[]))?; let mut zsess_in = ZhttpServerStreamSessionIn::new( log_id, id, rdata.credits, zreceiver, shared, refresh_session_timeout, ); // allow receiving subsequent messages enable_routing(); let deny = if rdata.ignore_policies { &[] } else { deny }; let mut last_redirect: Option<(url::Url, bool)> = None; let mut redirect_count = 0; loop { let (method, url, include_body) = match &last_redirect { Some((url, use_get)) => { let (method, include_body) = if *use_get { ("GET", false) } else { (rdata.method, true) }; (method, url, include_body) } None => (rdata.method, &initial_url, true), }; let url_host = match url.host_str() { Some(s) => s, None => return Err(Error::BadRequest), }; let tls_waker_data = RefWakerData::new(TlsWaker::new()); let (peer_addr, using_tls, mut stream) = { let mut client_connect = pin!(client_connect( log_id, rdata, url, resolver, deny, pool, &tls_waker_data )); loop { // ABR: select contains read let ret = select_2(client_connect.as_mut(), pin!(zsess_in.recv_msg())).await; match ret { Select2::R1(ret) => break ret?, Select2::R2(ret) => { let zreq = ret?; // ABR: handle_other server_handle_other(zreq, &mut zsess_in, &zsess_out).await?; } } } }; let done = match &mut stream { AsyncStream::Plain(stream) => { client_stream_handler( log_id, stream, zreq, method, url, include_body, rdata.follow_redirects, buf1, buf2, blocks_max, blocks_avail, messages_max, allow_compression, tmp_buf, &mut zsess_in, &zsess_out, response_received, refresh_stream_timeout, ) .await? } AsyncStream::Tls(stream) => { client_stream_handler( log_id, stream, zreq, method, url, include_body, rdata.follow_redirects, buf1, buf2, blocks_max, blocks_avail, messages_max, allow_compression, tmp_buf, &mut zsess_in, &zsess_out, response_received, refresh_stream_timeout, ) .await? } }; if done.is_persistent() { let additional_blocks = (buf2.capacity() / buffer_size) - 1; buf2.resize(buffer_size); blocks_avail.inc(additional_blocks).unwrap(); if pool .push( peer_addr, using_tls, url_host.to_string(), stream.into_inner(), CONNECTION_POOL_TTL, ) .is_ok() { debug!("client-conn {}: leaving connection intact", log_id); } } match done { ClientHandlerDone::Complete((), _) => break, ClientHandlerDone::Redirect(_, url, mut use_get) => { if redirect_count >= REDIRECTS_MAX { return Err(Error::TooManyRedirects); } redirect_count += 1; if let Some((_, b)) = &last_redirect { use_get = use_get || *b; } last_redirect = Some((url, use_get)); } } } Ok(()) } #[allow(clippy::too_many_arguments)] async fn client_stream_connection_inner( token: CancellationToken, log_id: &str, id: &[u8], zreq: arena::Rc, buffer_size: usize, blocks_max: usize, blocks_avail: &Counter, messages_max: usize, rb_tmp: &Rc, packet_buf: Rc>>, tmp_buf: Rc>>, stream_timeout_duration: Duration, allow_compression: bool, deny: &[IpNet], instance_id: &str, resolver: &resolver::Resolver, pool: &ConnectionPool, zreceiver: &TrackedAsyncLocalReceiver<'_, (arena::Rc, usize)>, zsender: AsyncLocalSender, shared: arena::Rc, enable_routing: &E, ) -> Result<(), Error> where E: Fn(), { let reactor = Reactor::current().unwrap(); let mut buf1 = VecRingBuffer::new(buffer_size, rb_tmp); let mut buf2 = VecRingBuffer::new(buffer_size, rb_tmp); let stream_timeout = Timeout::new(reactor.now() + stream_timeout_duration); let session_timeout = Timeout::new(reactor.now() + ZHTTP_SESSION_TIMEOUT); let refresh_stream_timeout = || { stream_timeout.set_deadline(reactor.now() + stream_timeout_duration); }; let refresh_session_timeout = || { session_timeout.set_deadline(reactor.now() + ZHTTP_SESSION_TIMEOUT); }; let mut response_received = false; let ret = { let handler = pin!(client_stream_connect( log_id, id, zreq, &mut buf1, &mut buf2, buffer_size, blocks_max, blocks_avail, messages_max, allow_compression, &packet_buf, &tmp_buf, deny, instance_id, resolver, pool, zreceiver, &zsender, shared.get(), enable_routing, &mut response_received, &refresh_stream_timeout, &refresh_session_timeout, )); match select_4( handler, stream_timeout.elapsed(), session_timeout.elapsed(), token.cancelled(), ) .await { Select4::R1(ret) => ret, Select4::R2(_) => Err(Error::StreamTimeout), Select4::R3(_) => return Err(Error::SessionTimeout), Select4::R4(_) => return Err(Error::Stopped), } }; match ret { Ok(()) => {} Err(e) => { let handler_caused = matches!( &e, Error::BadMessage | Error::Handler | Error::HandlerCancel ); if !handler_caused { let shared = shared.get(); let msg = if let Some(addr) = shared.to_addr().get() { let mut zresp = if response_received { zhttppacket::Response::new_cancel(b"", &[]) } else { zhttppacket::Response::new_error( b"", &[], zhttppacket::ResponseErrorData { condition: e.to_condition(), rejected_info: None, }, ) }; let ids = [zhttppacket::Id { id, seq: Some(shared.out_seq()), }]; zresp.from = instance_id.as_bytes(); zresp.ids = &ids; zresp.multi = true; let packet_buf = &mut *packet_buf.borrow_mut(); let msg = make_zhttp_response(addr, zresp, packet_buf)?; Some(msg) } else { None }; if let Some(msg) = msg { // best effort let _ = zsender.try_send(msg); shared.inc_out_seq(); } } return Err(e); } } Ok(()) } #[allow(clippy::too_many_arguments)] pub async fn client_stream_connection( token: CancellationToken, log_id: &str, id: &[u8], zreq: arena::Rc, buffer_size: usize, blocks_max: usize, blocks_avail: &Counter, messages_max: usize, rb_tmp: &Rc, packet_buf: Rc>>, tmp_buf: Rc>>, timeout: Duration, allow_compression: bool, deny: &[IpNet], instance_id: &str, resolver: &resolver::Resolver, pool: &ConnectionPool, zreceiver: AsyncLocalReceiver<(arena::Rc, usize)>, zsender: AsyncLocalSender, shared: arena::Rc, enable_routing: &E, ) where E: Fn(), { let value_active = TrackFlag::default(); let zreceiver = TrackedAsyncLocalReceiver::new(zreceiver, &value_active); match track_future( client_stream_connection_inner( token, log_id, id, zreq, buffer_size, blocks_max, blocks_avail, messages_max, rb_tmp, packet_buf, tmp_buf, timeout, allow_compression, deny, instance_id, resolver, pool, &zreceiver, zsender, shared, enable_routing, ), &value_active, ) .await { Ok(()) => debug!("client-conn {}: finished", log_id), Err(e) => { let level = match e { Error::ValueActive => Level::Error, _ => Level::Debug, }; log!(level, "client-conn {}: process error: {:?}", log_id, e); } } } pub mod testutil { use super::*; use crate::buffer::TmpBuffer; use crate::channel; use crate::waker; use std::fmt; use std::future::Future; use std::io::Read; use std::rc::Rc; use std::sync::Arc; use std::task::{Context, Poll, Waker}; use std::time::Instant; pub struct NoopWaker {} #[allow(clippy::new_without_default)] impl NoopWaker { pub fn new() -> Self { Self {} } pub fn into_std(self: Rc) -> Waker { waker::into_std(self) } } impl waker::RcWake for NoopWaker { fn wake(self: Rc) {} } pub struct StepExecutor<'a, F> { reactor: &'a Reactor, fut: Pin>, } impl<'a, F> StepExecutor<'a, F> where F: Future, { pub fn new(reactor: &'a Reactor, fut: F) -> Self { Self { reactor, fut: Box::pin(fut), } } pub fn step(&mut self) -> Poll { self.reactor.poll_nonblocking(self.reactor.now()).unwrap(); let waker = Rc::new(NoopWaker::new()).into_std(); let mut cx = Context::from_waker(&waker); self.fut.as_mut().poll(&mut cx) } pub fn advance_time(&mut self, now: Instant) { self.reactor.poll_nonblocking(now).unwrap(); } } #[track_caller] pub fn check_poll(p: Poll>) -> Option where E: fmt::Debug, { match p { Poll::Ready(v) => match v { Ok(t) => Some(t), Err(e) => panic!("check_poll error: {:?}", e), }, Poll::Pending => None, } } pub struct FakeSock { inbuf: Vec, outbuf: Vec, out_allow: usize, } #[allow(clippy::new_without_default)] impl FakeSock { pub fn new() -> Self { Self { inbuf: Vec::with_capacity(16384), outbuf: Vec::with_capacity(16384), out_allow: 0, } } pub fn add_readable(&mut self, buf: &[u8]) { self.inbuf.extend_from_slice(buf); } pub fn take_writable(&mut self) -> Vec { mem::take(&mut self.outbuf) } pub fn allow_write(&mut self, size: usize) { self.out_allow += size; } pub fn clear_write_allowed(&mut self) { self.out_allow = 0; } } impl Read for FakeSock { fn read(&mut self, buf: &mut [u8]) -> Result { if self.inbuf.is_empty() { return Err(io::Error::from(io::ErrorKind::WouldBlock)); } let size = cmp::min(buf.len(), self.inbuf.len()); buf[..size].copy_from_slice(&self.inbuf[..size]); let mut rest = self.inbuf.split_off(size); mem::swap(&mut self.inbuf, &mut rest); Ok(size) } } impl Write for FakeSock { fn write(&mut self, buf: &[u8]) -> Result { if !buf.is_empty() && self.out_allow == 0 { return Err(io::Error::from(io::ErrorKind::WouldBlock)); } let size = cmp::min(buf.len(), self.out_allow); let buf = &buf[..size]; self.outbuf.extend_from_slice(buf); self.out_allow -= size; Ok(buf.len()) } fn write_vectored(&mut self, bufs: &[io::IoSlice]) -> Result { let mut total = 0; for buf in bufs { if !buf.is_empty() && self.out_allow == 0 { if total == 0 { return Err(io::Error::from(io::ErrorKind::WouldBlock)); } break; } let size = cmp::min(buf.len(), self.out_allow); let buf = &buf[..size]; self.outbuf.extend_from_slice(buf.as_ref()); self.out_allow -= size; total += buf.len(); } Ok(total) } fn flush(&mut self) -> Result<(), io::Error> { Ok(()) } } pub struct AsyncFakeSock { pub inner: Rc>, } impl AsyncFakeSock { pub fn new(sock: Rc>) -> Self { Self { inner: sock } } } impl AsyncRead for AsyncFakeSock { fn poll_read( self: Pin<&mut Self>, _cx: &mut Context, buf: &mut [u8], ) -> Poll> { let inner = &mut *self.inner.borrow_mut(); match inner.read(buf) { Ok(usize) => Poll::Ready(Ok(usize)), Err(e) if e.kind() == io::ErrorKind::WouldBlock => Poll::Pending, Err(e) => Poll::Ready(Err(e)), } } fn cancel(&mut self) {} } impl AsyncWrite for AsyncFakeSock { fn poll_write( self: Pin<&mut Self>, _cx: &mut Context, buf: &[u8], ) -> Poll> { let inner = &mut *self.inner.borrow_mut(); match inner.write(buf) { Ok(usize) => Poll::Ready(Ok(usize)), Err(e) if e.kind() == io::ErrorKind::WouldBlock => Poll::Pending, Err(e) => Poll::Ready(Err(e)), } } fn poll_write_vectored( self: Pin<&mut Self>, _cx: &mut Context, bufs: &[io::IoSlice], ) -> Poll> { let inner = &mut *self.inner.borrow_mut(); match inner.write_vectored(bufs) { Ok(usize) => Poll::Ready(Ok(usize)), Err(e) if e.kind() == io::ErrorKind::WouldBlock => Poll::Pending, Err(e) => Poll::Ready(Err(e)), } } fn poll_close(self: Pin<&mut Self>, _cx: &mut Context) -> Poll> { Poll::Ready(Ok(())) } fn is_writable(&self) -> bool { true } fn cancel(&mut self) {} } impl Identify for AsyncFakeSock { fn set_id(&mut self, _id: &str) { // do nothing } } pub struct SimpleCidProvider { pub cid: ArrayString<32>, } impl CidProvider for SimpleCidProvider { fn get_new_assigned_cid(&mut self) -> ArrayString<32> { self.cid } } #[allow(clippy::too_many_arguments)] async fn server_req_handler_fut( sock: Rc>, secure: bool, s_from_conn: channel::LocalSender, r_to_conn: channel::LocalReceiver<(arena::Rc, usize)>, packet_buf: Rc>>, buf1: &mut VecRingBuffer, buf2: &mut VecRingBuffer, body_buf: &mut ContiguousBuffer, ) -> Result { let mut sock = AsyncFakeSock::new(sock); let f = TrackFlag::default(); let r_to_conn = TrackedAsyncLocalReceiver::new(AsyncLocalReceiver::new(r_to_conn), &f); let s_from_conn = AsyncLocalSender::new(s_from_conn); server_req_handler( "1", &mut sock, None, secure, buf1, buf2, body_buf, &packet_buf, &s_from_conn, &r_to_conn, ) .await } pub struct BenchServerReqHandlerArgs { sock: Rc>, buf1: VecRingBuffer, buf2: VecRingBuffer, body_buf: ContiguousBuffer, } pub struct BenchServerReqHandler { reactor: Reactor, msg_mem: Arc>, scratch_mem: Rc>>>, resp_mem: Rc>, rb_tmp: Rc, packet_buf: Rc>>, } #[allow(clippy::new_without_default)] impl BenchServerReqHandler { pub fn new() -> Self { Self { reactor: Reactor::new(100), msg_mem: Arc::new(arena::ArcMemory::new(1)), scratch_mem: Rc::new(arena::RcMemory::new(1)), resp_mem: Rc::new(arena::RcMemory::new(1)), rb_tmp: Rc::new(TmpBuffer::new(1024)), packet_buf: Rc::new(RefCell::new(vec![0; 2048])), } } pub fn init(&self) -> BenchServerReqHandlerArgs { let buffer_size = 1024; BenchServerReqHandlerArgs { sock: Rc::new(RefCell::new(FakeSock::new())), buf1: VecRingBuffer::new(buffer_size, &self.rb_tmp), buf2: VecRingBuffer::new(buffer_size, &self.rb_tmp), body_buf: ContiguousBuffer::new(buffer_size), } } pub fn run(&self, args: &mut BenchServerReqHandlerArgs) { let reactor = &self.reactor; let msg_mem = &self.msg_mem; let scratch_mem = &self.scratch_mem; let resp_mem = &self.resp_mem; let packet_buf = &self.packet_buf; let sock = &args.sock; let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let fut = { let sock = args.sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); server_req_handler_fut( sock, false, s_from_conn, r_to_conn, packet_buf.clone(), &mut args.buf1, &mut args.buf2, &mut args.body_buf, ) }; let mut executor = StepExecutor::new(reactor, fut); assert_eq!(check_poll(executor.step()), None); let req_data = concat!( "GET /path HTTP/1.1\r\n", "Host: example.com\r\n", "Connection: close\r\n", "\r\n" ) .as_bytes(); sock.borrow_mut().add_readable(req_data); sock.borrow_mut().allow_write(1024); assert_eq!(check_poll(executor.step()), None); // read message let _ = r_from_conn.try_recv().unwrap(); let msg = concat!( "T100:2:id,1:1,4:code,3:200#6:reason,2:OK,7:h", "eaders,34:30:12:Content-Type,10:text/plain,]]4:body,6:hell", "o\n,}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), scratch_mem) .unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, resp_mem).unwrap(); assert!(s_to_conn.try_send((resp, 0)).is_ok()); assert_eq!(check_poll(executor.step()), Some(false)); let data = sock.borrow_mut().take_writable(); let expected = concat!( "HTTP/1.1 200 OK\r\n", "Content-Type: text/plain\r\n", "Connection: close\r\n", "Content-Length: 6\r\n", "\r\n", "hello\n", ); assert_eq!(str::from_utf8(&data).unwrap(), expected); } } async fn server_req_connection_inner_fut( token: CancellationToken, sock: Rc>, secure: bool, s_from_conn: channel::LocalSender, r_to_conn: channel::LocalReceiver<(arena::Rc, usize)>, rb_tmp: Rc, packet_buf: Rc>>, ) -> Result<(), Error> { let mut cid = ArrayString::from_str("1").unwrap(); let mut cid_provider = SimpleCidProvider { cid }; let sock = AsyncFakeSock::new(sock); let f = TrackFlag::default(); let r_to_conn = TrackedAsyncLocalReceiver::new(AsyncLocalReceiver::new(r_to_conn), &f); let s_from_conn = AsyncLocalSender::new(s_from_conn); let buffer_size = 1024; let timeout = Duration::from_millis(5_000); server_req_connection_inner( token, &mut cid, &mut cid_provider, sock, None, secure, buffer_size, buffer_size, &rb_tmp, packet_buf, timeout, s_from_conn, &r_to_conn, ) .await } pub struct BenchServerReqConnection { reactor: Reactor, msg_mem: Arc>, scratch_mem: Rc>>>, resp_mem: Rc>, rb_tmp: Rc, packet_buf: Rc>>, } #[allow(clippy::new_without_default)] impl BenchServerReqConnection { pub fn new() -> Self { Self { reactor: Reactor::new(100), msg_mem: Arc::new(arena::ArcMemory::new(1)), scratch_mem: Rc::new(arena::RcMemory::new(1)), resp_mem: Rc::new(arena::RcMemory::new(1)), rb_tmp: Rc::new(TmpBuffer::new(1024)), packet_buf: Rc::new(RefCell::new(vec![0; 2048])), } } pub fn init(&self) -> Rc> { Rc::new(RefCell::new(FakeSock::new())) } pub fn run(&self, sock: &Rc>) { let reactor = &self.reactor; let msg_mem = &self.msg_mem; let scratch_mem = &self.scratch_mem; let resp_mem = &self.resp_mem; let rb_tmp = &self.rb_tmp; let packet_buf = &self.packet_buf; let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (_cancel, token) = CancellationToken::new(&reactor.local_registration_memory()); let fut = { let sock = sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); server_req_connection_inner_fut( token, sock, false, s_from_conn, r_to_conn, rb_tmp.clone(), packet_buf.clone(), ) }; let mut executor = StepExecutor::new(reactor, fut); assert_eq!(check_poll(executor.step()), None); let req_data = concat!( "GET /path HTTP/1.1\r\n", "Host: example.com\r\n", "Connection: close\r\n", "\r\n" ) .as_bytes(); sock.borrow_mut().add_readable(req_data); sock.borrow_mut().allow_write(1024); assert_eq!(check_poll(executor.step()), None); // read message let _ = r_from_conn.try_recv().unwrap(); let msg = concat!( "T100:2:id,1:1,4:code,3:200#6:reason,2:OK,7:h", "eaders,34:30:12:Content-Type,10:text/plain,]]4:body,6:hell", "o\n,}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), scratch_mem) .unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, resp_mem).unwrap(); assert!(s_to_conn.try_send((resp, 0)).is_ok()); assert_eq!(check_poll(executor.step()), Some(())); let data = sock.borrow_mut().take_writable(); let expected = concat!( "HTTP/1.1 200 OK\r\n", "Content-Type: text/plain\r\n", "Connection: close\r\n", "Content-Length: 6\r\n", "\r\n", "hello\n", ); assert_eq!(str::from_utf8(&data).unwrap(), expected); } } #[allow(clippy::too_many_arguments)] async fn server_stream_handler_fut( sock: Rc>, secure: bool, s_from_conn: channel::LocalSender, s_stream_from_conn: channel::LocalSender<(ArrayVec, zmq::Message)>, r_to_conn: channel::LocalReceiver<(arena::Rc, usize)>, packet_buf: Rc>>, tmp_buf: Rc>>, buf1: &mut VecRingBuffer, buf2: &mut VecRingBuffer, shared: arena::Rc, ) -> Result { let mut sock = AsyncFakeSock::new(sock); let f = TrackFlag::default(); let r_to_conn = TrackedAsyncLocalReceiver::new(AsyncLocalReceiver::new(r_to_conn), &f); let s_from_conn = AsyncLocalSender::new(s_from_conn); let s_stream_from_conn = AsyncLocalSender::new(s_stream_from_conn); server_stream_handler( "1", &mut sock, None, secure, buf1, buf2, 2, &Counter::new(0), 10, false, &packet_buf, &tmp_buf, "test", &s_from_conn, &s_stream_from_conn, &r_to_conn, shared.get(), &|| {}, &|| {}, ) .await } pub struct BenchServerStreamHandlerArgs { sock: Rc>, buf1: VecRingBuffer, buf2: VecRingBuffer, } pub struct BenchServerStreamHandler { reactor: Reactor, msg_mem: Arc>, scratch_mem: Rc>>>, resp_mem: Rc>, shared_mem: Rc>, rb_tmp: Rc, packet_buf: Rc>>, tmp_buf: Rc>>, } #[allow(clippy::new_without_default)] impl BenchServerStreamHandler { pub fn new() -> Self { Self { reactor: Reactor::new(100), msg_mem: Arc::new(arena::ArcMemory::new(1)), scratch_mem: Rc::new(arena::RcMemory::new(1)), resp_mem: Rc::new(arena::RcMemory::new(1)), shared_mem: Rc::new(arena::RcMemory::new(1)), rb_tmp: Rc::new(TmpBuffer::new(1024)), packet_buf: Rc::new(RefCell::new(vec![0; 2048])), tmp_buf: Rc::new(RefCell::new(vec![0; 1024])), } } pub fn init(&self) -> BenchServerStreamHandlerArgs { let buffer_size = 1024; BenchServerStreamHandlerArgs { sock: Rc::new(RefCell::new(FakeSock::new())), buf1: VecRingBuffer::new(buffer_size, &self.rb_tmp), buf2: VecRingBuffer::new(buffer_size, &self.rb_tmp), } } pub fn run(&self, args: &mut BenchServerStreamHandlerArgs) { let reactor = &self.reactor; let msg_mem = &self.msg_mem; let scratch_mem = &self.scratch_mem; let resp_mem = &self.resp_mem; let shared_mem = &self.shared_mem; let packet_buf = &self.packet_buf; let tmp_buf = &self.tmp_buf; let sock = &args.sock; let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (s_stream_from_conn, _r_stream_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let fut = { let sock = args.sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); let shared = arena::Rc::new(StreamSharedData::new(), shared_mem).unwrap(); server_stream_handler_fut( sock, false, s_from_conn, s_stream_from_conn, r_to_conn, packet_buf.clone(), tmp_buf.clone(), &mut args.buf1, &mut args.buf2, shared, ) }; let mut executor = StepExecutor::new(reactor, fut); assert_eq!(check_poll(executor.step()), None); let req_data = concat!("GET /path HTTP/1.1\r\n", "Host: example.com\r\n", "\r\n").as_bytes(); sock.borrow_mut().add_readable(req_data); sock.borrow_mut().allow_write(1024); assert_eq!(check_poll(executor.step()), None); // read message let _ = r_from_conn.try_recv().unwrap(); let msg = concat!( "T127:2:id,1:1,6:reason,2:OK,7:headers,34:30:12:Content-Typ", "e,10:text/plain,]]3:seq,1:0#4:from,7:handler,4:code,3:200#", "4:body,6:hello\n,}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), scratch_mem) .unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, resp_mem).unwrap(); assert!(s_to_conn.try_send((resp, 0)).is_ok()); assert_eq!(check_poll(executor.step()), Some(true)); let data = sock.borrow_mut().take_writable(); let expected = concat!( "HTTP/1.1 200 OK\r\n", "Content-Type: text/plain\r\n", "Content-Length: 6\r\n", "\r\n", "hello\n", ); assert_eq!(str::from_utf8(&data).unwrap(), expected); } } #[allow(clippy::too_many_arguments)] async fn server_stream_connection_inner_fut( token: CancellationToken, sock: Rc>, secure: bool, s_from_conn: channel::LocalSender, s_stream_from_conn: channel::LocalSender<(ArrayVec, zmq::Message)>, r_to_conn: channel::LocalReceiver<(arena::Rc, usize)>, rb_tmp: Rc, packet_buf: Rc>>, tmp_buf: Rc>>, shared: arena::Rc, ) -> Result<(), Error> { let mut cid = ArrayString::from_str("1").unwrap(); let mut cid_provider = SimpleCidProvider { cid }; let sock = AsyncFakeSock::new(sock); let f = TrackFlag::default(); let r_to_conn = TrackedAsyncLocalReceiver::new(AsyncLocalReceiver::new(r_to_conn), &f); let s_from_conn = AsyncLocalSender::new(s_from_conn); let s_stream_from_conn = AsyncLocalSender::new(s_stream_from_conn); let buffer_size = 1024; let timeout = Duration::from_millis(5_000); server_stream_connection_inner( token, &mut cid, &mut cid_provider, sock, None, secure, buffer_size, 2, &Counter::new(0), 10, &rb_tmp, packet_buf, tmp_buf, timeout, false, "test", s_from_conn, s_stream_from_conn, &r_to_conn, shared, ) .await } pub struct BenchServerStreamConnection { reactor: Reactor, msg_mem: Arc>, scratch_mem: Rc>>>, resp_mem: Rc>, shared_mem: Rc>, rb_tmp: Rc, packet_buf: Rc>>, tmp_buf: Rc>>, } #[allow(clippy::new_without_default)] impl BenchServerStreamConnection { pub fn new() -> Self { Self { reactor: Reactor::new(100), msg_mem: Arc::new(arena::ArcMemory::new(1)), scratch_mem: Rc::new(arena::RcMemory::new(1)), resp_mem: Rc::new(arena::RcMemory::new(1)), shared_mem: Rc::new(arena::RcMemory::new(1)), rb_tmp: Rc::new(TmpBuffer::new(1024)), packet_buf: Rc::new(RefCell::new(vec![0; 2048])), tmp_buf: Rc::new(RefCell::new(vec![0; 1024])), } } pub fn init(&self) -> Rc> { Rc::new(RefCell::new(FakeSock::new())) } pub fn run(&self, sock: &Rc>) { let reactor = &self.reactor; let msg_mem = &self.msg_mem; let scratch_mem = &self.scratch_mem; let resp_mem = &self.resp_mem; let shared_mem = &self.shared_mem; let rb_tmp = &self.rb_tmp; let packet_buf = &self.packet_buf; let tmp_buf = &self.tmp_buf; let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (s_stream_from_conn, _r_stream_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (_cancel, token) = CancellationToken::new(&reactor.local_registration_memory()); let fut = { let sock = sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); let shared = arena::Rc::new(StreamSharedData::new(), shared_mem).unwrap(); server_stream_connection_inner_fut( token, sock, false, s_from_conn, s_stream_from_conn, r_to_conn, rb_tmp.clone(), packet_buf.clone(), tmp_buf.clone(), shared, ) }; let mut executor = StepExecutor::new(reactor, fut); assert_eq!(check_poll(executor.step()), None); let req_data = concat!("GET /path HTTP/1.1\r\n", "Host: example.com\r\n", "\r\n").as_bytes(); sock.borrow_mut().add_readable(req_data); sock.borrow_mut().allow_write(1024); assert_eq!(check_poll(executor.step()), None); // read message let _ = r_from_conn.try_recv().unwrap(); let msg = concat!( "T127:2:id,1:1,6:reason,2:OK,7:headers,34:30:12:Content-Typ", "e,10:text/plain,]]3:seq,1:0#4:from,7:handler,4:code,3:200#", "4:body,6:hello\n,}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), scratch_mem) .unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, resp_mem).unwrap(); assert!(s_to_conn.try_send((resp, 0)).is_ok()); // connection reusable assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); let expected = concat!( "HTTP/1.1 200 OK\r\n", "Content-Type: text/plain\r\n", "Content-Length: 6\r\n", "\r\n", "hello\n", ); assert_eq!(str::from_utf8(&data).unwrap(), expected); } } } #[cfg(test)] mod tests { use super::testutil::*; use super::*; use crate::buffer::TmpBuffer; use crate::channel; use crate::websocket::Decoder; use std::rc::Rc; use std::sync::Arc; use std::task::Poll; use std::time::Instant; use test_log::test; #[test] fn ws_ext_header() { let config = websocket::PerMessageDeflateConfig::default(); let mut dest = ArrayVec::::new(); write_ws_ext_header_value(&config, &mut dest).unwrap(); let expected = "permessage-deflate"; assert_eq!(str::from_utf8(&dest).unwrap(), expected); let mut config = websocket::PerMessageDeflateConfig::default(); config.client_no_context_takeover = true; let mut dest = ArrayVec::::new(); write_ws_ext_header_value(&config, &mut dest).unwrap(); let expected = "permessage-deflate; client_no_context_takeover"; assert_eq!(str::from_utf8(&dest).unwrap(), expected); } #[test] fn message_tracker() { let mut t = MessageTracker::new(2); assert_eq!(t.in_progress(), false); assert_eq!(t.current(), None); t.start(websocket::OPCODE_TEXT).unwrap(); assert_eq!(t.in_progress(), true); assert_eq!(t.current(), Some((websocket::OPCODE_TEXT, 0, false))); t.extend(5); assert_eq!(t.in_progress(), true); assert_eq!(t.current(), Some((websocket::OPCODE_TEXT, 5, false))); t.consumed(2, false); assert_eq!(t.in_progress(), true); assert_eq!(t.current(), Some((websocket::OPCODE_TEXT, 3, false))); t.done(); assert_eq!(t.in_progress(), false); assert_eq!(t.current(), Some((websocket::OPCODE_TEXT, 3, true))); t.start(websocket::OPCODE_TEXT).unwrap(); assert_eq!(t.in_progress(), true); assert_eq!(t.current(), Some((websocket::OPCODE_TEXT, 3, true))); t.consumed(3, false); assert_eq!(t.current(), Some((websocket::OPCODE_TEXT, 0, true))); t.consumed(0, true); assert_eq!(t.current(), Some((websocket::OPCODE_TEXT, 0, false))); t.done(); assert_eq!(t.current(), Some((websocket::OPCODE_TEXT, 0, true))); t.consumed(0, true); assert_eq!(t.current(), None); for _ in 0..t.items.capacity() { t.start(websocket::OPCODE_TEXT).unwrap(); t.done(); } let r = t.start(websocket::OPCODE_TEXT); assert!(r.is_err()); } #[test] fn early_body() { let reactor = Reactor::new(100); let sock = Rc::new(RefCell::new(FakeSock::new())); sock.borrow_mut().allow_write(1024); let sock = RefCell::new(AsyncFakeSock::new(sock)); let rb_tmp = Rc::new(TmpBuffer::new(12)); let mut buf1 = VecRingBuffer::new(12, &rb_tmp); let mut buf2 = VecRingBuffer::new(12, &rb_tmp); buf2.write(b"foo").unwrap(); let handler = RequestSendHeader::new( io_split(&sock), &mut buf1, &mut buf2, http1::ServerProtocol::new(), 3, ); assert_eq!(handler.early_body.borrow().overflow.is_none(), true); handler.append_body(b"hello", false, "").unwrap(); assert_eq!(handler.early_body.borrow().overflow.is_none(), true); handler.append_body(b" world", false, "").unwrap(); assert_eq!(handler.early_body.borrow().overflow.is_some(), true); handler.append_body(b"!", false, "").unwrap(); handler.append_body(b"!", false, "").unwrap_err(); { let mut executor = StepExecutor::new(&reactor, handler.send_header()); assert_eq!(check_poll(executor.step()), Some(())); } assert_eq!(handler.early_body.borrow().overflow.is_none(), true); let handler = handler.send_header_done(); let header = sock.borrow_mut().inner.borrow_mut().take_writable(); assert_eq!(header, b"foo"); let w = handler.w.borrow(); let mut buf_arr = [&b""[..]; VECTORED_MAX - 2]; let bufs = w.buf.read_bufs(&mut buf_arr); assert_eq!(bufs[0], b"hello wor"); assert_eq!(bufs[1], b"ld!"); } async fn server_req_fut( token: CancellationToken, sock: Rc>, secure: bool, s_from_conn: channel::LocalSender, r_to_conn: channel::LocalReceiver<(arena::Rc, usize)>, ) -> Result<(), Error> { let mut cid = ArrayString::from_str("1").unwrap(); let mut cid_provider = SimpleCidProvider { cid }; let sock = AsyncFakeSock::new(sock); let f = TrackFlag::default(); let r_to_conn = TrackedAsyncLocalReceiver::new(AsyncLocalReceiver::new(r_to_conn), &f); let s_from_conn = AsyncLocalSender::new(s_from_conn); let buffer_size = 1024; let rb_tmp = Rc::new(TmpBuffer::new(1024)); let packet_buf = Rc::new(RefCell::new(vec![0; 2048])); let timeout = Duration::from_millis(5_000); server_req_connection_inner( token, &mut cid, &mut cid_provider, sock, None, secure, buffer_size, buffer_size, &rb_tmp, packet_buf, timeout, s_from_conn, &r_to_conn, ) .await } #[test] fn server_req_without_body() { let reactor = Reactor::new(100); let msg_mem = Arc::new(arena::ArcMemory::new(1)); let scratch_mem = Rc::new(arena::RcMemory::new(1)); let resp_mem = Rc::new(arena::RcMemory::new(1)); let sock = Rc::new(RefCell::new(FakeSock::new())); let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (_cancel, token) = CancellationToken::new(&reactor.local_registration_memory()); let fut = { let sock = sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); server_req_fut(token, sock, false, s_from_conn, r_to_conn) }; let mut executor = StepExecutor::new(&reactor, fut); assert_eq!(check_poll(executor.step()), None); // no messages yet assert_eq!(r_from_conn.try_recv().is_err(), true); // fill the connection's outbound message queue assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_ok(), true); assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_err(), true); drop(s_from_conn); let req_data = concat!( "GET /path HTTP/1.1\r\n", "Host: example.com\r\n", "Connection: close\r\n", "\r\n" ) .as_bytes(); sock.borrow_mut().add_readable(req_data); // connection won't be able to send a message yet assert_eq!(check_poll(executor.step()), None); // read bogus message let msg = r_from_conn.try_recv().unwrap(); assert_eq!(msg.is_empty(), true); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); // now connection will be able to send a message assert_eq!(check_poll(executor.step()), None); // read real message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "T148:2:id,1:1,3:ext,15:5:multi,4:true!}6:method,3:GET,3:ur", "i,23:http://example.com/path,7:headers,52:22:4:Host,11:exa", "mple.com,]22:10:Connection,5:close,]]}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); let msg = concat!( "T100:2:id,1:1,4:code,3:200#6:reason,2:OK,7:h", "eaders,34:30:12:Content-Type,10:text/plain,]]4:body,6:hell", "o\n,}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert_eq!(s_to_conn.try_send((resp, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); assert_eq!(data.is_empty(), true); sock.borrow_mut().allow_write(1024); assert_eq!(check_poll(executor.step()), Some(())); let data = sock.borrow_mut().take_writable(); let expected = concat!( "HTTP/1.1 200 OK\r\n", "Content-Type: text/plain\r\n", "Connection: close\r\n", "Content-Length: 6\r\n", "\r\n", "hello\n", ); assert_eq!(str::from_utf8(&data).unwrap(), expected); } #[test] fn server_req_with_body() { let reactor = Reactor::new(100); let msg_mem = Arc::new(arena::ArcMemory::new(1)); let scratch_mem = Rc::new(arena::RcMemory::new(1)); let resp_mem = Rc::new(arena::RcMemory::new(1)); let sock = Rc::new(RefCell::new(FakeSock::new())); let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (_cancel, token) = CancellationToken::new(&reactor.local_registration_memory()); let fut = { let sock = sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); server_req_fut(token, sock, false, s_from_conn, r_to_conn) }; let mut executor = StepExecutor::new(&reactor, fut); assert_eq!(check_poll(executor.step()), None); // no messages yet assert_eq!(r_from_conn.try_recv().is_err(), true); // fill the connection's outbound message queue assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_ok(), true); assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_err(), true); drop(s_from_conn); let req_data = concat!( "POST /path HTTP/1.1\r\n", "Host: example.com\r\n", "Content-Length: 6\r\n", "Connection: close\r\n", "\r\n", "hello\n" ) .as_bytes(); sock.borrow_mut().add_readable(req_data); // connection won't be able to send a message yet assert_eq!(check_poll(executor.step()), None); // read bogus message let msg = r_from_conn.try_recv().unwrap(); assert_eq!(msg.is_empty(), true); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); // now connection will be able to send a message assert_eq!(check_poll(executor.step()), None); // read real message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "T191:2:id,1:1,3:ext,15:5:multi,4:true!}6:method,4:POST,3:u", "ri,23:http://example.com/path,7:headers,78:22:4:Host,11:ex", "ample.com,]22:14:Content-Length,1:6,]22:10:Connection,5:cl", "ose,]]4:body,6:hello\n,}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); let msg = concat!( "T100:2:id,1:1,4:code,3:200#6:reason,2:OK,7:h", "eaders,34:30:12:Content-Type,10:text/plain,]]4:body,6:hell", "o\n,}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert_eq!(s_to_conn.try_send((resp, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); assert_eq!(data.is_empty(), true); sock.borrow_mut().allow_write(1024); assert_eq!(check_poll(executor.step()), Some(())); let data = sock.borrow_mut().take_writable(); let expected = concat!( "HTTP/1.1 200 OK\r\n", "Content-Type: text/plain\r\n", "Connection: close\r\n", "Content-Length: 6\r\n", "\r\n", "hello\n", ); assert_eq!(str::from_utf8(&data).unwrap(), expected); } #[test] fn server_req_timeout() { let now = Instant::now(); let reactor = Reactor::new_with_time(100, now); let sock = Rc::new(RefCell::new(FakeSock::new())); let (_s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, _r_from_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (_cancel, token) = CancellationToken::new(&reactor.local_registration_memory()); let fut = { let sock = sock.clone(); server_req_fut(token, sock, false, s_from_conn, r_to_conn) }; let mut executor = StepExecutor::new(&reactor, fut); assert_eq!(check_poll(executor.step()), None); executor.advance_time(now + Duration::from_millis(5_000)); match executor.step() { Poll::Ready(Err(Error::StreamTimeout)) => {} _ => panic!("unexpected state"), } } #[test] fn server_req_pipeline() { let reactor = Reactor::new(100); let msg_mem = Arc::new(arena::ArcMemory::new(1)); let scratch_mem = Rc::new(arena::RcMemory::new(1)); let resp_mem = Rc::new(arena::RcMemory::new(1)); let sock = Rc::new(RefCell::new(FakeSock::new())); let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (_cancel, token) = CancellationToken::new(&reactor.local_registration_memory()); let fut = { let sock = sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); server_req_fut(token, sock, false, s_from_conn, r_to_conn) }; let mut executor = StepExecutor::new(&reactor, fut); assert_eq!(check_poll(executor.step()), None); // no messages yet assert_eq!(r_from_conn.try_recv().is_err(), true); // fill the connection's outbound message queue assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_ok(), true); assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_err(), true); drop(s_from_conn); let req_data = concat!( "GET /path1 HTTP/1.1\r\n", "Host: example.com\r\n", "\r\n", "GET /path2 HTTP/1.1\r\n", "Host: example.com\r\n", "\r\n", ) .as_bytes(); sock.borrow_mut().add_readable(req_data); // connection won't be able to send a message yet assert_eq!(check_poll(executor.step()), None); // read bogus message let msg = r_from_conn.try_recv().unwrap(); assert_eq!(msg.is_empty(), true); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); // now connection will be able to send a message assert_eq!(check_poll(executor.step()), None); // read real message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "T123:2:id,1:1,3:ext,15:5:multi,4:true!}6:method,3:GET,3:ur", "i,24:http://example.com/path1,7:headers,26:22:4:Host,11:ex", "ample.com,]]}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); let msg = concat!( "T100:2:id,1:1,4:code,3:200#6:reason,2:OK,7:h", "eaders,34:30:12:Content-Type,10:text/plain,]]4:body,6:hell", "o\n,}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert_eq!(s_to_conn.try_send((resp, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); assert_eq!(data.is_empty(), true); sock.borrow_mut().allow_write(1024); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); let expected = concat!( "HTTP/1.1 200 OK\r\n", "Content-Type: text/plain\r\n", "Content-Length: 6\r\n", "\r\n", "hello\n", ); assert_eq!(str::from_utf8(&data).unwrap(), expected); // read real message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "T123:2:id,1:1,3:ext,15:5:multi,4:true!}6:method,3:GET,3:ur", "i,24:http://example.com/path2,7:headers,26:22:4:Host,11:ex", "ample.com,]]}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); let msg = concat!( "T100:2:id,1:1,4:code,3:200#6:reason,2:OK,7:h", "eaders,34:30:12:Content-Type,10:text/plain,]]4:body,6:hell", "o\n,}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert_eq!(s_to_conn.try_send((resp, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); let expected = concat!( "HTTP/1.1 200 OK\r\n", "Content-Type: text/plain\r\n", "Content-Length: 6\r\n", "\r\n", "hello\n", ); assert_eq!(str::from_utf8(&data).unwrap(), expected); } #[test] fn server_req_secure() { let reactor = Reactor::new(100); let msg_mem = Arc::new(arena::ArcMemory::new(1)); let scratch_mem = Rc::new(arena::RcMemory::new(1)); let resp_mem = Rc::new(arena::RcMemory::new(1)); let sock = Rc::new(RefCell::new(FakeSock::new())); let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (_cancel, token) = CancellationToken::new(&reactor.local_registration_memory()); let fut = { let sock = sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); server_req_fut(token, sock, true, s_from_conn, r_to_conn) }; let mut executor = StepExecutor::new(&reactor, fut); assert_eq!(check_poll(executor.step()), None); // no messages yet assert_eq!(r_from_conn.try_recv().is_err(), true); // fill the connection's outbound message queue assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_ok(), true); assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_err(), true); drop(s_from_conn); let req_data = concat!( "GET /path HTTP/1.1\r\n", "Host: example.com\r\n", "Connection: close\r\n", "\r\n" ) .as_bytes(); sock.borrow_mut().add_readable(req_data); // connection won't be able to send a message yet assert_eq!(check_poll(executor.step()), None); // read bogus message let msg = r_from_conn.try_recv().unwrap(); assert_eq!(msg.is_empty(), true); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); // now connection will be able to send a message assert_eq!(check_poll(executor.step()), None); // read real message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "T149:2:id,1:1,3:ext,15:5:multi,4:true!}6:method,3:GET,3:ur", "i,24:https://example.com/path,7:headers,52:22:4:Host,11:ex", "ample.com,]22:10:Connection,5:close,]]}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); let msg = concat!( "T100:2:id,1:1,4:code,3:200#6:reason,2:OK,7:h", "eaders,34:30:12:Content-Type,10:text/plain,]]4:body,6:hell", "o\n,}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert_eq!(s_to_conn.try_send((resp, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); assert_eq!(data.is_empty(), true); sock.borrow_mut().allow_write(1024); assert_eq!(check_poll(executor.step()), Some(())); let data = sock.borrow_mut().take_writable(); let expected = concat!( "HTTP/1.1 200 OK\r\n", "Content-Type: text/plain\r\n", "Connection: close\r\n", "Content-Length: 6\r\n", "\r\n", "hello\n", ); assert_eq!(str::from_utf8(&data).unwrap(), expected); } async fn server_stream_fut( token: CancellationToken, sock: Rc>, secure: bool, allow_compression: bool, s_from_conn: channel::LocalSender, s_stream_from_conn: channel::LocalSender<(ArrayVec, zmq::Message)>, r_to_conn: channel::LocalReceiver<(arena::Rc, usize)>, ) -> Result<(), Error> { let mut cid = ArrayString::from_str("1").unwrap(); let mut cid_provider = SimpleCidProvider { cid }; let sock = AsyncFakeSock::new(sock); let f = TrackFlag::default(); let r_to_conn = TrackedAsyncLocalReceiver::new(AsyncLocalReceiver::new(r_to_conn), &f); let s_from_conn = AsyncLocalSender::new(s_from_conn); let s_stream_from_conn = AsyncLocalSender::new(s_stream_from_conn); let buffer_size = 1024; let rb_tmp = Rc::new(TmpBuffer::new(1024)); let packet_buf = Rc::new(RefCell::new(vec![0; 2048])); let tmp_buf = Rc::new(RefCell::new(vec![0; buffer_size])); let timeout = Duration::from_millis(5_000); let shared_mem = Rc::new(arena::RcMemory::new(1)); let shared = arena::Rc::new(StreamSharedData::new(), &shared_mem).unwrap(); server_stream_connection_inner( token, &mut cid, &mut cid_provider, sock, None, secure, buffer_size, 3, &Counter::new(1), 10, &rb_tmp, packet_buf, tmp_buf, timeout, allow_compression, "test", s_from_conn, s_stream_from_conn, &r_to_conn, shared, ) .await } #[test] fn server_stream_without_body() { let reactor = Reactor::new(100); let msg_mem = Arc::new(arena::ArcMemory::new(1)); let scratch_mem = Rc::new(arena::RcMemory::new(1)); let resp_mem = Rc::new(arena::RcMemory::new(1)); let sock = Rc::new(RefCell::new(FakeSock::new())); let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (s_stream_from_conn, _r_stream_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (_cancel, token) = CancellationToken::new(&reactor.local_registration_memory()); let fut = { let sock = sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); server_stream_fut( token, sock, false, false, s_from_conn, s_stream_from_conn, r_to_conn, ) }; let mut executor = StepExecutor::new(&reactor, fut); assert_eq!(check_poll(executor.step()), None); // no messages yet assert_eq!(r_from_conn.try_recv().is_err(), true); // fill the connection's outbound message queue assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_ok(), true); assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_err(), true); drop(s_from_conn); let req_data = concat!("GET /path HTTP/1.1\r\n", "Host: example.com\r\n", "\r\n").as_bytes(); sock.borrow_mut().add_readable(req_data); // connection won't be able to send a message yet assert_eq!(check_poll(executor.step()), None); // read bogus message let msg = r_from_conn.try_recv().unwrap(); assert_eq!(msg.is_empty(), true); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); // now connection will be able to send a message assert_eq!(check_poll(executor.step()), None); // read real message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "T179:4:from,4:test,2:id,1:1,3:seq,1:0#3:ext,15:5:multi,4:t", "rue!}6:method,3:GET,3:uri,23:http://example.com/path,7:hea", "ders,26:22:4:Host,11:example.com,]]7:credits,4:1024#6:stre", "am,4:true!}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); let msg = concat!( "T127:2:id,1:1,6:reason,2:OK,7:headers,34:30:12:Content-Typ", "e,10:text/plain,]]3:seq,1:0#4:from,7:handler,4:code,3:200#", "4:body,6:hello\n,}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert_eq!(s_to_conn.try_send((resp, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); assert_eq!(data.is_empty(), true); sock.borrow_mut().allow_write(1024); // connection reusable assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); let expected = concat!( "HTTP/1.1 200 OK\r\n", "Content-Type: text/plain\r\n", "Content-Length: 6\r\n", "\r\n", "hello\n", ); assert_eq!(str::from_utf8(&data).unwrap(), expected); } #[test] fn server_stream_with_body() { let reactor = Reactor::new(100); let msg_mem = Arc::new(arena::ArcMemory::new(1)); let scratch_mem = Rc::new(arena::RcMemory::new(1)); let resp_mem = Rc::new(arena::RcMemory::new(1)); let sock = Rc::new(RefCell::new(FakeSock::new())); let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (s_stream_from_conn, r_stream_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (_cancel, token) = CancellationToken::new(&reactor.local_registration_memory()); let fut = { let sock = sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); server_stream_fut( token, sock, false, false, s_from_conn, s_stream_from_conn, r_to_conn, ) }; let mut executor = StepExecutor::new(&reactor, fut); assert_eq!(check_poll(executor.step()), None); // no messages yet assert_eq!(r_from_conn.try_recv().is_err(), true); // fill the connection's outbound message queue assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_ok(), true); assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_err(), true); drop(s_from_conn); let req_data = concat!( "POST /path HTTP/1.1\r\n", "Host: example.com\r\n", "Content-Length: 6\r\n", "\r\n", "hello\n" ) .as_bytes(); sock.borrow_mut().add_readable(req_data); // connection won't be able to send a message yet assert_eq!(check_poll(executor.step()), None); // read bogus message let msg = r_from_conn.try_recv().unwrap(); assert_eq!(msg.is_empty(), true); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); // now connection will be able to send a message assert_eq!(check_poll(executor.step()), None); // read real message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "T220:4:from,4:test,2:id,1:1,3:seq,1:0#3:ext,15:5:multi,4:t", "rue!}6:method,4:POST,3:uri,23:http://example.com/path,7:he", "aders,52:22:4:Host,11:example.com,]22:14:Content-Length,1:", "6,]]7:credits,4:1024#4:more,4:true!6:stream,4:true!}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); let msg = concat!("T69:7:credits,4:1024#3:seq,1:0#2:id,1:1,4:from,7:handler,4:type,6:credit,}",); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert_eq!(s_to_conn.try_send((resp, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); // read message let (addr, msg) = r_stream_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); assert_eq!(addr.as_ref(), "handler".as_bytes()); let buf = &msg[..]; let expected = concat!( "T74:4:from,4:test,2:id,1:1,3:seq,1:1#3:ext,15:5:multi,4:tr", "ue!}4:body,6:hello\n,}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); let msg = concat!( "T127:2:id,1:1,6:reason,2:OK,7:headers,34:30:12:Content-Typ", "e,10:text/plain,]]3:seq,1:1#4:from,7:handler,4:code,3:200#", "4:body,6:hello\n,}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert_eq!(s_to_conn.try_send((resp, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); assert_eq!(data.is_empty(), true); sock.borrow_mut().allow_write(1024); // connection reusable assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); let expected = concat!( "HTTP/1.1 200 OK\r\n", "Content-Type: text/plain\r\n", "Content-Length: 6\r\n", "\r\n", "hello\n", ); assert_eq!(str::from_utf8(&data).unwrap(), expected); } #[test] fn server_stream_chunked() { let reactor = Reactor::new(100); let msg_mem = Arc::new(arena::ArcMemory::new(2)); let scratch_mem = Rc::new(arena::RcMemory::new(2)); let resp_mem = Rc::new(arena::RcMemory::new(2)); let sock = Rc::new(RefCell::new(FakeSock::new())); let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (s_stream_from_conn, _r_stream_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (_cancel, token) = CancellationToken::new(&reactor.local_registration_memory()); let fut = { let sock = sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); server_stream_fut( token, sock, false, false, s_from_conn, s_stream_from_conn, r_to_conn, ) }; let mut executor = StepExecutor::new(&reactor, fut); assert_eq!(check_poll(executor.step()), None); // no messages yet assert_eq!(r_from_conn.try_recv().is_err(), true); // fill the connection's outbound message queue assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_ok(), true); assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_err(), true); drop(s_from_conn); let req_data = concat!("GET /path HTTP/1.1\r\n", "Host: example.com\r\n", "\r\n").as_bytes(); sock.borrow_mut().add_readable(req_data); // connection won't be able to send a message yet assert_eq!(check_poll(executor.step()), None); // read bogus message let msg = r_from_conn.try_recv().unwrap(); assert_eq!(msg.is_empty(), true); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); // now connection will be able to send a message assert_eq!(check_poll(executor.step()), None); // read real message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "T179:4:from,4:test,2:id,1:1,3:seq,1:0#3:ext,15:5:multi,4:t", "rue!}6:method,3:GET,3:uri,23:http://example.com/path,7:hea", "ders,26:22:4:Host,11:example.com,]]7:credits,4:1024#6:stre", "am,4:true!}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); let msg = concat!( "T125:4:more,4:true!2:id,1:1,6:reason,2:OK,7:headers,34:30:", "12:Content-Type,10:text/plain,]]3:seq,1:0#4:from,7:handler", ",4:code,3:200#}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert_eq!(s_to_conn.try_send((resp, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); let msg = concat!("T52:3:seq,1:1#2:id,1:1,4:from,7:handler,4:body,6:hello\n,}"); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert_eq!(s_to_conn.try_send((resp, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); assert_eq!(data.is_empty(), true); sock.borrow_mut().allow_write(1024); // connection reusable assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); let expected = concat!( "HTTP/1.1 200 OK\r\n", "Content-Type: text/plain\r\n", "Connection: Transfer-Encoding\r\n", "Transfer-Encoding: chunked\r\n", "\r\n", "6\r\n", "hello\n", "\r\n", "0\r\n", "\r\n", ); assert_eq!(str::from_utf8(&data).unwrap(), expected); } #[test] fn server_stream_early_response() { let reactor = Reactor::new(100); let msg_mem = Arc::new(arena::ArcMemory::new(1)); let scratch_mem = Rc::new(arena::RcMemory::new(1)); let resp_mem = Rc::new(arena::RcMemory::new(1)); let sock = Rc::new(RefCell::new(FakeSock::new())); let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (s_stream_from_conn, _r_stream_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (_cancel, token) = CancellationToken::new(&reactor.local_registration_memory()); let fut = { let sock = sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); server_stream_fut( token, sock, false, false, s_from_conn, s_stream_from_conn, r_to_conn, ) }; let mut executor = StepExecutor::new(&reactor, fut); assert_eq!(check_poll(executor.step()), None); // no messages yet assert_eq!(r_from_conn.try_recv().is_err(), true); // fill the connection's outbound message queue assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_ok(), true); assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_err(), true); drop(s_from_conn); let req_data = concat!( "POST /path HTTP/1.1\r\n", "Host: example.com\r\n", "Content-Length: 6\r\n", "\r\n", ) .as_bytes(); sock.borrow_mut().add_readable(req_data); // connection won't be able to send a message yet assert_eq!(check_poll(executor.step()), None); // read bogus message let msg = r_from_conn.try_recv().unwrap(); assert_eq!(msg.is_empty(), true); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); // now connection will be able to send a message assert_eq!(check_poll(executor.step()), None); // read real message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "T220:4:from,4:test,2:id,1:1,3:seq,1:0#3:ext,15:5:multi,4:t", "rue!}6:method,4:POST,3:uri,23:http://example.com/path,7:he", "aders,52:22:4:Host,11:example.com,]22:14:Content-Length,1:", "6,]]7:credits,4:1024#4:more,4:true!6:stream,4:true!}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); let msg = concat!( "T150:2:id,1:1,6:reason,11:Bad Request,7:headers,34:30:12:C", "ontent-Type,10:text/plain,]]3:seq,1:0#4:from,7:handler,4:c", "ode,3:400#4:body,18:stopping this now\n,}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert_eq!(s_to_conn.try_send((resp, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); assert_eq!(data.is_empty(), true); sock.borrow_mut().allow_write(1024); assert_eq!(check_poll(executor.step()), Some(())); let data = sock.borrow_mut().take_writable(); let expected = concat!( "HTTP/1.1 400 Bad Request\r\n", "Content-Type: text/plain\r\n", "Connection: close\r\n", "Content-Length: 18\r\n", "\r\n", "stopping this now\n", ); assert_eq!(str::from_utf8(&data).unwrap(), expected); } #[test] fn server_stream_expand_write_buffer() { let reactor = Reactor::new(100); let msg_mem = Arc::new(arena::ArcMemory::new(1)); let scratch_mem = Rc::new(arena::RcMemory::new(1)); let resp_mem = Rc::new(arena::RcMemory::new(1)); let sock = Rc::new(RefCell::new(FakeSock::new())); let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (s_stream_from_conn, r_stream_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (_cancel, token) = CancellationToken::new(&reactor.local_registration_memory()); let fut = { let sock = sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); server_stream_fut( token, sock, false, false, s_from_conn, s_stream_from_conn, r_to_conn, ) }; let mut executor = StepExecutor::new(&reactor, fut); let req_data = concat!("GET /path HTTP/1.1\r\n", "Host: example.com\r\n", "\r\n").as_bytes(); sock.borrow_mut().add_readable(req_data); assert_eq!(check_poll(executor.step()), None); let msg = r_from_conn.try_recv().unwrap(); // no other messages assert!(r_from_conn.try_recv().is_err()); let buf = &msg[..]; let expected = concat!( "T179:4:from,4:test,2:id,1:1,3:seq,1:0#3:ext,15:5:multi,4:t", "rue!}6:method,3:GET,3:uri,23:http://example.com/path,7:hea", "ders,26:22:4:Host,11:example.com,]]7:credits,4:1024#6:stre", "am,4:true!}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); sock.borrow_mut().allow_write(1024); let msg = concat!( "T125:2:id,1:1,6:reason,2:OK,7:headers,34:30:12:Content-Typ", "e,10:text/plain,]]3:seq,1:0#4:from,7:handler,4:code,3:200#", "4:more,4:true!}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert!(s_to_conn.try_send((resp, 0)).is_ok()); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); let expected = concat!( "HTTP/1.1 200 OK\r\n", "Content-Type: text/plain\r\n", "Connection: Transfer-Encoding\r\n", "Transfer-Encoding: chunked\r\n", "\r\n", ); assert_eq!(str::from_utf8(&data).unwrap(), expected); // no more messages yet assert!(r_stream_from_conn.try_recv().is_err()); sock.borrow_mut().clear_write_allowed(); let body = vec![0; 1024]; let mut rdata = zhttppacket::ResponseData::new(); rdata.body = body.as_slice(); rdata.more = true; let resp = zhttppacket::Response::new_data( b"handler", &[zhttppacket::Id { id: b"1", seq: Some(1), }], rdata, ); let mut buf = [0; 2048]; let size = resp.serialize(&mut buf).unwrap(); let msg = zmq::Message::from(&buf[..size]); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert!(s_to_conn.try_send((resp, 0)).is_ok()); assert_eq!(check_poll(executor.step()), None); // read message let (_, msg) = r_stream_from_conn.try_recv().unwrap(); // no other messages assert!(r_stream_from_conn.try_recv().is_err()); let buf = &msg[..]; let expected = concat!( "T91:4:from,4:test,2:id,1:1,3:seq,1:1#3:ext,15:5:multi,4:tr", "ue!}4:type,6:credit,7:credits,4:1024#}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); } #[test] fn server_websocket() { let reactor = Reactor::new(100); let msg_mem = Arc::new(arena::ArcMemory::new(2)); let scratch_mem = Rc::new(arena::RcMemory::new(2)); let resp_mem = Rc::new(arena::RcMemory::new(2)); let sock = Rc::new(RefCell::new(FakeSock::new())); let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (s_stream_from_conn, r_stream_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (_cancel, token) = CancellationToken::new(&reactor.local_registration_memory()); let fut = { let sock = sock.clone(); server_stream_fut( token, sock, false, false, s_from_conn, s_stream_from_conn, r_to_conn, ) }; let mut executor = StepExecutor::new(&reactor, fut); let req_data = concat!( "GET /path HTTP/1.1\r\n", "Host: example.com\r\n", "Upgrade: websocket\r\n", "Sec-WebSocket-Version: 13\r\n", "Sec-WebSocket-Key: abcde\r\n", "\r\n" ) .as_bytes(); sock.borrow_mut().add_readable(req_data); assert_eq!(check_poll(executor.step()), None); // read message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "T255:4:from,4:test,2:id,1:1,3:seq,1:0#3:ext,15:5:multi,4:t", "rue!}6:method,3:GET,3:uri,21:ws://example.com/path,7:heade", "rs,119:22:4:Host,11:example.com,]22:7:Upgrade,9:websocket,", "]30:21:Sec-WebSocket-Version,2:13,]29:17:Sec-WebSocket-Key", ",5:abcde,]]7:credits,4:1024#}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); let msg = concat!( "T98:2:id,1:1,6:reason,19:Switching Protocols,3:seq,1:0#4:f", "rom,7:handler,4:code,3:101#7:credits,4:1024#}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert_eq!(s_to_conn.try_send((resp, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); assert_eq!(data.is_empty(), true); sock.borrow_mut().allow_write(1024); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); let expected = concat!( "HTTP/1.1 101 Switching Protocols\r\n", "Upgrade: websocket\r\n", "Connection: Upgrade\r\n", "Sec-WebSocket-Accept: 8m4i+0BpIKblsbf+VgYANfQKX4w=\r\n", "\r\n", ); assert_eq!(str::from_utf8(&data).unwrap(), expected); // send message let mut data = vec![0; 1024]; let body = b"hello"; let size = websocket::write_header( true, false, websocket::OPCODE_TEXT, body.len(), None, &mut data, ) .unwrap(); data[size..(size + body.len())].copy_from_slice(body); let data = &data[..(size + body.len())]; sock.borrow_mut().add_readable(data); assert_eq!(check_poll(executor.step()), None); // read message let (addr, msg) = r_stream_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); assert_eq!(addr.as_ref(), "handler".as_bytes()); let buf = &msg[..]; let expected = concat!( "T96:4:from,4:test,2:id,1:1,3:seq,1:1#3:ext,15:5:multi,4:tr", "ue!}12:content-type,4:text,4:body,5:hello,}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); // recv message let msg = concat!( "T99:4:from,7:handler,2:id,1:1,3:seq,1:1#3:ext,15:5:multi,4", ":true!}12:content-type,4:text,4:body,5:world,}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert_eq!(s_to_conn.try_send((resp, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); let fi = websocket::read_header(&data).unwrap(); assert_eq!(fi.fin, true); assert_eq!(fi.opcode, websocket::OPCODE_TEXT); assert!(data.len() >= fi.payload_offset + fi.payload_size); let content = &data[fi.payload_offset..(fi.payload_offset + fi.payload_size)]; assert_eq!(str::from_utf8(content).unwrap(), "world"); } #[test] fn server_websocket_with_deflate() { let reactor = Reactor::new(100); let msg_mem = Arc::new(arena::ArcMemory::new(2)); let scratch_mem = Rc::new(arena::RcMemory::new(2)); let resp_mem = Rc::new(arena::RcMemory::new(2)); let sock = Rc::new(RefCell::new(FakeSock::new())); let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (s_stream_from_conn, r_stream_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (_cancel, token) = CancellationToken::new(&reactor.local_registration_memory()); let fut = { let sock = sock.clone(); server_stream_fut( token, sock, false, true, s_from_conn, s_stream_from_conn, r_to_conn, ) }; let mut executor = StepExecutor::new(&reactor, fut); let req_data = concat!( "GET /path HTTP/1.1\r\n", "Host: example.com\r\n", "Upgrade: websocket\r\n", "Sec-WebSocket-Version: 13\r\n", "Sec-WebSocket-Key: abcde\r\n", "Sec-WebSocket-Extensions: permessage-deflate\r\n", "\r\n" ) .as_bytes(); sock.borrow_mut().add_readable(req_data); assert_eq!(check_poll(executor.step()), None); // read message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "T309:4:from,4:test,2:id,1:1,3:seq,1:0#3:ext,15:5:multi,4:t", "rue!}6:method,3:GET,3:uri,21:ws://example.com/path,7:heade", "rs,173:22:4:Host,11:example.com,]22:7:Upgrade,9:websocket,", "]30:21:Sec-WebSocket-Version,2:13,]29:17:Sec-WebSocket-Key", ",5:abcde,]50:24:Sec-WebSocket-Extensions,18:permessage-def", "late,]]7:credits,4:1024#}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); let msg = concat!( "T98:2:id,1:1,6:reason,19:Switching Protocols,3:seq,1:0#4:f", "rom,7:handler,4:code,3:101#7:credits,4:1024#}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert_eq!(s_to_conn.try_send((resp, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); assert_eq!(data.is_empty(), true); sock.borrow_mut().allow_write(1024); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); let expected = concat!( "HTTP/1.1 101 Switching Protocols\r\n", "Upgrade: websocket\r\n", "Connection: Upgrade\r\n", "Sec-WebSocket-Accept: 8m4i+0BpIKblsbf+VgYANfQKX4w=\r\n", "Sec-WebSocket-Extensions: permessage-deflate\r\n", "\r\n", ); assert_eq!(str::from_utf8(&data).unwrap(), expected); // send message let mut data = vec![0; 1024]; let body = { let src = b"hello"; let mut enc = websocket::DeflateEncoder::new(); let mut dest = vec![0; 1024]; let (read, written, output_end) = enc.encode(src, true, &mut dest).unwrap(); assert_eq!(read, 5); assert_eq!(output_end, true); dest.truncate(written); dest }; let size = websocket::write_header( true, true, websocket::OPCODE_TEXT, body.len(), None, &mut data, ) .unwrap(); data[size..(size + body.len())].copy_from_slice(&body); let data = &data[..(size + body.len())]; sock.borrow_mut().add_readable(data); assert_eq!(check_poll(executor.step()), None); // read message let (addr, msg) = r_stream_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); assert_eq!(addr.as_ref(), "handler".as_bytes()); let buf = &msg[..]; let expected = concat!( "T96:4:from,4:test,2:id,1:1,3:seq,1:1#3:ext,15:5:multi,4:tr", "ue!}12:content-type,4:text,4:body,5:hello,}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); // recv message let msg = concat!( "T99:4:from,7:handler,2:id,1:1,3:seq,1:1#3:ext,15:5:multi,4", ":true!}12:content-type,4:text,4:body,5:world,}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert_eq!(s_to_conn.try_send((resp, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); let fi = websocket::read_header(&data).unwrap(); assert_eq!(fi.fin, true); assert_eq!(fi.opcode, websocket::OPCODE_TEXT); assert!(data.len() >= fi.payload_offset + fi.payload_size); let content = { let src = &data[fi.payload_offset..(fi.payload_offset + fi.payload_size)]; let mut dec = websocket::DeflateDecoder::new(); let mut dest = vec![0; 1024]; let (read, written, output_end) = dec.decode(src, true, &mut dest).unwrap(); assert_eq!(read, src.len()); assert_eq!(output_end, true); dest.truncate(written); dest }; assert_eq!(str::from_utf8(&content).unwrap(), "world"); } #[test] fn server_websocket_expand_write_buffer() { let reactor = Reactor::new(100); let msg_mem = Arc::new(arena::ArcMemory::new(2)); let scratch_mem = Rc::new(arena::RcMemory::new(2)); let resp_mem = Rc::new(arena::RcMemory::new(2)); let sock = Rc::new(RefCell::new(FakeSock::new())); let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (s_stream_from_conn, r_stream_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let (_cancel, token) = CancellationToken::new(&reactor.local_registration_memory()); let fut = { let sock = sock.clone(); server_stream_fut( token, sock, false, false, s_from_conn, s_stream_from_conn, r_to_conn, ) }; let mut executor = StepExecutor::new(&reactor, fut); let req_data = concat!( "GET /path HTTP/1.1\r\n", "Host: example.com\r\n", "Upgrade: websocket\r\n", "Sec-WebSocket-Version: 13\r\n", "Sec-WebSocket-Key: abcde\r\n", "\r\n" ) .as_bytes(); sock.borrow_mut().add_readable(req_data); assert_eq!(check_poll(executor.step()), None); // read message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "T255:4:from,4:test,2:id,1:1,3:seq,1:0#3:ext,15:5:multi,4:t", "rue!}6:method,3:GET,3:uri,21:ws://example.com/path,7:heade", "rs,119:22:4:Host,11:example.com,]22:7:Upgrade,9:websocket,", "]30:21:Sec-WebSocket-Version,2:13,]29:17:Sec-WebSocket-Key", ",5:abcde,]]7:credits,4:1024#}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); let msg = concat!( "T98:2:id,1:1,6:reason,19:Switching Protocols,3:seq,1:0#4:f", "rom,7:handler,4:code,3:101#7:credits,4:1024#}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert_eq!(s_to_conn.try_send((resp, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); assert_eq!(data.is_empty(), true); sock.borrow_mut().allow_write(1024); assert_eq!(check_poll(executor.step()), None); let data = sock.borrow_mut().take_writable(); let expected = concat!( "HTTP/1.1 101 Switching Protocols\r\n", "Upgrade: websocket\r\n", "Connection: Upgrade\r\n", "Sec-WebSocket-Accept: 8m4i+0BpIKblsbf+VgYANfQKX4w=\r\n", "\r\n", ); assert_eq!(str::from_utf8(&data).unwrap(), expected); sock.borrow_mut().clear_write_allowed(); let body = vec![0; 1024]; let mut rdata = zhttppacket::ResponseData::new(); rdata.body = body.as_slice(); rdata.content_type = Some(zhttppacket::ContentType::Text); let resp = zhttppacket::Response::new_data( b"handler", &[zhttppacket::Id { id: b"1", seq: Some(1), }], rdata, ); let mut buf = [0; 2048]; let size = resp.serialize(&mut buf).unwrap(); let msg = zmq::Message::from(&buf[..size]); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let resp = zhttppacket::OwnedResponse::parse(msg, 0, scratch).unwrap(); let resp = arena::Rc::new(resp, &resp_mem).unwrap(); assert!(s_to_conn.try_send((resp, 0)).is_ok()); assert_eq!(check_poll(executor.step()), None); // read message let (_, msg) = r_stream_from_conn.try_recv().unwrap(); // no other messages assert!(r_stream_from_conn.try_recv().is_err()); let buf = &msg[..]; let expected = concat!( "T91:4:from,4:test,2:id,1:1,3:seq,1:1#3:ext,15:5:multi,4:tr", "ue!}4:type,6:credit,7:credits,4:1024#}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); } async fn client_req_fut( id: Option>, zreq: arena::Rc, sock: Rc>, s_from_conn: channel::LocalSender, ) -> Result<(), Error> { let mut sock = AsyncFakeSock::new(sock); let s_from_conn = AsyncLocalSender::new(s_from_conn); let buffer_size = 1024; let rb_tmp = Rc::new(TmpBuffer::new(buffer_size)); let mut buf1 = VecRingBuffer::new(buffer_size, &rb_tmp); let mut buf2 = VecRingBuffer::new(buffer_size, &rb_tmp); let mut body_buf = ContiguousBuffer::new(buffer_size); let packet_buf = RefCell::new(vec![0; 2048]); let zreq = zreq.get().get(); let rdata = match &zreq.ptype { zhttppacket::RequestPacket::Data(rdata) => rdata, _ => panic!("unexpected init packet"), }; let url = url::Url::parse(rdata.uri).unwrap(); let msg = match client_req_handler( "test", id.as_deref(), &mut sock, zreq, rdata.method, &url, true, false, &mut buf1, &mut buf2, &mut body_buf, &packet_buf, ) .await? { ClientHandlerDone::Complete(r, _) => r, ClientHandlerDone::Redirect(_, _, _) => panic!("unexpected redirect"), }; s_from_conn.send(msg).await?; Ok(()) } #[test] fn client_req_without_id() { let reactor = Reactor::new(100); let msg_mem = Arc::new(arena::ArcMemory::new(1)); let scratch_mem = Rc::new(arena::RcMemory::new(1)); let req_mem = Rc::new(arena::RcMemory::new(1)); let data = concat!( "T74:7:headers,16:12:3:Foo,3:Bar,]]3:uri,19:https://example.co", "m,6:method,3:GET,}", ) .as_bytes(); let msg = zmq::Message::from(data); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let zreq = zhttppacket::OwnedRequest::parse(msg, 0, scratch).unwrap(); let zreq = arena::Rc::new(zreq, &req_mem).unwrap(); let sock = Rc::new(RefCell::new(FakeSock::new())); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let fut = { let sock = sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); client_req_fut(None, zreq, sock, s_from_conn) }; let mut executor = StepExecutor::new(&reactor, fut); assert_eq!(check_poll(executor.step()), None); // no messages yet assert_eq!(r_from_conn.try_recv().is_err(), true); // fill the handler's outbound message queue assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_ok(), true); assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_err(), true); drop(s_from_conn); // no data yet assert_eq!(sock.borrow_mut().take_writable().is_empty(), true); sock.borrow_mut().allow_write(1024); assert_eq!(check_poll(executor.step()), None); let expected = concat!( "GET / HTTP/1.1\r\n", "Host: example.com\r\n", "Foo: Bar\r\n", "\r\n", ); let buf = sock.borrow_mut().take_writable(); assert_eq!(str::from_utf8(&buf).unwrap(), expected); // handler won't be able to send a message yet assert_eq!(check_poll(executor.step()), None); // read bogus message let msg = r_from_conn.try_recv().unwrap(); assert_eq!(msg.is_empty(), true); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let resp_data = concat!( "HTTP/1.1 200 OK\r\n", "Content-Type: text/plain\r\n", "Content-Length: 6\r\n", "\r\n", "hello\n", ) .as_bytes(); sock.borrow_mut().add_readable(resp_data); // now handler will be able to send a message and finish assert_eq!(check_poll(executor.step()), Some(())); // read real message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "T117:4:code,3:200#6:reason,2:OK,7:headers,60:30:12:Content", "-Type,10:text/plain,]22:14:Content-Length,1:6,]]4:body,6:h", "ello\n,}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); } #[test] fn client_req_with_id() { let reactor = Reactor::new(100); let msg_mem = Arc::new(arena::ArcMemory::new(1)); let scratch_mem = Rc::new(arena::RcMemory::new(1)); let req_mem = Rc::new(arena::RcMemory::new(1)); let data = concat!( "T83:7:headers,16:12:3:Foo,3:Bar,]]3:uri,19:https://example.co", "m,6:method,3:GET,2:id,1:1,}", ) .as_bytes(); let msg = zmq::Message::from(data); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let zreq = zhttppacket::OwnedRequest::parse(msg, 0, scratch).unwrap(); let zreq = arena::Rc::new(zreq, &req_mem).unwrap(); let sock = Rc::new(RefCell::new(FakeSock::new())); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let fut = { let sock = sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); client_req_fut(Some(b"1".to_vec()), zreq, sock, s_from_conn) }; let mut executor = StepExecutor::new(&reactor, fut); assert_eq!(check_poll(executor.step()), None); // no messages yet assert_eq!(r_from_conn.try_recv().is_err(), true); // fill the handler's outbound message queue assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_ok(), true); assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_err(), true); drop(s_from_conn); // no data yet assert_eq!(sock.borrow_mut().take_writable().is_empty(), true); sock.borrow_mut().allow_write(1024); assert_eq!(check_poll(executor.step()), None); let expected = concat!( "GET / HTTP/1.1\r\n", "Host: example.com\r\n", "Foo: Bar\r\n", "\r\n", ); let buf = sock.borrow_mut().take_writable(); assert_eq!(str::from_utf8(&buf).unwrap(), expected); // handler won't be able to send a message yet assert_eq!(check_poll(executor.step()), None); // read bogus message let msg = r_from_conn.try_recv().unwrap(); assert_eq!(msg.is_empty(), true); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let resp_data = concat!( "HTTP/1.1 200 OK\r\n", "Content-Type: text/plain\r\n", "Content-Length: 6\r\n", "\r\n", "hello\n", ) .as_bytes(); sock.borrow_mut().add_readable(resp_data); // now handler will be able to send a message and finish assert_eq!(check_poll(executor.step()), Some(())); // read real message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "T126:2:id,1:1,4:code,3:200#6:reason,2:OK,7:headers,60:30:1", "2:Content-Type,10:text/plain,]22:14:Content-Length,1:6,]]4", ":body,6:hello\n,}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); } async fn client_stream_fut( id: Vec, zreq: arena::Rc, sock: Rc>, allow_compression: bool, r_to_conn: channel::LocalReceiver<(arena::Rc, usize)>, s_from_conn: channel::LocalSender, shared: arena::Rc, ) -> Result<(), Error> { let mut sock = AsyncFakeSock::new(sock); let f = TrackFlag::default(); let r_to_conn = TrackedAsyncLocalReceiver::new(AsyncLocalReceiver::new(r_to_conn), &f); let s_from_conn = AsyncLocalSender::new(s_from_conn); let buffer_size = 1024; let rb_tmp = Rc::new(TmpBuffer::new(buffer_size)); let mut buf1 = VecRingBuffer::new(buffer_size, &rb_tmp); let mut buf2 = VecRingBuffer::new(buffer_size, &rb_tmp); let packet_buf = RefCell::new(vec![0; 2048]); let tmp_buf = Rc::new(RefCell::new(vec![0; buffer_size])); let mut response_received = false; let refresh_stream_timeout = || {}; let refresh_session_timeout = || {}; let zreq = zreq.get().get(); let rdata = match &zreq.ptype { zhttppacket::RequestPacket::Data(rdata) => rdata, _ => panic!("unexpected init packet"), }; let url = url::Url::parse(rdata.uri).unwrap(); let log_id = "test"; let instance_id = "test"; let zsess_out = ZhttpServerStreamSessionOut::new( instance_id, &id, &packet_buf, &s_from_conn, shared.get(), ); zsess_out.check_send().await; zsess_out.try_send_msg(zhttppacket::Response::new_keep_alive(b"", &[]))?; let mut zsess_in = ZhttpServerStreamSessionIn::new( log_id, &id, rdata.credits, &r_to_conn, shared.get(), &refresh_session_timeout, ); let _persistent = client_stream_handler( "test", &mut sock, zreq, rdata.method, &url, true, false, &mut buf1, &mut buf2, 3, &Counter::new(1), 10, allow_compression, &tmp_buf, &mut zsess_in, &zsess_out, &mut response_received, &refresh_stream_timeout, ) .await?; Ok(()) } #[test] fn client_stream() { let reactor = Reactor::new(100); let msg_mem = Arc::new(arena::ArcMemory::new(2)); let scratch_mem = Rc::new(arena::RcMemory::new(2)); let req_mem = Rc::new(arena::RcMemory::new(2)); let data = concat!( "T165:7:credits,4:1024#4:more,4:true!7:headers,34:30:12:Conten", "t-Type,10:text/plain,]]3:uri,24:https://example.com/path,6:me", "thod,4:POST,3:seq,1:0#2:id,1:1,4:from,7:handler,}", ) .as_bytes(); let msg = zmq::Message::from(data); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let zreq = zhttppacket::OwnedRequest::parse(msg, 0, scratch).unwrap(); let zreq = arena::Rc::new(zreq, &req_mem).unwrap(); let sock = Rc::new(RefCell::new(FakeSock::new())); let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let fut = { let sock = sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); let shared_mem = Rc::new(arena::RcMemory::new(1)); let shared = arena::Rc::new(StreamSharedData::new(), &shared_mem).unwrap(); let addr = ArrayVec::try_from(b"handler".as_slice()).unwrap(); shared.get().set_to_addr(Some(addr)); client_stream_fut( b"1".to_vec(), zreq, sock, false, r_to_conn, s_from_conn, shared, ) }; let mut executor = StepExecutor::new(&reactor, fut); // fill the handler's outbound message queue assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_ok(), true); assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_err(), true); drop(s_from_conn); // handler won't be able to send a message yet assert_eq!(check_poll(executor.step()), None); // read bogus message let msg = r_from_conn.try_recv().unwrap(); assert_eq!(msg.is_empty(), true); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); // now handler will be able to send a message assert_eq!(check_poll(executor.step()), None); // read real message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "handler T79:4:from,4:test,2:id,1:1,3:seq,1:0#3:ext,15:5:mu", "lti,4:true!}4:type,10:keep-alive,}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); // no data yet assert_eq!(sock.borrow_mut().take_writable().is_empty(), true); sock.borrow_mut().allow_write(1024); assert_eq!(check_poll(executor.step()), None); let expected = concat!( "POST /path HTTP/1.1\r\n", "Host: example.com\r\n", "Content-Type: text/plain\r\n", "Connection: Transfer-Encoding\r\n", "Transfer-Encoding: chunked\r\n", "\r\n", ); let buf = sock.borrow_mut().take_writable(); assert_eq!(str::from_utf8(&buf).unwrap(), expected); // read message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "handler T91:4:from,4:test,2:id,1:1,3:seq,1:1#3:ext,15:5:mu", "lti,4:true!}4:type,6:credit,7:credits,4:1024#}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); let msg = concat!("T52:3:seq,1:1#2:id,1:1,4:from,7:handler,4:body,6:hello\n,}"); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let req = zhttppacket::OwnedRequest::parse(msg, 0, scratch).unwrap(); let req = arena::Rc::new(req, &req_mem).unwrap(); assert_eq!(s_to_conn.try_send((req, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); let expected = concat!("6\r\nhello\n\r\n0\r\n\r\n",); let buf = sock.borrow_mut().take_writable(); assert_eq!(str::from_utf8(&buf).unwrap(), expected); assert_eq!(check_poll(executor.step()), None); // no more messages yet assert_eq!(r_from_conn.try_recv().is_err(), true); let resp_data = concat!( "HTTP/1.1 200 OK\r\n", "Content-Type: text/plain\r\n", "Content-Length: 6\r\n", "\r\n", "hello\n", ) .as_bytes(); sock.borrow_mut().add_readable(resp_data); assert_eq!(check_poll(executor.step()), None); // read message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "handler T173:4:from,4:test,2:id,1:1,3:seq,1:2#3:ext,15:5:m", "ulti,4:true!}4:code,3:200#6:reason,2:OK,7:headers,60:30:12", ":Content-Type,10:text/plain,]22:14:Content-Length,1:6,]]4:", "more,4:true!}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); assert_eq!(check_poll(executor.step()), Some(())); // read message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "handler T74:4:from,4:test,2:id,1:1,3:seq,1:3#3:ext,15:5:mu", "lti,4:true!}4:body,6:hello\n,}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); } #[test] fn client_stream_expand_write_buffer() { let reactor = Reactor::new(100); let msg_mem = Arc::new(arena::ArcMemory::new(2)); let scratch_mem = Rc::new(arena::RcMemory::new(2)); let req_mem = Rc::new(arena::RcMemory::new(2)); let data = concat!( "T165:7:credits,4:1024#4:more,4:true!7:headers,34:30:12:Conten", "t-Type,10:text/plain,]]3:uri,24:https://example.com/path,6:me", "thod,4:POST,3:seq,1:0#2:id,1:1,4:from,7:handler,}", ) .as_bytes(); let msg = zmq::Message::from(data); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let zreq = zhttppacket::OwnedRequest::parse(msg, 0, scratch).unwrap(); let zreq = arena::Rc::new(zreq, &req_mem).unwrap(); let sock = Rc::new(RefCell::new(FakeSock::new())); let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let fut = { let sock = sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); let shared_mem = Rc::new(arena::RcMemory::new(1)); let shared = arena::Rc::new(StreamSharedData::new(), &shared_mem).unwrap(); let addr = ArrayVec::try_from(b"handler".as_slice()).unwrap(); shared.get().set_to_addr(Some(addr)); client_stream_fut( b"1".to_vec(), zreq, sock, false, r_to_conn, s_from_conn, shared, ) }; let mut executor = StepExecutor::new(&reactor, fut); assert_eq!(check_poll(executor.step()), None); // read message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert!(r_from_conn.try_recv().is_err()); let buf = &msg[..]; let expected = concat!( "handler T79:4:from,4:test,2:id,1:1,3:seq,1:0#3:ext,15:5:mu", "lti,4:true!}4:type,10:keep-alive,}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); // no data yet assert!(sock.borrow_mut().take_writable().is_empty()); sock.borrow_mut().allow_write(1024); assert_eq!(check_poll(executor.step()), None); let expected = concat!( "POST /path HTTP/1.1\r\n", "Host: example.com\r\n", "Content-Type: text/plain\r\n", "Connection: Transfer-Encoding\r\n", "Transfer-Encoding: chunked\r\n", "\r\n", ); let buf = sock.borrow_mut().take_writable(); assert_eq!(str::from_utf8(&buf).unwrap(), expected); sock.borrow_mut().clear_write_allowed(); // read message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert!(r_from_conn.try_recv().is_err()); let buf = &msg[..]; let expected = concat!( "handler T91:4:from,4:test,2:id,1:1,3:seq,1:1#3:ext,15:5:mu", "lti,4:true!}4:type,6:credit,7:credits,4:1024#}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); let body = vec![0; 1024]; let mut rdata = zhttppacket::RequestData::new(); rdata.body = body.as_slice(); rdata.more = true; let req = zhttppacket::Request::new_data( b"handler", &[zhttppacket::Id { id: b"1", seq: Some(1), }], rdata, ); let mut buf = [0; 2048]; let size = req.serialize(&mut buf).unwrap(); let msg = zmq::Message::from(&buf[..size]); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let req = zhttppacket::OwnedRequest::parse(msg, 0, scratch).unwrap(); let req = arena::Rc::new(req, &req_mem).unwrap(); assert!(s_to_conn.try_send((req, 0)).is_ok()); assert_eq!(check_poll(executor.step()), None); // read message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert!(r_from_conn.try_recv().is_err()); let buf = &msg[..]; let expected = concat!( "handler T91:4:from,4:test,2:id,1:1,3:seq,1:2#3:ext,15:5:mu", "lti,4:true!}4:type,6:credit,7:credits,4:1024#}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); } #[test] fn client_websocket() { let reactor = Reactor::new(100); let msg_mem = Arc::new(arena::ArcMemory::new(2)); let scratch_mem = Rc::new(arena::RcMemory::new(2)); let req_mem = Rc::new(arena::RcMemory::new(2)); let data = concat!( "T115:7:credits,4:1024#7:headers,16:12:3:Foo,3:Bar,]]3:uri,22:", "wss://example.com/path,3:seq,1:0#2:id,1:1,4:from,7:handler,}", ) .as_bytes(); let msg = zmq::Message::from(data); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let zreq = zhttppacket::OwnedRequest::parse(msg, 0, scratch).unwrap(); let zreq = arena::Rc::new(zreq, &req_mem).unwrap(); let sock = Rc::new(RefCell::new(FakeSock::new())); let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let fut = { let sock = sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); let shared_mem = Rc::new(arena::RcMemory::new(1)); let shared = arena::Rc::new(StreamSharedData::new(), &shared_mem).unwrap(); let addr = ArrayVec::try_from(b"handler".as_slice()).unwrap(); shared.get().set_to_addr(Some(addr)); client_stream_fut( b"1".to_vec(), zreq, sock, false, r_to_conn, s_from_conn, shared, ) }; let mut executor = StepExecutor::new(&reactor, fut); // fill the handler's outbound message queue assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_ok(), true); assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_err(), true); drop(s_from_conn); // handler won't be able to send a message yet assert_eq!(check_poll(executor.step()), None); // read bogus message let msg = r_from_conn.try_recv().unwrap(); assert_eq!(msg.is_empty(), true); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); // now handler will be able to send a message assert_eq!(check_poll(executor.step()), None); // read real message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "handler T79:4:from,4:test,2:id,1:1,3:seq,1:0#3:ext,15:5:mu", "lti,4:true!}4:type,10:keep-alive,}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); // no data yet assert_eq!(sock.borrow_mut().take_writable().is_empty(), true); sock.borrow_mut().allow_write(1024); assert_eq!(check_poll(executor.step()), None); let buf = sock.borrow_mut().take_writable(); // use httparse to fish out Sec-WebSocket-Key let ws_key = { let mut headers = [httparse::EMPTY_HEADER; HEADERS_MAX]; let mut req = httparse::Request::new(&mut headers); match req.parse(&buf) { Ok(httparse::Status::Complete(_)) => {} _ => panic!("unexpected parse status"), } let mut ws_key = String::new(); for h in req.headers { if h.name.eq_ignore_ascii_case("Sec-WebSocket-Key") { ws_key = String::from_utf8(h.value.to_vec()).unwrap(); } } ws_key }; let expected = format!( concat!( "GET /path HTTP/1.1\r\n", "Host: example.com\r\n", "Upgrade: websocket\r\n", "Connection: Upgrade\r\n", "Sec-WebSocket-Version: 13\r\n", "Sec-WebSocket-Key: {}\r\n", "Foo: Bar\r\n", "\r\n", ), ws_key ); assert_eq!(str::from_utf8(&buf).unwrap(), expected); // no more messages yet assert_eq!(r_from_conn.try_recv().is_err(), true); let ws_accept = calculate_ws_accept(ws_key.as_bytes()).unwrap(); let resp_data = format!( concat!( "HTTP/1.1 101 Switching Protocols\r\n", "Upgrade: websocket\r\n", "Connection: Upgrade\r\n", "Sec-WebSocket-Accept: {}\r\n", "\r\n", ), ws_accept ); sock.borrow_mut().add_readable(resp_data.as_bytes()); assert_eq!(check_poll(executor.step()), None); // read message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = format!( concat!( "handler T249:4:from,4:test,2:id,1:1,3:seq,1:1#3:ext,15:5:m", "ulti,4:true!}}4:code,3:101#6:reason,19:Switching Protocols", ",7:headers,114:22:7:Upgrade,9:websocket,]24:10:Connection,", "7:Upgrade,]56:20:Sec-WebSocket-Accept,28:{},]]7:credits,4:", "1024#}}", ), ws_accept ); assert_eq!(str::from_utf8(buf).unwrap(), expected); // send message let mut data = vec![0; 1024]; let body = b"hello"; let size = websocket::write_header( true, false, websocket::OPCODE_TEXT, body.len(), None, &mut data, ) .unwrap(); data[size..(size + body.len())].copy_from_slice(body); let data = &data[..(size + body.len())]; sock.borrow_mut().add_readable(data); assert_eq!(check_poll(executor.step()), None); // read message let msg = r_from_conn.try_recv().unwrap(); let buf = &msg[..]; let expected = concat!( "handler T96:4:from,4:test,2:id,1:1,3:seq,1:2#3:ext,15:5:mu", "lti,4:true!}12:content-type,4:text,4:body,5:hello,}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); // recv message let msg = concat!( "T99:4:from,7:handler,2:id,1:1,3:seq,1:1#3:ext,15:5:multi,4", ":true!}12:content-type,4:text,4:body,5:world,}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let req = zhttppacket::OwnedRequest::parse(msg, 0, scratch).unwrap(); let req = arena::Rc::new(req, &req_mem).unwrap(); assert_eq!(s_to_conn.try_send((req, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); let mut data = sock.borrow_mut().take_writable(); let fi = websocket::read_header(&data).unwrap(); assert_eq!(fi.fin, true); assert_eq!(fi.opcode, websocket::OPCODE_TEXT); assert!(data.len() >= fi.payload_offset + fi.payload_size); let content = &mut data[fi.payload_offset..(fi.payload_offset + fi.payload_size)]; websocket::apply_mask(content, fi.mask.unwrap(), 0); assert_eq!(str::from_utf8(content).unwrap(), "world"); } #[test] fn client_websocket_with_deflate() { let reactor = Reactor::new(100); let msg_mem = Arc::new(arena::ArcMemory::new(2)); let scratch_mem = Rc::new(arena::RcMemory::new(2)); let req_mem = Rc::new(arena::RcMemory::new(2)); let data = concat!( "T115:7:credits,4:1024#7:headers,16:12:3:Foo,3:Bar,]]3:uri,22:", "wss://example.com/path,3:seq,1:0#2:id,1:1,4:from,7:handler,}", ) .as_bytes(); let msg = zmq::Message::from(data); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let zreq = zhttppacket::OwnedRequest::parse(msg, 0, scratch).unwrap(); let zreq = arena::Rc::new(zreq, &req_mem).unwrap(); let sock = Rc::new(RefCell::new(FakeSock::new())); let (s_to_conn, r_to_conn) = channel::local_channel(1, 1, &reactor.local_registration_memory()); let (s_from_conn, r_from_conn) = channel::local_channel(1, 2, &reactor.local_registration_memory()); let fut = { let sock = sock.clone(); let s_from_conn = s_from_conn .try_clone(&reactor.local_registration_memory()) .unwrap(); let shared_mem = Rc::new(arena::RcMemory::new(1)); let shared = arena::Rc::new(StreamSharedData::new(), &shared_mem).unwrap(); let addr = ArrayVec::try_from(b"handler".as_slice()).unwrap(); shared.get().set_to_addr(Some(addr)); client_stream_fut( b"1".to_vec(), zreq, sock, true, r_to_conn, s_from_conn, shared, ) }; let mut executor = StepExecutor::new(&reactor, fut); // fill the handler's outbound message queue assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_ok(), true); assert_eq!(s_from_conn.try_send(zmq::Message::new()).is_err(), true); drop(s_from_conn); // handler won't be able to send a message yet assert_eq!(check_poll(executor.step()), None); // read bogus message let msg = r_from_conn.try_recv().unwrap(); assert_eq!(msg.is_empty(), true); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); // now handler will be able to send a message assert_eq!(check_poll(executor.step()), None); // read real message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = concat!( "handler T79:4:from,4:test,2:id,1:1,3:seq,1:0#3:ext,15:5:mu", "lti,4:true!}4:type,10:keep-alive,}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); // no data yet assert_eq!(sock.borrow_mut().take_writable().is_empty(), true); sock.borrow_mut().allow_write(1024); assert_eq!(check_poll(executor.step()), None); let buf = sock.borrow_mut().take_writable(); // use httparse to fish out Sec-WebSocket-Key let ws_key = { let mut headers = [httparse::EMPTY_HEADER; HEADERS_MAX]; let mut req = httparse::Request::new(&mut headers); match req.parse(&buf) { Ok(httparse::Status::Complete(_)) => {} _ => panic!("unexpected parse status"), } let mut ws_key = String::new(); for h in req.headers { if h.name.eq_ignore_ascii_case("Sec-WebSocket-Key") { ws_key = String::from_utf8(h.value.to_vec()).unwrap(); } } ws_key }; let expected = format!( concat!( "GET /path HTTP/1.1\r\n", "Host: example.com\r\n", "Upgrade: websocket\r\n", "Connection: Upgrade\r\n", "Sec-WebSocket-Version: 13\r\n", "Sec-WebSocket-Key: {}\r\n", "Sec-WebSocket-Extensions: permessage-deflate\r\n", "Foo: Bar\r\n", "\r\n", ), ws_key ); assert_eq!(str::from_utf8(&buf).unwrap(), expected); // no more messages yet assert_eq!(r_from_conn.try_recv().is_err(), true); let ws_accept = calculate_ws_accept(ws_key.as_bytes()).unwrap(); let resp_data = format!( concat!( "HTTP/1.1 101 Switching Protocols\r\n", "Upgrade: websocket\r\n", "Connection: Upgrade\r\n", "Sec-WebSocket-Accept: {}\r\n", "Sec-WebSocket-Extensions: permessage-deflate\r\n", "\r\n", ), ws_accept ); sock.borrow_mut().add_readable(resp_data.as_bytes()); assert_eq!(check_poll(executor.step()), None); // read message let msg = r_from_conn.try_recv().unwrap(); // no other messages assert_eq!(r_from_conn.try_recv().is_err(), true); let buf = &msg[..]; let expected = format!( concat!( "handler T303:4:from,4:test,2:id,1:1,3:seq,1:1#3:ext,15:5:m", "ulti,4:true!}}4:code,3:101#6:reason,19:Switching Protocols", ",7:headers,168:22:7:Upgrade,9:websocket,]24:10:Connection,", "7:Upgrade,]56:20:Sec-WebSocket-Accept,28:{},]50:24:Sec-Web", "Socket-Extensions,18:permessage-deflate,]]7:credits,4:1024", "#}}", ), ws_accept ); assert_eq!(str::from_utf8(buf).unwrap(), expected); // send message let mut data = vec![0; 1024]; let body = { let src = b"hello"; let mut enc = websocket::DeflateEncoder::new(); let mut dest = vec![0; 1024]; let (read, written, output_end) = enc.encode(src, true, &mut dest).unwrap(); assert_eq!(read, 5); assert_eq!(output_end, true); dest.truncate(written); dest }; let size = websocket::write_header( true, true, websocket::OPCODE_TEXT, body.len(), None, &mut data, ) .unwrap(); data[size..(size + body.len())].copy_from_slice(&body); let data = &data[..(size + body.len())]; sock.borrow_mut().add_readable(data); assert_eq!(check_poll(executor.step()), None); // read message let msg = r_from_conn.try_recv().unwrap(); let buf = &msg[..]; let expected = concat!( "handler T96:4:from,4:test,2:id,1:1,3:seq,1:2#3:ext,15:5:mu", "lti,4:true!}12:content-type,4:text,4:body,5:hello,}", ); assert_eq!(str::from_utf8(buf).unwrap(), expected); // recv message let msg = concat!( "T99:4:from,7:handler,2:id,1:1,3:seq,1:1#3:ext,15:5:multi,4", ":true!}12:content-type,4:text,4:body,5:world,}", ); let msg = zmq::Message::from(msg.as_bytes()); let msg = arena::Arc::new(msg, &msg_mem).unwrap(); let scratch = arena::Rc::new(RefCell::new(zhttppacket::ParseScratch::new()), &scratch_mem).unwrap(); let req = zhttppacket::OwnedRequest::parse(msg, 0, scratch).unwrap(); let req = arena::Rc::new(req, &req_mem).unwrap(); assert_eq!(s_to_conn.try_send((req, 0)).is_ok(), true); assert_eq!(check_poll(executor.step()), None); let mut data = sock.borrow_mut().take_writable(); let fi = websocket::read_header(&data).unwrap(); assert_eq!(fi.fin, true); assert_eq!(fi.opcode, websocket::OPCODE_TEXT); assert!(data.len() >= fi.payload_offset + fi.payload_size); let content = { let src = &mut data[fi.payload_offset..(fi.payload_offset + fi.payload_size)]; websocket::apply_mask(src, fi.mask.unwrap(), 0); let mut dec = websocket::DeflateDecoder::new(); let mut dest = vec![0; 1024]; let (read, written, output_end) = dec.decode(src, true, &mut dest).unwrap(); assert_eq!(read, src.len()); assert_eq!(output_end, true); dest.truncate(written); dest }; assert_eq!(str::from_utf8(&content).unwrap(), "world"); } #[test] fn bench_server_req_handler() { let t = BenchServerReqHandler::new(); t.run(&mut t.init()); } #[test] fn bench_server_req_connection() { let t = BenchServerReqConnection::new(); t.run(&mut t.init()); } #[test] fn bench_server_stream_handler() { let t = BenchServerStreamHandler::new(); t.run(&mut t.init()); } #[test] fn bench_server_stream_connection() { let t = BenchServerStreamConnection::new(); t.run(&mut t.init()); } } pushpin-1.39.1/src/counter.rs000066400000000000000000000050361457610542000161170ustar00rootroot00000000000000/* * Copyright (C) 2023 Fastly, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use std::sync::atomic::{AtomicUsize, Ordering}; #[derive(Debug)] pub struct CounterError; /// An unsigned integer that can be shared between threads. Counter is backed by an AtomicUsize /// and performs operations with Relaxed memory ordering, so its value cannot be reliably assumed /// to be in sync with other atomic values, including other Counter values. pub struct Counter(AtomicUsize); impl Counter { pub fn new(value: usize) -> Self { Self(AtomicUsize::new(value)) } pub fn inc(&self, amount: usize) -> Result<(), CounterError> { if amount == 0 { return Ok(()); } loop { let value = self.0.load(Ordering::Relaxed); if amount > usize::MAX - value { return Err(CounterError); } if self .0 .compare_exchange(value, value + amount, Ordering::Relaxed, Ordering::Relaxed) .is_ok() { break; } } Ok(()) } pub fn dec(&self, amount: usize) -> Result<(), CounterError> { if amount == 0 { return Ok(()); } loop { let value = self.0.load(Ordering::Relaxed); if amount > value { return Err(CounterError); } if self .0 .compare_exchange(value, value - amount, Ordering::Relaxed, Ordering::Relaxed) .is_ok() { break; } } Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn counter() { let c = Counter::new(2); assert!(c.dec(1).is_ok()); assert!(c.dec(1).is_ok()); assert!(c.dec(1).is_err()); assert!(c.inc(1).is_ok()); assert!(c.dec(2).is_err()); assert!(c.dec(1).is_ok()); assert!(c.inc(usize::MAX).is_ok()); assert!(c.inc(1).is_err()); } } pushpin-1.39.1/src/cpp/000077500000000000000000000000001457610542000146505ustar00rootroot00000000000000pushpin-1.39.1/src/cpp/bufferlist.cpp000066400000000000000000000063031457610542000175230ustar00rootroot00000000000000/* * Copyright (C) 2013 Fanout, Inc. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "bufferlist.h" #include BufferList::BufferList() : size_(0), offset_(0) { } void BufferList::findPos(int pos, int *bufferIndex, int *offset) const { assert(pos < size_); int at = 0; int curOffset = offset_; while(true) { const QByteArray &buf = bufs_[at]; if(curOffset + pos < buf.size()) break; ++at; pos -= (buf.size() - curOffset); curOffset = 0; } *bufferIndex = at; *offset = curOffset + pos; } QByteArray BufferList::mid(int pos, int size) const { assert(pos >= 0); if(size_ == 0 || size == 0 || pos >= size_) return QByteArray(); int toRead; if(size > 0) toRead = qMin(size, size_ - pos); else toRead = size_ - pos; assert(!bufs_.isEmpty()); int at; int offset; findPos(pos, &at, &offset); // if we're reading the exact size of the current buffer, cheaply // return it if(offset == 0 && bufs_[at].size() == toRead) return bufs_[at]; QByteArray out; out.resize(toRead); char *outp = out.data(); while(toRead > 0) { const QByteArray &buf = bufs_[at]; int bsize = qMin(buf.size() - offset, toRead); memcpy(outp, buf.data() + offset, bsize); if(offset + bsize >= buf.size()) { ++at; offset = 0; } toRead -= bsize; outp += bsize; } return out; } void BufferList::clear() { bufs_.clear(); size_ = 0; offset_ = 0; } void BufferList::append(const QByteArray &buf) { if(buf.size() < 1) return; bufs_ += buf; size_ += buf.size(); } QByteArray BufferList::take(int size) { if(size_ == 0 || size == 0) return QByteArray(); int toRead; if(size > 0) toRead = qMin(size, size_); else toRead = size_; assert(!bufs_.isEmpty()); // if we're reading the exact size of the first buffer, cheaply // return it if(offset_ == 0 && bufs_.first().size() == toRead) { size_ -= toRead; return bufs_.takeFirst(); } QByteArray out; out.resize(toRead); char *outp = out.data(); while(toRead > 0) { const QByteArray &buf = bufs_.first(); int bsize = qMin(buf.size() - offset_, toRead); memcpy(outp, buf.data() + offset_, bsize); if(offset_ + bsize >= buf.size()) { bufs_.removeFirst(); offset_ = 0; } else offset_ += bsize; toRead -= bsize; size_ -= bsize; outp += bsize; } return out; } QByteArray BufferList::toByteArray() { if(size_ == 0) return QByteArray(); QByteArray out; while(!bufs_.isEmpty()) { if(offset_ > 0) { out += bufs_.first().mid(offset_); offset_ = 0; bufs_.removeFirst(); } else out += bufs_.takeFirst(); } // keep the rewritten buffer as the only buffer bufs_ += out; return out; } pushpin-1.39.1/src/cpp/bufferlist.h000066400000000000000000000024201457610542000171640ustar00rootroot00000000000000/* * Copyright (C) 2013 Fanout, Inc. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef BUFFERLIST_H #define BUFFERLIST_H #include #include class BufferList { public: BufferList(); int size() const { return size_; } bool isEmpty() const { return size_ == 0; } QByteArray mid(int pos, int size = -1) const; void clear(); void append(const QByteArray &buf); QByteArray take(int size = -1); QByteArray toByteArray(); // non-const because we rewrite the list BufferList & operator+=(const QByteArray &buf) { append(buf); return *this; } private: QList bufs_; int size_; int offset_; void findPos(int pos, int *bufferIndex, int *offset) const; }; #endif pushpin-1.39.1/src/cpp/callback.h000066400000000000000000000055141457610542000165620ustar00rootroot00000000000000/* * Copyright (C) 2023 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef CALLBACK_H #define CALLBACK_H #include #include template class Callback { public: typedef void (*CallbackFunc)(void *data, T); Callback() : activeCalls_(0), destroyed_(0) { } ~Callback() { if(destroyed_) *destroyed_ = true; } void add(CallbackFunc cb, void *data) { targets_ += Target(cb, data); } void remove(void *data) { // mark for removal, but don't actually remove for(int n = 0; n < targets_.count(); ++n) { Target &t = targets_[n]; if(t.second == data) { t.second = 0; } } // only actually remove if not in the middle of a call if(activeCalls_ == 0) { removeMarked(); } } void call(T value) { activeCalls_ += 1; for(int n = 0; n < targets_.count(); ++n) { const Target &t = targets_[n]; // skip if marked for removal if(!t.second) { continue; } CallbackFunc f = t.first; void *data = t.second; bool *prevDestroyed = destroyed_; bool destroyed = false; destroyed_ = &destroyed; f(data, value); if(destroyed) { if(prevDestroyed) { *prevDestroyed = true; } return; } destroyed_ = prevDestroyed; } assert(activeCalls_ >= 1); activeCalls_ -= 1; if(activeCalls_ == 0) { removeMarked(); } } private: typedef QPair Target; QList targets_; bool activeCalls_; bool *destroyed_; void removeMarked() { assert(activeCalls_ == 0); for(int n = 0; n < targets_.count(); ++n) { if(!targets_[n].second) { targets_.removeAt(n); --n; // adjust position } } } }; #endif pushpin-1.39.1/src/cpp/config.cpp000066400000000000000000000021301457610542000166150ustar00rootroot00000000000000/* * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "config.h" #include "rust/build_config.h" namespace Config { static thread_local Config *g_config = 0; Config & get() { if(!g_config) { Config *c = new Config; BuildConfig *bc = build_config_new(); c->version = QString(bc->version); c->configDir = QString(bc->config_dir); c->libDir = QString(bc->lib_dir); build_config_destroy(bc); g_config = c; } return *g_config; } } pushpin-1.39.1/src/cpp/config.h000066400000000000000000000016231457610542000162700ustar00rootroot00000000000000/* * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef CONFIG_H #define CONFIG_H #include namespace Config { class Config { public: QString version; QString configDir; QString libDir; }; // value is thread local Config & get(); } #endif pushpin-1.39.1/src/cpp/cors.cpp000066400000000000000000000064231457610542000163270ustar00rootroot00000000000000/* * Copyright (C) 2015 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "cors.h" #include "httpheaders.h" namespace Cors { static bool isSimpleHeader(const QByteArray &in) { return (qstricmp(in.data(), "Cache-Control") == 0 || qstricmp(in.data(), "Content-Language") == 0 || qstricmp(in.data(), "Content-Length") == 0 || qstricmp(in.data(), "Content-Type") == 0 || qstricmp(in.data(), "Expires") == 0 || qstricmp(in.data(), "Last-Modified") == 0 || qstricmp(in.data(), "Pragma") == 0); } static bool headerNamesContains(const QList &names, const QByteArray &name) { foreach(const QByteArray &i, names) { if(qstricmp(name.data(), i.data()) == 0) return true; } return false; } static bool headerNameStartsWith(const QByteArray &name, const char *value) { return (qstrnicmp(name.data(), value, qstrlen(value)) == 0); } void applyCorsHeaders(const HttpHeaders &requestHeaders, HttpHeaders *responseHeaders) { if(!responseHeaders->contains("Access-Control-Allow-Methods")) { QByteArray method = requestHeaders.get("Access-Control-Request-Method"); if(!method.isEmpty()) *responseHeaders += HttpHeader("Access-Control-Allow-Methods", method); else *responseHeaders += HttpHeader("Access-Control-Allow-Methods", "OPTIONS, HEAD, GET, POST, PUT, DELETE"); } if(!responseHeaders->contains("Access-Control-Allow-Headers")) { QList allowHeaders; foreach(const QByteArray &h, requestHeaders.getAll("Access-Control-Request-Headers", true)) { if(!h.isEmpty()) allowHeaders += h; } if(!allowHeaders.isEmpty()) *responseHeaders += HttpHeader("Access-Control-Allow-Headers", HttpHeaders::join(allowHeaders)); } if(!responseHeaders->contains("Access-Control-Expose-Headers")) { QList exposeHeaders; foreach(const HttpHeader &h, *responseHeaders) { if(!isSimpleHeader(h.first) && !headerNameStartsWith(h.first, "Access-Control-") && !headerNameStartsWith(h.first, "Grip-") && !headerNamesContains(exposeHeaders, h.first)) exposeHeaders += h.first; } if(!exposeHeaders.isEmpty()) *responseHeaders += HttpHeader("Access-Control-Expose-Headers", HttpHeaders::join(exposeHeaders)); } if(!responseHeaders->contains("Access-Control-Allow-Credentials")) *responseHeaders += HttpHeader("Access-Control-Allow-Credentials", "true"); if(!responseHeaders->contains("Access-Control-Allow-Origin")) { QByteArray origin = requestHeaders.get("Origin"); if(origin.isEmpty()) origin = "*"; *responseHeaders += HttpHeader("Access-Control-Allow-Origin", origin); } if(!responseHeaders->contains("Access-Control-Max-Age")) *responseHeaders += HttpHeader("Access-Control-Max-Age", "3600"); } } pushpin-1.39.1/src/cpp/cors.h000066400000000000000000000015451457610542000157740ustar00rootroot00000000000000/* * Copyright (C) 2015 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef CORS_H #define CORS_H class HttpHeaders; namespace Cors { void applyCorsHeaders(const HttpHeaders &requestHeaders, HttpHeaders *responseHeaders); } #endif pushpin-1.39.1/src/cpp/cpp.pri000066400000000000000000000144141457610542000161520ustar00rootroot00000000000000QMAKE_CXXFLAGS += $$(CXXFLAGS) QMAKE_CFLAGS += $$(CFLAGS) QMAKE_LFLAGS += $$(LDFLAGS) SRC_DIR = $$PWD QZMQ_DIR = $$SRC_DIR/qzmq RUST_DIR = $$SRC_DIR/.. INCLUDEPATH += $$SRC_DIR INCLUDEPATH += $$QZMQ_DIR/src include($$QZMQ_DIR/src/src.pri) DEFINES += NO_IRISNET HEADERS += $$SRC_DIR/processquit.h SOURCES += $$SRC_DIR/processquit.cpp INCLUDEPATH += $$RUST_DIR HEADERS += \ $$SRC_DIR/tnetstring.h \ $$SRC_DIR/httpheaders.h \ $$SRC_DIR/zhttprequestpacket.h \ $$SRC_DIR/zhttpresponsepacket.h \ $$SRC_DIR/log.h \ $$SRC_DIR/bufferlist.h \ $$SRC_DIR/layertracker.h SOURCES += \ $$SRC_DIR/tnetstring.cpp \ $$SRC_DIR/httpheaders.cpp \ $$SRC_DIR/zhttprequestpacket.cpp \ $$SRC_DIR/zhttpresponsepacket.cpp \ $$SRC_DIR/log.cpp \ $$SRC_DIR/bufferlist.cpp \ $$SRC_DIR/layertracker.cpp HEADERS += \ $$SRC_DIR/packet/httprequestdata.h \ $$SRC_DIR/packet/httpresponsedata.h \ $$SRC_DIR/packet/retryrequestpacket.h \ $$SRC_DIR/packet/wscontrolpacket.h \ $$SRC_DIR/packet/statspacket.h \ $$SRC_DIR/packet/zrpcrequestpacket.h \ $$SRC_DIR/packet/zrpcresponsepacket.h SOURCES += \ $$SRC_DIR/packet/retryrequestpacket.cpp \ $$SRC_DIR/packet/wscontrolpacket.cpp \ $$SRC_DIR/packet/statspacket.cpp \ $$SRC_DIR/packet/zrpcrequestpacket.cpp \ $$SRC_DIR/packet/zrpcresponsepacket.cpp HEADERS += \ $$SRC_DIR/callback.h \ $$SRC_DIR/config.h \ $$SRC_DIR/timerwheel.h \ $$SRC_DIR/jwt.h \ $$SRC_DIR/rtimer.h \ $$SRC_DIR/logutil.h \ $$SRC_DIR/uuidutil.h \ $$SRC_DIR/zutil.h \ $$SRC_DIR/httprequest.h \ $$SRC_DIR/websocket.h \ $$SRC_DIR/zhttpmanager.h \ $$SRC_DIR/zhttprequest.h \ $$SRC_DIR/zwebsocket.h \ $$SRC_DIR/zrpcmanager.h \ $$SRC_DIR/zrpcrequest.h \ $$SRC_DIR/statusreasons.h \ $$SRC_DIR/inspectdata.h \ $$SRC_DIR/cors.h \ $$SRC_DIR/simplehttpserver.h \ $$SRC_DIR/stats.h \ $$SRC_DIR/statsmanager.h \ $$SRC_DIR/settings.h SOURCES += \ $$SRC_DIR/config.cpp \ $$SRC_DIR/timerwheel.cpp \ $$SRC_DIR/jwt.cpp \ $$SRC_DIR/rtimer.cpp \ $$SRC_DIR/logutil.cpp \ $$SRC_DIR/uuidutil.cpp \ $$SRC_DIR/zutil.cpp \ $$SRC_DIR/zhttpmanager.cpp \ $$SRC_DIR/zhttprequest.cpp \ $$SRC_DIR/zwebsocket.cpp \ $$SRC_DIR/zrpcmanager.cpp \ $$SRC_DIR/zrpcrequest.cpp \ $$SRC_DIR/statusreasons.cpp \ $$SRC_DIR/cors.cpp \ $$SRC_DIR/simplehttpserver.cpp \ $$SRC_DIR/stats.cpp \ $$SRC_DIR/statsmanager.cpp \ $$SRC_DIR/settings.cpp PSRC_DIR = $$SRC_DIR/proxy HEADERS += \ $$PSRC_DIR/testhttprequest.h \ $$PSRC_DIR/testwebsocket.h \ $$PSRC_DIR/websocketoverhttp.h \ $$PSRC_DIR/zrpcchecker.h \ $$PSRC_DIR/sockjsmanager.h \ $$PSRC_DIR/sockjssession.h \ $$PSRC_DIR/inspectrequest.h \ $$PSRC_DIR/acceptrequest.h \ $$PSRC_DIR/connectionmanager.h \ $$PSRC_DIR/wscontrolmanager.h \ $$PSRC_DIR/wscontrolsession.h \ $$PSRC_DIR/acceptdata.h \ $$PSRC_DIR/routesfile.h \ $$PSRC_DIR/domainmap.h \ $$PSRC_DIR/zroutes.h \ $$PSRC_DIR/xffrule.h \ $$PSRC_DIR/requestsession.h \ $$PSRC_DIR/proxyutil.h \ $$PSRC_DIR/proxysession.h \ $$PSRC_DIR/wsproxysession.h \ $$PSRC_DIR/updater.h \ $$PSRC_DIR/engine.h \ $$PSRC_DIR/app.h \ $$PSRC_DIR/main.h SOURCES += \ $$PSRC_DIR/testhttprequest.cpp \ $$PSRC_DIR/testwebsocket.cpp \ $$PSRC_DIR/websocketoverhttp.cpp \ $$PSRC_DIR/zrpcchecker.cpp \ $$PSRC_DIR/sockjsmanager.cpp \ $$PSRC_DIR/sockjssession.cpp \ $$PSRC_DIR/inspectrequest.cpp \ $$PSRC_DIR/acceptrequest.cpp \ $$PSRC_DIR/connectionmanager.cpp \ $$PSRC_DIR/wscontrolmanager.cpp \ $$PSRC_DIR/wscontrolsession.cpp \ $$PSRC_DIR/routesfile.cpp \ $$PSRC_DIR/domainmap.cpp \ $$PSRC_DIR/zroutes.cpp \ $$PSRC_DIR/requestsession.cpp \ $$PSRC_DIR/proxyutil.cpp \ $$PSRC_DIR/proxysession.cpp \ $$PSRC_DIR/wsproxysession.cpp \ $$PSRC_DIR/updater.cpp \ $$PSRC_DIR/engine.cpp \ $$PSRC_DIR/app.cpp \ $$PSRC_DIR/main.cpp HSRC_DIR = $$SRC_DIR/handler HEADERS += \ $$HSRC_DIR/deferred.h \ $$HSRC_DIR/variantutil.h \ $$HSRC_DIR/jsonpointer.h \ $$HSRC_DIR/jsonpatch.h \ $$HSRC_DIR/detectrule.h \ $$HSRC_DIR/lastids.h \ $$HSRC_DIR/cidset.h \ $$HSRC_DIR/sessionrequest.h \ $$HSRC_DIR/requeststate.h \ $$HSRC_DIR/wscontrolmessage.h \ $$HSRC_DIR/publishformat.h \ $$HSRC_DIR/publishitem.h \ $$HSRC_DIR/instruct.h \ $$HSRC_DIR/format.h \ $$HSRC_DIR/idformat.h \ $$HSRC_DIR/httpsession.h \ $$HSRC_DIR/httpsessionupdatemanager.h \ $$HSRC_DIR/wssession.h \ $$HSRC_DIR/publishlastids.h \ $$HSRC_DIR/controlrequest.h \ $$HSRC_DIR/conncheckworker.h \ $$HSRC_DIR/refreshworker.h \ $$HSRC_DIR/ratelimiter.h \ $$HSRC_DIR/sequencer.h \ $$HSRC_DIR/filter.h \ $$HSRC_DIR/filterstack.h \ $$HSRC_DIR/handlerengine.h \ $$HSRC_DIR/handlerapp.h \ $$HSRC_DIR/main.h SOURCES += \ $$HSRC_DIR/deferred.cpp \ $$HSRC_DIR/variantutil.cpp \ $$HSRC_DIR/jsonpointer.cpp \ $$HSRC_DIR/jsonpatch.cpp \ $$HSRC_DIR/sessionrequest.cpp \ $$HSRC_DIR/requeststate.cpp \ $$HSRC_DIR/wscontrolmessage.cpp \ $$HSRC_DIR/publishformat.cpp \ $$HSRC_DIR/publishitem.cpp \ $$HSRC_DIR/instruct.cpp \ $$HSRC_DIR/format.cpp \ $$HSRC_DIR/idformat.cpp \ $$HSRC_DIR/httpsession.cpp \ $$HSRC_DIR/httpsessionupdatemanager.cpp \ $$HSRC_DIR/wssession.cpp \ $$HSRC_DIR/publishlastids.cpp \ $$HSRC_DIR/controlrequest.cpp \ $$HSRC_DIR/conncheckworker.cpp \ $$HSRC_DIR/refreshworker.cpp \ $$HSRC_DIR/ratelimiter.cpp \ $$HSRC_DIR/sequencer.cpp \ $$HSRC_DIR/filter.cpp \ $$HSRC_DIR/filterstack.cpp \ $$HSRC_DIR/handlerengine.cpp \ $$HSRC_DIR/handlerapp.cpp \ $$HSRC_DIR/handlermain.cpp MSRC_DIR = $$SRC_DIR/m2adapter HEADERS += \ $$MSRC_DIR/m2requestpacket.h \ $$MSRC_DIR/m2responsepacket.h \ $$MSRC_DIR/m2adapterapp.h \ $$MSRC_DIR/main.h SOURCES += \ $$MSRC_DIR/m2requestpacket.cpp \ $$MSRC_DIR/m2responsepacket.cpp \ $$MSRC_DIR/m2adapterapp.cpp \ $$MSRC_DIR/m2adaptermain.cpp RSRC_DIR = $$SRC_DIR/runner HEADERS += \ $$RSRC_DIR/template.h \ $$RSRC_DIR/service.h \ $$RSRC_DIR/listenport.h \ $$RSRC_DIR/condureservice.h \ $$RSRC_DIR/mongrel2service.h \ $$RSRC_DIR/m2adapterservice.h \ $$RSRC_DIR/zurlservice.h \ $$RSRC_DIR/pushpinproxyservice.h \ $$RSRC_DIR/pushpinhandlerservice.h \ $$RSRC_DIR/runnerapp.h \ $$RSRC_DIR/main.h SOURCES += \ $$RSRC_DIR/template.cpp \ $$RSRC_DIR/service.cpp \ $$RSRC_DIR/condureservice.cpp \ $$RSRC_DIR/mongrel2service.cpp \ $$RSRC_DIR/m2adapterservice.cpp \ $$RSRC_DIR/zurlservice.cpp \ $$RSRC_DIR/pushpinproxyservice.cpp \ $$RSRC_DIR/pushpinhandlerservice.cpp \ $$RSRC_DIR/runnerapp.cpp \ $$RSRC_DIR/runnermain.cpp pushpin-1.39.1/src/cpp/cpp.pro000066400000000000000000000003731457610542000161570ustar00rootroot00000000000000TEMPLATE = lib CONFIG -= app_bundle CONFIG += staticlib c++14 QT -= gui QT += network TARGET = pushpin-cpp cpp_build_dir = $$OUT_PWD MOC_DIR = $$cpp_build_dir/moc OBJECTS_DIR = $$cpp_build_dir/obj include($$cpp_build_dir/conf.pri) include(cpp.pri) pushpin-1.39.1/src/cpp/handler/000077500000000000000000000000001457610542000162655ustar00rootroot00000000000000pushpin-1.39.1/src/cpp/handler/cidset.h000066400000000000000000000015331457610542000177130ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef CIDSET_H #define CIDSET_H #include #include #include typedef QSet CidSet; Q_DECLARE_METATYPE(CidSet); #endif pushpin-1.39.1/src/cpp/handler/conncheckworker.cpp000066400000000000000000000047071457610542000221660ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "conncheckworker.h" #include "qtcompat.h" #include "zrpcrequest.h" #include "controlrequest.h" #include "statsmanager.h" ConnCheckWorker::ConnCheckWorker(ZrpcRequest *req, ZrpcManager *proxyControlClient, StatsManager *stats, QObject *parent) : Deferred(parent), req_(req) { req_->setParent(this); QVariantHash args = req_->args(); if(!args.contains("ids") || typeId(args["ids"]) != QMetaType::QVariantList) { respondError("bad-request"); return; } QVariantList vids = args["ids"].toList(); foreach(const QVariant &vid, vids) { if(typeId(vid) != QMetaType::QByteArray) { respondError("bad-request"); return; } cids_ += QString::fromUtf8(vid.toByteArray()); } foreach(const QString &cid, cids_) { if(!stats->checkConnection(cid.toUtf8())) missing_ += cid; } if(!missing_.isEmpty()) { // ask the proxy about any cids we don't know about Deferred *d = ControlRequest::connCheck(proxyControlClient, missing_, this); finishedConnection_ = d->finished.connect(boost::bind(&ConnCheckWorker::proxyConnCheck_finished, this, boost::placeholders::_1)); return; } doFinish(); } void ConnCheckWorker::respondError(const QByteArray &condition) { req_->respondError(condition); setFinished(true); } void ConnCheckWorker::doFinish() { foreach(const QString &cid, missing_) cids_.remove(cid); QVariantList result; foreach(const QString &cid, cids_) result += cid.toUtf8(); req_->respond(result); setFinished(true); } void ConnCheckWorker::proxyConnCheck_finished(const DeferredResult &result) { if(result.success) { CidSet found = result.value.value(); foreach(const QString &cid, found) missing_.remove(cid); doFinish(); } else { respondError("proxy-request-failed"); } } pushpin-1.39.1/src/cpp/handler/conncheckworker.h000066400000000000000000000025461457610542000216320ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef CONNCHECKWORKER_H #define CONNCHECKWORKER_H #include #include "deferred.h" #include "cidset.h" #include using Connection = boost::signals2::scoped_connection; class ZrpcRequest; class ZrpcManager; class StatsManager; class ConnCheckWorker : public Deferred { Q_OBJECT public: ConnCheckWorker(ZrpcRequest *req, ZrpcManager *proxyControlClient, StatsManager *stats, QObject *parent = 0); private: ZrpcRequest *req_; CidSet cids_; CidSet missing_; Connection finishedConnection_; void respondError(const QByteArray &condition); void doFinish(); private: void proxyConnCheck_finished(const DeferredResult &result); }; #endif pushpin-1.39.1/src/cpp/handler/controlrequest.cpp000066400000000000000000000070351457610542000220670ustar00rootroot00000000000000/* * Copyright (C) 2016-2017 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "controlrequest.h" #include "packet/statspacket.h" #include "qtcompat.h" #include "deferred.h" #include "zrpcrequest.h" namespace ControlRequest { class ConnCheck : public Deferred { Q_OBJECT Connection finishedConnection; public: ConnCheck(ZrpcManager *controlClient, const CidSet &cids, QObject *parent = 0) : Deferred(parent) { ZrpcRequest *req = new ZrpcRequest(controlClient, this); finishedConnection = req->finished.connect(boost::bind(&ConnCheck::req_finished, this, req)); QVariantList vcids; foreach(const QString &cid, cids) vcids += cid.toUtf8(); QVariantHash args; args["ids"] = vcids; req->start("conncheck", args); } private: void req_finished(ZrpcRequest *req) { if(req->success()) { QVariant vresult = req->result(); if(typeId(vresult) != QMetaType::QVariantList) { setFinished(false); return; } QVariantList result = vresult.toList(); CidSet out; foreach(const QVariant &vcid, result) { if(typeId(vcid) != QMetaType::QByteArray) { setFinished(false); return; } out += QString::fromUtf8(vcid.toByteArray()); } setFinished(true, QVariant::fromValue(out)); } else { setFinished(false, req->errorCondition()); } } }; class Refresh : public Deferred { Q_OBJECT Connection finishedConnection; public: Refresh(ZrpcManager *controlClient, const QByteArray &cid, QObject *parent) : Deferred(parent) { ZrpcRequest *req = new ZrpcRequest(controlClient, this); finishedConnection = req->finished.connect(boost::bind(&Refresh::req_finished, this, req)); QVariantHash args; args["cid"] = cid; req->start("refresh", args); } void req_finished(ZrpcRequest *req) { if(req->success()) setFinished(true); else setFinished(false, req->errorConditionString()); } }; class Report : public Deferred { Q_OBJECT Connection finishedConnection; public: Report(ZrpcManager *controlClient, const StatsPacket &packet, QObject *parent) : Deferred(parent) { ZrpcRequest *req = new ZrpcRequest(controlClient, this); finishedConnection = req->finished.connect(boost::bind(&Report::req_finished, this, req)); QVariantHash args; args["stats"] = packet.toVariant(); req->start("report", args); } void req_finished(ZrpcRequest *req) { if(req->success()) setFinished(true); else setFinished(false, req->errorCondition()); } }; Deferred *connCheck(ZrpcManager *controlClient, const CidSet &cids, QObject *parent) { return new ConnCheck(controlClient, cids, parent); } Deferred *refresh(ZrpcManager *controlClient, const QByteArray &cid, QObject *parent) { return new Refresh(controlClient, cid, parent); } Deferred *report(ZrpcManager *controlClient, const StatsPacket &packet, QObject *parent) { return new Report(controlClient, packet, parent); } } #include "controlrequest.moc" pushpin-1.39.1/src/cpp/handler/controlrequest.h000066400000000000000000000023411457610542000215270ustar00rootroot00000000000000/* * Copyright (C) 2016-2017 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef CONTROLREQUEST_H #define CONTROLREQUEST_H #include "cidset.h" #include using Connection = boost::signals2::scoped_connection; class QObject; class ZrpcManager; class StatsPacket; class Deferred; namespace ControlRequest { Deferred *connCheck(ZrpcManager *controlClient, const CidSet &cids, QObject *parent = 0); Deferred *refresh(ZrpcManager *controlClient, const QByteArray &cid, QObject *parent = 0); Deferred *report(ZrpcManager *controlClient, const StatsPacket &packet, QObject *parent = 0); } #endif pushpin-1.39.1/src/cpp/handler/deferred.cpp000066400000000000000000000021601457610542000205500ustar00rootroot00000000000000/* * Copyright (C) 2015 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "deferred.h" Deferred::Deferred(QObject *parent) : QObject(parent) { qRegisterMetaType(); } Deferred::~Deferred() { } void Deferred::cancel() { delete this; } void Deferred::setFinished(bool ok, const QVariant &value) { result_.success = ok; result_.value = value; QMetaObject::invokeMethod(this, "doFinish", Qt::QueuedConnection); } void Deferred::doFinish() { finished(result_); delete this; } pushpin-1.39.1/src/cpp/handler/deferred.h000066400000000000000000000026211457610542000202170ustar00rootroot00000000000000/* * Copyright (C) 2015 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef DEFERRED_H #define DEFERRED_H #include #include #include class DeferredResult { public: bool success; QVariant value; DeferredResult() : success(false) { } DeferredResult(bool _success, const QVariant &_value = QVariant()) : success(_success), value(_value) { } }; Q_DECLARE_METATYPE(DeferredResult) class Deferred : public QObject { Q_OBJECT public: virtual ~Deferred(); virtual void cancel(); boost::signals2::signal finished; protected: Deferred(QObject *parent = 0); void setFinished(bool ok, const QVariant &value = QVariant()); private slots: void doFinish(); private: DeferredResult result_; }; #endif pushpin-1.39.1/src/cpp/handler/detectrule.h000066400000000000000000000017521457610542000206030ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef DETECTRULE_H #define DETECTRULE_H #include #include #include class DetectRule { public: QString domain; QByteArray pathPrefix; QString sidPtr; QString jsonParam; }; typedef QList DetectRuleList; Q_DECLARE_METATYPE(DetectRuleList); #endif pushpin-1.39.1/src/cpp/handler/filter.cpp000066400000000000000000000142521457610542000202620ustar00rootroot00000000000000/* * Copyright (C) 2016-2019 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "filter.h" #include "log.h" #include "format.h" #include "idformat.h" namespace { class SkipSelfFilter : public Filter { public: SkipSelfFilter() : Filter("skip-self") { } virtual SendAction sendAction() const { QString user = context().subscriptionMeta.value("user"); QString sender = context().publishMeta.value("sender"); if(!user.isEmpty() && !sender.isEmpty() && sender == user) return Drop; return Send; } }; class SkipUsersFilter : public Filter { public: SkipUsersFilter() : Filter("skip-users") { } virtual SendAction sendAction() const { QString user = context().subscriptionMeta.value("user"); QStringList skip_users; foreach(const QString &part, context().publishMeta.value("skip_users").split(',')) { QString s = part.trimmed(); if(!s.isEmpty()) skip_users += s; } if(!user.isEmpty() && skip_users.contains(user)) return Drop; return Send; } }; class RequireSubFilter : public Filter { public: RequireSubFilter() : Filter("require-sub") { } virtual SendAction sendAction() const { QString require_sub = context().publishMeta.value("require_sub"); if(!require_sub.isEmpty() && !context().prevIds.keys().contains(require_sub)) return Drop; return Send; } }; class BuildIdFilter : public Filter { public: IdFormat::ContentRenderer *idContentRenderer; BuildIdFilter() : Filter("build-id"), idContentRenderer(0) { } ~BuildIdFilter() { delete idContentRenderer; } bool ensureInit() { if(!idContentRenderer) { QString idFormat = context().subscriptionMeta.value("id_format"); if(idFormat.isNull()) { setError("no sub meta 'id_format'"); return false; } QHash idTemplateVars; QHashIterator it(context().prevIds); while(it.hasNext()) { it.next(); idTemplateVars.insert(it.key(), it.value().toUtf8()); } QString _error; QByteArray id; if(!idTemplateVars.isEmpty()) { id = IdFormat::renderId(idFormat.toUtf8(), idTemplateVars, &_error); if(id.isNull()) { setError(QString("failed to render ID: %1").arg(_error)); return false; } } bool hex = false; QString idEncoding = context().subscriptionMeta.value("id_encoding"); if(!idEncoding.isNull()) { if(idEncoding == "hex") { hex = true; } else { setError(QString("unsupported encoding: %1").arg(idEncoding)); return false; } } idContentRenderer = new IdFormat::ContentRenderer(id, hex); } return true; } virtual QByteArray update(const QByteArray &data) { if(!ensureInit()) return QByteArray(); QByteArray buf = idContentRenderer->update(data); if(buf.isNull()) { setError(idContentRenderer->errorMessage()); return QByteArray(); } return buf; } virtual QByteArray finalize() { if(!ensureInit()) return QByteArray(); QByteArray buf = idContentRenderer->finalize(); if(buf.isNull()) { setError(idContentRenderer->errorMessage()); return QByteArray(); } return buf; } }; class VarSubstFormatHandler : public Format::Handler { public: QHash vars; virtual QByteArray handle(char type, const QByteArray &arg, QString *error) const { if(type != 's') { *error = QString("Unknown directive '%1'").arg(type); return QByteArray(); } if(arg.isNull()) { *error = QString("Directive 's' requires argument"); return QByteArray(); } QString value = vars.value(arg); if(value.isNull()) { *error = QString("No such variable '%1'").arg(QString::fromUtf8(arg)); return QByteArray(); } return value.toUtf8(); } }; class VarSubstFilter : public Filter { public: VarSubstFilter() : Filter("var-subst") { } virtual QByteArray update(const QByteArray &data) { VarSubstFormatHandler handler; handler.vars = context().subscriptionMeta; QString errorMessage; QByteArray buf = Format::process(data, &handler, 0, &errorMessage); if(buf.isNull()) { setError(errorMessage); return QByteArray(); } return buf; } virtual QByteArray finalize() { return QByteArray(""); } }; } Filter::Filter(const QString &name) : name_(name) { } Filter::~Filter() { } Filter::SendAction Filter::sendAction() const { return Send; } QByteArray Filter::update(const QByteArray &data) { return data; } QByteArray Filter::finalize() { return QByteArray(""); } QByteArray Filter::process(const QByteArray &data) { QByteArray out = update(data); if(out.isNull()) return QByteArray(); QByteArray buf = finalize(); if(buf.isNull()) return QByteArray(); return out + buf; } Filter *Filter::create(const QString &name) { if(name == "skip-self") return new SkipSelfFilter; else if(name == "skip-users") return new SkipUsersFilter; else if(name == "require-sub") return new RequireSubFilter; else if(name == "build-id") return new BuildIdFilter; else if(name == "var-subst") return new VarSubstFilter; else return 0; } QStringList Filter::names() { return (QStringList() << "skip-self" << "skip-users" << "require-sub" << "build-id" << "var-subst"); } Filter::Targets Filter::targets(const QString &name) { if(name == "skip-self") return Filter::MessageDelivery; else if(name == "skip-users") return Filter::MessageDelivery; else if(name == "require-sub") return Filter::MessageDelivery; else if(name == "build-id") return Filter::Targets(Filter::MessageContent | Filter::ProxyContent); else if(name == "var-subst") return Filter::MessageContent; else return Filter::Targets(0); } pushpin-1.39.1/src/cpp/handler/filter.h000066400000000000000000000035211457610542000177240ustar00rootroot00000000000000/* * Copyright (C) 2016-2019 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef FILTER_H #define FILTER_H #include #include #include class Filter { public: enum SendAction { Send, Drop }; enum Targets { MessageDelivery = 0x01, MessageContent = 0x02, ProxyContent = 0x04, }; class Context { public: QHash prevIds; QHash subscriptionMeta; QHash publishMeta; }; Filter(const QString &name = QString()); virtual ~Filter(); const QString & name() const { return name_; } const Context & context() const { return context_; } QString errorMessage() const { return errorMessage_; } void setContext(const Context &context) { context_ = context; } virtual SendAction sendAction() const; // return null array on error virtual QByteArray update(const QByteArray &data); virtual QByteArray finalize(); QByteArray process(const QByteArray &data); static Filter *create(const QString &name); static QStringList names(); static Targets targets(const QString &name); protected: void setError(const QString &s) { errorMessage_ = s; } private: QString name_; Context context_; QString errorMessage_; }; #endif pushpin-1.39.1/src/cpp/handler/filterstack.cpp000066400000000000000000000040461457610542000213100ustar00rootroot00000000000000/* * Copyright (C) 2017 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "filterstack.h" FilterStack::FilterStack(const Filter::Context &context, const QStringList &filters) { foreach(const QString &name, filters) { Filter *f = Filter::create(name); if(f) { f->setContext(context); filters_ += f; } } } FilterStack::FilterStack(const Filter::Context &context, const QList &filters) { filters_ = filters; foreach(Filter *f, filters_) f->setContext(context); } FilterStack::~FilterStack() { qDeleteAll(filters_); } Filter::SendAction FilterStack::sendAction() const { foreach(Filter *f, filters_) { SendAction a = f->sendAction(); if(a == Drop) return Drop; } return Send; } QByteArray FilterStack::update(const QByteArray &data) { QByteArray buf = data; foreach(Filter *f, filters_) { buf = f->update(buf); if(buf.isNull()) { setError(QString("%1: %2").arg(f->name(), f->errorMessage())); return QByteArray(); } } return buf; } QByteArray FilterStack::finalize() { QByteArray out(""); foreach(Filter *f, filters_) { if(!out.isEmpty()) { out = f->update(out); if(out.isNull()) { setError(QString("%1: %2").arg(f->name(), f->errorMessage())); return QByteArray(); } } QByteArray buf = f->finalize(); if(buf.isNull()) { setError(QString("%1: %2").arg(f->name(), f->errorMessage())); return QByteArray(); } out += buf; } return out; } pushpin-1.39.1/src/cpp/handler/filterstack.h000066400000000000000000000023211457610542000207470ustar00rootroot00000000000000/* * Copyright (C) 2017 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef FILTERSTACK_H #define FILTERSTACK_H #include #include "filter.h" class FilterStack : public Filter { public: FilterStack(const Filter::Context &context, const QStringList &filters); // takes ownership of filters in list FilterStack(const Filter::Context &context, const QList &filters); ~FilterStack(); // reimplemented virtual SendAction sendAction() const; virtual QByteArray update(const QByteArray &data); virtual QByteArray finalize(); private: QList filters_; }; #endif pushpin-1.39.1/src/cpp/handler/format.cpp000066400000000000000000000071301457610542000202620ustar00rootroot00000000000000/* * Copyright (C) 2019 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "format.h" #include namespace Format { QByteArray process(const QByteArray &format, Handler *handler, int *partialPos, QString *error) { QByteArray out(""); for(int n = 0; n < format.length(); ++n) { char c = format.at(n); if(c == '%') { int markerPos = n; if(n + 1 >= format.length()) { if(partialPos) { *partialPos = markerPos; return out; } else { if(error) *error = QString("Expected directive after '%' at position %1").arg(n); return QByteArray(); } } ++n; c = format.at(n); if(c == '(') { int fieldStart = n; if(n + 1 >= format.length()) { if(partialPos) { *partialPos = markerPos; return out; } else { if(error) *error = QString("Expected character after '(' at position %1").arg(n); return QByteArray(); } } ++n; QByteArray arg; // scan for ')' bool ok = false; for(; n < format.length(); ++n) { c = format.at(n); if(c == '\\') { if(n + 1 >= format.length()) { if(partialPos) { *partialPos = markerPos; return out; } else { if(error) *error = QString("Expected character after '\\' at position %1").arg(n); return QByteArray(); } } ++n; c = format.at(n); arg += c; } else if(c == ')') { ok = true; break; } else { arg += c; } } if(!ok) { if(partialPos) { *partialPos = markerPos; return out; } else { if(error) *error = QString("Unterminated field starting at position %1").arg(fieldStart); return QByteArray(); } } if(n + 1 >= format.length()) { if(partialPos) { *partialPos = markerPos; return out; } else { if(error) *error = QString("Expected directive after ')' at position %1").arg(n); return QByteArray(); } } ++n; c = format.at(n); QString _error; QByteArray result = handler->handle(c, arg, &_error); if(result.isNull()) { if(error) *error = QString("%1 at position %2").arg(_error, QString::number(n)); return QByteArray(); } out += result; } else if(c == '%') { out += c; } else if(isalpha(c)) { QString _error; QByteArray result = handler->handle(c, QByteArray(), &_error); if(result.isNull()) { if(error) *error = QString("%1 at position %2").arg(_error, QString::number(n)); return QByteArray(); } out += result; } else { if(error) *error = QString("Unknown directive '%1' at position %2").arg(QString(c), QString::number(n)); return QByteArray(); } } else { out += c; } } if(partialPos) *partialPos = format.length(); return out; } } pushpin-1.39.1/src/cpp/handler/format.h000066400000000000000000000020761457610542000177330ustar00rootroot00000000000000/* * Copyright (C) 2019 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef FORMAT_H #define FORMAT_H #include #include namespace Format { class Handler { public: virtual ~Handler() {} // returns null array on error virtual QByteArray handle(char type, const QByteArray &arg, QString *error) const = 0; }; QByteArray process(const QByteArray &format, Handler *handler, int *partialPos = 0, QString *error = 0); } #endif pushpin-1.39.1/src/cpp/handler/handlerapp.cpp000066400000000000000000000353041457610542000211140ustar00rootroot00000000000000/* * Copyright (C) 2015-2022 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "handlerapp.h" #include #include #include #include #include #include #include "processquit.h" #include "log.h" #include "settings.h" #include "handlerengine.h" #include "config.h" #define DEFAULT_HTTP_MAX_HEADERS_SIZE 10000 #define DEFAULT_HTTP_MAX_BODY_SIZE 1000000 static void trimlist(QStringList *list) { for(int n = 0; n < list->count(); ++n) { if((*list)[n].isEmpty()) { list->removeAt(n); --n; // adjust position } } } static QStringList expandSpecs(const QStringList &l, int peerCount) { if(l.count() == 1 && l[0].startsWith("ipc:") && peerCount > 1) { QString base = l[0]; QStringList out; for(int i = 0; i < peerCount; ++i) out += base + QString("-%1").arg(i); return out; } return l; } static QString firstSpec(const QString &s, int peerCount) { if(s.startsWith("ipc:") && peerCount > 1) return s + "-0"; return s; } enum CommandLineParseResult { CommandLineOk, CommandLineError, CommandLineVersionRequested, CommandLineHelpRequested }; class ArgsData { public: QString configFile; QString logFile; int logLevel; QString ipcPrefix; int portOffset; ArgsData() : logLevel(-1), portOffset(-1) { } }; static CommandLineParseResult parseCommandLine(QCommandLineParser *parser, ArgsData *args, QString *errorMessage) { parser->setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions); const QCommandLineOption configFileOption("config", "Config file.", "file"); parser->addOption(configFileOption); const QCommandLineOption logFileOption("logfile", "File to log to.", "file"); parser->addOption(logFileOption); const QCommandLineOption logLevelOption("loglevel", "Log level (default: 2).", "x"); parser->addOption(logLevelOption); const QCommandLineOption verboseOption("verbose", "Verbose output. Same as --loglevel=3."); parser->addOption(verboseOption); const QCommandLineOption ipcPrefixOption("ipc-prefix", "Override ipc_prefix config option.", "prefix"); parser->addOption(ipcPrefixOption); const QCommandLineOption portOffsetOption("port-offset", "Override port_offset config option.", "offset"); parser->addOption(portOffsetOption); const QCommandLineOption helpOption = parser->addHelpOption(); const QCommandLineOption versionOption = parser->addVersionOption(); if(!parser->parse(QCoreApplication::arguments())) { *errorMessage = parser->errorText(); return CommandLineError; } if(parser->isSet(versionOption)) return CommandLineVersionRequested; if(parser->isSet(helpOption)) return CommandLineHelpRequested; if(parser->isSet(configFileOption)) args->configFile = parser->value(configFileOption); if(parser->isSet(logFileOption)) args->logFile = parser->value(logFileOption); if(parser->isSet(logLevelOption)) { bool ok; int x = parser->value(logLevelOption).toInt(&ok); if(!ok || x < 0) { *errorMessage = "error: loglevel must be greater than or equal to 0"; return CommandLineError; } args->logLevel = x; } if(parser->isSet(verboseOption)) args->logLevel = 3; if(parser->isSet(ipcPrefixOption)) args->ipcPrefix = parser->value(ipcPrefixOption); if(parser->isSet(portOffsetOption)) { bool ok; int x = parser->value(portOffsetOption).toInt(&ok); if(!ok || x < 0) { *errorMessage = "error: port-offset must be greater than or equal to 0"; return CommandLineError; } args->portOffset = x; } return CommandLineOk; } class HandlerApp::Private : public QObject { Q_OBJECT public: HandlerApp *q; ArgsData args; HandlerEngine *engine; Connection quitConnection; Connection hupConnection; Private(HandlerApp *_q) : QObject(_q), q(_q), engine(0) { quitConnection = ProcessQuit::instance()->quit.connect(boost::bind(&Private::doQuit, this)); hupConnection = ProcessQuit::instance()->hup.connect(boost::bind(&Private::reload, this)); } void start() { QCoreApplication::setApplicationName("pushpin-handler"); QCoreApplication::setApplicationVersion(Config::get().version); QCommandLineParser parser; parser.setApplicationDescription("Pushpin handler component."); QString errorMessage; switch(parseCommandLine(&parser, &args, &errorMessage)) { case CommandLineOk: break; case CommandLineError: fprintf(stderr, "%s\n\n%s", qPrintable(errorMessage), qPrintable(parser.helpText())); q->quit(1); return; case CommandLineVersionRequested: printf("%s %s\n", qPrintable(QCoreApplication::applicationName()), qPrintable(QCoreApplication::applicationVersion())); q->quit(0); return; case CommandLineHelpRequested: parser.showHelp(); Q_UNREACHABLE(); } if(args.logLevel != -1) log_setOutputLevel(args.logLevel); else log_setOutputLevel(LOG_LEVEL_INFO); if(!args.logFile.isEmpty()) { if(!log_setFile(args.logFile)) { log_error("failed to open log file: %s", qPrintable(args.logFile)); q->quit(1); return; } } log_info("starting..."); QString configFile = args.configFile; if(configFile.isEmpty()) configFile = QDir(Config::get().configDir).filePath("pushpin.conf"); // QSettings doesn't inform us if the config file doesn't exist, so do that ourselves { QFile file(configFile); if(!file.open(QIODevice::ReadOnly)) { log_error("failed to open %s, and --config not passed", qPrintable(configFile)); q->quit(0); return; } } Settings settings(configFile); if(!args.ipcPrefix.isEmpty()) settings.setIpcPrefix(args.ipcPrefix); if(args.portOffset != -1) settings.setPortOffset(args.portOffset); QStringList services = settings.value("runner/services").toStringList(); QStringList condure_in_stream_specs = settings.value("proxy/condure_in_stream_specs").toStringList(); trimlist(&condure_in_stream_specs); QStringList condure_out_specs = settings.value("proxy/condure_out_specs").toStringList(); trimlist(&condure_out_specs); int proxyWorkerCount = settings.value("proxy/workers", 1).toInt(); QStringList m2a_in_stream_specs = settings.value("handler/m2a_in_stream_specs").toStringList(); trimlist(&m2a_in_stream_specs); QStringList m2a_out_specs = settings.value("handler/m2a_out_specs").toStringList(); trimlist(&m2a_out_specs); QStringList intreq_out_specs = settings.value("handler/proxy_intreq_out_specs").toStringList(); trimlist(&intreq_out_specs); QStringList intreq_out_stream_specs = settings.value("handler/proxy_intreq_out_stream_specs").toStringList(); trimlist(&intreq_out_stream_specs); QStringList intreq_in_specs = settings.value("handler/proxy_intreq_in_specs").toStringList(); trimlist(&intreq_in_specs); QStringList proxy_inspect_specs = settings.value("handler/proxy_inspect_specs").toStringList(); trimlist(&proxy_inspect_specs); QString proxy_inspect_spec = settings.value("handler/proxy_inspect_spec").toString(); if(!proxy_inspect_spec.isEmpty()) proxy_inspect_specs += proxy_inspect_spec; QStringList proxy_accept_specs = settings.value("handler/proxy_accept_specs").toStringList(); trimlist(&proxy_accept_specs); QString proxy_accept_spec = settings.value("handler/proxy_accept_spec").toString(); if(!proxy_accept_spec.isEmpty()) proxy_accept_specs += proxy_accept_spec; QStringList proxy_retry_out_specs = settings.value("handler/proxy_retry_out_specs").toStringList(); trimlist(&proxy_retry_out_specs); QString proxy_retry_out_spec = settings.value("handler/proxy_retry_out_spec").toString(); if(!proxy_retry_out_spec.isEmpty()) proxy_retry_out_specs += proxy_retry_out_spec; QStringList ws_control_init_specs = settings.value("handler/proxy_ws_control_init_specs").toStringList(); trimlist(&ws_control_init_specs); QStringList ws_control_stream_specs = settings.value("handler/proxy_ws_control_stream_specs").toStringList(); trimlist(&ws_control_stream_specs); QString stats_spec = settings.value("handler/stats_spec").toString(); QString command_spec = settings.value("handler/command_spec").toString(); QString state_spec = settings.value("handler/state_spec").toString(); QStringList proxy_stats_specs = settings.value("handler/proxy_stats_specs").toStringList(); trimlist(&proxy_stats_specs); QString proxy_stats_spec = settings.value("handler/proxy_stats_spec").toString(); if(!proxy_stats_spec.isEmpty()) proxy_stats_specs += proxy_stats_spec; QString proxy_command_spec = settings.value("handler/proxy_command_spec").toString(); QString push_in_spec = settings.value("handler/push_in_spec").toString(); QStringList push_in_sub_specs = settings.value("handler/push_in_sub_specs").toStringList(); trimlist(&push_in_sub_specs); QString push_in_sub_spec = settings.value("handler/push_in_sub_spec").toString(); if(!push_in_sub_spec.isEmpty()) push_in_sub_specs += push_in_sub_spec; bool push_in_sub_connect = settings.value("handler/push_in_sub_connect").toBool(); QString push_in_http_addr = settings.value("handler/push_in_http_addr").toString(); int push_in_http_port = settings.adjustedPort("handler/push_in_http_port"); int push_in_http_max_headers_size = settings.value("handler/push_in_max_headers_size", DEFAULT_HTTP_MAX_HEADERS_SIZE).toInt(); int push_in_http_max_body_size = settings.value("handler/push_in_max_body_size", DEFAULT_HTTP_MAX_BODY_SIZE).toInt(); bool ok; int ipcFileMode = settings.value("handler/ipc_file_mode", -1).toString().toInt(&ok, 8); bool shareAll = settings.value("handler/share_all").toBool(); int messageRate = settings.value("handler/message_rate", -1).toInt(); int messageHwm = settings.value("handler/message_hwm", -1).toInt(); int messageBlockSize = settings.value("handler/message_block_size", -1).toInt(); int messageWait = settings.value("handler/message_wait", 5000).toInt(); int idCacheTtl = settings.value("handler/id_cache_ttl", 0).toInt(); int clientMaxconn = settings.value("runner/client_maxconn", 50000).toInt(); int connectionSubscriptionMax = settings.value("handler/connection_subscription_max", 20).toInt(); int subscriptionLinger = settings.value("handler/subscription_linger", 60).toInt(); int statsConnectionSend = settings.value("global/stats_connection_send", true).toBool(); int statsConnectionTtl = settings.value("global/stats_connection_ttl", 120).toInt(); int statsSubscriptionTtl = settings.value("handler/stats_subscription_ttl", 60).toInt(); int statsReportInterval = settings.value("handler/stats_report_interval", 10).toInt(); QString statsFormat = settings.value("handler/stats_format").toString(); QString prometheusPort = settings.value("handler/prometheus_port").toString(); QString prometheusPrefix = settings.value("handler/prometheus_prefix").toString(); if(m2a_in_stream_specs.isEmpty() || m2a_out_specs.isEmpty()) { log_error("must set m2a_in_stream_specs and m2a_out_specs"); q->quit(0); return; } if(proxy_inspect_specs.isEmpty() || proxy_accept_specs.isEmpty() || proxy_retry_out_specs.isEmpty()) { log_error("must set proxy_inspect_specs, proxy_accept_specs, and proxy_retry_out_specs"); q->quit(0); return; } HandlerEngine::Configuration config; config.appVersion = Config::get().version; config.instanceId = "pushpin-handler_" + QByteArray::number(QCoreApplication::applicationPid()); if(!services.contains("mongrel2") && (!condure_in_stream_specs.isEmpty() || !condure_out_specs.isEmpty())) { config.serverInStreamSpecs = condure_in_stream_specs; config.serverOutSpecs = condure_out_specs; } else { config.serverInStreamSpecs = m2a_in_stream_specs; config.serverOutSpecs = m2a_out_specs; } config.clientOutSpecs = expandSpecs(intreq_out_specs, proxyWorkerCount); config.clientOutStreamSpecs = expandSpecs(intreq_out_stream_specs, proxyWorkerCount); config.clientInSpecs = expandSpecs(intreq_in_specs, proxyWorkerCount); config.inspectSpecs = expandSpecs(proxy_inspect_specs, proxyWorkerCount); config.acceptSpecs = expandSpecs(proxy_accept_specs, proxyWorkerCount); config.retryOutSpecs = expandSpecs(proxy_retry_out_specs, proxyWorkerCount); config.wsControlInitSpecs = expandSpecs(ws_control_init_specs, proxyWorkerCount); config.wsControlStreamSpecs = expandSpecs(ws_control_stream_specs, proxyWorkerCount); config.statsSpec = stats_spec; config.commandSpec = command_spec; config.stateSpec = state_spec; config.proxyStatsSpecs = expandSpecs(proxy_stats_specs, proxyWorkerCount); config.proxyCommandSpec = firstSpec(proxy_command_spec, proxyWorkerCount); config.pushInSpec = push_in_spec; config.pushInSubSpecs = push_in_sub_specs; config.pushInSubConnect = push_in_sub_connect; config.pushInHttpAddr = QHostAddress(push_in_http_addr); config.pushInHttpPort = push_in_http_port; config.pushInHttpMaxHeadersSize = push_in_http_max_headers_size; config.pushInHttpMaxBodySize = push_in_http_max_body_size; config.ipcFileMode = ipcFileMode; config.shareAll = shareAll; config.messageRate = messageRate; config.messageHwm = messageHwm; config.messageBlockSize = messageBlockSize; config.messageWait = messageWait; config.idCacheTtl = idCacheTtl; config.connectionsMax = clientMaxconn; config.connectionSubscriptionMax = connectionSubscriptionMax; config.subscriptionLinger = subscriptionLinger; config.statsConnectionSend = statsConnectionSend; config.statsConnectionTtl = statsConnectionTtl; config.statsSubscriptionTtl = statsSubscriptionTtl; config.statsReportInterval = statsReportInterval; config.statsFormat = statsFormat; config.prometheusPort = prometheusPort; config.prometheusPrefix = prometheusPrefix; engine = new HandlerEngine(this); if(!engine->start(config)) { q->quit(0); return; } log_info("started"); } private: void reload() { log_info("reloading"); log_rotate(); engine->reload(); } void doQuit() { log_info("stopping..."); // remove the handler, so if we get another signal then we crash out ProcessQuit::cleanup(); delete engine; engine = 0; log_info("stopped"); q->quit(0); } }; HandlerApp::HandlerApp(QObject *parent) : QObject(parent) { d = new Private(this); } HandlerApp::~HandlerApp() { delete d; } void HandlerApp::start() { d->start(); } #include "handlerapp.moc" pushpin-1.39.1/src/cpp/handler/handlerapp.h000066400000000000000000000021341457610542000205540ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef HANDLERAPP_H #define HANDLERAPP_H #include #include using SignalInt = boost::signals2::signal; using Connection = boost::signals2::scoped_connection; class HandlerApp : public QObject { Q_OBJECT public: HandlerApp(QObject *parent = 0); ~HandlerApp(); void start(); SignalInt quit; private: class Private; friend class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/handler/handlerengine.cpp000066400000000000000000002513551457610542000216070ustar00rootroot00000000000000/* * Copyright (C) 2015-2023 Fanout, Inc. * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "handlerengine.h" #include #include #include #include #include #include #include #include #include "qzmqsocket.h" #include "qzmqvalve.h" #include "qzmqreqmessage.h" #include "qtcompat.h" #include "tnetstring.h" #include "rtimer.h" #include "log.h" #include "packet/httprequestdata.h" #include "packet/httpresponsedata.h" #include "packet/retryrequestpacket.h" #include "packet/wscontrolpacket.h" #include "packet/statspacket.h" #include "inspectdata.h" #include "zutil.h" #include "zrpcmanager.h" #include "zrpcrequest.h" #include "zhttpmanager.h" #include "zhttprequest.h" #include "statsmanager.h" #include "deferred.h" #include "simplehttpserver.h" #include "variantutil.h" #include "detectrule.h" #include "lastids.h" #include "cidset.h" #include "sessionrequest.h" #include "requeststate.h" #include "wscontrolmessage.h" #include "publishformat.h" #include "publishitem.h" #include "jsonpointer.h" #include "publishlastids.h" #include "instruct.h" #include "httpsession.h" #include "wssession.h" #include "controlrequest.h" #include "conncheckworker.h" #include "refreshworker.h" #include "ratelimiter.h" #include "httpsessionupdatemanager.h" #include "sequencer.h" #include "filterstack.h" #define DEFAULT_HWM 101000 #define SUB_SNDHWM 0 // infinite #define RETRY_WAIT_TIME 0 #define WSCONTROL_WAIT_TIME 0 #define STATE_RPC_TIMEOUT 1000 #define PROXY_RPC_TIMEOUT 10000 #define DEFAULT_WS_KEEPALIVE_TIMEOUT 55 #define DEFAULT_WS_SENDDELAYED_TIMEOUT 1 #define SUBSCRIBED_DELAY 1000 #define INSPECT_WORKERS_MAX 10 #define ACCEPT_WORKERS_MAX 10 using namespace VariantUtil; static QList parseItems(const QVariantList &vitems, bool *ok = 0, QString *errorMessage = 0) { QList out; foreach(const QVariant &vitem, vitems) { bool ok_; PublishItem item = PublishItem::fromVariant(vitem, QString(), &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return QList(); } out += item; } setSuccess(ok, errorMessage); return out; } class InspectWorker : public Deferred { Q_OBJECT public: ZrpcRequest *req; ZrpcManager *stateClient; bool shareAll; HttpRequestData requestData; bool truncated; bool autoShare; QString sid; LastIds lastIds; map finishedConnection; InspectWorker(ZrpcRequest *_req, ZrpcManager *_stateClient, bool _shareAll, QObject *parent = 0) : Deferred(parent), req(_req), stateClient(_stateClient), shareAll(_shareAll), truncated(false), autoShare(false) { req->setParent(this); if(req->method() == "inspect") { QVariantHash args = req->args(); if(!args.contains("method") || typeId(args["method"]) != QMetaType::QByteArray) { respondError("bad-request"); return; } requestData.method = QString::fromLatin1(args["method"].toByteArray()); if(!args.contains("uri") || typeId(args["uri"]) != QMetaType::QByteArray) { respondError("bad-request"); return; } requestData.uri = QUrl(args["uri"].toString(), QUrl::StrictMode); if(!requestData.uri.isValid()) { respondError("bad-request"); return; } if(!args.contains("headers") || typeId(args["headers"]) != QMetaType::QVariantList) { respondError("bad-request"); return; } foreach(const QVariant &vheader, args["headers"].toList()) { if(typeId(vheader) != QMetaType::QVariantList) { respondError("bad-request"); return; } QVariantList vlist = vheader.toList(); if(vlist.count() != 2 || typeId(vlist[0]) != QMetaType::QByteArray || typeId(vlist[1]) != QMetaType::QByteArray) { respondError("bad-request"); return; } requestData.headers += HttpHeader(vlist[0].toByteArray(), vlist[1].toByteArray()); } if(!args.contains("body") || typeId(args["body"]) != QMetaType::QByteArray) { respondError("bad-request"); return; } requestData.body = args["body"].toByteArray(); truncated = false; if(args.contains("truncated")) { if(typeId(args["truncated"]) != QMetaType::Bool) { respondError("bad-request"); return; } truncated = args["truncated"].toBool(); } bool getSession = false; if(args.contains("get-session")) { if(typeId(args["get-session"]) != QMetaType::Bool) { respondError("bad-request"); return; } getSession = args["get-session"].toBool(); } autoShare = false; if(args.contains("auto-share")) { if(typeId(args["auto-share"]) != QMetaType::Bool) { respondError("bad-request"); return; } autoShare = args["auto-share"].toBool(); } if(getSession && stateClient) { // determine session info Deferred *d = SessionRequest::detectRulesGet(stateClient, requestData.uri.host().toUtf8(), requestData.uri.path(QUrl::FullyEncoded).toUtf8(), this); finishedConnection[d] = d->finished.connect(boost::bind(&InspectWorker::sessionDetectRulesGet_finished, this, boost::placeholders::_1)); return; } doFinish(); } else { respondError("method-not-found"); } } private: void respondError(const QByteArray &condition) { req->respondError(condition); setFinished(true); } void doFinish() { QVariantHash result; result["no-proxy"] = false; if(autoShare && requestData.method == "GET") { // auto share matches requests based on URI path (not query) and // Grip-Last headers. the reason the query part is not // considered is because it may vary per client and Grip-Last // supersedes whatever is in the query QUrl uri = requestData.uri; uri.setQuery(QString()); // remove the query part QList gripLastHeaders = requestData.headers.getAll("Grip-Last"); std::sort(gripLastHeaders.begin(), gripLastHeaders.end()); QByteArray key = "auto|" + uri.toEncoded(); foreach(const QByteArray &h, gripLastHeaders) key += '|' + h; result["sharing-key"] = key; } else if(shareAll) result["sharing-key"] = requestData.method.toLatin1() + '|' + requestData.uri.toEncoded(); if(!sid.isEmpty()) { result["sid"] = sid.toUtf8(); if(!lastIds.isEmpty()) { QVariantHash vlastIds; QHashIterator it(lastIds); while(it.hasNext()) { it.next(); vlastIds.insert(it.key(), it.value().toUtf8()); } result["last-ids"] = vlastIds; } } req->respond(result); setFinished(true); } private: void sessionDetectRulesGet_finished(const DeferredResult &result) { if(result.success) { QList rules = result.value.value(); log_debug("retrieved %d rules", rules.count()); foreach(const DetectRule &rule, rules) { QByteArray jsonData; if(!rule.jsonParam.isEmpty()) { QUrlQuery tmp(QString::fromUtf8(requestData.body)); jsonData = tmp.queryItemValue(rule.jsonParam, QUrl::FullyDecoded).toUtf8(); } else { jsonData = requestData.body; } QJsonParseError e; QJsonDocument doc = QJsonDocument::fromJson(jsonData, &e); if(e.error != QJsonParseError::NoError) continue; QVariant vdata; if(doc.isObject()) vdata = doc.object().toVariantMap(); else if(doc.isArray()) vdata = doc.array().toVariantList(); else continue; JsonPointer ptr = JsonPointer::resolve(&vdata, rule.sidPtr); if(!ptr.isNull() && ptr.exists()) { bool ok; sid = getString(ptr.value(), &ok); if(!ok) continue; break; } } if(!sid.isEmpty()) { Deferred *d = SessionRequest::getLastIds(stateClient, sid, this); finishedConnection[d] = d->finished.connect(boost::bind(&InspectWorker::sessionGetLastIds_finished, this, boost::placeholders::_1)); return; } } else { // log error but keep going log_error("failed to detect session: condition=%d", result.value.toInt()); } doFinish(); } private: void sessionGetLastIds_finished(const DeferredResult &result) { if(result.success) { lastIds = result.value.value(); } else { QByteArray errorCondition = result.value.toByteArray(); if(errorCondition != "item-not-found") { // log error but keep going log_error("failed to detect session: condition=%d", result.value.toInt()); } } doFinish(); } }; class Subscription; class CommonState { public: QHash httpSessions; QHash wsSessions; QHash > responseSessionsByChannel; QHash > streamSessionsByChannel; QHash > wsSessionsByChannel; PublishLastIds publishLastIds; QHash subs; CommonState() : publishLastIds(1000000) { } }; class AcceptWorker : public Deferred { Q_OBJECT public: ZrpcRequest *req; ZrpcManager *stateClient; CommonState *cs; ZhttpManager *zhttpIn; ZhttpManager *zhttpOut; StatsManager *stats; RateLimiter *updateLimiter; HttpSessionUpdateManager *httpSessionUpdateManager; QString route; QString statsRoute; QString channelPrefix; QStringList implicitChannels; bool trusted; QHash requestStates; HttpRequestData requestData; HttpRequestData origRequestData; bool haveInspectInfo; InspectData inspectInfo; HttpResponseData responseData; bool responseSent; QString sid; LastIds lastIds; QList sessions; int connectionSubscriptionMax; QSet needRemoveFromStats; map finishedConnection; AcceptWorker(ZrpcRequest *_req, ZrpcManager *_stateClient, CommonState *_cs, ZhttpManager *_zhttpIn, ZhttpManager *_zhttpOut, StatsManager *_stats, RateLimiter *_updateLimiter, HttpSessionUpdateManager *_httpSessionUpdateManager, int _connectionSubscriptionMax, QObject *parent = 0) : Deferred(parent), req(_req), stateClient(_stateClient), cs(_cs), zhttpIn(_zhttpIn), zhttpOut(_zhttpOut), stats(_stats), updateLimiter(_updateLimiter), httpSessionUpdateManager(_httpSessionUpdateManager), trusted(false), haveInspectInfo(false), responseSent(false), connectionSubscriptionMax(_connectionSubscriptionMax) { req->setParent(this); } ~AcceptWorker() { foreach(const QByteArray &cid, needRemoveFromStats) stats->removeConnection(cid, false); } // NOTE: to ensure sequential processing of conn-max packets, this // method must process any such packets contained within the accept // request before returning. the conn-max packets must not be processed // asynchronously void start() { QVariantHash args = req->args(); // process conn-max packets before doing anything else if(args.contains("conn-max")) { if(typeId(args["conn-max"]) != QMetaType::QVariantList) { respondError("bad-request"); return; } QVariantList packets = args["conn-max"].toList(); foreach(const QVariant &data, packets) { StatsPacket p; if(!p.fromVariant("conn-max", data) || p.type != StatsPacket::ConnectionsMax) { respondError("bad-request"); return; } stats->processExternalPacket(p, false); } } if(args.contains("route")) { if(typeId(args["route"]) != QMetaType::QByteArray) { respondError("bad-request"); return; } route = QString::fromUtf8(args["route"].toByteArray()); } if(args.contains("separate-stats")) { if(typeId(args["separate-stats"]) != QMetaType::Bool) { respondError("bad-request"); return; } bool separateStats = args["separate-stats"].toBool(); if(!route.isEmpty() && separateStats) statsRoute = route; } if(args.contains("channel-prefix")) { if(typeId(args["channel-prefix"]) != QMetaType::QByteArray) { respondError("bad-request"); return; } channelPrefix = QString::fromUtf8(args["channel-prefix"].toByteArray()); } if(args.contains("channels")) { if(typeId(args["channels"]) != QMetaType::QVariantList) { respondError("bad-request"); return; } QVariantList vchannels = args["channels"].toList(); foreach(const QVariant &v, vchannels) { if(typeId(v) != QMetaType::QByteArray) { respondError("bad-request"); return; } implicitChannels += QString::fromUtf8(v.toByteArray()); } } if(args.contains("trusted")) { if(typeId(args["trusted"]) != QMetaType::Bool) { respondError("bad-request"); return; } trusted = args["trusted"].toBool(); } // parse requests if(!args.contains("requests") || typeId(args["requests"]) != QMetaType::QVariantList) { respondError("bad-request"); return; } foreach(const QVariant &vr, args["requests"].toList()) { RequestState rs = RequestState::fromVariant(vr); if(rs.rid.first.isEmpty()) { respondError("bad-request"); return; } requestStates.insert(rs.rid, rs); } // parse request-data requestData = parseRequestData(args, "request-data"); if(requestData.method.isEmpty()) { respondError("bad-request"); return; } // parse orig-request-data origRequestData = parseRequestData(args, "orig-request-data"); if(origRequestData.method.isEmpty()) { respondError("bad-request"); return; } // parse response if(!args.contains("response") || typeId(args["response"]) != QMetaType::QVariantHash) { respondError("bad-request"); return; } QVariantHash rd = args["response"].toHash(); if(!rd.contains("code") || !canConvert(rd["code"], QMetaType::Int)) { respondError("bad-request"); return; } responseData.code = rd["code"].toInt(); if(!rd.contains("reason") || typeId(rd["reason"]) != QMetaType::QByteArray) { respondError("bad-request"); return; } responseData.reason = rd["reason"].toByteArray(); if(!rd.contains("headers") || typeId(rd["headers"]) != QMetaType::QVariantList) { respondError("bad-request"); return; } foreach(const QVariant &vheader, rd["headers"].toList()) { if(typeId(vheader) != QMetaType::QVariantList) { respondError("bad-request"); return; } QVariantList vlist = vheader.toList(); if(vlist.count() != 2 || typeId(vlist[0]) != QMetaType::QByteArray || typeId(vlist[1]) != QMetaType::QByteArray) { respondError("bad-request"); return; } responseData.headers += HttpHeader(vlist[0].toByteArray(), vlist[1].toByteArray()); } if(!rd.contains("body") || typeId(rd["body"]) != QMetaType::QByteArray) { respondError("bad-request"); return; } responseData.body = rd["body"].toByteArray(); if(args.contains("inspect")) { if(typeId(args["inspect"]) != QMetaType::QVariantHash) { respondError("bad-request"); return; } QVariantHash vinspect = args["inspect"].toHash(); if(!vinspect.contains("no-proxy") || typeId(vinspect["no-proxy"]) != QMetaType::Bool) { respondError("bad-request"); return; } inspectInfo.doProxy = !vinspect["no-proxy"].toBool(); inspectInfo.sharingKey.clear(); if(vinspect.contains("sharing-key")) { if(typeId(vinspect["sharing-key"]) != QMetaType::QByteArray) { respondError("bad-request"); return; } inspectInfo.sharingKey = vinspect["sharing-key"].toByteArray(); } if(vinspect.contains("sid")) { if(typeId(vinspect["sid"]) != QMetaType::QByteArray) { respondError("bad-request"); return; } inspectInfo.sid = vinspect["sid"].toByteArray(); } if(vinspect.contains("last-ids")) { if(typeId(vinspect["last-ids"]) != QMetaType::QVariantHash) { respondError("bad-request"); return; } QVariantHash vlastIds = vinspect["last-ids"].toHash(); QHashIterator it(vlastIds); while(it.hasNext()) { it.next(); if(typeId(it.value()) != QMetaType::QByteArray) { respondError("bad-request"); return; } QByteArray key = it.key().toUtf8(); QByteArray val = it.value().toByteArray(); inspectInfo.lastIds.insert(key, val); } } inspectInfo.userData = vinspect["user-data"]; haveInspectInfo = true; } if(args.contains("response-sent")) { if(typeId(args["response-sent"]) != QMetaType::Bool) { respondError("bad-request"); return; } responseSent = args["response-sent"].toBool(); } bool useSession = false; if(args.contains("use-session")) { if(typeId(args["use-session"]) != QMetaType::Bool) { respondError("bad-request"); return; } useSession = args["use-session"].toBool(); } sid = QString::fromUtf8(responseData.headers.get("Grip-Session-Id")); QList rules; QList ruleHeaders = responseData.headers.getAllAsParameters("Grip-Session-Detect", HttpHeaders::ParseAllParameters); foreach(const HttpHeaderParameters ¶ms, ruleHeaders) { if(params.contains("path-prefix") && params.contains("sid-ptr")) { DetectRule rule; rule.domain = requestData.uri.host(); rule.pathPrefix = params.get("path-prefix"); rule.sidPtr = QString::fromUtf8(params.get("sid-ptr")); if(params.contains("json-param")) rule.jsonParam = QString::fromUtf8(params.get("json-param")); rules += rule; } } QList lastHeaders = responseData.headers.getAllAsParameters("Grip-Last"); foreach(const HttpHeaderParameters ¶ms, lastHeaders) { lastIds.insert(params[0].first, params.get("last-id")); } // we need to "atomically" process conn-max packets and add // connections to the stats manager. we do this by processing the // conn-max packets above and adding to the stats manager below, // without returning to the event loop in between foreach(const RequestState &rs, requestStates) { QByteArray cid = rs.rid.first + ':' + rs.rid.second; int reportOffset = stats->connectionSendEnabled() ? -1 : qMax(rs.unreportedTime, 0); needRemoveFromStats += cid; stats->addConnection(cid, statsRoute.toUtf8(), StatsManager::Http, rs.logicalPeerAddress, rs.isHttps, true, reportOffset); } if(useSession && stateClient) { if(!rules.isEmpty()) { Deferred *d = SessionRequest::detectRulesSet(stateClient, rules, this); finishedConnection[d] = d->finished.connect(boost::bind(&AcceptWorker::sessionDetectRulesSet_finished, this, boost::placeholders::_1)); } else { afterSetRules(); } return; } afterSessionCalls(); } QList takeSessions() { QList out = sessions; sessions.clear(); foreach(HttpSession *hs, out) hs->setParent(0); return out; } Signal sessionsReady; boost::signals2::signal retryPacketReady; private: static HttpRequestData parseRequestData(const QVariantHash &args, const QString &field) { if(!args.contains(field) || typeId(args[field]) != QMetaType::QVariantHash) return HttpRequestData(); QVariantHash rd = args[field].toHash(); if(!rd.contains("method") || typeId(rd["method"]) != QMetaType::QByteArray) return HttpRequestData(); HttpRequestData out; out.method = QString::fromLatin1(rd["method"].toByteArray()); if(!rd.contains("uri") || typeId(rd["uri"]) != QMetaType::QByteArray) return HttpRequestData(); out.uri = QUrl(rd["uri"].toString(), QUrl::StrictMode); if(!out.uri.isValid()) return HttpRequestData(); if(!rd.contains("headers") || typeId(rd["headers"]) != QMetaType::QVariantList) return HttpRequestData(); foreach(const QVariant &vheader, rd["headers"].toList()) { if(typeId(vheader) != QMetaType::QVariantList) return HttpRequestData(); QVariantList vlist = vheader.toList(); if(vlist.count() != 2 || typeId(vlist[0]) != QMetaType::QByteArray || typeId(vlist[1]) != QMetaType::QByteArray) return HttpRequestData(); out.headers += HttpHeader(vlist[0].toByteArray(), vlist[1].toByteArray()); } if(!rd.contains("body") || typeId(rd["body"]) != QMetaType::QByteArray) return HttpRequestData(); out.body = rd["body"].toByteArray(); return out; } void respondError(const QByteArray &condition, const QVariant &result = QVariant()) { req->respondError(condition, result); setFinished(true); } void afterSetRules() { if(!sid.isEmpty()) { Deferred *d = SessionRequest::createOrUpdate(stateClient, sid, lastIds, this); finishedConnection[d] = d->finished.connect(boost::bind(&AcceptWorker::sessionCreateOrUpdate_finished, this, boost::placeholders::_1)); } else { afterSessionCalls(); } } void afterSessionCalls() { bool ok; QString errorMessage; Instruct instruct = Instruct::fromResponse(responseData, &ok, &errorMessage); if(!ok) { respondError("bad-format", errorMessage.toUtf8()); return; } // don't relay these headers. their meaning is handled by // zurl and they only apply to the outgoing hop. instruct.response.headers.removeAll("Connection"); instruct.response.headers.removeAll("Keep-Alive"); instruct.response.headers.removeAll("Content-Encoding"); instruct.response.headers.removeAll("Transfer-Encoding"); if(instruct.holdMode == Instruct::NoHold && instruct.nextLink.isEmpty()) { QVariantHash result; if(!responseSent) { // apply ProxyContent filters of all channels QStringList allFilters; foreach(const Instruct::Channel &c, instruct.channels) { foreach(const QString &filter, c.filters) { if((Filter::targets(filter) & Filter::ProxyContent) && !allFilters.contains(filter)) allFilters += filter; } } Filter::Context fc; fc.subscriptionMeta = instruct.meta; FilterStack fs(fc, allFilters); QByteArray body = fs.process(instruct.response.body); if(body.isNull()) { req->respondError("bad-format", QString("filter error: %1").arg(fs.errorMessage()).toUtf8()); setFinished(true); return; } instruct.response.headers.removeAll("Content-Length"); QVariantHash vresponse; vresponse["code"] = instruct.response.code; vresponse["reason"] = instruct.response.reason; QVariantList vheaders; foreach(const HttpHeader &h, instruct.response.headers) { QVariantList vheader; vheader += h.first; vheader += h.second; vheaders += QVariant(vheader); } vresponse["headers"] = vheaders; vresponse["body"] = body; result["response"] = vresponse; } req->respond(result); setFinished(true); return; } QByteArray reqFrom = req->from(); QVariantHash result; result["accepted"] = true; req->respond(result); log_debug("accepting %d requests from %s", requestStates.count(), reqFrom.data()); if(instruct.holdMode == Instruct::ResponseHold) { bool conflict = false; foreach(const Instruct::Channel &c, instruct.channels) { if(!c.prevId.isNull()) { QString name = channelPrefix + c.name; QString lastId = cs->publishLastIds.value(name); if(!lastId.isNull() && lastId != c.prevId) { log_debug("last ID inconsistency (got=%s, expected=%s), retrying", qPrintable(c.prevId), qPrintable(lastId)); cs->publishLastIds.remove(name); conflict = true; // NOTE: don't exit loop here. we want to clear // the last ids of all conflicting channels } } } if(conflict) { RetryRequestPacket rp; foreach(const RequestState &rs, requestStates) { QByteArray cid = rs.rid.first + ':' + rs.rid.second; needRemoveFromStats.remove(cid); int unreportedTime = stats->removeConnection(cid, true, reqFrom); RetryRequestPacket::Request rpreq; rpreq.rid = rs.rid; rpreq.https = rs.isHttps; rpreq.peerAddress = rs.peerAddress; rpreq.debug = rs.debug; rpreq.autoCrossOrigin = rs.autoCrossOrigin; rpreq.jsonpCallback = rs.jsonpCallback; rpreq.jsonpExtendedResponse = rs.jsonpExtendedResponse; if(!stats->connectionSendEnabled()) rpreq.unreportedTime = unreportedTime; rpreq.inSeq = rs.inSeq; rpreq.outSeq = rs.outSeq; rpreq.outCredits = rs.outCredits; rpreq.userData = rs.userData; rp.requests += rpreq; } rp.requestData = origRequestData; if(haveInspectInfo) { rp.haveInspectInfo = true; rp.inspectInfo.doProxy = inspectInfo.doProxy; rp.inspectInfo.sharingKey = inspectInfo.sharingKey; rp.inspectInfo.sid = inspectInfo.sid; rp.inspectInfo.lastIds = inspectInfo.lastIds; rp.inspectInfo.userData = inspectInfo.userData; } // if prev-id set on channels, set as inspect lastids so the proxy // will pass as Grip-Last in the next request foreach(const Instruct::Channel &c, instruct.channels) { if(!c.prevId.isNull()) { if(!rp.haveInspectInfo) { rp.haveInspectInfo = true; rp.inspectInfo.doProxy = true; } rp.inspectInfo.lastIds.insert(c.name.toUtf8(), c.prevId.toUtf8()); } } rp.route = route.toUtf8(); rp.retrySeq = stats->lastRetrySeq(reqFrom); retryPacketReady(reqFrom, rp); setFinished(true); return; } } foreach(const RequestState &rs, requestStates) { ZhttpRequest::Rid rid(rs.rid.first, rs.rid.second); if(zhttpIn->serverRequestByRid(rid)) { log_error("received accept request for rid we already have (%s, %s), skipping", rid.first.data(), rid.second.data()); continue; } ZhttpRequest::ServerState ss; ss.rid = ZhttpRequest::Rid(rs.rid.first, rs.rid.second); ss.peerAddress = rs.peerAddress; ss.requestMethod = requestData.method; ss.requestUri = requestData.uri; ss.requestUri.setScheme(rs.isHttps ? "https" : "http"); ss.requestHeaders = requestData.headers; ss.requestBody = requestData.body; ss.responseCode = rs.responseCode; ss.inSeq = rs.inSeq; ss.outSeq = rs.outSeq; ss.outCredits = rs.outCredits; ss.userData = rs.userData; // take over responsibility for request ZhttpRequest *httpReq = zhttpIn->createRequestFromState(ss); QSet implicitChannelsSet; foreach(const QString &channel, implicitChannels) implicitChannelsSet += channel; HttpSession::AcceptData adata; adata.from = reqFrom; adata.requestData = origRequestData; adata.logicalPeerAddress = rs.logicalPeerAddress; adata.debug = rs.debug; adata.isRetry = rs.isRetry; adata.autoCrossOrigin = rs.autoCrossOrigin; adata.jsonpCallback = rs.jsonpCallback; adata.jsonpExtendedResponse = rs.jsonpExtendedResponse; adata.unreportedTime = rs.unreportedTime; adata.route = route; adata.statsRoute = statsRoute; adata.channelPrefix = channelPrefix; adata.implicitChannels = implicitChannelsSet; adata.sid = sid; adata.responseSent = responseSent; adata.trusted = trusted; adata.haveInspectInfo = haveInspectInfo; adata.inspectInfo = inspectInfo; QByteArray cid = rid.first + ':' + rid.second; needRemoveFromStats.remove(cid); sessions += new HttpSession(httpReq, adata, instruct, zhttpOut, stats, updateLimiter, &cs->publishLastIds, httpSessionUpdateManager, connectionSubscriptionMax, this); } // engine should directly connect to this and register the holds // immediately, to avoid a race with the lastId check sessionsReady(); setFinished(true); } private: void sessionDetectRulesSet_finished(const DeferredResult &result) { if(!result.success) log_error("couldn't store detection rules: condition=%d", result.value.toInt()); afterSetRules(); } void sessionCreateOrUpdate_finished(const DeferredResult &result) { if(!result.success) log_error("couldn't create/update session: condition=%d", result.value.toInt()); afterSessionCalls(); } }; class Subscription : public QObject { Q_OBJECT public: Subscription(const QString &channel) : channel_(channel), timer_(0) { } ~Subscription() { if(timer_) { timer_->stop(); timer_->disconnect(this); timer_->setParent(0); timer_->deleteLater(); } } const QString & channel() const { return channel_; } void start() { timer_ = new QTimer(this); connect(timer_, &QTimer::timeout, this, &Subscription::timer_timeout); timer_->setSingleShot(true); timer_->start(SUBSCRIBED_DELAY); } Signal subscribed; private: QString channel_; QTimer *timer_; private slots: void timer_timeout() { subscribed(); } }; class HandlerEngine::Private : public QObject { Q_OBJECT public: class PublishAction : public RateLimiter::Action { public: HandlerEngine::Private *ep; QPointer target; PublishItem item; QList exposeHeaders; PublishAction(HandlerEngine::Private *_ep, QObject *_target, const PublishItem &_item, const QList &_exposeHeaders = QList()) : ep(_ep), target(_target), item(_item), exposeHeaders(_exposeHeaders) { } virtual bool execute() { if(!target) return false; ep->publishSend(target, item, exposeHeaders); return true; } }; struct WSSessionConnections { Connection sendConnection; Connection expConnection; Connection errorConnection; }; HandlerEngine *q; Configuration config; ZhttpManager *zhttpIn; ZhttpManager *zhttpOut; ZrpcManager *inspectServer; ZrpcManager *acceptServer; ZrpcManager *stateClient; ZrpcManager *controlServer; ZrpcManager *proxyControlClient; QZmq::Socket *inPullSock; QZmq::Valve *inPullValve; QZmq::Socket *inSubSock; QZmq::Valve *inSubValve; QZmq::Socket *retrySock; QZmq::Socket *wsControlInitSock; QZmq::Valve *wsControlInitValve; QZmq::Socket *wsControlStreamSock; QZmq::Valve *wsControlStreamValve; QZmq::Socket *statsSock; QZmq::Socket *proxyStatsSock; QZmq::Valve *proxyStatsValve; SimpleHttpServer *controlHttpServer; StatsManager *stats; std::unique_ptr publishLimiter; std::unique_ptr updateLimiter; HttpSessionUpdateManager *httpSessionUpdateManager; Sequencer *sequencer; CommonState cs; QSet inspectWorkers; QSet acceptWorkers; QSet deferreds; Deferred *report; Connection inspectReqReadyConnection; Connection acceptReqReadyConnection; Connection controlReqReadyConnection; Connection controlServerConnection; Connection itemReadyConnection; map finishedConnection; map subscribedConnection; map retryPacketReadyConnection; map sessionsReadyConnection; Connection connectionsRefreshedConnection; Connection unsubscribedConnection; Connection reportedConnection; map wsSessionConnectionMap; Connection pullConnection; Connection controlInitValveConnection; Connection controlStreamValveConnection; Connection inSubValveConnection; Connection proxyStatConnection; Private(HandlerEngine *_q) : QObject(_q), q(_q), zhttpIn(0), zhttpOut(0), inspectServer(0), acceptServer(0), stateClient(0), controlServer(0), proxyControlClient(0), inPullSock(0), inPullValve(0), inSubSock(0), inSubValve(0), retrySock(0), wsControlInitSock(0), wsControlInitValve(0), wsControlStreamSock(0), wsControlStreamValve(0), statsSock(0), proxyStatsSock(0), proxyStatsValve(0), controlHttpServer(0), stats(0), report(0) { qRegisterMetaType(); publishLimiter = std::make_unique(); updateLimiter = std::make_unique(); httpSessionUpdateManager = new HttpSessionUpdateManager(this); sequencer = new Sequencer(&cs.publishLastIds, this); itemReadyConnection = sequencer->itemReady.connect(boost::bind(&Private::sequencer_itemReady, this, boost::placeholders::_1)); } ~Private() { qDeleteAll(inspectWorkers); qDeleteAll(acceptWorkers); qDeleteAll(deferreds); qDeleteAll(cs.wsSessions); qDeleteAll(cs.httpSessions); qDeleteAll(cs.subs); } bool start(const Configuration &_config) { config = _config; // up to 10 timers per connection RTimer::init(config.connectionsMax * 10); publishLimiter->setRate(config.messageRate); publishLimiter->setHwm(config.messageHwm); updateLimiter->setRate(10); updateLimiter->setBatchWaitEnabled(true); sequencer->setWaitMax(config.messageWait); sequencer->setIdCacheTtl(config.idCacheTtl); zhttpIn = new ZhttpManager(this); zhttpIn->setInstanceId(config.instanceId); zhttpIn->setServerInStreamSpecs(config.serverInStreamSpecs); zhttpIn->setServerOutSpecs(config.serverOutSpecs); zhttpOut = new ZhttpManager(this); zhttpOut->setInstanceId(config.instanceId); zhttpOut->setClientOutSpecs(config.clientOutSpecs); zhttpOut->setClientOutStreamSpecs(config.clientOutStreamSpecs); zhttpOut->setClientInSpecs(config.clientInSpecs); log_info("zhttp in stream: %s", qPrintable(config.serverInStreamSpecs.join(", "))); log_info("zhttp out: %s", qPrintable(config.serverOutSpecs.join(", "))); if(!config.inspectSpecs.isEmpty()) { inspectServer = new ZrpcManager(this); inspectServer->setBind(false); inspectServer->setIpcFileMode(config.ipcFileMode); inspectReqReadyConnection = inspectServer->requestReady.connect(boost::bind(&Private::inspectServer_requestReady, this)); if(!inspectServer->setServerSpecs(config.inspectSpecs)) { // zrpcmanager logs error return false; } log_info("inspect server: %s", qPrintable(config.inspectSpecs.join(", "))); } if(!config.acceptSpecs.isEmpty()) { acceptServer = new ZrpcManager(this); acceptServer->setBind(false); acceptServer->setIpcFileMode(config.ipcFileMode); acceptReqReadyConnection = acceptServer->requestReady.connect(boost::bind(&Private::acceptServer_requestReady, this)); if(!acceptServer->setServerSpecs(config.acceptSpecs)) { // zrpcmanager logs error return false; } log_info("accept server: %s", qPrintable(config.acceptSpecs.join(", "))); } if(!config.stateSpec.isEmpty()) { stateClient = new ZrpcManager(this); stateClient->setBind(true); stateClient->setIpcFileMode(config.ipcFileMode); stateClient->setTimeout(STATE_RPC_TIMEOUT); if(!stateClient->setClientSpecs(QStringList() << config.stateSpec)) { // zrpcmanager logs error return false; } log_info("state client: %s", qPrintable(config.stateSpec)); } if(!config.commandSpec.isEmpty()) { controlServer = new ZrpcManager(this); controlServer->setBind(true); controlServer->setIpcFileMode(config.ipcFileMode); controlReqReadyConnection = controlServer->requestReady.connect(boost::bind(&Private::controlServer_requestReady, this)); if(!controlServer->setServerSpecs(QStringList() << config.commandSpec)) { // zrpcmanager logs error return false; } log_info("control server: %s", qPrintable(config.commandSpec)); } if(!config.pushInSpec.isEmpty()) { inPullSock = new QZmq::Socket(QZmq::Socket::Pull, this); inPullSock->setHwm(DEFAULT_HWM); QString errorMessage; if(!ZUtil::setupSocket(inPullSock, config.pushInSpec, true, config.ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } inPullValve = new QZmq::Valve(inPullSock, this); pullConnection = inPullValve->readyRead.connect(boost::bind(&Private::inPull_readyRead, this, boost::placeholders::_1)); log_info("in pull: %s", qPrintable(config.pushInSpec)); } if(!config.pushInSubSpecs.isEmpty()) { inSubSock = new QZmq::Socket(QZmq::Socket::Sub, this); inSubSock->setSendHwm(SUB_SNDHWM); inSubSock->setShutdownWaitTime(0); QString errorMessage; if(!ZUtil::setupSocket(inSubSock, config.pushInSubSpecs, !config.pushInSubConnect, config.ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } if(config.pushInSubConnect) { // some sane TCP keep-alive settings // idle=30, cnt=6, intvl=5 inSubSock->setTcpKeepAliveEnabled(true); inSubSock->setTcpKeepAliveParameters(30, 6, 5); } inSubValve = new QZmq::Valve(inSubSock, this); inSubValveConnection = inSubValve->readyRead.connect(boost::bind(&Private::inSub_readyRead, this, boost::placeholders::_1)); log_info("in sub: %s", qPrintable(config.pushInSubSpecs.join(", "))); } if(!config.retryOutSpecs.isEmpty()) { retrySock = new QZmq::Socket(QZmq::Socket::Router, this); retrySock->setImmediateEnabled(true); retrySock->setHwm(DEFAULT_HWM); retrySock->setShutdownWaitTime(RETRY_WAIT_TIME); retrySock->setRouterMandatoryEnabled(true); foreach(const QString &spec, config.retryOutSpecs) { QString errorMessage; if(!ZUtil::setupSocket(retrySock, spec, false, config.ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } } log_info("retry: %s", qPrintable(config.retryOutSpecs.join(", "))); } if(!config.wsControlInitSpecs.isEmpty() && !config.wsControlStreamSpecs.isEmpty()) { wsControlInitSock = new QZmq::Socket(QZmq::Socket::Pull, this); wsControlInitSock->setHwm(DEFAULT_HWM); foreach(const QString &spec, config.wsControlInitSpecs) { QString errorMessage; if(!ZUtil::setupSocket(wsControlInitSock, spec, false, config.ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } } wsControlInitValve = new QZmq::Valve(wsControlInitSock, this); controlInitValveConnection = wsControlInitValve->readyRead.connect(boost::bind(&Private::wsControlInit_readyRead, this, boost::placeholders::_1)); log_info("ws control init: %s", qPrintable(config.wsControlInitSpecs.join(", "))); wsControlStreamSock = new QZmq::Socket(QZmq::Socket::Router, this); wsControlStreamSock->setIdentity(config.instanceId); wsControlStreamSock->setImmediateEnabled(true); wsControlStreamSock->setHwm(DEFAULT_HWM); wsControlStreamSock->setShutdownWaitTime(WSCONTROL_WAIT_TIME); foreach(const QString &spec, config.wsControlStreamSpecs) { QString errorMessage; if(!ZUtil::setupSocket(wsControlStreamSock, spec, false, config.ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } } wsControlStreamValve = new QZmq::Valve(wsControlStreamSock, this); controlStreamValveConnection = wsControlStreamValve->readyRead.connect(boost::bind(&Private::wsControlStream_readyRead, this, boost::placeholders::_1)); log_info("ws control stream: %s", qPrintable(config.wsControlStreamSpecs.join(", "))); } stats = new StatsManager(config.connectionsMax, config.connectionsMax * config.connectionSubscriptionMax, this); connectionsRefreshedConnection = stats->connectionsRefreshed.connect(boost::bind(&Private::stats_connectionsRefreshed, this, boost::placeholders::_1)); unsubscribedConnection = stats->unsubscribed.connect(boost::bind(&Private::stats_unsubscribed, this, boost::placeholders::_1, boost::placeholders::_2)); reportedConnection = stats->reported.connect(boost::bind(&Private::stats_reported, this, boost::placeholders::_1)); stats->setConnectionSendEnabled(config.statsConnectionSend); stats->setConnectionTtl(config.statsConnectionTtl); stats->setSubscriptionTtl(config.statsSubscriptionTtl); stats->setSubscriptionLinger(config.subscriptionLinger); stats->setReportInterval(config.statsReportInterval); if(config.statsFormat == "json") { stats->setOutputFormat(StatsManager::JsonFormat); } else { stats->setOutputFormat(StatsManager::TnetStringFormat); } if(!config.statsSpec.isEmpty()) { stats->setInstanceId(config.instanceId); stats->setIpcFileMode(config.ipcFileMode); if(!stats->setSpec(config.statsSpec)) { // statsmanager logs error return false; } log_info("stats: %s", qPrintable(config.statsSpec)); } if(!config.prometheusPort.isEmpty()) { stats->setPrometheusPrefix(config.prometheusPrefix); if(!stats->setPrometheusPort(config.prometheusPort)) { log_error("unable to bind to prometheus port: %s", qPrintable(config.prometheusPort)); return false; } } if(!config.proxyStatsSpecs.isEmpty()) { proxyStatsSock = new QZmq::Socket(QZmq::Socket::Sub, this); proxyStatsSock->setHwm(DEFAULT_HWM); proxyStatsSock->setShutdownWaitTime(0); proxyStatsSock->subscribe(""); foreach(const QString &spec, config.proxyStatsSpecs) { QString errorMessage; if(!ZUtil::setupSocket(proxyStatsSock, spec, false, config.ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } } proxyStatsValve = new QZmq::Valve(proxyStatsSock, this); proxyStatConnection = proxyStatsValve->readyRead.connect(boost::bind(&Private::proxyStats_readyRead, this, boost::placeholders::_1)); log_info("proxy stats: %s", qPrintable(config.proxyStatsSpecs.join(", "))); } if(!config.proxyCommandSpec.isEmpty()) { proxyControlClient = new ZrpcManager(this); proxyControlClient->setIpcFileMode(config.ipcFileMode); proxyControlClient->setTimeout(PROXY_RPC_TIMEOUT); if(!proxyControlClient->setClientSpecs(QStringList() << config.proxyCommandSpec)) { // zrpcmanager logs error return false; } log_info("proxy control client: %s", qPrintable(config.proxyCommandSpec)); } if(config.pushInHttpPort != -1) { controlHttpServer = new SimpleHttpServer(config.pushInHttpMaxHeadersSize, config.pushInHttpMaxBodySize, this); controlServerConnection = controlHttpServer->requestReady.connect(boost::bind(&Private::controlHttpServer_requestReady, this)); controlHttpServer->listen(config.pushInHttpAddr, config.pushInHttpPort); log_info("http control server: %s:%d", qPrintable(config.pushInHttpAddr.toString()), config.pushInHttpPort); } if(inPullValve) inPullValve->open(); if(inSubValve) inSubValve->open(); if(wsControlInitValve) wsControlInitValve->open(); if(wsControlStreamValve) wsControlStreamValve->open(); if(proxyStatsValve) proxyStatsValve->open(); return true; } void reload() { // nothing to do } private: void handlePublishItem(const PublishItem &item) { // only sequence if someone is listening, because we // clear lastId on unsubscribe and don't want it to // be set again until after a subscription returns bool seq = (!item.noSeq && cs.subs.contains(item.channel)); sequencer->addItem(item, seq); } void writeRetryPacket(const QByteArray &instanceAddress, const RetryRequestPacket &packet) { if(!retrySock) { log_error("retry: can't write, no socket"); return; } QVariant vout = packet.toVariant(); if(log_outputLevel() >= LOG_LEVEL_DEBUG) log_debug("OUT retry: to=%s %s", instanceAddress.data(), qPrintable(TnetString::variantToString(vout, -1))); QList msg; msg += instanceAddress; msg += QByteArray(); msg += TnetString::fromVariant(vout); retrySock->write(msg); } void writeWsControlItems(const QByteArray &instanceAddress, const QList &items) { if(!wsControlStreamSock) { log_error("wscontrol: can't write, no socket"); return; } WsControlPacket out; out.from = config.instanceId; out.items = items; QVariant vout = out.toVariant(); if(log_outputLevel() >= LOG_LEVEL_DEBUG) log_debug("OUT wscontrol: to=%s %s", instanceAddress.data(), qPrintable(TnetString::variantToString(vout, -1))); QList msg; msg += instanceAddress; msg += QByteArray(); msg += TnetString::fromVariant(vout); wsControlStreamSock->write(msg); } void addSub(const QString &channel) { if(!cs.subs.contains(channel)) { Subscription *sub = new Subscription(channel); subscribedConnection[sub] = sub->subscribed.connect(boost::bind(&Private::sub_subscribed, this, sub)); cs.subs.insert(channel, sub); sub->start(); if(inSubSock) { log_debug("SUB socket subscribe: %s", qPrintable(channel)); inSubSock->subscribe(channel.toUtf8()); } } } void removeSub(const QString &channel) { if(cs.subs.contains(channel)) { Subscription *sub = cs.subs[channel]; cs.subs.remove(channel); subscribedConnection.erase(sub); delete sub; sequencer->clearPendingForChannel(channel); cs.publishLastIds.remove(channel); if(inSubSock) { log_debug("SUB socket unsubscribe: %s", qPrintable(channel)); inSubSock->unsubscribe(channel.toUtf8()); } } } void removeWsSession(WsSession *s) { removeSessionChannels(s); log_debug("removed ws session: %s", qPrintable(s->cid)); cs.wsSessions.remove(s->cid); wsSessionConnectionMap.erase(s); delete s; } void httpControlRespond(SimpleHttpRequest *req, int code, const QByteArray &reason, const QString &body, const QByteArray &contentType = QByteArray(), const HttpHeaders &headers = HttpHeaders(), int items = -1) { HttpHeaders outHeaders = headers; if(!contentType.isEmpty()) outHeaders += HttpHeader("Content-Type", contentType); else outHeaders += HttpHeader("Content-Type", "text/plain"); req->respond(code, reason, outHeaders, body.toUtf8()); req->finished.connect(boost::bind(&SimpleHttpRequest::deleteLater, req)); QString msg = QString("control: %1 %2 code=%3 %4").arg(req->requestMethod(), QString::fromUtf8(req->requestUri()), QString::number(code), QString::number(body.size())); if(items > -1) msg += QString(" items=%1").arg(items); log_info("%s", qPrintable(msg)); } void publishSend(QObject *target, const PublishItem &item, const QList &exposeHeaders) { const PublishFormat &f = item.format; if(f.type == PublishFormat::HttpResponse || f.type == PublishFormat::HttpStream) { HttpSession *hs = qobject_cast(target); hs->publish(item, exposeHeaders); } else if(f.type == PublishFormat::WebSocketMessage) { WsSession *s = qobject_cast(target); if(f.haveContentFilters) { // ensure content filters match QStringList contentFilters; foreach(const QString &f, s->channelFilters[item.channel]) { if(Filter::targets(f) & Filter::MessageContent) contentFilters += f; } if(contentFilters != f.contentFilters) { QString errorMessage = QString("content filter mismatch: subscription=%1 message=%2").arg(contentFilters.join(","), f.contentFilters.join(",")); log_debug("%s", qPrintable(errorMessage)); return; } } Filter::Context fc; fc.subscriptionMeta = s->meta; fc.publishMeta = item.meta; FilterStack filters(fc, s->channelFilters[item.channel]); if(filters.sendAction() == Filter::Drop) return; // TODO: hint support for websockets? if(f.action != PublishFormat::Send && f.action != PublishFormat::Close && f.action != PublishFormat::Refresh) return; WsControlPacket::Item i; i.cid = s->cid.toUtf8(); if(f.action == PublishFormat::Send) { QByteArray body = filters.process(f.body); if(body.isNull()) { log_debug("filter error: %s", qPrintable(filters.errorMessage())); return; } i.type = WsControlPacket::Item::Send; switch(f.messageType) { case PublishFormat::Text: i.contentType = "text"; break; case PublishFormat::Binary: i.contentType = "binary"; break; case PublishFormat::Ping: i.contentType = "ping"; break; case PublishFormat::Pong: i.contentType = "pong"; break; default: return; // unrecognized type, skip } i.message = body; } else if(f.action == PublishFormat::Close) { i.type = WsControlPacket::Item::Close; i.code = f.code; i.reason = f.reason; } else if(f.action == PublishFormat::Refresh) { i.type = WsControlPacket::Item::Refresh; } writeWsControlItems(s->peer, QList() << i); } } int blocksForData(int size) const { if(config.messageBlockSize <= 0) return -1; return (size + config.messageBlockSize - 1) / config.messageBlockSize; } void updateSessions(const QString &channel = QString()) { if(!channel.isNull()) { QSet sessions = cs.responseSessionsByChannel.value(channel); foreach(HttpSession *hs, sessions) hs->update(); sessions = cs.streamSessionsByChannel.value(channel); foreach(HttpSession *hs, sessions) hs->update(); } else { foreach(HttpSession *hs, cs.httpSessions) hs->update(); } } void recoverCommand() { cs.publishLastIds.clear(); updateSessions(); } void removeSessionChannel(HttpSession *hs, const QString &channel) { Instruct::HoldMode mode = hs->holdMode(); assert(mode == Instruct::ResponseHold || mode == Instruct::StreamHold); QHash > *sessionsByChannel; QString modeStr; if(mode == Instruct::ResponseHold) { sessionsByChannel = &cs.responseSessionsByChannel; modeStr = "response"; } else // StreamHold { sessionsByChannel = &cs.streamSessionsByChannel; modeStr = "stream"; } if(!sessionsByChannel->contains(channel)) return; QSet &cur = (*sessionsByChannel)[channel]; if(!cur.contains(hs)) return; cur.remove(hs); if(!cur.isEmpty()) { stats->addSubscription(modeStr, channel, cur.count()); } else { sessionsByChannel->remove(channel); // linger the unsub in case client long-polls again bool linger = (mode == Instruct::ResponseHold); stats->removeSubscription(modeStr, channel, linger); } } void removeSessionChannel(WsSession *s, const QString &channel) { if(!cs.wsSessionsByChannel.contains(channel)) return; QSet &cur = cs.wsSessionsByChannel[channel]; if(!cur.contains(s)) return; cur.remove(s); if(!cur.isEmpty()) { stats->addSubscription("ws", channel, cur.count()); } else { cs.wsSessionsByChannel.remove(channel); stats->removeSubscription("ws", channel, false); } } void removeSessionChannels(WsSession *s) { foreach(const QString &channel, s->channels) removeSessionChannel(s, channel); } static void hs_subscribe_cb(void *data, std::tuple value) { Private *self = (Private *)data; self->hs_subscribe(std::get<0>(value), std::get<1>(value)); } static void hs_unsubscribe_cb(void *data, std::tuple value) { Private *self = (Private *)data; self->hs_unsubscribe(std::get<0>(value), std::get<1>(value)); } static void hs_finished_cb(void *data, std::tuple value) { Private *self = (Private *)data; self->hs_finished(std::get<0>(value)); } void inspectServer_requestReady() { if(inspectWorkers.count() >= INSPECT_WORKERS_MAX) return; ZrpcRequest *req = inspectServer->takeNext(); if(!req) return; InspectWorker *w = new InspectWorker(req, stateClient, config.shareAll, this); finishedConnection[w] = w->finished.connect(boost::bind(&Private::inspectWorker_finished, this, boost::placeholders::_1, w)); inspectWorkers += w; } void acceptServer_requestReady() { if(acceptWorkers.count() >= ACCEPT_WORKERS_MAX) return; ZrpcRequest *req = acceptServer->takeNext(); if(!req) return; if(req->method() == "accept") { // NOTE: to ensure sequential processing of conn-max packets, // we need to process any such packets contained within the // accept request immediately before returning to the event loop. // the start() call will do this AcceptWorker *w = new AcceptWorker(req, stateClient, &cs, zhttpIn, zhttpOut, stats, updateLimiter.get(), httpSessionUpdateManager, config.connectionSubscriptionMax, this); finishedConnection[w] = w->finished.connect(boost::bind(&Private::acceptWorker_finished, this, boost::placeholders::_1, w)); sessionsReadyConnection[w] = w->sessionsReady.connect(boost::bind(&Private::acceptWorker_sessionsReady, this, w)); retryPacketReadyConnection[w] = w->retryPacketReady.connect(boost::bind(&Private::acceptWorker_retryPacketReady, this, boost::placeholders::_1, boost::placeholders::_2)); acceptWorkers += w; w->start(); } else if(req->method() == "conn-max") { QVariantHash args = req->args(); if(args.contains("conn-max")) { if(typeId(args["conn-max"]) == QMetaType::QVariantList) { QVariantList packets = args["conn-max"].toList(); foreach(const QVariant &data, packets) { StatsPacket p; if(!p.fromVariant("conn-max", data) || p.type != StatsPacket::ConnectionsMax) continue; stats->processExternalPacket(p, false); } } } delete req; } else { req->respondError("method-not-found"); delete req; } } void controlServer_requestReady() { ZrpcRequest *req = controlServer->takeNext(); if(!req) return; if(log_outputLevel() >= LOG_LEVEL_DEBUG) log_debug("IN command: %s args=%s", qPrintable(req->method()), qPrintable(TnetString::variantToString(req->args(), -1))); if(req->method() == "conncheck") { ConnCheckWorker *w = new ConnCheckWorker(req, proxyControlClient, stats, this); finishedConnection[w] = w->finished.connect(boost::bind(&Private::deferred_finished, this, boost::placeholders::_1, w)); deferreds += w; } else if(req->method() == "get-zmq-uris") { QVariantHash out; if(!config.commandSpec.isEmpty()) out["command"] = config.commandSpec.toUtf8(); if(!config.pushInSpec.isEmpty()) out["publish-pull"] = config.pushInSpec.toUtf8(); if(!config.pushInSubSpecs.isEmpty() && !config.pushInSubConnect) out["publish-sub"] = config.pushInSubSpecs[0].toUtf8(); req->respond(out); delete req; } else if(req->method() == "recover") { recoverCommand(); req->respond(); delete req; } else if(req->method() == "refresh") { RefreshWorker *w = new RefreshWorker(req, proxyControlClient, &cs.wsSessionsByChannel, this); finishedConnection[w] = w->finished.connect(boost::bind(&Private::deferred_finished, this, boost::placeholders::_1, w)); deferreds += w; } else if(req->method() == "publish") { QVariantHash args = req->args(); if(!args.contains("items")) { req->respondError("bad-request", "Invalid format: object does not contain 'items'"); delete req; return; } if(typeId(args["items"]) != QMetaType::QVariantList) { req->respondError("bad-request", "Invalid format: object contains 'items' with wrong type"); delete req; return; } QVariantList vitems = args["items"].toList(); bool ok; QString errorMessage; QList items = parseItems(vitems, &ok, &errorMessage); if(!ok) { req->respondError("bad-request", QString("Invalid format: %1").arg(errorMessage)); delete req; return; } req->respond(); delete req; foreach(const PublishItem &item, items) handlePublishItem(item); } else { req->respondError("method-not-found"); delete req; } } void sequencer_itemReady(const PublishItem &item) { QList responseSessions; QList streamSessions; QList wsSessions; QSet sids; int largestBlocks = -1; if(item.size >= 0) largestBlocks = blocksForData(item.size); if(item.formats.contains(PublishFormat::HttpResponse)) { if(item.size < 0) largestBlocks = qMax(blocksForData(item.formats[PublishFormat::HttpResponse].body.size()), largestBlocks); QSet sessions = cs.responseSessionsByChannel.value(item.channel); foreach(HttpSession *hs, sessions) { assert(hs->holdMode() == Instruct::ResponseHold); assert(hs->channels().contains(item.channel)); responseSessions += hs; if(!hs->sid().isEmpty()) sids += hs->sid(); } } if(item.formats.contains(PublishFormat::HttpStream)) { if(item.size < 0) largestBlocks = qMax(blocksForData(item.formats[PublishFormat::HttpStream].body.size()), largestBlocks); QSet sessions = cs.streamSessionsByChannel.value(item.channel); foreach(HttpSession *hs, sessions) { // note: we used to assert that the session was currently a // stream hold and subscribed to the target channel, // however with the new grip-link stuff it is possible for // the session to temporarily switch to NoHold, and for // channels to become unsubscribed. so we'll do a // conditional statement instead if(!hs->channels().contains(item.channel)) continue; streamSessions += hs; if(!hs->sid().isEmpty()) sids += hs->sid(); } } if(item.formats.contains(PublishFormat::WebSocketMessage)) { if(item.size < 0) largestBlocks = qMax(blocksForData(item.formats[PublishFormat::WebSocketMessage].body.size()), largestBlocks); QSet wsbc = cs.wsSessionsByChannel.value(item.channel); foreach(WsSession *s, wsbc) { assert(s->channels.contains(item.channel)); wsSessions += s; if(!s->sid.isEmpty()) sids += s->sid; } } // always add for non-identified route stats->addMessageReceived(QByteArray(), largestBlocks); if(!responseSessions.isEmpty()) { PublishItem i = item; i.format = item.formats.value(PublishFormat::HttpResponse); i.formats.clear(); PublishFormat &f = i.format; QList exposeHeaders = f.headers.getAll("Grip-Expose-Headers"); // remove grip headers from the push for(int n = 0; n < f.headers.count(); ++n) { // strip out grip headers if(qstrnicmp(f.headers[n].first.data(), "Grip-", 5) == 0) { f.headers.removeAt(n); --n; // adjust position } } log_debug("relaying to %d http-response subscribers", responseSessions.count()); // FIXME: if bodyPatch is used then body is empty. we should // really be calculating blocks after applying patch int blocks; if(item.size >= 0) blocks = blocksForData(item.size); else blocks = blocksForData(f.body.size()); foreach(HttpSession *hs, responseSessions) { QString statsRoute = hs->statsRoute(); if(!publishLimiter->addAction(statsRoute, new PublishAction(this, hs, i, exposeHeaders), blocks != -1 ? blocks : 1)) { if(!statsRoute.isEmpty()) log_warning("exceeded publish hwm (%d) for route %s, dropping message", config.messageHwm, qPrintable(statsRoute)); else log_warning("exceeded publish hwm (%d), dropping message", config.messageHwm); } stats->addMessageSent(statsRoute.toUtf8(), "http-response", blocks); } stats->addMessage(i.channel, i.id, "http-response", responseSessions.count(), blocks != -1 ? blocks * responseSessions.count() : -1); } if(!streamSessions.isEmpty()) { PublishItem i = item; i.format = item.formats.value(PublishFormat::HttpStream); i.formats.clear(); PublishFormat &f = i.format; log_debug("relaying to %d http-stream subscribers", streamSessions.count()); int blocks; if(item.size >= 0) blocks = blocksForData(item.size); else blocks = blocksForData(f.body.size()); foreach(HttpSession *hs, streamSessions) { QString statsRoute = hs->statsRoute(); if(!publishLimiter->addAction(statsRoute, new PublishAction(this, hs, i), blocks != -1 ? blocks : 1)) { if(!statsRoute.isEmpty()) log_warning("exceeded publish hwm (%d) for route %s, dropping message", config.messageHwm, qPrintable(statsRoute)); else log_warning("exceeded publish hwm (%d), dropping message", config.messageHwm); } stats->addMessageSent(statsRoute.toUtf8(), "http-stream", blocks); } stats->addMessage(i.channel, i.id, "http-stream", streamSessions.count(), blocks != -1 ? blocks * streamSessions.count() : -1); } if(!wsSessions.isEmpty()) { PublishItem i = item; i.format = item.formats.value(PublishFormat::WebSocketMessage); i.formats.clear(); PublishFormat &f = i.format; log_debug("relaying to %d ws-message subscribers", wsSessions.count()); int blocks; if(item.size >= 0) blocks = blocksForData(item.size); else blocks = blocksForData(f.body.size()); foreach(WsSession *s, wsSessions) { QString statsRoute = s->statsRoute; if(!publishLimiter->addAction(statsRoute, new PublishAction(this, s, i), blocks != -1 ? blocks : 1)) { if(!statsRoute.isEmpty()) log_warning("exceeded publish hwm (%d) for route %s, dropping message", config.messageHwm, qPrintable(statsRoute)); else log_warning("exceeded publish hwm (%d), dropping message", config.messageHwm); } stats->addMessageSent(statsRoute.toUtf8(), "ws-message", blocks); } stats->addMessage(i.channel, i.id, "ws-message", wsSessions.count(), blocks != -1 ? blocks * wsSessions.count() : -1); } int receivers = responseSessions.count() + streamSessions.count() + wsSessions.count(); log_info("publish channel=%s receivers=%d", qPrintable(item.channel), receivers); if(!item.id.isNull() && !sids.isEmpty() && stateClient) { // update sessions' last-id QHash sidLastIds; foreach(const QString &sid, sids) { LastIds lastIds; lastIds[item.channel] = item.id; sidLastIds[sid] = lastIds; } Deferred *d = SessionRequest::updateMany(stateClient, sidLastIds, this); finishedConnection[d] = d->finished.connect(boost::bind(&Private::sessionUpdateMany_finished, this, boost::placeholders::_1, d)); deferreds += d; } } private: void report_finished(const DeferredResult &result) { Q_UNUSED(result); finishedConnection.erase(report); deferreds.remove(report); report = 0; } void sessionUpdateMany_finished(const DeferredResult &result, Deferred *d) { finishedConnection.erase(d); deferreds.remove(d); if(!result.success) log_error("couldn't update session: condition=%d", result.value.toInt()); } void sessionCreateOrUpdate_finished(const DeferredResult &result, Deferred *d) { finishedConnection.erase(d); deferreds.remove(d); if(!result.success) log_error("couldn't create/update session: condition=%d", result.value.toInt()); } void inspectWorker_finished(const DeferredResult &result, InspectWorker *w) { Q_UNUSED(result); finishedConnection.erase(w); inspectWorkers.remove(w); // try to read again inspectServer_requestReady(); } void acceptWorker_finished(const DeferredResult &result, AcceptWorker *w ) { Q_UNUSED(result); finishedConnection.erase(w); sessionsReadyConnection.erase(w); retryPacketReadyConnection.erase(w); acceptWorkers.remove(w); // try to read again acceptServer_requestReady(); } void deferred_finished(const DeferredResult &result, Deferred *w) { Q_UNUSED(result); finishedConnection.erase(w); deferreds.remove(w); } void sub_subscribed(Subscription *sub) { updateSessions(sub->channel()); } void acceptWorker_sessionsReady(AcceptWorker *w) { QList sessions = w->takeSessions(); foreach(HttpSession *hs, sessions) { // NOTE: for performance reasons we do not call hs->setParent and // instead leave the object unparented hs->subscribeCallback().add(Private::hs_subscribe_cb, this); hs->unsubscribeCallback().add(Private::hs_unsubscribe_cb, this); hs->finishedCallback().add(Private::hs_finished_cb, this); cs.httpSessions.insert(hs->rid(), hs); hs->start(); } } void acceptWorker_retryPacketReady(const QByteArray &instanceAddress, const RetryRequestPacket &packet) { writeRetryPacket(instanceAddress, packet); } void stats_connectionsRefreshed(const QList &ids) { if(stateClient) { // find sids of the connections QHash sidLastIds; foreach(const QByteArray &id, ids) { int at = id.indexOf(':'); assert(at != -1); ZhttpRequest::Rid rid(id.mid(0, at), id.mid(at + 1)); HttpSession *hs = cs.httpSessions.value(rid); if(hs && !hs->sid().isEmpty()) sidLastIds[hs->sid()] = LastIds(); } if(!sidLastIds.isEmpty()) { Deferred *d = SessionRequest::updateMany(stateClient, sidLastIds, this); finishedConnection[d] = d->finished.connect(boost::bind(&Private::sessionUpdateMany_finished, this, boost::placeholders::_1, d)); deferreds += d; } } } void stats_unsubscribed(const QString &mode, const QString &channel) { // NOTE: this callback may be invoked while looping over certain structures, // so be careful what you touch Q_UNUSED(mode); if(!cs.responseSessionsByChannel.contains(channel) && !cs.streamSessionsByChannel.contains(channel) && !cs.wsSessionsByChannel.contains(channel)) removeSub(channel); } void stats_reported(const QList &packets) { // only one outstanding report at a time if(report) return; // consolidate data StatsPacket all; all.type = StatsPacket::Report; all.connectionsMax = 0; all.connectionsMinutes = 0; all.messagesReceived = 0; all.messagesSent = 0; all.httpResponseMessagesSent = 0; foreach(const StatsPacket &p, packets) { all.connectionsMax += qMax(p.connectionsMax, 0); all.connectionsMinutes += qMax(p.connectionsMinutes, 0); all.messagesReceived += qMax(p.messagesReceived, 0); all.messagesSent += qMax(p.messagesSent, 0); all.httpResponseMessagesSent += qMax(p.httpResponseMessagesSent, 0); } report = ControlRequest::report(proxyControlClient, all, this); finishedConnection[report] = report->finished.connect(boost::bind(&Private::report_finished, this, boost::placeholders::_1)); deferreds += report; } QVariant parseJsonOrTnetstring(const QByteArray &message, bool *ok = 0, QString *errorMessage = 0) { QVariant data; bool ok_; if(message.length() > 0 && message[0] == 'J') { QJsonParseError e; QJsonDocument doc = QJsonDocument::fromJson(message.mid(1), &e); if(e.error != QJsonParseError::NoError) { if(errorMessage) *errorMessage = QString("received message with invalid format (json parse failed)"); if(ok) *ok = false; return data; } if(doc.isObject()) { data = doc.object().toVariantMap(); } else { if(errorMessage) *errorMessage = QString("received message with invalid format (not a valid json object)"); if(ok) *ok = false; return data; } } else { int offset = 0; if(message.length() > 0 && message[0] == 'T') { offset = 1; } data = TnetString::toVariant(message, offset, &ok_); if(!ok_) { if(errorMessage) *errorMessage = QString("received message with invalid format (tnetstring parse failed)"); if(ok) *ok = false; return data; } } if(ok) *ok = true; return data; } void inPull_readyRead(const QList &message) { if(message.count() != 1) { log_warning("IN pull: received message with parts != 1, skipping"); return; } bool ok; QString errorMessage; QVariant data = parseJsonOrTnetstring(message[0], &ok, &errorMessage); if(!ok) { log_warning("IN pull: %s, skipping", qPrintable(errorMessage)); return; } if(log_outputLevel() >= LOG_LEVEL_DEBUG) log_debug("IN pull: %s", qPrintable(TnetString::variantToString(data, -1))); PublishItem item = PublishItem::fromVariant(data, QString(), &ok, &errorMessage); if(!ok) { log_warning("IN pull: received message with invalid format: %s, skipping", qPrintable(errorMessage)); return; } handlePublishItem(item); } void inSub_readyRead(const QList &message) { if(message.count() != 2) { log_warning("IN sub: received message with parts != 2, skipping"); return; } bool ok; QString errorMessage; QVariant data = parseJsonOrTnetstring(message[1], &ok, &errorMessage); if(!ok) { log_warning("IN sub: %s, skipping", qPrintable(errorMessage)); return; } QString channel = QString::fromUtf8(message[0]); if(log_outputLevel() >= LOG_LEVEL_DEBUG) log_debug("IN sub: channel=%s %s", qPrintable(channel), qPrintable(TnetString::variantToString(data, -1))); PublishItem item = PublishItem::fromVariant(data, channel, &ok, &errorMessage); if(!ok) { log_warning("IN sub: received message with invalid format: %s, skipping", qPrintable(errorMessage)); return; } handlePublishItem(item); } void wsControlInit_readyRead(const QList &message) { if(message.count() != 1) { log_warning("IN wscontrol: received message with parts != 1, skipping"); return; } wsControlIn_readyRead(message[0]); } void wsControlStream_readyRead(const QList &message) { QZmq::ReqMessage req(message); if(req.content().count() != 1) { log_warning("IN wscontrol: received message with parts != 1, skipping"); return; } wsControlIn_readyRead(req.content()[0]); } void wsControlIn_readyRead(const QByteArray &message) { bool ok; QVariant data = TnetString::toVariant(message, 0, &ok); if(!ok) { log_warning("IN wscontrol: received message with invalid format (tnetstring parse failed), skipping"); return; } if(log_outputLevel() >= LOG_LEVEL_DEBUG) log_debug("IN wscontrol: %s", qPrintable(TnetString::variantToString(data, -1))); WsControlPacket packet; if(!packet.fromVariant(data)) { log_warning("IN wscontrol: received message with invalid format, skipping"); return; } QStringList createOrUpdateSids; QHash updateSids; QList outItems; foreach(const WsControlPacket::Item &item, packet.items) { if(item.type != WsControlPacket::Item::Ack && !item.requestId.isEmpty()) { // ack receipt WsControlPacket::Item i; i.cid = item.cid; i.type = WsControlPacket::Item::Ack; i.requestId = item.requestId; outItems += i; } if(item.type == WsControlPacket::Item::Here) { WsSession *s = cs.wsSessions.value(item.cid); if(!s) { s = new WsSession(this); wsSessionConnectionMap[s] = { s->send.connect(boost::bind(&Private::wssession_send, this, boost::placeholders::_1, boost::placeholders::_2, boost::placeholders::_3, s)), s->expired.connect(boost::bind(&Private::wssession_expired, this, s)), s->error.connect(boost::bind(&Private::wssession_error, this, s)) }; s->peer = packet.from; s->cid = QString::fromUtf8(item.cid); s->ttl = item.ttl; s->requestData.uri = item.uri; s->refreshExpiration(); cs.wsSessions.insert(s->cid, s); log_debug("added ws session: %s", qPrintable(s->cid)); } s->route = item.route; s->statsRoute = item.separateStats ? item.route : QString(); s->channelPrefix = QString::fromUtf8(item.channelPrefix); if(!s->sid.isEmpty()) updateSids[s->sid] = LastIds(); continue; } // any other type must be for a known cid WsSession *s = cs.wsSessions.value(QString::fromUtf8(item.cid)); if(!s) { // send cancel, causing the proxy to close the connection. client // will need to retry to repair WsControlPacket::Item i; i.cid = item.cid; i.type = WsControlPacket::Item::Cancel; outItems += i; continue; } if(item.type == WsControlPacket::Item::KeepAlive) { s->ttl = item.ttl; s->refreshExpiration(); } else if(item.type == WsControlPacket::Item::Gone || item.type == WsControlPacket::Item::Cancel) { removeWsSession(s); } else if(item.type == WsControlPacket::Item::Grip) { QJsonParseError e; QJsonDocument doc = QJsonDocument::fromJson(item.message, &e); if(e.error != QJsonParseError::NoError || (!doc.isObject() && !doc.isArray())) { log_debug("grip control message is not valid json"); return; } if(doc.isObject()) data = doc.object().toVariantMap(); else // isArray data = doc.array().toVariantList(); QString errorMessage; WsControlMessage cm = WsControlMessage::fromVariant(data, &ok, &errorMessage); if(!ok) { log_debug("failed to parse grip control message: %s", qPrintable(errorMessage)); return; } if(cm.type == WsControlMessage::Subscribe) { if(s->channels.count() < config.connectionSubscriptionMax) { QString channel = s->channelPrefix + cm.channel; s->channels += channel; s->channelFilters[channel] = cm.filters; if(!cs.wsSessionsByChannel.contains(channel)) cs.wsSessionsByChannel.insert(channel, QSet()); cs.wsSessionsByChannel[channel] += s; log_debug("ws session %s subscribed to %s", qPrintable(s->cid), qPrintable(channel)); stats->addSubscription("ws", channel, cs.wsSessionsByChannel.value(channel).count()); addSub(channel); log_info("subscribe %s channel=%s", qPrintable(s->requestData.uri.toString(QUrl::FullyEncoded)), qPrintable(channel)); } else { log_warning("ws session %s: too many subscriptions", qPrintable(s->cid)); } } else if(cm.type == WsControlMessage::Unsubscribe) { QString channel = s->channelPrefix + cm.channel; if(!s->implicitChannels.contains(channel)) { s->channels.remove(channel); s->channelFilters.remove(channel); removeSessionChannel(s, channel); } } else if(cm.type == WsControlMessage::Detach) { WsControlPacket::Item i; i.cid = item.cid; i.type = WsControlPacket::Item::Detach; outItems += i; } else if(cm.type == WsControlMessage::Session) { if(!cm.sessionId.isEmpty()) { s->sid = cm.sessionId; createOrUpdateSids += cm.sessionId; } else { s->sid.clear(); } } else if(cm.type == WsControlMessage::SetMeta) { if(!cm.metaValue.isNull()) s->meta[cm.metaName] = cm.metaValue; else s->meta.remove(cm.metaName); } else if(cm.type == WsControlMessage::KeepAlive) { WsControlPacket::Item i; i.cid = item.cid; i.type = WsControlPacket::Item::KeepAliveSetup; if(!cm.content.isNull()) { QByteArray contentType; switch(cm.messageType) { case WsControlMessage::Text: contentType = "text"; break; case WsControlMessage::Binary: contentType = "binary"; break; case WsControlMessage::Ping: contentType = "ping"; break; case WsControlMessage::Pong: contentType = "pong"; break; default: continue; // unrecognized type, ignore } s->keepAliveType = contentType; s->keepAliveMessage = cm.content; if(cm.keepAliveMode == "interval") i.keepAliveMode = "interval"; else i.keepAliveMode = "idle"; i.timeout = (cm.timeout > 0 ? cm.timeout : DEFAULT_WS_KEEPALIVE_TIMEOUT); } else { s->keepAliveType.clear(); s->keepAliveMessage.clear(); } outItems += i; } else if(cm.type == WsControlMessage::SendDelayed) { QByteArray contentType; switch(cm.messageType) { case WsControlMessage::Text: contentType = "text"; break; case WsControlMessage::Binary: contentType = "binary"; break; case WsControlMessage::Ping: contentType = "ping"; break; case WsControlMessage::Pong: contentType = "pong"; break; default: continue; // unrecognized type, ignore } int timeout = (cm.timeout > 0 ? cm.timeout : DEFAULT_WS_SENDDELAYED_TIMEOUT); s->sendDelayed(contentType, cm.content, timeout); } else if(cm.type == WsControlMessage::FlushDelayed) { s->flushDelayed(); } } else if(item.type == WsControlPacket::Item::NeedKeepAlive) { if(!s->keepAliveMessage.isNull()) { WsControlPacket::Item i; i.cid = s->cid.toUtf8(); i.type = WsControlPacket::Item::Send; i.contentType = s->keepAliveType; i.message = s->keepAliveMessage; outItems += i; stats->addActivity(s->statsRoute.toUtf8(), 1); } } else if(item.type == WsControlPacket::Item::Subscribe) { QString channel = QString::fromUtf8(item.channel); s->channels += channel; s->implicitChannels += channel; if(!cs.wsSessionsByChannel.contains(channel)) cs.wsSessionsByChannel.insert(channel, QSet()); cs.wsSessionsByChannel[channel] += s; log_debug("ws session %s subscribed to %s", qPrintable(s->cid), qPrintable(channel)); stats->addSubscription("ws", channel, cs.wsSessionsByChannel.value(channel).count()); addSub(channel); log_info("subscribe %s channel=%s", qPrintable(s->requestData.uri.toString(QUrl::FullyEncoded)), qPrintable(channel)); } else if(item.type == WsControlPacket::Item::Ack) { int reqId = item.requestId.toInt(); s->ack(reqId); } } if(!outItems.isEmpty()) writeWsControlItems(packet.from, outItems); if(stateClient) { foreach(const QString &sid, createOrUpdateSids) { Deferred *d = SessionRequest::createOrUpdate(stateClient, sid, LastIds(), this); finishedConnection[d] = d->finished.connect(boost::bind(&Private::sessionCreateOrUpdate_finished, this, boost::placeholders::_1, d)); deferreds += d; } if(!updateSids.isEmpty()) { Deferred *d = SessionRequest::updateMany(stateClient, updateSids, this); finishedConnection[d] = d->finished.connect(boost::bind(&Private::sessionUpdateMany_finished, this, boost::placeholders::_1, d)); deferreds += d; } } } void proxyStats_readyRead(const QList &message) { if(message.count() != 1) { log_warning("IN proxy stats: received message with parts != 1, skipping"); return; } int at = message[0].indexOf(' '); if(at == -1) { log_warning("IN proxy stats: received message with invalid format, skipping"); return; } QByteArray type = message[0].mid(0, at); if(at + 1 >= message[0].length() || message[0][at + 1] != 'T') { log_warning("IN proxy stats: received message with unsupported format, skipping"); return; } bool ok; QVariant data = TnetString::toVariant(message[0], at + 2, &ok); if(!ok) { log_warning("IN proxy stats: received message with invalid format (tnetstring parse failed), skipping"); return; } if(log_outputLevel() >= LOG_LEVEL_DEBUG) log_debug("IN proxy stats: %s %s", type.data(), qPrintable(TnetString::variantToString(data, -1))); StatsPacket p; if(!p.fromVariant(type, data)) { log_warning("IN proxy stats: received message with invalid format, skipping"); return; } if(p.type == StatsPacket::Activity) { if(p.count > 0) { // merge with our own stats stats->addActivity(p.route, p.count); } } else if(p.type == StatsPacket::Counts) { if(p.requestsReceived > 0) { // merge with our own stats stats->addRequestsReceived(p.requestsReceived); } } else if(p.type == StatsPacket::Connected || p.type == StatsPacket::Disconnected) { if(stats->connectionSendEnabled()) { // track proxy connections for reporting bool localReplaced = stats->processExternalPacket(p, false); if(!localReplaced) { // forward the packet. this will stamp the from field and keep the rest stats->sendPacket(p); } } } else if(p.type == StatsPacket::Report) { bool mergeConnectionReport = !stats->connectionSendEnabled(); // merge into local report and don't forward stats->processExternalPacket(p, mergeConnectionReport); } } void controlHttpServer_requestReady() { SimpleHttpRequest *req = controlHttpServer->takeNext(); if(!req) return; QByteArray path = req->requestUri(); if(path.length() > 1 && path[path.length() - 1] == '/') path.truncate(path.length() - 1); HttpHeaders headers = req->requestHeaders(); QByteArray responseContentType; if(headers.contains("Accept")) { foreach(const HttpHeaderParameters ¶ms, headers.getAllAsParameters("Accept")) { if(params.isEmpty() || params[0].first.isEmpty()) continue; QByteArray type = params[0].first; if(type == "text/plain" || type == "text/*" || type == "*/*" || type == "*") { responseContentType = "text/plain"; } else if(type == "application/json" || type == "application/*") { responseContentType = "application/json"; } } if(responseContentType.isEmpty()) { httpControlRespond(req, 406, "Not Acceptable", "Not Acceptable. Supported formats are text/plain and application/json.\n"); return; } } else { responseContentType = "text/plain"; } if(path == "/") { httpControlRespond(req, 200, "OK", "Pushpin API\n"); } else if(path == "/publish") { if(req->requestMethod() == "POST") { QJsonParseError e; QJsonDocument doc = QJsonDocument::fromJson(req->requestBody(), &e); if(e.error != QJsonParseError::NoError) { httpControlRespond(req, 400, "Bad Request", "Body is not valid JSON.\n"); return; } if(!doc.isObject()) { httpControlRespond(req, 400, "Bad Request", "Invalid format.\n"); return; } QVariantMap mdata = doc.object().toVariantMap(); QVariantList vitems; if(!mdata.contains("items")) { httpControlRespond(req, 400, "Bad Request", "Invalid format: object does not contain 'items'\n"); return; } if(typeId(mdata["items"]) != QMetaType::QVariantList) { httpControlRespond(req, 400, "Bad Request", "Invalid format: object contains 'items' with wrong type\n"); return; } vitems = mdata["items"].toList(); bool ok; QString errorMessage; QList items = parseItems(vitems, &ok, &errorMessage); if(!ok) { httpControlRespond(req, 400, "Bad Request", QString("Invalid format: %1\n").arg(errorMessage)); return; } QString message = "Published"; if(responseContentType == "application/json") { QVariantMap obj; obj["message"] = message; QString body = QJsonDocument(QJsonObject::fromVariantMap(obj)).toJson(QJsonDocument::Compact); httpControlRespond(req, 200, "OK", body + "\n", responseContentType, HttpHeaders(), items.count()); } else // text/plain { httpControlRespond(req, 200, "OK", message + "\n", responseContentType, HttpHeaders(), items.count()); } foreach(const PublishItem &item, items) handlePublishItem(item); } else { HttpHeaders headers; headers += HttpHeader("Allow", "POST"); httpControlRespond(req, 405, "Method Not Allowed", "Method not allowed: " + req->requestMethod() + ".\n", QByteArray(), headers); } } else if(path == "/recover") { if(req->requestMethod() == "POST") { QString message = "Updated"; if(responseContentType == "application/json") { QVariantMap obj; obj["message"] = message; QString body = QJsonDocument(QJsonObject::fromVariantMap(obj)).toJson(QJsonDocument::Compact); httpControlRespond(req, 200, "OK", body + "\n", responseContentType, HttpHeaders()); } else // text/plain { httpControlRespond(req, 200, "OK", message + "\n", responseContentType, HttpHeaders()); } recoverCommand(); } else { HttpHeaders headers; headers += HttpHeader("Allow", "POST"); httpControlRespond(req, 405, "Method Not Allowed", "Method not allowed: " + req->requestMethod() + ".\n", QByteArray(), headers); } } else { httpControlRespond(req, 404, "Not Found", "Not Found\n"); } } private slots: void hs_subscribe(HttpSession *hs, const QString &channel) { Instruct::HoldMode mode = hs->holdMode(); assert(mode == Instruct::ResponseHold || mode == Instruct::StreamHold); QHash > *sessionsByChannel; QString modeStr; if(mode == Instruct::ResponseHold) { log_debug("adding response hold on %s", qPrintable(channel)); sessionsByChannel = &cs.responseSessionsByChannel; modeStr = "response"; } else // StreamHold { log_debug("adding stream hold on %s", qPrintable(channel)); sessionsByChannel = &cs.streamSessionsByChannel; modeStr = "stream"; } if(!sessionsByChannel->contains(channel)) sessionsByChannel->insert(channel, QSet()); (*sessionsByChannel)[channel] += hs; stats->addSubscription(modeStr, channel, sessionsByChannel->value(channel).count()); addSub(channel); QString msg = QString("subscribe %1 channel=%2").arg(hs->requestUri().toString(QUrl::FullyEncoded), channel); if(hs->isRetry()) msg += " retry"; log_info("%s", qPrintable(msg)); } void hs_unsubscribe(HttpSession *hs, const QString &channel) { removeSessionChannel(hs, channel); } void hs_finished(HttpSession *hs) { QByteArray addr = hs->retryToAddress(); RetryRequestPacket rp = hs->retryPacket(); cs.httpSessions.remove(hs->rid()); hs->subscribeCallback().remove(this); hs->unsubscribeCallback().remove(this); hs->finishedCallback().remove(this); hs->deleteLater(); if(!rp.requests.isEmpty()) writeRetryPacket(addr, rp); } void wssession_send(int reqId, const QByteArray &type, const QByteArray &message, WsSession *s) { WsControlPacket::Item i; i.cid = s->cid.toUtf8(); i.requestId = QByteArray::number(reqId); i.type = WsControlPacket::Item::Send; i.contentType = type; i.message = message; i.queue = true; writeWsControlItems(s->peer, QList() << i); } void wssession_expired(WsSession *s) { removeWsSession(s); } void wssession_error(WsSession *s) { log_debug("ws session %s control error", qPrintable(s->cid)); WsControlPacket::Item i; i.cid = s->cid.toUtf8(); i.type = WsControlPacket::Item::Cancel; writeWsControlItems(s->peer, QList() << i); removeWsSession(s); } }; HandlerEngine::HandlerEngine(QObject *parent) : QObject(parent) { d = new Private(this); } HandlerEngine::~HandlerEngine() { delete d; } bool HandlerEngine::start(const Configuration &config) { return d->start(config); } void HandlerEngine::reload() { d->reload(); } #include "handlerengine.moc" pushpin-1.39.1/src/cpp/handler/handlerengine.h000066400000000000000000000054341457610542000212470ustar00rootroot00000000000000/* * Copyright (C) 2015-2023 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef HANDLERENGINE_H #define HANDLERENGINE_H #include #include #include #include #include using std::map; using Signal = boost::signals2::signal; using Connection = boost::signals2::scoped_connection; class HandlerEngine : public QObject { Q_OBJECT public: class Configuration { public: QString appVersion; QByteArray instanceId; QStringList serverInStreamSpecs; QStringList serverOutSpecs; QStringList clientOutSpecs; QStringList clientOutStreamSpecs; QStringList clientInSpecs; QStringList inspectSpecs; QStringList acceptSpecs; QStringList retryOutSpecs; QStringList wsControlInitSpecs; QStringList wsControlStreamSpecs; QString statsSpec; QString commandSpec; QString stateSpec; QStringList proxyStatsSpecs; QString proxyCommandSpec; QString pushInSpec; QStringList pushInSubSpecs; bool pushInSubConnect; QHostAddress pushInHttpAddr; int pushInHttpPort; int pushInHttpMaxHeadersSize; int pushInHttpMaxBodySize; int ipcFileMode; bool shareAll; int messageRate; int messageHwm; int messageBlockSize; int messageWait; int idCacheTtl; int connectionsMax; int connectionSubscriptionMax; int subscriptionLinger; bool statsConnectionSend; int statsConnectionTtl; int statsSubscriptionTtl; int statsReportInterval; QString statsFormat; QString prometheusPort; QString prometheusPrefix; Configuration() : pushInSubConnect(false), pushInHttpPort(-1), pushInHttpMaxHeadersSize(-1), pushInHttpMaxBodySize(-1), ipcFileMode(-1), shareAll(false), messageRate(-1), messageHwm(-1), messageBlockSize(-1), messageWait(-1), idCacheTtl(-1), connectionsMax(-1), connectionSubscriptionMax(-1), subscriptionLinger(-1), statsConnectionSend(false), statsConnectionTtl(-1), statsSubscriptionTtl(-1), statsReportInterval(-1) { } }; HandlerEngine(QObject *parent = 0); ~HandlerEngine(); bool start(const Configuration &config); void reload(); private: class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/handler/handlermain.cpp000066400000000000000000000027541457610542000212630ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include #include #include "rtimer.h" #include "handlerapp.h" class HandlerAppMain { public: HandlerApp *app; void start() { app = new HandlerApp; app->quit.connect(boost::bind(&HandlerAppMain::app_quit, this, boost::placeholders::_1)); app->start(); } void app_quit(int returnCode) { delete app; QCoreApplication::exit(returnCode); } }; extern "C" { int handler_main(int argc, char **argv) { QCoreApplication qapp(argc, argv); HandlerAppMain appMain; QTimer::singleShot(0, [&appMain]() {appMain.start();}); int ret = qapp.exec(); // ensure deferred deletes are processed QCoreApplication::instance()->sendPostedEvents(); // deinit here, after all event loop activity has completed RTimer::deinit(); return ret; } } pushpin-1.39.1/src/cpp/handler/httpsession.cpp000066400000000000000000001122411457610542000213550ustar00rootroot00000000000000/* * Copyright (C) 2016-2023 Fanout, Inc. * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "httpsession.h" #include #include #include #include #include #include #include "qtcompat.h" #include "rtimer.h" #include "log.h" #include "bufferlist.h" #include "packet/retryrequestpacket.h" #include "zhttpmanager.h" #include "zhttprequest.h" #include "cors.h" #include "jsonpatch.h" #include "statsmanager.h" #include "logutil.h" #include "variantutil.h" #include "publishitem.h" #include "publishformat.h" #include "ratelimiter.h" #include "publishlastids.h" #include "httpsessionupdatemanager.h" #include "filterstack.h" #define RETRY_TIMEOUT 1000 #define RETRY_MAX 5 #define RETRY_RAND_MAX 1000 #define KEEPALIVE_RAND_MAX 1000 #define UPDATES_PER_ACTION_MAX 100 #define PUBLISH_QUEUE_MAX 100 static QByteArray applyBodyPatch(const QByteArray &in, const QVariantList &bodyPatch) { QByteArray body; QJsonParseError e; QJsonDocument doc = QJsonDocument::fromJson(in, &e); if(e.error == QJsonParseError::NoError && (doc.isObject() || doc.isArray())) { QVariant vbody; if(doc.isObject()) vbody = doc.object().toVariantMap(); else // isArray vbody = doc.array().toVariantList(); QString errorMessage; vbody = JsonPatch::patch(vbody, bodyPatch, &errorMessage); if(vbody.isValid()) vbody = VariantUtil::convertToJsonStyle(vbody); if(vbody.isValid() && (typeId(vbody) == QMetaType::QVariantMap || typeId(vbody) == QMetaType::QVariantList)) { QJsonDocument doc; if(typeId(vbody) == QMetaType::QVariantMap) doc = QJsonDocument(QJsonObject::fromVariantMap(vbody.toMap())); else // List doc = QJsonDocument(QJsonArray::fromVariantList(vbody.toList())); body = doc.toJson(QJsonDocument::Compact); if(in.endsWith("\r\n")) body += "\r\n"; else if(in.endsWith("\n")) body += '\n'; } else { log_debug("httpsession: failed to apply JSON patch: %s", qPrintable(errorMessage)); } } else { log_debug("httpsession: failed to parse original response body as JSON"); } return body; } class HttpSession::Private : public QObject { Q_OBJECT public: enum State { NotStarted, SendingFirstInstructResponse, WaitingToUpdate, Pausing, Proxying, SendingQueue, Holding, Closing }; enum Priority { LowPriority, HighPriority }; class UpdateAction : public RateLimiter::Action { public: QSet sessions; virtual bool execute() { if(sessions.isEmpty()) return false; foreach(HttpSession *q, sessions) q->d->doUpdate(); return true; } }; friend class UpdateAction; HttpSession *q; State state; ZhttpRequest *req; AcceptData adata; Instruct instruct; QHash channels; RTimer *timer; RTimer *retryTimer; StatsManager *stats; ZhttpManager *outZhttp; ZhttpRequest *outReq; // for fetching next links RateLimiter *updateLimiter; PublishLastIds *publishLastIds; HttpSessionUpdateManager *updateManager; BufferList firstInstructResponse; bool haveOutReqHeaders; int sentOutReqData; int retries; QString errorMessage; QUrl currentUri; QUrl nextUri; bool needUpdate; Priority needUpdatePriority; UpdateAction *pendingAction; QList publishQueue; QByteArray retryToAddress; RetryRequestPacket retryPacket; LogUtil::Config logConfig; FilterStack *responseFilters; QSet activeChannels; int connectionSubscriptionMax; bool needRemoveFromStats; Callback> subscribeCallback; Callback> unsubscribeCallback; Callback> finishedCallback; Connection bytesWrittenConnection; Connection writeBytesChangedConnection; Connection errorConnection; Connection pausedConnection; Connection readyReadOutConnection; Connection errorOutConnection; Connection timerConnection; Connection retryTimerConnection; Private(HttpSession *_q, ZhttpRequest *_req, const HttpSession::AcceptData &_adata, const Instruct &_instruct, ZhttpManager *_outZhttp, StatsManager *_stats, RateLimiter *_updateLimiter, PublishLastIds *_publishLastIds, HttpSessionUpdateManager *_updateManager, int _connectionSubscriptionMax) : QObject(_q), q(_q), req(_req), stats(_stats), outZhttp(_outZhttp), outReq(0), updateLimiter(_updateLimiter), publishLastIds(_publishLastIds), updateManager(_updateManager), haveOutReqHeaders(false), sentOutReqData(0), retries(0), needUpdate(false), pendingAction(0), responseFilters(0), connectionSubscriptionMax(_connectionSubscriptionMax), needRemoveFromStats(true) { state = NotStarted; req->setParent(this); bytesWrittenConnection = req->bytesWritten.connect(boost::bind(&Private::req_bytesWritten, this, boost::placeholders::_1)); writeBytesChangedConnection = req->writeBytesChanged.connect(boost::bind(&Private::req_writeBytesChanged, this)); errorConnection = req->error.connect(boost::bind(&Private::req_error, this)); timer = new RTimer; timerConnection = timer->timeout.connect(boost::bind(&Private::timer_timeout, this)); retryTimer = new RTimer; retryTimerConnection = retryTimer->timeout.connect(boost::bind(&Private::retryTimer_timeout, this)); retryTimer->setSingleShot(true); adata = _adata; instruct = _instruct; currentUri = req->requestUri(); if(!instruct.nextLink.isEmpty()) nextUri = currentUri.resolved(instruct.nextLink); } ~Private() { cleanup(); if(needRemoveFromStats) { ZhttpRequest::Rid rid = req->rid(); QByteArray cid = rid.first + ':' + rid.second; stats->removeConnection(cid, false); } updateManager->unregisterSession(q); timerConnection.disconnect(); timer->setParent(0); timer->deleteLater(); retryTimerConnection.disconnect(); retryTimer->setParent(0); retryTimer->deleteLater(); } void start() { assert(state == NotStarted); // set up implicit channels QPointer self = this; foreach(const QString &name, adata.implicitChannels) { if(!channels.contains(name)) { Instruct::Channel c; c.name = name; channels.insert(name, c); subscribeCallback.call({q, name}); assert(self); // deleting here would leak subscriptions/connections } } if(instruct.channels.count() > connectionSubscriptionMax) { instruct.channels = instruct.channels.mid(0, connectionSubscriptionMax); log_warning("httpsession: too many subscriptions"); } // need to send initial content? if((instruct.holdMode == Instruct::NoHold || instruct.holdMode == Instruct::StreamHold) && !adata.responseSent) { // send initial response HttpHeaders headers = instruct.response.headers; headers.removeAll("Content-Length"); if(adata.autoCrossOrigin) Cors::applyCorsHeaders(req->requestHeaders(), &headers); incCounter(Stats::ClientHeaderBytesSent, ZhttpManager::estimateResponseHeaderBytes(instruct.response.code, instruct.response.reason, headers)); req->beginResponse(instruct.response.code, instruct.response.reason, headers); if(!instruct.response.body.isEmpty()) { // apply ProxyContent filters of all channels QStringList allFilters; foreach(const Instruct::Channel &c, instruct.channels) { foreach(const QString &filter, c.filters) { if((Filter::targets(filter) & Filter::ProxyContent) && !allFilters.contains(filter)) allFilters += filter; } } Filter::Context fc; fc.subscriptionMeta = instruct.meta; FilterStack fs(fc, allFilters); instruct.response.body = fs.process(instruct.response.body); if(instruct.response.body.isNull()) { errorMessage = QString("filter error: %1").arg(fs.errorMessage()); doError(); return; } state = SendingFirstInstructResponse; firstInstructResponse += instruct.response.body; tryWriteFirstInstructResponse(); return; } } firstInstructResponseDone(); } void update(Priority priority) { if(state == Proxying || state == SendingQueue) { // if we are already in the process of updating, flag to update // again after current one finishes if(needUpdate) { // if needUpdate was already flagged, then raise // priority if needed if(priority == HighPriority) needUpdatePriority = priority; } else { needUpdate = true; needUpdatePriority = priority; } return; } if(state == WaitingToUpdate) { if(priority == HighPriority) { // switching to high priority cleanupAction(); state = Holding; } else { // already waiting, do nothing return; } } if(state != Holding) return; needUpdate = false; if(instruct.holdMode != Instruct::ResponseHold && nextUri.isEmpty()) { // can't update without valid link return; } // turn off update timer during update updateManager->unregisterSession(q); if(priority == HighPriority) { doUpdate(); } else // LowPriority { state = WaitingToUpdate; QString key = QString::fromUtf8(nextUri.toEncoded()); UpdateAction *action = static_cast(updateLimiter->lastAction(key)); if(!action || action->sessions.count() >= UPDATES_PER_ACTION_MAX) { action = new UpdateAction; updateLimiter->addAction(key, action); } action->sessions += q; pendingAction = action; } } void publish(const PublishItem &item, const QList &exposeHeaders) { const PublishFormat &f = item.format; if(f.type == PublishFormat::HttpResponse) { if(state != Holding) return; assert(instruct.holdMode == Instruct::ResponseHold); if(!channels.contains(item.channel)) { log_debug("httpsession: received publish for channel with no subscription, dropping"); return; } Instruct::Channel &channel = channels[item.channel]; if(!channel.prevId.isNull()) { if(channel.prevId != item.prevId) { log_debug("last ID inconsistency (got=%s, expected=%s), retrying", qPrintable(item.prevId), qPrintable(channel.prevId)); publishLastIds->remove(item.channel); update(LowPriority); return; } channel.prevId = item.id; } if(f.haveContentFilters) { // ensure content filters match QStringList contentFilters; foreach(const QString &f, channels[item.channel].filters) { if(Filter::targets(f) & Filter::MessageContent) contentFilters += f; } if(contentFilters != f.contentFilters) { errorMessage = QString("content filter mismatch: subscription=%1 message=%2").arg(contentFilters.join(","), f.contentFilters.join(",")); doError(); return; } } QHash prevIds; QHashIterator it(channels); while(it.hasNext()) { it.next(); const Instruct::Channel &c = it.value(); prevIds[c.name] = c.prevId; } Filter::Context fc; fc.prevIds = prevIds; fc.subscriptionMeta = instruct.meta; fc.publishMeta = item.meta; FilterStack fs(fc, channels[item.channel].filters); if(fs.sendAction() == Filter::Drop) return; // NOTE: http-response mode doesn't support a close // action since it's better to send a real response if(f.action == PublishFormat::Send) { QByteArray body; if(f.haveBodyPatch) { body = applyBodyPatch(instruct.response.body, f.bodyPatch); } else { body = f.body; } body = fs.process(body); if(body.isNull()) { errorMessage = QString("filter error: %1").arg(fs.errorMessage()); doError(); return; } respond(f.code, f.reason, f.headers, body, exposeHeaders); } else if(f.action == PublishFormat::Hint) { update(HighPriority); } } else if(f.type == PublishFormat::HttpStream) { if(state == WaitingToUpdate || state == Proxying || state == SendingQueue || state == Holding) { if(publishQueue.count() < PUBLISH_QUEUE_MAX) { publishQueue += item; if(state == Holding) trySendQueue(); } else { log_debug("httpsession: publish queue at max, dropping"); } } } } private: void cleanup() { cleanupAction(); delete outReq; outReq = 0; delete responseFilters; responseFilters = 0; } void cleanupAction() { if(pendingAction) { pendingAction->sessions.remove(q); pendingAction = 0; } } void setupKeepAlive() { if(instruct.keepAliveTimeout >= 0) { int timeout = instruct.keepAliveTimeout * 1000; timeout = qMax(timeout - (int)(QRandomGenerator::global()->generate() % KEEPALIVE_RAND_MAX), 0); timer->setSingleShot(true); timer->start(timeout); } } void adjustKeepAlive() { // if idle mode, restart the timer. else leave alone if(timer && instruct.keepAliveMode == Instruct::Idle) setupKeepAlive(); } void prepareToClose() { state = Closing; publishQueue.clear(); timer->stop(); updateManager->unregisterSession(q); } void tryWriteFirstInstructResponse() { int avail = req->writeBytesAvailable(); if(avail > 0) { writeBody(firstInstructResponse.take(avail)); if(firstInstructResponse.isEmpty()) firstInstructResponseDone(); } } void firstInstructResponseDone() { if(instruct.holdMode == Instruct::NoHold) { // NoHold instruct MUST have had a link to make it this far assert(!nextUri.isEmpty()); doUpdate(); } else // ResponseHold, StreamHold { prepareToSendQueueOrHold(true); } } void doUpdate() { pendingAction = 0; if(instruct.holdMode == Instruct::ResponseHold) { state = Pausing; // stop activity while pausing timer->stop(); pausedConnection = req->paused.connect(boost::bind(&Private::req_paused, this)); req->pause(); } else { state = Proxying; requestNextLink(); } } void prepareToSendQueueOrHold(bool first = false) { assert(instruct.holdMode != Instruct::NoHold); if(instruct.holdMode == Instruct::StreamHold) { // if prev ids used but not next link, error out if(nextUri.isEmpty()) { foreach(const Instruct::Channel &c, instruct.channels) { if(!c.prevId.isNull()) { errorMessage = QString("channel '%1' specifies prev-id, but no next link found").arg(c.name); doError(); return; } } } } QList channelsRemoved; QHashIterator it(channels); while(it.hasNext()) { it.next(); const QString &name = it.key(); if(adata.implicitChannels.contains(name)) continue; bool found = false; foreach(const Instruct::Channel &c, instruct.channels) { if(adata.channelPrefix + c.name == name) { found = true; break; } } if(!found) { channelsRemoved += name; channels.remove(name); } } QList channelsAdded; foreach(const Instruct::Channel &c, instruct.channels) { QString name = adata.channelPrefix + c.name; if(!channels.contains(name)) { channelsAdded += name; channels.insert(name, c); } else { // update channel properties channels[name].prevId = c.prevId; channels[name].filters = c.filters; } } QPointer self = this; foreach(const QString &channel, channelsRemoved) { unsubscribeCallback.call({q, channel}); assert(self); // deleting here would leak subscriptions/connections } foreach(const QString &channel, channelsAdded) { subscribeCallback.call({q, channel}); assert(self); // deleting here would leak subscriptions/connections } if(instruct.holdMode == Instruct::ResponseHold) { state = Holding; // set timeout if(instruct.timeout >= 0) { timer->setSingleShot(true); timer->start(instruct.timeout * 1000); } } else // StreamHold { // if conflict on first hold, immediately recover. we don't // do this on subsequent holds because there may be queued // messages available to resolve the conflict if(first) { bool conflict = false; foreach(const Instruct::Channel &c, instruct.channels) { if(!c.prevId.isNull()) { QString name = adata.channelPrefix + c.name; QString lastId = publishLastIds->value(name); if(!lastId.isNull() && lastId != c.prevId) { log_debug("last ID inconsistency (got=%s, expected=%s), retrying", qPrintable(c.prevId), qPrintable(lastId)); publishLastIds->remove(name); conflict = true; // NOTE: don't exit loop here. we want to clear // the last ids of all conflicting channels } } } if(conflict) { // update expects us to be in Holding state state = Holding; update(LowPriority); return; } } // drop any non-matching queued items while(!publishQueue.isEmpty()) { PublishItem &item = publishQueue.first(); if(!channels.contains(item.channel)) { // we don't care about this channel anymore publishQueue.removeFirst(); continue; } Instruct::Channel &channel = channels[item.channel]; if(!channel.prevId.isNull() && channel.prevId != item.prevId) { // item doesn't follow the hold publishQueue.removeFirst(); continue; } break; } if(!publishQueue.isEmpty()) { state = SendingQueue; trySendQueue(); } else { sendQueueDone(); } } } void trySendQueue() { assert(instruct.holdMode == Instruct::StreamHold); while(!publishQueue.isEmpty() && req->writeBytesAvailable() > 0) { PublishItem item = publishQueue.takeFirst(); if(!channels.contains(item.channel)) { log_debug("httpsession: received publish for channel with no subscription, dropping"); continue; } Instruct::Channel &channel = channels[item.channel]; if(!channel.prevId.isNull()) { if(channel.prevId != item.prevId) { log_debug("last ID inconsistency (got=%s, expected=%s), retrying", qPrintable(item.prevId), qPrintable(channel.prevId)); publishLastIds->remove(item.channel); publishQueue.clear(); update(LowPriority); break; } channel.prevId = item.id; } PublishFormat &f = item.format; if(f.haveContentFilters) { // ensure content filters match QStringList contentFilters; foreach(const QString &f, channels[item.channel].filters) { if(Filter::targets(f) & Filter::MessageContent) contentFilters += f; } if(contentFilters != f.contentFilters) { errorMessage = QString("content filter mismatch: subscription=%1 message=%2").arg(contentFilters.join(","), f.contentFilters.join(",")); doError(); break; } } QHash prevIds; QHashIterator it(channels); while(it.hasNext()) { it.next(); const Instruct::Channel &c = it.value(); prevIds[c.name] = c.prevId; } Filter::Context fc; fc.prevIds = prevIds; fc.subscriptionMeta = instruct.meta; fc.publishMeta = item.meta; FilterStack fs(fc, channels[item.channel].filters); if(fs.sendAction() == Filter::Drop) continue; if(f.action == PublishFormat::Send) { QByteArray body = fs.process(f.body); if(body.isNull()) { errorMessage = QString("filter error: %1").arg(fs.errorMessage()); doError(); break; } writeBody(body); // restart keep alive timer adjustKeepAlive(); if(!nextUri.isEmpty() && instruct.nextLinkTimeout >= 0) { activeChannels += item.channel; if(activeChannels.count() == channels.count()) { activeChannels.clear(); updateManager->registerSession(q, instruct.nextLinkTimeout, nextUri); } } } else if(f.action == PublishFormat::Hint) { // clear queue since any items will be redundant publishQueue.clear(); update(HighPriority); break; } else if(f.action == PublishFormat::Close) { prepareToClose(); req->endBody(); break; } } if(state == SendingQueue) { if(publishQueue.isEmpty()) sendQueueDone(); } else if(state == Holding) { if(!publishQueue.isEmpty()) { // if backlogged, turn off timers until we're able to send again timer->stop(); updateManager->unregisterSession(q); } } } void sendQueueDone() { state = Holding; activeChannels.clear(); // start keep alive timer, if it wasn't started already if(!timer->isActive()) setupKeepAlive(); if(!nextUri.isEmpty() && instruct.nextLinkTimeout >= 0) updateManager->registerSession(q, instruct.nextLinkTimeout, nextUri); if(needUpdate) update(needUpdatePriority); } void respond(int _code, const QByteArray &_reason, const HttpHeaders &_headers, const QByteArray &_body) { prepareToClose(); int code = _code; QByteArray reason = _reason; HttpHeaders headers = _headers; QByteArray body = _body; headers.removeAll("Content-Length"); // this will be reset if needed if(adata.autoCrossOrigin) { if(!adata.jsonpCallback.isEmpty()) { if(adata.jsonpExtendedResponse) { QVariantMap result; result["code"] = code; result["reason"] = QString::fromUtf8(reason); // need to compact headers into a map QVariantMap vheaders; foreach(const HttpHeader &h, headers) { // don't add the same header name twice. we'll collect all values for a single header bool found = false; QMapIterator it(vheaders); while(it.hasNext()) { it.next(); const QString &name = it.key(); QByteArray uname = name.toUtf8(); if(qstricmp(uname.data(), h.first.data()) == 0) { found = true; break; } } if(found) continue; QList values = headers.getAll(h.first); QString mergedValue; for(int n = 0; n < values.count(); ++n) { mergedValue += QString::fromUtf8(values[n]); if(n + 1 < values.count()) mergedValue += ", "; } vheaders[h.first] = mergedValue; } result["headers"] = vheaders; result["body"] = QString::fromUtf8(body); QByteArray resultJson = QJsonDocument(QJsonObject::fromVariantMap(result)).toJson(QJsonDocument::Compact); body = "/**/" + adata.jsonpCallback + '(' + resultJson + ");\n"; } else { if(body.endsWith("\r\n")) body.truncate(body.size() - 2); else if(body.endsWith("\n")) body.truncate(body.size() - 1); body = "/**/" + adata.jsonpCallback + '(' + body + ");\n"; } headers.removeAll("Content-Type"); headers += HttpHeader("Content-Type", "application/javascript"); code = 200; reason = "OK"; } else { Cors::applyCorsHeaders(req->requestHeaders(), &headers); } } incCounter(Stats::ClientHeaderBytesSent, ZhttpManager::estimateResponseHeaderBytes(code, reason, headers)); req->beginResponse(code, reason, headers); writeBody(body); req->endBody(); } void respond(int code, const QByteArray &reason, const HttpHeaders &_headers, const QByteArray &body, const QList &exposeHeaders) { // inherit headers from the timeout response HttpHeaders headers = instruct.response.headers; foreach(const HttpHeader &h, _headers) headers.removeAll(h.first); foreach(const HttpHeader &h, _headers) headers += h; // if Grip-Expose-Headers was provided in the push, apply now if(!exposeHeaders.isEmpty()) { for(int n = 0; n < headers.count(); ++n) { const HttpHeader &h = headers[n]; bool found = false; foreach(const QByteArray &e, exposeHeaders) { if(qstricmp(h.first.data(), e.data()) == 0) { found = true; break; } } if(found) { headers.removeAt(n); --n; // adjust position } } } respond(code, reason, headers, body); } void doFinish(bool retry = false) { ZhttpRequest::Rid rid = req->rid(); QByteArray cid = rid.first + ':' + rid.second; log_debug("httpsession: cleaning up %s", cid.data()); cleanup(); QPointer self = this; QHashIterator it(channels); while(it.hasNext()) { it.next(); const QString &channel = it.key(); unsubscribeCallback.call({q, channel}); assert(self); // deleting here would leak subscriptions/connections } if(retry) { // refresh before remove, to ensure transition stats->refreshConnection(cid); needRemoveFromStats = false; int unreportedTime = stats->removeConnection(cid, true, adata.from); ZhttpRequest::ServerState ss = req->serverState(); RetryRequestPacket rp; RetryRequestPacket::Request rpreq; rpreq.rid = rid; rpreq.https = (req->requestUri().scheme() == "https"); rpreq.peerAddress = req->peerAddress(); rpreq.debug = adata.debug; rpreq.autoCrossOrigin = adata.autoCrossOrigin; rpreq.jsonpCallback = adata.jsonpCallback; rpreq.jsonpExtendedResponse = adata.jsonpExtendedResponse; if(!stats->connectionSendEnabled()) rpreq.unreportedTime = unreportedTime; rpreq.inSeq = ss.inSeq; rpreq.outSeq = ss.outSeq; rpreq.outCredits = ss.outCredits; rpreq.userData = ss.userData; rp.requests += rpreq; rp.requestData = adata.requestData; if(adata.haveInspectInfo) { rp.haveInspectInfo = true; rp.inspectInfo.doProxy = adata.inspectInfo.doProxy; rp.inspectInfo.sharingKey = adata.inspectInfo.sharingKey; rp.inspectInfo.sid = adata.inspectInfo.sid; rp.inspectInfo.lastIds = adata.inspectInfo.lastIds; rp.inspectInfo.userData = adata.inspectInfo.userData; } // if prev-id set on channels, set as inspect lastids so the proxy // will pass as Grip-Last in the next request QHashIterator it(channels); while(it.hasNext()) { it.next(); const Instruct::Channel &c = it.value(); if(!c.prevId.isNull()) { if(!rp.haveInspectInfo) { rp.haveInspectInfo = true; rp.inspectInfo.doProxy = true; } rp.inspectInfo.lastIds.insert(c.name.toUtf8(), c.prevId.toUtf8()); } } rp.route = adata.route.toUtf8(); rp.retrySeq = stats->lastRetrySeq(adata.from); retryToAddress = adata.from; retryPacket = rp; } else { needRemoveFromStats = false; stats->removeConnection(cid, false); } finishedCallback.call({q}); } void requestNextLink() { log_debug("httpsession: next: %s", qPrintable(instruct.nextLink.toString())); if(!outZhttp) { errorMessage = "Instruct contained link, but handler not configured for outbound requests."; QMetaObject::invokeMethod(this, "doError", Qt::QueuedConnection); return; } haveOutReqHeaders = false; sentOutReqData = 0; outReq = outZhttp->createRequest(); outReq->setParent(this); readyReadOutConnection = outReq->readyRead.connect(boost::bind(&Private::outReq_readyRead, this)); errorOutConnection = outReq->error.connect(boost::bind(&Private::outReq_error, this)); int currentPort = currentUri.port(currentUri.scheme() == "https" ? 443 : 80); int nextPort = nextUri.port(currentUri.scheme() == "https" ? 443 : 80); QVariantHash passthroughData; passthroughData["route"] = adata.route.toUtf8(); // if next link points to the same service as the current request, // then we can assume the network would send the request back to // us, so we can handle it internally. if the link points to a // different service, then we can't make this assumption and need // to make the request over the network. note that such a request // could still end up looping back to us if(nextUri.scheme() == currentUri.scheme() && nextUri.host() == currentUri.host() && nextPort == currentPort) { // tell the proxy that we prefer the request to be handled // internally, using the same route passthroughData["prefer-internal"] = true; } // these fields are needed in case proxy routing is not used if(adata.trusted) passthroughData["trusted"] = true; // share requests to the same URI passthroughData["auto-share"] = true; outReq->setPassthroughData(passthroughData); HttpHeaders headers; foreach(const Instruct::Channel &c, channels.values()) { if(!c.prevId.isNull()) headers += HttpHeader("Grip-Last", c.name.toUtf8() + "; last-id=" + c.prevId.toUtf8()); } outReq->start("GET", nextUri, headers); outReq->endBody(); } void tryProcessOutReq() { if(outReq) { if(!haveOutReqHeaders) return; if(outReq->responseCode() < 200 || outReq->responseCode() >= 300) { outReq_error(); return; } if(outReq->bytesAvailable() > 0) { // stop keep alive timer only if we have to send data. if the // response body is empty, then the timer is left alone timer->stop(); int avail = req->writeBytesAvailable(); if(avail <= 0) return; QByteArray buf = outReq->readBody(avail); if(responseFilters) { buf = responseFilters->update(buf); if(buf.isNull()) { logRequestError(outReq->requestMethod(), outReq->requestUri(), outReq->requestHeaders()); errorMessage = QString("filter error: %1").arg(responseFilters->errorMessage()); doError(); return; } } writeBody(buf); sentOutReqData += buf.size(); } if(outReq->bytesAvailable() == 0 && outReq->isFinished()) { if(responseFilters) { QByteArray buf = responseFilters->finalize(); if(buf.isNull()) { logRequestError(outReq->requestMethod(), outReq->requestUri(), outReq->requestHeaders()); errorMessage = QString("filter error: %1").arg(responseFilters->errorMessage()); doError(); return; } delete responseFilters; responseFilters = 0; if(!buf.isEmpty()) { writeBody(buf); sentOutReqData += buf.size(); } } HttpResponseData responseData; responseData.code = outReq->responseCode(); responseData.reason = outReq->responseReason(); responseData.headers = outReq->responseHeaders(); logRequest(outReq->requestMethod(), outReq->requestUri(), outReq->requestHeaders(), responseData.code, sentOutReqData); retries = 0; delete outReq; outReq = 0; bool ok; Instruct i = Instruct::fromResponse(responseData, &ok, &errorMessage); if(!ok) { doError(); return; } // subsequent response must be non-hold or stream hold if(i.holdMode != Instruct::NoHold && i.holdMode != Instruct::StreamHold) { errorMessage = "Next link returned non-stream hold."; doError(); return; } instruct = i; currentUri = nextUri; if(!instruct.nextLink.isEmpty()) nextUri = currentUri.resolved(instruct.nextLink); else nextUri.clear(); if(instruct.channels.count() > connectionSubscriptionMax) { instruct.channels = instruct.channels.mid(0, connectionSubscriptionMax); log_warning("httpsession: too many subscriptions"); } if(instruct.holdMode == Instruct::StreamHold) { if(instruct.keepAliveTimeout < 0) timer->stop(); prepareToSendQueueOrHold(); } } } if(state == Proxying && !outReq) { if(!nextUri.isEmpty()) { if(req->writeBytesAvailable() > 0) requestNextLink(); } else { prepareToClose(); req->endBody(); } } } void logRequest(const QString &method, const QUrl &uri, const HttpHeaders &headers, int code, int bodySize) { LogUtil::RequestData rd; // only log route id if explicitly set if(!adata.statsRoute.isEmpty()) rd.routeId = adata.route; rd.status = LogUtil::Response; rd.requestData.method = method; rd.requestData.uri = uri; rd.requestData.headers = headers; rd.responseData.code = code; rd.responseBodySize = bodySize; LogUtil::logRequest(LOG_LEVEL_INFO, rd, logConfig); } void logRequestError(const QString &method, const QUrl &uri, const HttpHeaders &headers) { LogUtil::RequestData rd; // only log route id if explicitly set if(!adata.statsRoute.isEmpty()) rd.routeId = adata.route; rd.status = LogUtil::Error; rd.requestData.method = method; rd.requestData.uri = uri; rd.requestData.headers = headers; LogUtil::logRequest(LOG_LEVEL_INFO, rd, logConfig); } void incCounter(Stats::Counter c, int count = 1) { stats->incCounter(adata.statsRoute.toUtf8(), c, count); } void writeBody(const QByteArray &body) { incCounter(Stats::ClientContentBytesSent, body.size()); req->writeBody(body); } private slots: void doError() { if(instruct.holdMode == Instruct::ResponseHold) { QByteArray msg; if(adata.debug) msg = errorMessage.toUtf8(); else msg = "Error while proxying to origin."; HttpHeaders headers; headers += HttpHeader("Content-Type", "text/plain"); respond(502, "Bad Gateway", headers, msg + '\n'); } else // StreamHold, NoHold { prepareToClose(); if(adata.debug) writeBody("\n\n" + errorMessage.toUtf8() + '\n'); req->endBody(); } } void req_bytesWritten(int count) { Q_UNUSED(count); if(req->isFinished()) { doFinish(); return; } } void req_writeBytesChanged() { if(state == SendingFirstInstructResponse) { tryWriteFirstInstructResponse(); } else if(state == Proxying) { tryProcessOutReq(); } else if(state == SendingQueue || state == Holding) { trySendQueue(); } } void req_error() { doFinish(); } void req_paused() { doFinish(true); // finish for retry } void outReq_readyRead() { if(!haveOutReqHeaders) { haveOutReqHeaders = true; // apply ProxyContent filters of all channels QStringList allFilters; foreach(const Instruct::Channel &c, instruct.channels) { foreach(const QString &filter, c.filters) { if((Filter::targets(filter) & Filter::ProxyContent) && !allFilters.contains(filter)) allFilters += filter; } } Filter::Context fc; fc.subscriptionMeta = instruct.meta; responseFilters = new FilterStack(fc, allFilters); } tryProcessOutReq(); } void outReq_error() { logRequestError(outReq->requestMethod(), outReq->requestUri(), outReq->requestHeaders()); delete responseFilters; responseFilters = 0; delete outReq; outReq = 0; log_debug("httpsession: failed to retrieve next link"); // can't retry if we started sending data if(sentOutReqData <= 0 && retries < RETRY_MAX) { int delay = RETRY_TIMEOUT; for(int n = 0; n < retries; ++n) delay *= 2; delay += QRandomGenerator::global()->generate() % RETRY_RAND_MAX; log_debug("httpsession: trying again in %dms", delay); ++retries; retryTimer->start(delay); return; } else { errorMessage = "Failed to retrieve next link."; doError(); } } private: void timer_timeout() { if(instruct.holdMode == Instruct::ResponseHold) { // send timeout response respond(instruct.response.code, instruct.response.reason, instruct.response.headers, instruct.response.body); } else if(instruct.holdMode == Instruct::StreamHold) { writeBody(instruct.keepAliveData); setupKeepAlive(); stats->addActivity(adata.statsRoute.toUtf8(), 1); } } void retryTimer_timeout() { requestNextLink(); } }; HttpSession::HttpSession(ZhttpRequest *req, const HttpSession::AcceptData &adata, const Instruct &instruct, ZhttpManager *zhttpOut, StatsManager *stats, RateLimiter *updateLimiter, PublishLastIds *publishLastIds, HttpSessionUpdateManager *updateManager, int connectionSubscriptionMax, QObject *parent) : QObject(parent) { d = new Private(this, req, adata, instruct, zhttpOut, stats, updateLimiter, publishLastIds, updateManager, connectionSubscriptionMax); } HttpSession::~HttpSession() { delete d; } Instruct::HoldMode HttpSession::holdMode() const { if(d->instruct.holdMode == Instruct::NoHold) { // NoHold is a temporary internal state for stream return Instruct::StreamHold; } else return d->instruct.holdMode; } ZhttpRequest::Rid HttpSession::rid() const { return d->req->rid(); } QUrl HttpSession::requestUri() const { return d->req->requestUri(); } bool HttpSession::isRetry() const { return d->adata.isRetry; } QString HttpSession::statsRoute() const { return d->adata.statsRoute; } QString HttpSession::sid() const { return d->adata.sid; } QHash HttpSession::channels() const { return d->channels; } QHash HttpSession::meta() const { return d->instruct.meta; } QByteArray HttpSession::retryToAddress() const { return d->retryToAddress; } RetryRequestPacket HttpSession::retryPacket() const { return d->retryPacket; } void HttpSession::start() { d->start(); } void HttpSession::update() { d->update(Private::LowPriority); } void HttpSession::publish(const PublishItem &item, const QList &exposeHeaders) { d->publish(item, exposeHeaders); } Callback> & HttpSession::subscribeCallback() { return d->subscribeCallback; } Callback> & HttpSession::unsubscribeCallback() { return d->unsubscribeCallback; } Callback> & HttpSession::finishedCallback() { return d->finishedCallback; } #include "httpsession.moc" pushpin-1.39.1/src/cpp/handler/httpsession.h000066400000000000000000000060711457610542000210250ustar00rootroot00000000000000/* * Copyright (C) 2016-2023 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef HTTPSESSION_H #define HTTPSESSION_H #include #include "packet/httprequestdata.h" #include "packet/httpresponsedata.h" #include "callback.h" #include "inspectdata.h" #include "zhttprequest.h" #include "instruct.h" #include using Connection = boost::signals2::scoped_connection; class QTimer; class ZhttpManager; class StatsManager; class PublishItem; class RateLimiter; class PublishLastIds; class HttpSessionUpdateManager; class RetryRequestPacket; class HttpSession; class HttpSession : public QObject { Q_OBJECT public: class AcceptData { public: QByteArray from; QHostAddress logicalPeerAddress; bool debug; bool isRetry; bool autoCrossOrigin; QByteArray jsonpCallback; bool jsonpExtendedResponse; int unreportedTime; HttpRequestData requestData; QString route; QString statsRoute; QString channelPrefix; QSet implicitChannels; bool trusted; bool responseSent; QString sid; bool haveInspectInfo; InspectData inspectInfo; AcceptData() : debug(false), isRetry(false), autoCrossOrigin(false), jsonpExtendedResponse(false), unreportedTime(-1), trusted(false), responseSent(false), haveInspectInfo(false) { } }; HttpSession(ZhttpRequest *req, const HttpSession::AcceptData &adata, const Instruct &instruct, ZhttpManager *outZhttp, StatsManager *stats, RateLimiter *updateLimiter, PublishLastIds *publishLastIds, HttpSessionUpdateManager *updateManager, int connectionSubscriptionMax, QObject *parent = 0); ~HttpSession(); Instruct::HoldMode holdMode() const; ZhttpRequest::Rid rid() const; QUrl requestUri() const; bool isRetry() const; QString statsRoute() const; QString sid() const; QHash channels() const; QHash meta() const; QByteArray retryToAddress() const; RetryRequestPacket retryPacket() const; void start(); void update(); void publish(const PublishItem &item, const QList &exposeHeaders = QList()); // NOTE: for performance reasons we use callbacks instead of signals/slots Callback> & subscribeCallback(); Callback> & unsubscribeCallback(); Callback> & finishedCallback(); private: class Private; friend class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/handler/httpsessionupdatemanager.cpp000066400000000000000000000102231457610542000241100ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "httpsessionupdatemanager.h" #include #include #include "httpsession.h" class HttpSessionUpdateManager::Private : public QObject { Q_OBJECT public: class Bucket { public: QPair key; QSet sessions; QSet deferredSessions; QTimer *timer; }; HttpSessionUpdateManager *q; QHash, Bucket*> buckets; QHash bucketsByTimer; QHash bucketsBySession; Private(HttpSessionUpdateManager *_q) : QObject(_q), q(_q) { } ~Private() { QHashIterator, Bucket*> it(buckets); while(it.hasNext()) { it.next(); Bucket *bucket = it.value(); bucket->timer->disconnect(this); bucket->timer->setParent(0); bucket->timer->deleteLater(); delete bucket; } } void removeBucket(Bucket *bucket) { foreach(HttpSession *hs, bucket->sessions) bucketsBySession.remove(hs); bucketsByTimer.remove(bucket->timer); buckets.remove(bucket->key); bucket->timer->disconnect(this); bucket->timer->setParent(0); bucket->timer->deleteLater(); delete bucket; } void registerSession(HttpSession *hs, int timeout, const QUrl &uri) { QUrl tmp = uri; tmp.setQuery(QString()); // remove the query part QPair key(timeout, tmp); Bucket *bucket = buckets.value(key); if(bucket) { if(bucket->sessions.contains(hs)) { // if the session is already in this bucket, flag it // for later processing bucket->deferredSessions += hs; } else { // move the session to this bucket unregisterSession(hs); bucket->sessions += hs; bucketsBySession[hs] = bucket; } } else { // bucket doesn't exist. make it and put this session in it unregisterSession(hs); bucket = new Bucket; bucket->key = key; bucket->sessions += hs; bucket->timer = new QTimer(this); QObject::connect(bucket->timer, &QTimer::timeout, [this, timer=bucket->timer]() { this->timer_timeout(timer); }); buckets[key] = bucket; bucketsByTimer[bucket->timer] = bucket; bucketsBySession[hs] = bucket; bucket->timer->start(timeout * 1000); } } void unregisterSession(HttpSession *hs) { Bucket *bucket = bucketsBySession.value(hs); if(!bucket) return; bucket->sessions.remove(hs); bucket->deferredSessions.remove(hs); bucketsBySession.remove(hs); if(bucket->sessions.isEmpty()) removeBucket(bucket); } private: void timer_timeout(QTimer *timer) { Bucket *bucket = bucketsByTimer.value(timer); if(!bucket) return; QSet sessions; if(!bucket->deferredSessions.isEmpty()) { foreach(HttpSession *hs, bucket->sessions) { if(!bucket->deferredSessions.contains(hs)) { sessions += hs; bucketsBySession.remove(hs); } } bucket->sessions = bucket->deferredSessions; bucket->deferredSessions.clear(); bucket->timer->start(); } else { sessions = bucket->sessions; removeBucket(bucket); } foreach(HttpSession *hs, sessions) hs->update(); } }; HttpSessionUpdateManager::HttpSessionUpdateManager(QObject *parent) : QObject(parent) { d = new Private(this); } HttpSessionUpdateManager::~HttpSessionUpdateManager() { delete d; } void HttpSessionUpdateManager::registerSession(HttpSession *hs, int timeout, const QUrl &uri) { d->registerSession(hs, timeout, uri); } void HttpSessionUpdateManager::unregisterSession(HttpSession *hs) { d->unregisterSession(hs); } #include "httpsessionupdatemanager.moc" pushpin-1.39.1/src/cpp/handler/httpsessionupdatemanager.h000066400000000000000000000021431457610542000235570ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef HTTPSESSIONUPDATEMANAGER_H #define HTTPSESSIONUPDATEMANAGER_H #include class QUrl; class HttpSession; class HttpSessionUpdateManager : public QObject { public: HttpSessionUpdateManager(QObject *parent = 0); ~HttpSessionUpdateManager(); void registerSession(HttpSession *hs, int timeout, const QUrl &uri); void unregisterSession(HttpSession *hs); private: class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/handler/idformat.cpp000066400000000000000000000060211457610542000205750ustar00rootroot00000000000000/* * Copyright (C) 2017-2019 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "idformat.h" #include #include "format.h" namespace IdFormat { class IdFormatHandler : public Format::Handler { public: QHash vars; virtual QByteArray handle(char type, const QByteArray &arg, QString *error) const { if(type != 's') { *error = QString("Unknown directive '%1'").arg(type); return QByteArray(); } if(arg.isNull()) { *error = QString("Directive 's' requires argument"); return QByteArray(); } QByteArray value = vars.value(arg); if(value.isNull()) { *error = QString("No such variable '%1'").arg(QString::fromUtf8(arg)); return QByteArray(); } return value; } }; class ContentFormatHandler : public Format::Handler { public: QByteArray defaultId; bool hex; ContentFormatHandler() : hex(false) { } virtual QByteArray handle(char type, const QByteArray &arg, QString *error) const { if(type != 'I') { *error = QString("Unknown directive '%1'").arg(type); return QByteArray(); } QByteArray id; if(!arg.isNull()) { id = arg; } else { if(defaultId.isNull()) { *error = QString("No ID specified and no default ID in context"); return QByteArray(); } id = defaultId; } if(hex) { id = id.toHex(); } return id; } }; ContentRenderer::ContentRenderer(const QByteArray &defaultId, bool hex) : defaultId_(defaultId), hex_(hex) { } QByteArray ContentRenderer::update(const QByteArray &data) { buf_ += data; ContentFormatHandler handler; handler.defaultId = defaultId_; handler.hex = hex_; int partialPos; QByteArray ret = Format::process(buf_, &handler, &partialPos, &errorMessage_); if(!ret.isNull()) { buf_ = buf_.mid(partialPos); } return ret; } QByteArray ContentRenderer::finalize() { QByteArray data = buf_; buf_.clear(); ContentFormatHandler handler; handler.defaultId = defaultId_; handler.hex = hex_; return Format::process(data, &handler, 0, &errorMessage_); } QByteArray ContentRenderer::process(const QByteArray &data) { ContentFormatHandler handler; handler.defaultId = defaultId_; handler.hex = hex_; return Format::process(data, &handler, 0, &errorMessage_); } QByteArray renderId(const QByteArray &data, const QHash &vars, QString *error) { IdFormatHandler handler; handler.vars = vars; return Format::process(data, &handler, 0, error); } } pushpin-1.39.1/src/cpp/handler/idformat.h000066400000000000000000000024431457610542000202460ustar00rootroot00000000000000/* * Copyright (C) 2017 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef IDFORMAT_H #define IDFORMAT_H #include #include #include namespace IdFormat { class ContentRenderer { public: ContentRenderer(const QByteArray &defaultId, bool hex); // return null array on error QByteArray update(const QByteArray &data); QByteArray finalize(); QString errorMessage() { return errorMessage_; } QByteArray process(const QByteArray &data); private: QByteArray defaultId_; bool hex_; QByteArray buf_; QString errorMessage_; }; QByteArray renderId(const QByteArray &data, const QHash &vars, QString *error = 0); } #endif pushpin-1.39.1/src/cpp/handler/instruct.cpp000066400000000000000000000444421457610542000206540ustar00rootroot00000000000000/* * Copyright (C) 2016-2019 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "instruct.h" #include #include #include #include "qtcompat.h" #include "variantutil.h" #include "statusreasons.h" #define DEFAULT_RESPONSE_TIMEOUT 55 #define MINIMUM_RESPONSE_TIMEOUT 5 #define DEFAULT_NEXTLINK_TIMEOUT 120 using namespace VariantUtil; static int charToHex(char c) { if(c >= '0' && c <= '9') return c - '0'; else if(c >= 'a' && c <= 'f') return c - 'a' + 10; else if(c >= 'A' && c <= 'F') return c - 'A' + 10; else return -1; } static QByteArray unescape(const QByteArray &in) { QByteArray out; for(int n = 0; n < in.length(); ++n) { if(in[n] == '\\') { if(n + 1 >= in.length()) return QByteArray(); ++n; if(in[n] == '\\') { out += '\\'; } else if(in[n] == 'r') { out += '\r'; } else if(in[n] == 'n') { out += '\n'; } else if(in[n] == 'x') { if(n + 2 >= in.length()) return QByteArray(); int hi = charToHex(in[n + 1]); int lo = charToHex(in[n + 2]); n += 2; if(hi == -1 || lo == -1) return QByteArray(); unsigned int x = (hi << 4) + lo; out += (char)x; } } else out += in[n]; } return out; } Instruct Instruct::fromResponse(const HttpResponseData &response, bool *ok, QString *errorMessage) { HoldMode holdMode = NoHold; QList channels; int timeout = -1; QList exposeHeaders; KeepAliveMode keepAliveMode = NoKeepAlive; QByteArray keepAliveData; int keepAliveTimeout = -1; QHash meta; HttpResponseData newResponse; if(response.headers.contains("Grip-Hold")) { QByteArray gripHoldStr = response.headers.get("Grip-Hold"); if(gripHoldStr == "response") { holdMode = ResponseHold; } else if(gripHoldStr == "stream") { holdMode = StreamHold; } else { setError(ok, errorMessage, "Grip-Hold must be set to either 'response' or 'stream'"); return Instruct(); } } QList gripChannels = response.headers.getAllAsParameters("Grip-Channel"); foreach(const HttpHeaderParameters &gripChannel, gripChannels) { if(gripChannel.isEmpty()) { setError(ok, errorMessage, "failed to parse Grip-Channel"); return Instruct(); } Channel c; c.name = QString::fromUtf8(gripChannel[0].first); QByteArray param = gripChannel.get("prev-id"); if(!param.isNull()) c.prevId = QString::fromUtf8(param); for(int n = 1; n < gripChannel.count(); ++n) { const HttpHeaderParameter ¶m = gripChannel[n]; if(param.first == "filter") c.filters += QString::fromUtf8(param.second); } channels += c; } if(response.headers.contains("Grip-Timeout")) { bool x; timeout = response.headers.get("Grip-Timeout").toInt(&x); if(!x) { setError(ok, errorMessage, "failed to parse Grip-Timeout"); return Instruct(); } if(timeout < 0) { setError(ok, errorMessage, "Grip-Timeout has invalid value"); return Instruct(); } } exposeHeaders = response.headers.getAll("Grip-Expose-Headers"); HttpHeaderParameters keepAliveParams = response.headers.getAsParameters("Grip-Keep-Alive"); if(!keepAliveParams.isEmpty()) { QByteArray val = keepAliveParams[0].first; if(val.isEmpty()) { setError(ok, errorMessage, "Grip-Keep-Alive cannot be empty"); return Instruct(); } QByteArray mode = keepAliveParams.get("mode"); if(mode.isEmpty() || mode == "idle") { keepAliveMode = Idle; } else if(mode == "interval") { keepAliveMode = Interval; } else { setError(ok, errorMessage, QString("no such Grip-Keep-Alive mode '%1'").arg(QString::fromUtf8(mode))); return Instruct(); } if(keepAliveParams.contains("timeout")) { bool x; keepAliveTimeout = keepAliveParams.get("timeout").toInt(&x); if(!x) { setError(ok, errorMessage, "failed to parse Grip-Keep-Alive timeout value"); return Instruct(); } if(keepAliveTimeout < 0) { setError(ok, errorMessage, "Grip-Keep-Alive timeout has invalid value"); return Instruct(); } } else { keepAliveTimeout = DEFAULT_RESPONSE_TIMEOUT; } QByteArray format = keepAliveParams.get("format"); if(format.isEmpty() || format == "raw") { keepAliveData = val; } else if(format == "cstring") { keepAliveData = unescape(val); if(keepAliveData.isNull()) { setError(ok, errorMessage, "failed to parse Grip-Keep-Alive cstring format"); return Instruct(); } } else if(format == "base64") { keepAliveData = QByteArray::fromBase64(val); } else { setError(ok, errorMessage, QString("no such Grip-Keep-Alive format '%1'").arg(QString::fromUtf8(format))); return Instruct(); } } QList metaParams = response.headers.getAllAsParameters("Grip-Set-Meta", HttpHeaders::ParseAllParameters); foreach(const HttpHeaderParameters &metaParam, metaParams) { if(metaParam.isEmpty()) { setError(ok, errorMessage, "Grip-Set-Meta cannot be empty"); return Instruct(); } QString key = QString::fromUtf8(metaParam[0].first); QString val = QString::fromUtf8(metaParam[0].second); meta[key] = val; } newResponse = response; QByteArray statusHeader = response.headers.get("Grip-Status"); if(!statusHeader.isEmpty()) { QByteArray codeStr; QByteArray reason; int at = statusHeader.indexOf(' '); if(at != -1) { codeStr = statusHeader.mid(0, at); reason = statusHeader.mid(at + 1); } else { codeStr = statusHeader; } bool _ok; newResponse.code = codeStr.toInt(&_ok); if(!_ok || newResponse.code < 0 || newResponse.code > 999) { setError(ok, errorMessage, "Grip-Status contains invalid status code"); return Instruct(); } newResponse.reason = reason; } QUrl nextLink; int nextLinkTimeout = -1; foreach(const HttpHeaderParameters ¶ms, response.headers.getAllAsParameters("Grip-Link")) { if(params.count() >= 2 && params.get("rel") == "next") { QByteArray linkParam = params[0].first; if(linkParam.length() <= 2 || linkParam[0] != '<' || linkParam[linkParam.length() - 1] != '>') { setError(ok, errorMessage, "failed to parse Grip-Link value"); return Instruct(); } nextLink = QUrl::fromEncoded(linkParam.mid(1, linkParam.length() - 2)); if(!nextLink.isValid()) { setError(ok, errorMessage, "Grip-Link contains invalid link"); return Instruct(); } if(params.contains("timeout")) { bool x; nextLinkTimeout = params.get("timeout").toInt(&x); if(!x) { setError(ok, errorMessage, "failed to parse Grip-Link timeout value"); return Instruct(); } if(nextLinkTimeout < 0) { setError(ok, errorMessage, "Grip-Link timeout has invalid value"); return Instruct(); } } else { nextLinkTimeout = DEFAULT_NEXTLINK_TIMEOUT; } } } newResponse.headers.clear(); foreach(const HttpHeader &h, response.headers) { // strip out grip headers if(qstrnicmp(h.first.data(), "Grip-", 5) == 0) continue; if(!exposeHeaders.isEmpty()) { bool found = false; foreach(const QByteArray &e, exposeHeaders) { if(qstricmp(e.data(), h.first.data()) == 0) { found = true; break; } } if(!found) continue; } newResponse.headers += HttpHeader(h.first, h.second); } QByteArray contentType = response.headers.getAsFirstParameter("Content-Type"); if(contentType == "application/grip-instruct") { if(response.code != 200) { setError(ok, errorMessage, "response code for application/grip-instruct content must be 200"); return Instruct(); } QJsonParseError e; QJsonDocument doc = QJsonDocument::fromJson(response.body, &e); if(e.error != QJsonParseError::NoError) { setError(ok, errorMessage, "failed to parse application/grip-instruct content as JSON"); return Instruct(); } if(!doc.isObject()) { setError(ok, errorMessage, "instruct must be an object"); return Instruct(); } QVariantMap minstruct = doc.object().toVariantMap(); bool ok_; if(minstruct.contains("hold")) { if(typeId(minstruct["hold"]) != QMetaType::QVariantMap) { setError(ok, errorMessage, "instruct contains 'hold' with wrong type"); return Instruct(); } QString pn = "hold"; QVariant vhold = minstruct["hold"]; QString modeStr = getString(vhold, pn, "mode", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return Instruct(); } if(!modeStr.isNull()) { if(modeStr == "response") { holdMode = ResponseHold; } else if(modeStr == "stream") { holdMode = StreamHold; } else { setError(ok, errorMessage, "hold 'mode' must be set to either 'response' or 'stream'"); return Instruct(); } } else { holdMode = ResponseHold; } QVariantList vchannels = getList(vhold, pn, "channels", true, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return Instruct(); } foreach(const QVariant &vchannel, vchannels) { QString cpn = "channel"; Channel c; c.name = getString(vchannel, cpn, "name", true, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return Instruct(); } c.prevId = getString(vchannel, cpn, "prev-id", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return Instruct(); } QVariantList vfilters = getList(vchannel, cpn, "filters", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return Instruct(); } foreach(const QVariant &vfilter, vfilters) { QString filter = getString(vfilter, &ok_); if(!ok_) { setError(ok, errorMessage, "filters contains value with wrong type"); return Instruct(); } c.filters += filter; } channels += c; } if(keyedObjectContains(vhold, "timeout")) { QVariant vtimeout = keyedObjectGetValue(vhold, "timeout"); if(!canConvert(vtimeout, QMetaType::Int)) { setError(ok, errorMessage, QString("%1 contains 'timeout' with wrong type").arg(pn)); return Instruct(); } timeout = vtimeout.toInt(); if(timeout < 0) { setError(ok, errorMessage, QString("%1 contains 'timeout' with invalid value").arg(pn)); return Instruct(); } } QVariant vka = getKeyedObject(vhold, pn, "keep-alive", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return Instruct(); } if(isKeyedObject(vka)) { QString kpn = "keep-alive"; if(keyedObjectContains(vka, "content-bin")) { QString contentBin = getString(vka, kpn, "content-bin", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return Instruct(); } keepAliveData = QByteArray::fromBase64(contentBin.toUtf8()); } else if(keyedObjectContains(vka, "content")) { QVariant vcontent = keyedObjectGetValue(vka, "content"); if(typeId(vcontent) == QMetaType::QByteArray) keepAliveData = vcontent.toByteArray(); else if(typeId(vcontent) == QMetaType::QString) keepAliveData = vcontent.toString().toUtf8(); else { setError(ok, errorMessage, QString("%1 contains 'content' with wrong type").arg(kpn)); return Instruct(); } } if(keyedObjectContains(vka, "timeout")) { QVariant vtimeout = keyedObjectGetValue(vka, "timeout"); if(!canConvert(vtimeout, QMetaType::Int)) { setError(ok, errorMessage, QString("%1 contains 'timeout' with wrong type").arg(kpn)); return Instruct(); } keepAliveTimeout = vtimeout.toInt(); if(keepAliveTimeout < 0) { setError(ok, errorMessage, QString("%1 contains 'timeout' with invalid value").arg(kpn)); return Instruct(); } } else { keepAliveTimeout = 55; } } QVariant vmeta = getKeyedObject(vhold, pn, "meta", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return Instruct(); } if(vmeta.isValid()) { if(typeId(vmeta) == QMetaType::QVariantHash) { QVariantHash hmeta = vmeta.toHash(); QHashIterator it(hmeta); while(it.hasNext()) { it.next(); const QString &key = it.key(); const QVariant &vval = it.value(); QString val = getString(vval, &ok_); if(!ok_) { setError(ok, errorMessage, QString("'meta' contains '%1' with wrong type").arg(key)); return Instruct(); } meta[key] = val; } } else // Map { QVariantMap mmeta = vmeta.toMap(); QMapIterator it(mmeta); while(it.hasNext()) { it.next(); const QString &key = it.key(); const QVariant &vval = it.value(); QString val = getString(vval, &ok_); if(!ok_) { setError(ok, errorMessage, QString("'meta' contains '%1' with wrong type").arg(key)); return Instruct(); } meta[key] = val; } } } } newResponse.headers.clear(); newResponse.body.clear(); if(minstruct.contains("response")) { if(typeId(minstruct["response"]) != QMetaType::QVariantMap) { if(ok) *ok = false; return Instruct(); } QVariant in = minstruct["response"]; QString pn = "response"; if(keyedObjectContains(in, "code")) { QVariant vcode = keyedObjectGetValue(in, "code"); if(!canConvert(vcode, QMetaType::Int)) { setError(ok, errorMessage, QString("%1 contains 'code' with wrong type").arg(pn)); return Instruct(); } newResponse.code = vcode.toInt(); if(newResponse.code < 0 || newResponse.code > 999) { setError(ok, errorMessage, QString("%1 contains 'code' with invalid value").arg(pn)); return Instruct(); } // if code was supplied in json instruct, then // we need to clear the default reason newResponse.reason.clear(); } QString reasonStr = getString(in, pn, "reason", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return Instruct(); } if(!reasonStr.isEmpty()) newResponse.reason = reasonStr.toUtf8(); if(keyedObjectContains(in, "headers")) { QVariant vheaders = keyedObjectGetValue(in, "headers"); if(typeId(vheaders) == QMetaType::QVariantList) { foreach(const QVariant &vheader, vheaders.toList()) { if(typeId(vheader) != QMetaType::QVariantList) { setError(ok, errorMessage, "headers contains element with wrong type"); return Instruct(); } QVariantList lheader = vheader.toList(); if(lheader.count() != 2) { setError(ok, errorMessage, "headers contains list with wrong number of elements"); return Instruct(); } QString name = getString(lheader[0], &ok_); if(!ok_) { setError(ok, errorMessage, "header contains name element with wrong type"); return Instruct(); } QString val = getString(lheader[1], &ok_); if(!ok_) { setError(ok, errorMessage, "header contains value element with wrong type"); return Instruct(); } newResponse.headers += HttpHeader(name.toUtf8(), val.toUtf8()); } } else if(isKeyedObject(vheaders)) { if(typeId(vheaders) == QMetaType::QVariantHash) { QVariantHash hheaders = vheaders.toHash(); QHashIterator it(hheaders); while(it.hasNext()) { it.next(); const QString &key = it.key(); const QVariant &vval = it.value(); QString val = getString(vval, &ok_); if(!ok_) { setError(ok, errorMessage, QString("headers contains '%1' with wrong type").arg(key)); return Instruct(); } newResponse.headers += HttpHeader(key.toUtf8(), val.toUtf8()); } } else // Map { QVariantMap mheaders = vheaders.toMap(); QMapIterator it(mheaders); while(it.hasNext()) { it.next(); const QString &key = it.key(); const QVariant &vval = it.value(); QString val = getString(vval, &ok_); if(!ok_) { setError(ok, errorMessage, QString("headers contains '%1' with wrong type").arg(key)); return Instruct(); } newResponse.headers += HttpHeader(key.toUtf8(), val.toUtf8()); } } } else { setError(ok, errorMessage, QString("%1 contains 'headers' with wrong type").arg(pn)); return Instruct(); } } if(keyedObjectContains(in, "body-bin")) { QString bodyBin = getString(in, pn, "body-bin", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return Instruct(); } newResponse.body = QByteArray::fromBase64(bodyBin.toUtf8()); } else if(keyedObjectContains(in, "body")) { QVariant vcontent = keyedObjectGetValue(in, "body"); if(typeId(vcontent) == QMetaType::QByteArray) newResponse.body = vcontent.toByteArray(); else if(typeId(vcontent) == QMetaType::QString) newResponse.body = vcontent.toString().toUtf8(); else { setError(ok, errorMessage, QString("%1 contains 'body' with wrong type").arg(pn)); return Instruct(); } } } } if(newResponse.reason.isEmpty()) newResponse.reason = StatusReasons::getReason(newResponse.code); if(timeout == -1) timeout = DEFAULT_RESPONSE_TIMEOUT; timeout = qMax(timeout, MINIMUM_RESPONSE_TIMEOUT); if(keepAliveTimeout != -1) { if(keepAliveTimeout < 1) keepAliveTimeout = 1; } Instruct i; i.holdMode = holdMode; i.channels = channels; i.timeout = timeout; i.exposeHeaders = exposeHeaders; i.keepAliveMode = keepAliveMode; i.keepAliveData = keepAliveData; i.keepAliveTimeout = keepAliveTimeout; i.meta = meta; i.response = newResponse; i.nextLink = nextLink; i.nextLinkTimeout = nextLinkTimeout; if(ok) *ok = true; return i; } pushpin-1.39.1/src/cpp/handler/instruct.h000066400000000000000000000031701457610542000203120ustar00rootroot00000000000000/* * Copyright (C) 2016-2019 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef INSTRUCT_H #define INSTRUCT_H #include #include #include #include #include #include #include "packet/httpresponsedata.h" class Instruct { public: enum HoldMode { NoHold, ResponseHold, StreamHold }; enum KeepAliveMode { NoKeepAlive, Idle, Interval }; class Channel { public: QString name; QString prevId; QStringList filters; }; HoldMode holdMode; QList channels; int timeout; QList exposeHeaders; KeepAliveMode keepAliveMode; QByteArray keepAliveData; int keepAliveTimeout; QHash meta; HttpResponseData response; QUrl nextLink; int nextLinkTimeout; Instruct() : holdMode(NoHold), timeout(-1), keepAliveMode(NoKeepAlive), keepAliveTimeout(-1), nextLinkTimeout(-1) { } static Instruct fromResponse(const HttpResponseData &response, bool *ok = 0, QString *errorMessage = 0); }; #endif pushpin-1.39.1/src/cpp/handler/jsonpatch.cpp000066400000000000000000000230501457610542000207620ustar00rootroot00000000000000/* * Copyright (C) 2015 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "jsonpatch.h" #include #include "qtcompat.h" #include "jsonpointer.h" namespace JsonPatch { static void setSuccess(bool *ok, QString *errorMessage) { if(ok) *ok = true; if(errorMessage) errorMessage->clear(); } static void setError(bool *ok, QString *errorMessage, const QString &msg) { if(ok) *ok = false; if(errorMessage) *errorMessage = msg; } static bool isKeyedObject(const QVariant &in) { return (typeId(in) == QMetaType::QVariantHash || typeId(in) == QMetaType::QVariantMap); } static bool keyedObjectContains(const QVariant &in, const QString &name) { if(typeId(in) == QMetaType::QVariantHash) return in.toHash().contains(name); else if(typeId(in) == QMetaType::QVariantMap) return in.toMap().contains(name); else return false; } static QVariant keyedObjectGetValue(const QVariant &in, const QString &name) { if(typeId(in) == QMetaType::QVariantHash) return in.toHash().value(name); else if(typeId(in) == QMetaType::QVariantMap) return in.toMap().value(name); else return QVariant(); } static QVariant getChild(const QVariant &in, const QString &parentName, const QString &childName, bool required, bool *ok = 0, QString *errorMessage = 0) { if(!isKeyedObject(in)) { QString pn = !parentName.isEmpty() ? parentName : QString("value"); setError(ok, errorMessage, QString("%1 is not an object").arg(pn)); return QVariant(); } QString pn = !parentName.isEmpty() ? parentName : QString("object"); QVariant v; if(typeId(in) == QMetaType::QVariantHash) { QVariantHash h = in.toHash(); if(!h.contains(childName)) { if(required) setError(ok, errorMessage, QString("%1 does not contain '%2'").arg(pn, childName)); else setSuccess(ok, errorMessage); return QVariant(); } v = h[childName]; } else // Map { QVariantMap m = in.toMap(); if(!m.contains(childName)) { if(required) setError(ok, errorMessage, QString("%1 does not contain '%2'").arg(pn, childName)); else setSuccess(ok, errorMessage); return QVariant(); } v = m[childName]; } setSuccess(ok, errorMessage); return v; } static QString getString(const QVariant &in, bool *ok = 0) { if(typeId(in) == QMetaType::QString) { if(ok) *ok = true; return in.toString(); } else if(typeId(in) == QMetaType::QByteArray) { if(ok) *ok = true; return QString::fromUtf8(in.toByteArray()); } else { if(ok) *ok = false; return QString(); } } static QString getString(const QVariant &in, const QString &parentName, const QString &childName, bool required, bool *ok = 0, QString *errorMessage = 0) { bool ok_; QVariant v = getChild(in, parentName, childName, required, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return QString(); } if(!v.isValid() && !required) { setSuccess(ok, errorMessage); return QString(); } QString pn = !parentName.isEmpty() ? parentName : QString("object"); QString str = getString(v, &ok_); if(!ok_) { setError(ok, errorMessage, QString("%1 contains '%2' with wrong type").arg(pn, childName)); return QString(); } setSuccess(ok, errorMessage); return str; } // return true if item modified static bool convertToJsonStyleInPlace(QVariant *in) { // Hash -> Map // ByteArray (UTF-8) -> String bool changed = false; QMetaType::Type type = typeId(*in); if(type == QMetaType::QVariantHash) { QVariantMap vmap; QVariantHash vhash = in->toHash(); QHashIterator it(vhash); while(it.hasNext()) { it.next(); QVariant i = it.value(); convertToJsonStyleInPlace(&i); vmap[it.key()] = i; } *in = vmap; changed = true; } else if(type == QMetaType::QVariantList) { QVariantList vlist = in->toList(); for(int n = 0; n < vlist.count(); ++n) { QVariant i = vlist.at(n); convertToJsonStyleInPlace(&i); vlist[n] = i; } *in = vlist; changed = true; } else if(type == QMetaType::QByteArray) { *in = QVariant(QString::fromUtf8(in->toByteArray())); changed = true; } return changed; } static QVariant convertToJsonStyle(const QVariant &in) { QVariant v = in; convertToJsonStyleInPlace(&v); return v; } static bool _compareJsonValues(const QVariant &a, const QVariant &b) { if(typeId(a) == QMetaType::QVariantMap && typeId(b) == QMetaType::QVariantMap) { QVariantMap am = a.toMap(); QVariantMap bm = b.toMap(); if(am.count() != bm.count()) return false; QMapIterator it(am); while(it.hasNext()) { it.next(); const QString &key = it.key(); const QVariant &val = it.value(); if(!bm.contains(key)) return false; if(!_compareJsonValues(val, bm.value(key))) return false; } return true; } else if(typeId(a) == QMetaType::QVariantList && typeId(b) == QMetaType::QVariantList) { QVariantList al = a.toList(); QVariantList bl = b.toList(); if(al.count() != bl.count()) return false; for(int n = 0; n < al.count(); ++n) { if(!_compareJsonValues(al[n], bl[n])) return false; } return true; } else if(typeId(a) == QMetaType::QString && typeId(b) == QMetaType::QString) { return (a.toString() == b.toString()); } else if(typeId(a) == QMetaType::Bool && typeId(b) == QMetaType::Bool) { return (a.toBool() == b.toBool()); } else if(typeId(a) == QMetaType::UnknownType && typeId(b) == QMetaType::UnknownType) { return true; } else if(canConvert(a, QMetaType::Int) && canConvert(b, QMetaType::Int)) { return (a.toInt() == b.toInt()); } else return false; } static bool compareJsonValues(const QVariant &a, const QVariant &b) { QVariant ca = convertToJsonStyle(a); QVariant cb = convertToJsonStyle(b); return _compareJsonValues(ca, cb); } QVariant patch(const QVariant &data, const QVariantList &ops, QString *errorMessage) { QVariant out = data; foreach(const QVariant &vop, ops) { if(!isKeyedObject(vop)) { if(errorMessage) *errorMessage = "invalid op"; return QVariant(); } QString pn = "op"; bool ok; QString type = getString(vop, pn, "op", true, &ok, errorMessage); if(!ok) return QVariant(); QString path = getString(vop, pn, "path", true, &ok, errorMessage); if(!ok) return QVariant(); JsonPointer ptr; // for all ops except move, we can resolve the path now if(type != "move") { ptr = JsonPointer::resolve(&out, path, errorMessage); if(ptr.isNull()) return QVariant(); } if(type == "add") { if(!keyedObjectContains(vop, "value")) { if(errorMessage) *errorMessage = "op does not contain 'value'"; return QVariant(); } QVariant value = keyedObjectGetValue(vop, "value"); ptr.setValue(value); } else if(type == "remove") { if(!ptr.exists()) { if(errorMessage) *errorMessage = "location does not exist"; return QVariant(); } if(!ptr.remove()) return QVariant(); } else if(type == "replace") { if(!keyedObjectContains(vop, "value")) { if(errorMessage) *errorMessage = "op does not contain 'value'"; return QVariant(); } QVariant value = keyedObjectGetValue(vop, "value"); if(!ptr.exists()) { if(errorMessage) *errorMessage = "location does not exist"; return QVariant(); } ptr.setValue(value); } else if(type == "move") { QString from = getString(vop, pn, "from", true, &ok, errorMessage); if(!ok) return QVariant(); if(JsonPointer::isWithin(path, from)) { if(errorMessage) *errorMessage = "cannot move location into itself"; return QVariant(); } JsonPointer fromPtr = JsonPointer::resolve(&out, from, errorMessage); if(fromPtr.isNull()) return QVariant(); if(!fromPtr.exists()) { if(errorMessage) *errorMessage = "location does not exist"; return QVariant(); } QVariant value = fromPtr.take(); ptr = JsonPointer::resolve(&out, path, errorMessage); if(ptr.isNull()) return QVariant(); ptr.setValue(value); } else if(type == "copy") { QString from = getString(vop, pn, "from", true, &ok, errorMessage); if(!ok) return QVariant(); JsonPointer fromPtr = JsonPointer::resolve(&out, from, errorMessage); if(fromPtr.isNull()) return QVariant(); if(!fromPtr.exists()) { if(errorMessage) *errorMessage = "location does not exist"; return QVariant(); } ptr = JsonPointer::resolve(&out, path, errorMessage); if(ptr.isNull()) return QVariant(); ptr.setValue(fromPtr.value()); } else if(type == "test") { if(!keyedObjectContains(vop, "value")) { if(errorMessage) *errorMessage = "op does not contain 'value'"; return QVariant(); } QVariant value = keyedObjectGetValue(vop, "value"); QVariant cur = ptr.value(); if(!compareJsonValues(cur, value)) { if(errorMessage) *errorMessage = "tested values are not equal"; return QVariant(); } } else { if(errorMessage) *errorMessage = QString("unsupported op: %1").arg(type); return QVariant(); } } return out; } } pushpin-1.39.1/src/cpp/handler/jsonpatch.h000066400000000000000000000015671457610542000204400ustar00rootroot00000000000000/* * Copyright (C) 2015 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef JSONPATCH_H #define JSONPATCH_H #include namespace JsonPatch { QVariant patch(const QVariant &data, const QVariantList &ops, QString *errorMessage = 0); } #endif pushpin-1.39.1/src/cpp/handler/jsonpointer.cpp000066400000000000000000000264161457610542000213540ustar00rootroot00000000000000/* * Copyright (C) 2015 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "jsonpointer.h" #include #include #include "qtcompat.h" JsonPointer::JsonPointer() : isNull_(true) { } QVariant *JsonPointer::root() { return root_; } JsonPointer::ExecStatus JsonPointer::execute(const QVariant *i, int refIndex, ConstFunc func, void *data) const { // if there are more refs after current ref, step into current if(refIndex + 1 < refs_.count()) { const Ref &ref = refs_[refIndex]; assert(ref.type != Ref::Self); if(ref.type == Ref::Object) { if(typeId(*i) == QMetaType::QVariantHash) { QVariantHash h = i->toHash(); if(!h.contains(ref.name)) return ExecError; return execute(&h[ref.name], refIndex + 1, func, data); } else // Map { QVariantMap m = i->toMap(); if(!m.contains(ref.name)) return ExecError; return execute(&m[ref.name], refIndex + 1, func, data); } } else // Array { QVariantList l = i->toList(); if(ref.index < 0 || ref.index >= l.count()) return ExecError; return execute(&l[ref.index], refIndex + 1, func, data); } } // ensure ref is correct type const Ref &ref = refs_[refIndex]; if(ref.type == Ref::Object && (typeId(*i) != QMetaType::QVariantHash && typeId(*i) != QMetaType::QVariantMap)) return ExecError; else if(ref.type == Ref::Array && typeId(*i) != QMetaType::QVariantList) return ExecError; func(i, refs_[refIndex], data); return ExecContinue; } JsonPointer::ExecStatus JsonPointer::execute(QVariant *i, int refIndex, Func func, void *data) { // if there are more refs after current ref, step into current if(refIndex + 1 < refs_.count()) { const Ref &ref = refs_[refIndex]; if(ref.type == Ref::Object) { if(typeId(*i) == QMetaType::QVariantHash) { QVariantHash h = i->toHash(); if(!h.contains(ref.name)) return ExecError; ExecStatus ret = execute(&h[ref.name], refIndex + 1, func, data); if(ret == ExecChanged) *i = h; return ret; } else // Map { QVariantMap m = i->toMap(); if(!m.contains(ref.name)) return ExecError; ExecStatus ret = execute(&m[ref.name], refIndex + 1, func, data); if(ret == ExecChanged) *i = m; return ret; } } else if(ref.type == Ref::Array) { QVariantList l = i->toList(); if(ref.index < 0 || ref.index >= l.count()) return ExecError; ExecStatus ret = execute(&l[ref.index], refIndex + 1, func, data); if(ret == ExecChanged) *i = l; return ret; } } // ensure ref is correct type const Ref &ref = refs_[refIndex]; if(ref.type == Ref::Object && (typeId(*i) != QMetaType::QVariantHash && typeId(*i) != QMetaType::QVariantMap)) return ExecError; else if(ref.type == Ref::Array && typeId(*i) != QMetaType::QVariantList) return ExecError; if(func(i, refs_[refIndex], data)) return ExecChanged; else return ExecContinue; } bool JsonPointer::execute(ConstFunc func, void *data) const { if(!refs_.isEmpty()) { return (execute(root_, 0, func, data) != ExecError); } else { func(root_, Ref(), data); return true; } } bool JsonPointer::execute(Func func, void *data) { if(!refs_.isEmpty()) { return (execute(root_, 0, func, data) != ExecError); } else { func(root_, Ref(), data); return true; } } static void existsFunc(const QVariant *v, const JsonPointer::Ref &ref, void *data) { QVariant &ret = *((QVariant *)data); if(ref.type == JsonPointer::Ref::Self) { ret = true; } else if(ref.type == JsonPointer::Ref::Object) { if(typeId(*v) == QMetaType::QVariantHash) ret = v->toHash().contains(ref.name); else // Map ret = v->toMap().contains(ref.name); } else // Array { QVariantList l = v->toList(); ret = (ref.index >= 0 && ref.index < l.count()); } } bool JsonPointer::exists() const { QVariant ret; if(execute(existsFunc, &ret)) return ret.toBool(); else return false; } static void valueFunc(const QVariant *v, const JsonPointer::Ref &ref, void *data) { QVariant &ret = *((QVariant *)data); if(ref.type == JsonPointer::Ref::Self) { ret = *v; } else if(ref.type == JsonPointer::Ref::Object) { if(typeId(*v) == QMetaType::QVariantHash) ret = v->toHash().value(ref.name); else // Map ret = v->toMap().value(ref.name); } else // Array { QVariantList l = v->toList(); if(ref.index >= 0 && ref.index < l.count()) ret = l[ref.index]; } } QVariant JsonPointer::value() const { QVariant ret; if(execute(valueFunc, &ret)) return ret; else return QVariant(); } static bool removeFunc(QVariant *v, const JsonPointer::Ref &ref, void *data) { QVariant &ret = *((QVariant *)data); if(ref.type == JsonPointer::Ref::Self) { ret = false; return false; // technically an error, since we can't remove the root } else if(ref.type == JsonPointer::Ref::Object) { if(typeId(*v) == QMetaType::QVariantHash) { QVariantHash h = v->toHash(); if(h.contains(ref.name)) { ret = true; h.remove(ref.name); *v = h; return true; } else return false; } else // Map { QVariantMap m = v->toMap(); if(m.contains(ref.name)) { ret = true; m.remove(ref.name); *v = m; return true; } else return false; } } else // Array { QVariantList l = v->toList(); if(ref.index >= 0 && ref.index < l.count()) { ret = true; l.removeAt(ref.index); *v = l; return true; } else return false; } } bool JsonPointer::remove() { QVariant ret; if(execute(removeFunc, &ret)) return ret.toBool(); else return false; } static bool takeFunc(QVariant *v, const JsonPointer::Ref &ref, void *data) { QVariant &ret = *((QVariant *)data); if(ref.type == JsonPointer::Ref::Self) { ret = *v; return false; // technically an error, since we can't remove the root } else if(ref.type == JsonPointer::Ref::Object) { if(typeId(*v) == QMetaType::QVariantHash) { QVariantHash h = v->toHash(); if(h.contains(ref.name)) { ret = h.value(ref.name); h.remove(ref.name); *v = h; return true; } else return false; } else // Map { QVariantMap m = v->toMap(); if(m.contains(ref.name)) { ret = m.value(ref.name); m.remove(ref.name); *v = m; return true; } else return false; } } else // Array { QVariantList l = v->toList(); if(ref.index >= 0 && ref.index < l.count()) { ret = l[ref.index]; l.removeAt(ref.index); *v = l; return true; } else return false; } } QVariant JsonPointer::take() { QVariant ret; if(execute(takeFunc, &ret)) return ret; else return QVariant(); } static bool setValueFunc(QVariant *v, const JsonPointer::Ref &ref, void *_data) { QPair &data = *((QPair *)_data); if(ref.type == JsonPointer::Ref::Self) { *v = data.first; data.second = true; return true; } else if(ref.type == JsonPointer::Ref::Object) { if(typeId(*v) == QMetaType::QVariantHash) { QVariantHash h = v->toHash(); h[ref.name] = data.first; *v = h; data.second = true; return true; } else // Map { QVariantMap m = v->toMap(); m[ref.name] = data.first; *v = m; data.second = true; return true; } } else // Array { QVariantList l = v->toList(); if(ref.index == -1) { // append l += data.first; *v = l; data.second = true; return true; } else if(ref.index >= 0 && ref.index < l.count()) { l[ref.index] = data.first; *v = l; data.second = true; return true; } else return false; } } bool JsonPointer::setValue(const QVariant &value) { QPair data; data.first = value; if(execute(setValueFunc, &data)) return data.second.toBool(); else return false; } bool JsonPointer::isWithin(const QString &bPointerStr, const QString &aPointerStr) { if(!aPointerStr.startsWith('/') || !bPointerStr.startsWith('/') || aPointerStr == bPointerStr) return false; QStringList aParts = aPointerStr.split('/').mid(1); QStringList bParts = bPointerStr.split('/').mid(1); if(aParts.count() >= bParts.count()) return false; for(int n = 0; n < aParts.count(); ++n) { if(aParts[n] != bParts[n]) return false; } return true; } JsonPointer JsonPointer::resolve(QVariant *data, const QString &pointerStr, QString *errorMessage) { if(!pointerStr.startsWith('/')) { if(errorMessage) *errorMessage = "pointer must start with /"; return JsonPointer(); } JsonPointer ptr; ptr.isNull_ = false; ptr.root_ = data; // root if(pointerStr.length() == 1) return ptr; QVariant i = *ptr.root_; QStringList parts = pointerStr.split('/').mid(1); foreach(const QString &part, parts) { if(part.isEmpty()) { if(errorMessage) *errorMessage = "reference cannot be empty"; return JsonPointer(); } QString p = part; p.replace("~1", "/"); p.replace("~0", "~"); // validate and step into previous reference, if any if(!ptr.refs_.isEmpty()) { const Ref &prevRef = ptr.refs_[ptr.refs_.count() - 1]; if(prevRef.type == Ref::Object) { assert(typeId(i) == QMetaType::QVariantHash || typeId(i) == QMetaType::QVariantMap); if(typeId(i) == QMetaType::QVariantHash) { QVariantHash h = i.toHash(); if(!h.contains(prevRef.name)) { if(errorMessage) *errorMessage = QString("cannot step into undefined reference: key=%1").arg(prevRef.name); return JsonPointer(); } i = h[prevRef.name]; } else // Map { QVariantMap m = i.toMap(); if(!m.contains(prevRef.name)) { if(errorMessage) *errorMessage = QString("cannot step into undefined reference: key=%1").arg(prevRef.name); return JsonPointer(); } i = m[prevRef.name]; } } else // Array { QVariantList l = i.toList(); if(prevRef.index < 0 || prevRef.index >= l.count()) { if(errorMessage) *errorMessage = QString("cannot step into undefined reference: index=%1").arg(prevRef.index); return JsonPointer(); } i = l[prevRef.index]; } } if(typeId(i) == QMetaType::QVariantHash || typeId(i) == QMetaType::QVariantMap) { ptr.refs_ += Ref(p); } else if(typeId(i) == QMetaType::QVariantList) { QVariantList l = i.toList(); if(p == "-") { ptr.refs_ += Ref(-1); } else { bool ok; int index = p.toInt(&ok); if(!ok) { if(errorMessage) *errorMessage = "index must be an integer"; return JsonPointer(); } if(index < 0 || index >= l.count()) { if(errorMessage) *errorMessage = "index out of range"; return JsonPointer(); } ptr.refs_ += Ref(index); } } else { if(errorMessage) *errorMessage = "non-container value cannot have child reference"; return JsonPointer(); } } return ptr; } pushpin-1.39.1/src/cpp/handler/jsonpointer.h000066400000000000000000000041361457610542000210140ustar00rootroot00000000000000/* * Copyright (C) 2015-2020 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef JSONPOINTER_H #define JSONPOINTER_H #include #include #include class JsonPointer { public: class Ref { public: enum Type { Self, // used for root Object, Array }; Type type; QString name; int index; Ref() : type(Self), index(-1) { } Ref(const QString &_name) : type(Object), name(_name), index(-1) { } Ref(int _index) : type(Array), index(_index) { } }; typedef void (*ConstFunc)(const QVariant *v, const Ref &ref, void *data); // return true if data was modified typedef bool (*Func)(QVariant *v, const Ref &ref, void *data); JsonPointer(); inline bool isNull() const { return isNull_; } QVariant *root(); bool execute(ConstFunc func, void *data) const; bool execute(Func func, void *data); bool exists() const; QVariant value() const; QVariant take(); bool remove(); bool setValue(const QVariant &value); static bool isWithin(const QString &bPointerStr, const QString &aPointerStr); static JsonPointer resolve(QVariant *data, const QString &pointerStr, QString *errorMessage = 0); private: enum ExecStatus { ExecError, ExecContinue, ExecChanged }; bool isNull_; QVariant *root_; QVarLengthArray refs_; ExecStatus execute(const QVariant *i, int refIndex, ConstFunc func, void *data) const; ExecStatus execute(QVariant *i, int refIndex, Func func, void *data); }; #endif pushpin-1.39.1/src/cpp/handler/lastids.h000066400000000000000000000015521457610542000201040ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef LASTIDS_H #define LASTIDS_H #include #include #include typedef QHash LastIds; Q_DECLARE_METATYPE(LastIds); #endif pushpin-1.39.1/src/cpp/handler/main.h000066400000000000000000000001401457610542000173550ustar00rootroot00000000000000#ifndef HANDLER_MAIN_H #define HANDLER_MAIN_H int handler_main(int argc, char **argv); #endif pushpin-1.39.1/src/cpp/handler/publishformat.cpp000066400000000000000000000304401457610542000216510ustar00rootroot00000000000000/* * Copyright (C) 2016-2020 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "publishformat.h" #include "qtcompat.h" #include "variantutil.h" #include "statusreasons.h" using namespace VariantUtil; PublishFormat PublishFormat::fromVariant(Type type, const QVariant &in, bool *ok, QString *errorMessage) { QString pn; if(type == HttpResponse) pn = "'http-response'"; else if(type == HttpStream) pn = "'http-stream'"; else // WebSocketMessage pn = "'ws-message'"; if(!isKeyedObject(in)) { setError(ok, errorMessage, QString("%1 is not an object").arg(pn)); return PublishFormat(); } PublishFormat out(type); bool ok_; QString action = getString(in, pn, "action", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return PublishFormat(); } if(action == "hint") { out.action = Hint; } else if(action == "close") { out.action = Close; } else if(action == "refresh") { out.action = Refresh; } else if(action.isNull() || action == "send") // default { out.action = Send; } else { if(ok) *ok = false; return PublishFormat(); } if(type == HttpResponse) { if(out.action == Send) { if(keyedObjectContains(in, "code")) { QVariant vcode = keyedObjectGetValue(in, "code"); if(!canConvert(vcode, QMetaType::Int)) { setError(ok, errorMessage, QString("%1 contains 'code' with wrong type").arg(pn)); return PublishFormat(); } out.code = vcode.toInt(); if(out.code < 0 || out.code > 999) { setError(ok, errorMessage, QString("%1 contains 'code' with invalid value").arg(pn)); return PublishFormat(); } } else out.code = 200; QString reasonStr = getString(in, pn, "reason", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return PublishFormat(); } if(!reasonStr.isEmpty()) out.reason = reasonStr.toUtf8(); else out.reason = StatusReasons::getReason(out.code); if(keyedObjectContains(in, "headers")) { QVariant vheaders = keyedObjectGetValue(in, "headers"); if(typeId(vheaders) == QMetaType::QVariantList) { foreach(const QVariant &vheader, vheaders.toList()) { if(typeId(vheader) != QMetaType::QVariantList) { setError(ok, errorMessage, "headers contains element with wrong type"); return PublishFormat(); } QVariantList lheader = vheader.toList(); if(lheader.count() != 2) { setError(ok, errorMessage, "headers contains list with wrong number of elements"); return PublishFormat(); } QString name = getString(lheader[0], &ok_); if(!ok_) { setError(ok, errorMessage, "header contains name element with wrong type"); return PublishFormat(); } QString val = getString(lheader[1], &ok_); if(!ok_) { setError(ok, errorMessage, "header contains value element with wrong type"); return PublishFormat(); } out.headers += HttpHeader(name.toUtf8(), val.toUtf8()); } } else if(isKeyedObject(vheaders)) { if(typeId(vheaders) == QMetaType::QVariantHash) { QVariantHash hheaders = vheaders.toHash(); QHashIterator it(hheaders); while(it.hasNext()) { it.next(); const QString &key = it.key(); const QVariant &vval = it.value(); QString val = getString(vval, &ok_); if(!ok_) { setError(ok, errorMessage, QString("headers contains '%1' with wrong type").arg(key)); return PublishFormat(); } out.headers += HttpHeader(key.toUtf8(), val.toUtf8()); } } else // Map { QVariantMap mheaders = vheaders.toMap(); QMapIterator it(mheaders); while(it.hasNext()) { it.next(); const QString &key = it.key(); const QVariant &vval = it.value(); QString val = getString(vval, &ok_); if(!ok_) { setError(ok, errorMessage, QString("headers contains '%1' with wrong type").arg(key)); return PublishFormat(); } out.headers += HttpHeader(key.toUtf8(), val.toUtf8()); } } } else { setError(ok, errorMessage, QString("%1 contains 'headers' with wrong type").arg(pn)); return PublishFormat(); } } if(keyedObjectContains(in, "content-filters")) { QVariant vfilters = keyedObjectGetValue(in, "content-filters"); if(typeId(vfilters) != QMetaType::QVariantList) { setError(ok, errorMessage, QString("%1 contains 'content-filters' with wrong type").arg(pn)); return PublishFormat(); } QStringList filters; foreach(const QVariant &vfilter, vfilters.toList()) { QString filter = getString(vfilter, &ok_); if(!ok_) { setError(ok, errorMessage, "content-filters contains element with wrong type"); return PublishFormat(); } filters += filter; } out.haveContentFilters = true; out.contentFilters = filters; } if(typeId(in) == QMetaType::QVariantMap && keyedObjectContains(in, "body-bin")) // JSON input { QString bodyBin = getString(in, pn, "body-bin", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return PublishFormat(); } out.body = QByteArray::fromBase64(bodyBin.toUtf8()); } else if(keyedObjectContains(in, "body")) { QVariant vcontent = keyedObjectGetValue(in, "body"); if(typeId(vcontent) == QMetaType::QByteArray) out.body = vcontent.toByteArray(); else if(typeId(vcontent) == QMetaType::QString) out.body = vcontent.toString().toUtf8(); else { setError(ok, errorMessage, QString("%1 contains 'body' with wrong type").arg(pn)); return PublishFormat(); } } else if(keyedObjectContains(in, "body-patch")) { out.bodyPatch = getList(in, pn, "body-patch", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return PublishFormat(); } out.haveBodyPatch = true; } else { if(typeId(in) == QMetaType::QVariantMap) // JSON input setError(ok, errorMessage, QString("%1 does not contain 'body', 'body-bin', or 'body-patch'").arg(pn)); else setError(ok, errorMessage, QString("%1 does not contain 'body' or 'body-patch'").arg(pn)); return PublishFormat(); } } } else if(type == HttpStream) { if(out.action == Send) { if(keyedObjectContains(in, "content-filters")) { QVariant vfilters = keyedObjectGetValue(in, "content-filters"); if(typeId(vfilters) != QMetaType::QVariantList) { setError(ok, errorMessage, QString("%1 contains 'content-filters' with wrong type").arg(pn)); return PublishFormat(); } QStringList filters; foreach(const QVariant &vfilter, vfilters.toList()) { QString filter = getString(vfilter, &ok_); if(!ok_) { setError(ok, errorMessage, "content-filters contains element with wrong type"); return PublishFormat(); } filters += filter; } out.haveContentFilters = true; out.contentFilters = filters; } if(typeId(in) == QMetaType::QVariantMap && keyedObjectContains(in, "content-bin")) // JSON input { QString contentBin = getString(in, pn, "content-bin", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return PublishFormat(); } out.body = QByteArray::fromBase64(contentBin.toUtf8()); } else if(keyedObjectContains(in, "content")) { QVariant vcontent = keyedObjectGetValue(in, "content"); if(typeId(vcontent) == QMetaType::QByteArray) out.body = vcontent.toByteArray(); else if(typeId(vcontent) == QMetaType::QString) out.body = vcontent.toString().toUtf8(); else { setError(ok, errorMessage, QString("%1 contains 'content' with wrong type").arg(pn)); return PublishFormat(); } } else { if(typeId(in) == QMetaType::QVariantMap) // JSON input setError(ok, errorMessage, QString("%1 does not contain 'content' or 'content-bin'").arg(pn)); else setError(ok, errorMessage, QString("%1 does not contain 'content'").arg(pn)); return PublishFormat(); } } } else if(type == WebSocketMessage) { if(out.action == Send) { QString typeStr = getString(in, pn, "type", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return PublishFormat(); } if(!typeStr.isNull()) { if(typeStr == "text") out.messageType = Text; else if(typeStr == "binary") out.messageType = Binary; else if(typeStr == "ping") out.messageType = Ping; else if(typeStr == "pong") out.messageType = Pong; else { setError(ok, errorMessage, QString("%1 contains 'type' with unknown value").arg(pn)); return PublishFormat(); } } if(keyedObjectContains(in, "content-filters")) { QVariant vfilters = keyedObjectGetValue(in, "content-filters"); if(typeId(vfilters) != QMetaType::QVariantList) { setError(ok, errorMessage, QString("%1 contains 'content-filters' with wrong type").arg(pn)); return PublishFormat(); } QStringList filters; foreach(const QVariant &vfilter, vfilters.toList()) { QString filter = getString(vfilter, &ok_); if(!ok_) { setError(ok, errorMessage, "content-filters contains element with wrong type"); return PublishFormat(); } filters += filter; } out.haveContentFilters = true; out.contentFilters = filters; } if(keyedObjectContains(in, "content-bin")) { QVariant vcontentBin = keyedObjectGetValue(in, "content-bin"); if(typeId(in) == QMetaType::QVariantMap) // JSON input { if(typeId(vcontentBin) != QMetaType::QString) { setError(ok, errorMessage, QString("%1 contains 'content-bin' with wrong type").arg(pn)); return PublishFormat(); } out.body = QByteArray::fromBase64(vcontentBin.toString().toUtf8()); } else { if(typeId(vcontentBin) != QMetaType::QByteArray) { setError(ok, errorMessage, QString("%1 contains 'content-bin' with wrong type").arg(pn)); return PublishFormat(); } out.body = vcontentBin.toByteArray(); } if(((int)out.messageType) == -1) out.messageType = Binary; } else if(keyedObjectContains(in, "content")) { QVariant vcontent = keyedObjectGetValue(in, "content"); if(typeId(vcontent) == QMetaType::QByteArray) out.body = vcontent.toByteArray(); else if(typeId(vcontent) == QMetaType::QString) out.body = vcontent.toString().toUtf8(); else { setError(ok, errorMessage, QString("%1 contains 'content' with wrong type").arg(pn)); return PublishFormat(); } if(((int)out.messageType) == -1) out.messageType = Text; } else if(out.messageType == Text || out.messageType == Binary || ((int)out.messageType) == -1) { setError(ok, errorMessage, QString("%1 does not contain 'content' or 'content-bin'").arg(pn)); return PublishFormat(); } } else if(out.action == Close) { if(keyedObjectContains(in, "code")) { QVariant vcode = keyedObjectGetValue(in, "code"); if(!canConvert(vcode, QMetaType::Int)) { setError(ok, errorMessage, QString("%1 contains 'code' with wrong type").arg(pn)); return PublishFormat(); } out.code = vcode.toInt(); if(out.code < 0) { setError(ok, errorMessage, QString("%1 contains 'code' with invalid value").arg(pn)); return PublishFormat(); } } QString reasonStr = getString(in, pn, "reason", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return PublishFormat(); } if(!reasonStr.isEmpty()) out.reason = reasonStr.toUtf8(); } } if(ok) *ok = true; return out; } pushpin-1.39.1/src/cpp/handler/publishformat.h000066400000000000000000000035011457610542000213140ustar00rootroot00000000000000/* * Copyright (C) 2016-2020 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef PUBLISHFORMAT_H #define PUBLISHFORMAT_H #include #include #include #include "httpheaders.h" class PublishFormat { public: enum Type { HttpResponse, HttpStream, WebSocketMessage }; enum Action { Send, Hint, Refresh, Close }; enum MessageType { Text, Binary, Ping, Pong }; Type type; Action action; // response/stream/ws int code; // response/ws QByteArray reason; // response/ws HttpHeaders headers; // response QByteArray body; // response/stream/ws bool haveBodyPatch; // response QVariantList bodyPatch; // response MessageType messageType; // ws bool haveContentFilters; QStringList contentFilters; // response/stream/ws PublishFormat() : type((Type)-1), action(Send), code(-1), haveBodyPatch(false), messageType((MessageType)-1), haveContentFilters(false) { } PublishFormat(Type _type) : type(_type), action(Send), code(-1), haveBodyPatch(false), messageType((MessageType)-1), haveContentFilters(false) { } static PublishFormat fromVariant(Type type, const QVariant &in, bool *ok = 0, QString *errorMessage = 0); }; #endif pushpin-1.39.1/src/cpp/handler/publishitem.cpp000066400000000000000000000120441457610542000213170ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "publishitem.h" #include "qtcompat.h" #include "variantutil.h" using namespace VariantUtil; PublishItem PublishItem::fromVariant(const QVariant &vitem, const QString &channel, bool *ok, QString *errorMessage) { QString pn = "publish item object"; if(!isKeyedObject(vitem)) { setError(ok, errorMessage, QString("%1 is not an object").arg(pn)); return PublishItem(); } PublishItem item; bool ok_; if(!channel.isEmpty()) { item.channel = channel; } else { item.channel = getString(vitem, pn, "channel", true, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return PublishItem(); } } item.id = getString(vitem, pn, "id", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return PublishItem(); } item.prevId = getString(vitem, pn, "prev-id", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return PublishItem(); } QVariant vformats = getKeyedObject(vitem, pn, "formats", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return PublishItem(); } if(!vformats.isValid()) { vformats = createSameKeyedObject(vitem); QVariant v = keyedObjectGetValue(vitem, "http-response"); if(v.isValid()) keyedObjectInsert(&vformats, "http-response", v); v = keyedObjectGetValue(vitem, "http-stream"); if(v.isValid()) keyedObjectInsert(&vformats, "http-stream", v); v = keyedObjectGetValue(vitem, "ws-message"); if(v.isValid()) keyedObjectInsert(&vformats, "ws-message", v); } if(keyedObjectIsEmpty(vformats)) { setError(ok, errorMessage, "no formats specified"); return PublishItem(); } if(keyedObjectContains(vformats, "http-response")) { PublishFormat f = PublishFormat::fromVariant(PublishFormat::HttpResponse, keyedObjectGetValue(vformats, "http-response"), &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return PublishItem(); } item.formats.insert(f.type, f); } if(keyedObjectContains(vformats, "http-stream")) { PublishFormat f = PublishFormat::fromVariant(PublishFormat::HttpStream, keyedObjectGetValue(vformats, "http-stream"), &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return PublishItem(); } item.formats.insert(f.type, f); } if(keyedObjectContains(vformats, "ws-message")) { PublishFormat f = PublishFormat::fromVariant(PublishFormat::WebSocketMessage, keyedObjectGetValue(vformats, "ws-message"), &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return PublishItem(); } item.formats.insert(f.type, f); } QVariant vmeta = getKeyedObject(vitem, pn, "meta", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return PublishItem(); } if(vmeta.isValid()) { if(typeId(vmeta) == QMetaType::QVariantHash) { QVariantHash hmeta = vmeta.toHash(); QHashIterator it(hmeta); while(it.hasNext()) { it.next(); const QString &key = it.key(); const QVariant &vval = it.value(); QString val = getString(vval, &ok_); if(!ok_) { setError(ok, errorMessage, QString("'meta' contains '%1' with wrong type").arg(key)); return PublishItem(); } item.meta[key] = val; } } else // Map { QVariantMap mmeta = vmeta.toMap(); QMapIterator it(mmeta); while(it.hasNext()) { it.next(); const QString &key = it.key(); const QVariant &vval = it.value(); QString val = getString(vval, &ok_); if(!ok_) { setError(ok, errorMessage, QString("'meta' contains '%1' with wrong type").arg(key)); return PublishItem(); } item.meta[key] = val; } } } if(keyedObjectContains(vitem, "size")) { QVariant vsize = keyedObjectGetValue(vitem, "size"); if(!canConvert(vsize, QMetaType::Int)) { setError(ok, errorMessage, QString("%1 contains 'size' with wrong type").arg(pn)); return PublishItem(); } item.size = vsize.toInt(); if(item.size < 0) { setError(ok, errorMessage, QString("%1 contains 'size' with invalid value").arg(pn)); return PublishItem(); } } if(keyedObjectContains(vitem, "no-seq")) { QVariant vnoSeq = keyedObjectGetValue(vitem, "no-seq"); if(typeId(vnoSeq) != QMetaType::Bool) { setError(ok, errorMessage, QString("%1 contains 'no-seq' with wrong type").arg(pn)); return PublishItem(); } item.noSeq = vnoSeq.toBool(); } setSuccess(ok, errorMessage); return item; } pushpin-1.39.1/src/cpp/handler/publishitem.h000066400000000000000000000023541457610542000207670ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef PUBLISHITEM_H #define PUBLISHITEM_H #include #include #include #include "publishformat.h" class PublishItem { public: QString channel; QString id; QString prevId; QHash formats; QHash meta; int size; bool noSeq; PublishFormat format; // for single format items PublishItem() : size(-1), noSeq(false) { } static PublishItem fromVariant(const QVariant &vitem, const QString &channel = QString(), bool *ok = 0, QString *errorMessage = 0); }; #endif pushpin-1.39.1/src/cpp/handler/publishlastids.cpp000066400000000000000000000036741457610542000220350ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "publishlastids.h" #include PublishLastIds::PublishLastIds(int maxCapacity) : maxCapacity_(maxCapacity) { } void PublishLastIds::set(const QString &channel, const QString &id) { QDateTime now = QDateTime::currentDateTimeUtc(); if(table_.contains(channel)) { Item &i = table_[channel]; recentlyUsed_.remove(TimeStringPair(i.time, channel)); i.id = id; i.time = now; recentlyUsed_.insert(TimeStringPair(i.time, channel), i); } else { while(!table_.isEmpty() && table_.count() >= maxCapacity_) { // remove oldest QMutableMapIterator it(recentlyUsed_); assert(it.hasNext()); it.next(); QString channel = it.value().channel; it.remove(); table_.remove(channel); } Item i; i.channel = channel; i.id = id; i.time = now; table_.insert(channel, i); recentlyUsed_.insert(TimeStringPair(i.time, channel), i); } } void PublishLastIds::remove(const QString &channel) { if(table_.contains(channel)) { Item &i = table_[channel]; recentlyUsed_.remove(TimeStringPair(i.time, channel)); table_.remove(channel); } } void PublishLastIds::clear() { recentlyUsed_.clear(); table_.clear(); } QString PublishLastIds::value(const QString &channel) { return table_.value(channel).id; } pushpin-1.39.1/src/cpp/handler/publishlastids.h000066400000000000000000000024401457610542000214700ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef PUBLISHLASTIDS_H #define PUBLISHLASTIDS_H #include #include #include #include // cache with LRU expiration class PublishLastIds { public: PublishLastIds(int maxCapacity); void set(const QString &channel, const QString &id); void remove(const QString &channel); void clear(); QString value(const QString &channel); private: typedef QPair TimeStringPair; class Item { public: QString channel; QString id; QDateTime time; }; QHash table_; QMap recentlyUsed_; int maxCapacity_; }; #endif pushpin-1.39.1/src/cpp/handler/ratelimiter.cpp000066400000000000000000000130631457610542000213150ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "ratelimiter.h" #include #include #include #include #define MIN_BATCH_INTERVAL 25 class RateLimiter::Private : public QObject { Q_OBJECT public: class ActionItem { public: Action *action; int weight; ActionItem(Action *_action = 0, int _weight = 0) : action(_action), weight(_weight) { } }; class Bucket { public: QList actions; int weight; int debt; Bucket() : weight(0), debt(0) { } ~Bucket() { foreach(const ActionItem &i, actions) delete i.action; } }; int rate; int hwm; bool batchWaitEnabled; QMap buckets; QString lastKey; QTimer *timer; bool firstPass; int batchInterval; int batchSize; bool lastBatchEmpty; Private(QObject *_q) : QObject(_q), rate(-1), hwm(-1), batchWaitEnabled(false), batchInterval(-1), batchSize(-1), lastBatchEmpty(false) { timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &Private::timeout); } ~Private() { timer->disconnect(this); timer->setParent(0); timer->deleteLater(); } void setRate(int actionsPerSecond) { if(actionsPerSecond > 0) { rate = actionsPerSecond; if(rate >= 1000 / MIN_BATCH_INTERVAL) { batchInterval = MIN_BATCH_INTERVAL; batchSize = (rate * batchInterval + 999) / 1000; } else { batchInterval = 1000 / rate; batchSize = 1; } } else { rate = -1; batchInterval = -1; batchSize = -1; } setup(); } bool addAction(const QString &key, int weight, Action *action) { Bucket &bucket = buckets[key]; if(hwm > 0 && bucket.weight + weight > hwm) return false; bucket.actions += ActionItem(action, weight); bucket.weight += weight; setup(); return true; } private: void setup() { if(rate > 0) { if(!buckets.isEmpty() || !lastBatchEmpty) { if(timer->isActive()) { // after the first pass, switch to batch interval if(!firstPass) timer->setInterval(batchInterval); } else { // process first batch firstPass = true; if(batchWaitEnabled) { // if wait enabled, collect for awhile before processing timer->start(batchInterval); } else { // if wait not enabled, process immediately timer->start(0); } } } else { if(lastBatchEmpty) { // if we processed nothing on this pass, stop timer lastBatchEmpty = false; timer->stop(); } } } else { if(!buckets.isEmpty()) { if(timer->isActive()) { // ensure we're on fastest interval timer->setInterval(0); } else { // process first batch right away firstPass = true; timer->start(0); } } else { timer->stop(); } } } // return false if self destroyed bool processBatch() { if(buckets.isEmpty()) { lastBatchEmpty = true; return true; } lastBatchEmpty = false; QMap::iterator it; if(!lastKey.isNull()) { it = buckets.find(lastKey); if(it == buckets.end()) it = buckets.begin(); } else { it = buckets.begin(); } QPointer self = this; int processed = 0; while((batchSize < 1 || processed < batchSize) && it != buckets.end()) { Bucket &bucket = it.value(); QString key = it.key(); if(bucket.debt <= 0) { ActionItem ai = bucket.actions.takeFirst(); Action *action = ai.action; int weight = ai.weight; bucket.weight -= weight; bool ret = action->execute(); delete action; if(!self) return false; if(ret) { if(weight > 1) processed += weight; else ++processed; if(batchSize >= 1 && processed > batchSize) { bucket.debt += processed - batchSize; } } } else { --bucket.debt; ++processed; } if(bucket.actions.isEmpty() && bucket.debt <= 0) { lastKey = key; it = buckets.erase(it); } else { ++it; if(it == buckets.end()) it = buckets.begin(); } } if(it != buckets.end()) lastKey = it.key(); return true; } private slots: void timeout() { if(!processBatch()) return; firstPass = false; setup(); } }; RateLimiter::RateLimiter() { d = std::make_unique(this); } RateLimiter::~RateLimiter() = default; void RateLimiter::setRate(int actionsPerSecond) { d->setRate(actionsPerSecond); } void RateLimiter::setHwm(int hwm) { d->hwm = hwm; } void RateLimiter::setBatchWaitEnabled(bool on) { d->batchWaitEnabled = on; } bool RateLimiter::addAction(const QString &key, Action *action, int weight) { return d->addAction(key, weight, action); } RateLimiter::Action *RateLimiter::lastAction(const QString &key) const { if(d->buckets.contains(key)) { const Private::Bucket &bucket = d->buckets[key]; if(!bucket.actions.isEmpty()) return bucket.actions.last().action; } return 0; } #include "ratelimiter.moc" pushpin-1.39.1/src/cpp/handler/ratelimiter.h000066400000000000000000000023261457610542000207620ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef RATELIMITER_H #define RATELIMITER_H #include #include class RateLimiter : public QObject { Q_OBJECT public: class Action { public: virtual ~Action() {} virtual bool execute() = 0; }; RateLimiter(); ~RateLimiter(); void setRate(int actionsPerSecond); void setHwm(int hwm); void setBatchWaitEnabled(bool on); bool addAction(const QString &key, Action *action, int weight = 1); Action *lastAction(const QString &key) const; private: class Private; std::unique_ptr d; }; #endif pushpin-1.39.1/src/cpp/handler/refreshworker.cpp000066400000000000000000000047261457610542000216720ustar00rootroot00000000000000/* * Copyright (C) 2017-2020 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "refreshworker.h" #include "qtcompat.h" #include "zrpcrequest.h" #include "controlrequest.h" #include "statsmanager.h" #include "wssession.h" RefreshWorker::RefreshWorker(ZrpcRequest *req, ZrpcManager *proxyControlClient, QHash > *wsSessionsByChannel, QObject *parent) : Deferred(parent), ignoreErrors_(false), proxyControlClient_(proxyControlClient), req_(req) { req_->setParent(this); QVariantHash args = req_->args(); if(args.contains("cid")) { if(typeId(args["cid"]) != QMetaType::QByteArray) { respondError("bad-request"); return; } cids_ += QString::fromUtf8(args["cid"].toByteArray()); refreshNextCid(); } else if(args.contains("channel")) { if(typeId(args["channel"]) != QMetaType::QByteArray) { respondError("bad-request"); return; } QString channel = QString::fromUtf8(args["channel"].toByteArray()); QSet wsbc = wsSessionsByChannel->value(channel); foreach(WsSession *s, wsbc) { cids_ += s->cid; } ignoreErrors_ = true; refreshNextCid(); } else { respondError("bad-request"); return; } } void RefreshWorker::respondError(const QByteArray &condition) { req_->respondError(condition); setFinished(true); } void RefreshWorker::refreshNextCid() { if(cids_.isEmpty()) { req_->respond(); setFinished(true); return; } Deferred *d = ControlRequest::refresh(proxyControlClient_, cids_.takeFirst().toUtf8(), this); finishedConnection_ = d->finished.connect(boost::bind(&RefreshWorker::proxyRefresh_finished, this, boost::placeholders::_1)); } void RefreshWorker::proxyRefresh_finished(const DeferredResult &result) { if(result.success || ignoreErrors_) { refreshNextCid(); } else { respondError(result.value.toByteArray()); } } pushpin-1.39.1/src/cpp/handler/refreshworker.h000066400000000000000000000027311457610542000213310ustar00rootroot00000000000000/* * Copyright (C) 2017-2020 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef REFRESHWORKER_H #define REFRESHWORKER_H #include #include #include #include "deferred.h" #include using Connection = boost::signals2::scoped_connection; class ZrpcRequest; class ZrpcManager; class StatsManager; class WsSession; class RefreshWorker : public Deferred { Q_OBJECT public: RefreshWorker(ZrpcRequest *req, ZrpcManager *proxyControlClient, QHash > *wsSessionsByChannel, QObject *parent = 0); private: QStringList cids_; bool ignoreErrors_; ZrpcManager *proxyControlClient_; ZrpcRequest *req_; Connection finishedConnection_; void refreshNextCid(); void respondError(const QByteArray &condition); private slots: void proxyRefresh_finished(const DeferredResult &result); }; #endif pushpin-1.39.1/src/cpp/handler/requeststate.cpp000066400000000000000000000072361457610542000215320ustar00rootroot00000000000000/* * Copyright (C) 2016-2023 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "requeststate.h" #include "qtcompat.h" RequestState RequestState::fromVariant(const QVariant &in) { if(typeId(in) != QMetaType::QVariantHash) return RequestState(); QVariantHash r = in.toHash(); RequestState rs; if(!r.contains("rid") || typeId(r["rid"]) != QMetaType::QVariantHash) return RequestState(); QVariantHash vrid = r["rid"].toHash(); if(!vrid.contains("sender") || typeId(vrid["sender"]) != QMetaType::QByteArray) return RequestState(); if(!vrid.contains("id") || typeId(vrid["id"]) != QMetaType::QByteArray) return RequestState(); rs.rid = ZhttpRequest::Rid(vrid["sender"].toByteArray(), vrid["id"].toByteArray()); if(!r.contains("in-seq") || !canConvert(r["in-seq"], QMetaType::Int)) return RequestState(); rs.inSeq = r["in-seq"].toInt(); if(!r.contains("out-seq") || !canConvert(r["out-seq"], QMetaType::Int)) return RequestState(); rs.outSeq = r["out-seq"].toInt(); if(!r.contains("out-credits") || !canConvert(r["out-credits"], QMetaType::Int)) return RequestState(); rs.outCredits = r["out-credits"].toInt(); if(r.contains("response-code")) { if(!canConvert(r["response-code"], QMetaType::Int)) return RequestState(); rs.responseCode = r["response-code"].toInt(); } if(r.contains("peer-address")) { if(typeId(r["peer-address"]) != QMetaType::QByteArray) return RequestState(); if(!rs.peerAddress.setAddress(QString::fromUtf8(r["peer-address"].toByteArray()))) return RequestState(); } if(r.contains("logical-peer-address")) { if(typeId(r["logical-peer-address"]) != QMetaType::QByteArray) return RequestState(); if(!rs.logicalPeerAddress.setAddress(QString::fromUtf8(r["logical-peer-address"].toByteArray()))) return RequestState(); } if(r.contains("https")) { if(typeId(r["https"]) != QMetaType::Bool) return RequestState(); rs.isHttps = r["https"].toBool(); } if(r.contains("debug")) { if(typeId(r["debug"]) != QMetaType::Bool) return RequestState(); rs.debug = r["debug"].toBool(); } if(r.contains("is-retry")) { if(typeId(r["is-retry"]) != QMetaType::Bool) return RequestState(); rs.isRetry = r["is-retry"].toBool(); } if(r.contains("auto-cross-origin")) { if(typeId(r["auto-cross-origin"]) != QMetaType::Bool) return RequestState(); rs.autoCrossOrigin = r["auto-cross-origin"].toBool(); } if(r.contains("jsonp-callback")) { if(typeId(r["jsonp-callback"]) != QMetaType::QByteArray) return RequestState(); rs.jsonpCallback = r["jsonp-callback"].toByteArray(); } if(r.contains("jsonp-extended-response")) { if(typeId(r["jsonp-extended-response"]) != QMetaType::Bool) return RequestState(); rs.jsonpExtendedResponse = r["jsonp-extended-response"].toBool(); } if(r.contains("unreported-time")) { if(!canConvert(r["unreported-time"], QMetaType::Int)) return RequestState(); rs.unreportedTime = r["unreported-time"].toInt(); } if(r.contains("user-data")) { rs.userData = r["user-data"]; } return rs; } pushpin-1.39.1/src/cpp/handler/requeststate.h000066400000000000000000000026741457610542000212000ustar00rootroot00000000000000/* * Copyright (C) 2016-2023 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef REQUESTSTATE_H #define REQUESTSTATE_H #include #include #include #include "zhttprequest.h" class RequestState { public: ZhttpRequest::Rid rid; int responseCode; int inSeq; int outSeq; int outCredits; QHostAddress peerAddress; QHostAddress logicalPeerAddress; bool isHttps; bool debug; bool isRetry; bool autoCrossOrigin; QByteArray jsonpCallback; bool jsonpExtendedResponse; int unreportedTime; QVariant userData; RequestState() : responseCode(-1), inSeq(0), outSeq(0), outCredits(0), isHttps(false), debug(false), isRetry(false), autoCrossOrigin(false), jsonpExtendedResponse(false), unreportedTime(-1) { } static RequestState fromVariant(const QVariant &in); }; #endif pushpin-1.39.1/src/cpp/handler/sequencer.cpp000066400000000000000000000153711457610542000207720ustar00rootroot00000000000000/* * Copyright (C) 2016-2021 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "sequencer.h" #include #include #include "log.h" #include "publishitem.h" #include "publishlastids.h" #define CHANNEL_PENDING_MAX 100 #define DEFAULT_PENDING_EXPIRE 5000 #define EXPIRE_INTERVAL 1000 class Sequencer::Private : public QObject { Q_OBJECT public: class PendingItem { public: qint64 time; PublishItem item; }; class ChannelPendingItems { public: QHash itemsByPrevId; ~ChannelPendingItems() { qDeleteAll(itemsByPrevId); } }; class CachedId { public: qint64 expireTime; QString channel; QString id; }; Sequencer *q; PublishLastIds *lastIds; QHash pendingItemsByChannel; QMap, PendingItem*> pendingItemsByTime; QTimer *expireTimer; int pendingExpireMSecs; int idCacheTtl; QHash, CachedId*> idCacheById; QMap, CachedId*> idCacheByExpireTime; Private(Sequencer *_q, PublishLastIds *_publishLastIds) : QObject(_q), q(_q), lastIds(_publishLastIds), pendingExpireMSecs(DEFAULT_PENDING_EXPIRE), idCacheTtl(-1) { expireTimer = new QTimer(this); connect(expireTimer, &QTimer::timeout, this, &Private::expireTimer_timeout); } ~Private() { expireTimer->disconnect(this); expireTimer->setParent(0); expireTimer->deleteLater(); qDeleteAll(idCacheById); } void addItem(const PublishItem &item, bool seq) { qint64 now = QDateTime::currentMSecsSinceEpoch(); while(!idCacheByExpireTime.isEmpty()) { QMap, CachedId*>::iterator it = idCacheByExpireTime.begin(); CachedId *i = it.value(); if(i->expireTime > now) break; idCacheById.remove(QPair(i->channel, i->id)); idCacheByExpireTime.erase(it); delete i; } if(!item.id.isNull() && idCacheTtl > 0) { QPair idKey(item.channel, item.id); if(idCacheById.contains(idKey)) { // we've seen this ID recently, drop the message return; } CachedId *i = new CachedId; i->expireTime = now + (idCacheTtl * 1000); i->channel = item.channel; i->id = item.id; idCacheById.insert(idKey, i); idCacheByExpireTime.insert(QPair(i->expireTime, i), i); } if(!seq) { q->itemReady(item); return; } QString lastId = lastIds->value(item.channel); if(!lastId.isNull() && !item.prevId.isNull() && lastId != item.prevId) { ChannelPendingItems &channelPendingItems = pendingItemsByChannel[item.channel]; if(channelPendingItems.itemsByPrevId.contains(item.prevId)) { log_debug("sequencer: already have item for channel [%s] depending on prev-id [%s], dropping", qPrintable(item.channel), qPrintable(item.prevId)); return; } if(channelPendingItems.itemsByPrevId.count() >= CHANNEL_PENDING_MAX) { log_debug("sequencer: too many pending items for channel [%s], dropping", qPrintable(item.channel)); return; } PendingItem *i = new PendingItem; i->time = now; i->item = item; channelPendingItems.itemsByPrevId.insert(item.prevId, i); pendingItemsByTime.insert(QPair(i->time, i), i); if(!expireTimer->isActive()) expireTimer->start(EXPIRE_INTERVAL); return; } sendItem(item); } void clear(const QString &channel) { if(!pendingItemsByChannel.contains(channel)) return; ChannelPendingItems &channelPendingItems = pendingItemsByChannel[channel]; QHashIterator it(channelPendingItems.itemsByPrevId); while(it.hasNext()) { it.next(); PendingItem *i = it.value(); pendingItemsByTime.remove(QPair(i->time, i)); } pendingItemsByChannel.remove(channel); } void sendItem(const PublishItem &item) { if(!item.id.isNull()) lastIds->set(item.channel, item.id); else lastIds->remove(item.channel); q->itemReady(item); if(pendingItemsByChannel.contains(item.channel)) { ChannelPendingItems &channelPendingItems = pendingItemsByChannel[item.channel]; QString id = item.id; while(!id.isNull() && !channelPendingItems.itemsByPrevId.isEmpty()) { PendingItem *i = channelPendingItems.itemsByPrevId.value(id); if(!i) break; PublishItem pitem = i->item; channelPendingItems.itemsByPrevId.remove(i->item.prevId); pendingItemsByTime.remove(QPair(i->time, i)); delete i; if(!pitem.id.isNull()) lastIds->set(pitem.channel, pitem.id); else lastIds->remove(pitem.channel); q->itemReady(pitem); id = pitem.id; } if(channelPendingItems.itemsByPrevId.isEmpty()) { pendingItemsByChannel.remove(item.channel); if(pendingItemsByChannel.isEmpty()) expireTimer->stop(); } } } private slots: void expireTimer_timeout() { qint64 now = QDateTime::currentMSecsSinceEpoch(); qint64 threshold = now - pendingExpireMSecs; while(!pendingItemsByTime.isEmpty()) { QMap, PendingItem*>::iterator it = pendingItemsByTime.begin(); PendingItem *i = it.value(); if(i->time > threshold) break; log_debug("timing out item channel=[%s] id=[%s]", qPrintable(i->item.channel), qPrintable(i->item.id)); PublishItem item = i->item; ChannelPendingItems &channelPendingItems = pendingItemsByChannel[i->item.channel]; channelPendingItems.itemsByPrevId.remove(i->item.prevId); pendingItemsByTime.erase(it); delete i; if(channelPendingItems.itemsByPrevId.isEmpty()) { pendingItemsByChannel.remove(item.channel); if(pendingItemsByChannel.isEmpty()) expireTimer->stop(); } sendItem(item); } } }; Sequencer::Sequencer(PublishLastIds *publishLastIds, QObject *parent) : QObject(parent) { d = new Private(this, publishLastIds); } Sequencer::~Sequencer() { delete d; } void Sequencer::setWaitMax(int msecs) { d->pendingExpireMSecs = msecs; } void Sequencer::setIdCacheTtl(int secs) { d->idCacheTtl = secs; } void Sequencer::addItem(const PublishItem &item, bool seq) { d->addItem(item, seq); } void Sequencer::clearPendingForChannel(const QString &channel) { d->clear(channel); } #include "sequencer.moc" pushpin-1.39.1/src/cpp/handler/sequencer.h000066400000000000000000000025131457610542000204310ustar00rootroot00000000000000/* * Copyright (C) 2016-2021 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef SEQUENCER_H #define SEQUENCER_H #include #include class PublishLastIds; class PublishItem; class Sequencer : public QObject { Q_OBJECT public: Sequencer(PublishLastIds *publishLastIds, QObject *parent = 0); ~Sequencer(); void setWaitMax(int msecs); void setIdCacheTtl(int secs); // seq = false means ID cache handling only // note: may emit signals void addItem(const PublishItem &item, bool seq = true); void clearPendingForChannel(const QString &channel); boost::signals2::signal itemReady; private: class Private; friend class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/handler/sessionrequest.cpp000066400000000000000000000171631457610542000220750ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "sessionrequest.h" #include #include #include "qtcompat.h" #include "zrpcmanager.h" #include "zrpcrequest.h" #include "deferred.h" #include "detectrule.h" namespace SessionRequest { class DetectRulesSet : public Deferred { Q_OBJECT Connection finishedConnection; public: DetectRulesSet(ZrpcManager *stateClient, const QList &rules, QObject *parent = 0) : Deferred(parent) { ZrpcRequest *req = new ZrpcRequest(stateClient, this); finishedConnection = req->finished.connect(boost::bind(&DetectRulesSet::req_finished, this, req)); QVariantList rlist; foreach(const DetectRule &rule, rules) { QVariantHash i; i["domain"] = rule.domain.toUtf8(); i["path-prefix"] = rule.pathPrefix; i["sid-ptr"] = rule.sidPtr.toUtf8(); if(!rule.jsonParam.isEmpty()) i["json-param"] = rule.jsonParam.toUtf8(); rlist += i; } QVariantHash args; args["rules"] = rlist; req->start("session-detect-rules-set", args); } private: void req_finished(ZrpcRequest *req) { if(req->success()) { setFinished(true); } else { setFinished(false, req->errorCondition()); } } }; class DetectRulesGet : public Deferred { Q_OBJECT Connection finishedConnection; public: DetectRulesGet(ZrpcManager *stateClient, const QString &domain, const QByteArray &path, QObject *parent = 0) : Deferred(parent) { ZrpcRequest *req = new ZrpcRequest(stateClient, this); finishedConnection = req->finished.connect(boost::bind(&DetectRulesGet::req_finished, this, req)); QVariantHash args; args["domain"] = domain.toUtf8(); args["path"] = path; req->start("session-detect-rules-get", args); } private: void req_finished(ZrpcRequest *req) { if(req->success()) { QVariant vresult = req->result(); if(typeId(vresult) != QMetaType::QVariantList) { setFinished(false); return; } QVariantList result = vresult.toList(); QList rules; foreach(const QVariant &vr, result) { if(typeId(vr) != QMetaType::QVariantHash) { setFinished(false); return; } QVariantHash r = vr.toHash(); DetectRule rule; if(!r.contains("domain") || typeId(r["domain"]) != QMetaType::QByteArray) { setFinished(false); return; } rule.domain = QString::fromUtf8(r["domain"].toByteArray()); if(!r.contains("path-prefix") || typeId(r["path-prefix"]) != QMetaType::QByteArray) { setFinished(false); return; } rule.pathPrefix = r["path-prefix"].toByteArray(); if(!r.contains("sid-ptr") || typeId(r["sid-ptr"]) != QMetaType::QByteArray) { setFinished(false); return; } rule.sidPtr = QString::fromUtf8(r["sid-ptr"].toByteArray()); if(r.contains("json-param")) { if(typeId(r["json-param"]) != QMetaType::QByteArray) { setFinished(false); return; } rule.jsonParam = QString::fromUtf8(r["json-param"].toByteArray()); } rules += rule; } setFinished(true, QVariant::fromValue(rules)); } else { setFinished(false, req->errorCondition()); } } }; class CreateOrUpdate : public Deferred { Q_OBJECT Connection finishedConnection; public: CreateOrUpdate(ZrpcManager *stateClient, const QString &sid, const LastIds &lastIds, QObject *parent = 0) : Deferred(parent) { ZrpcRequest *req = new ZrpcRequest(stateClient, this); finishedConnection = req->finished.connect(boost::bind(&CreateOrUpdate::req_finished, this, req)); QVariantHash args; args["sid"] = sid.toUtf8(); QVariantHash vlastIds; QHashIterator it(lastIds); while(it.hasNext()) { it.next(); vlastIds.insert(it.key(), it.value().toUtf8()); } args["last-ids"] = vlastIds; req->start("session-create-or-update", args); } private: void req_finished(ZrpcRequest *req) { if(req->success()) { setFinished(true); } else { setFinished(false, req->errorCondition()); } } }; class UpdateMany : public Deferred { Q_OBJECT Connection finishedConnection; public: UpdateMany(ZrpcManager *stateClient, const QHash &sidLastIds, QObject *parent = 0) : Deferred(parent) { ZrpcRequest *req = new ZrpcRequest(stateClient, this); finishedConnection = req->finished.connect(boost::bind(&UpdateMany::req_finished, this, req)); QVariantHash vsidLastIds; QHashIterator it(sidLastIds); while(it.hasNext()) { it.next(); const QString &sid = it.key(); const LastIds &lastIds = it.value(); QVariantHash vlastIds; QHashIterator it(lastIds); while(it.hasNext()) { it.next(); vlastIds.insert(it.key(), it.value().toUtf8()); } vsidLastIds.insert(sid, vlastIds); } QVariantHash args; args["sid-last-ids"] = vsidLastIds; req->start("session-update-many", args); } private: void req_finished(ZrpcRequest *req) { if(req->success()) { setFinished(true); } else { setFinished(false, req->errorCondition()); } } }; class GetLastIds : public Deferred { Q_OBJECT Connection finishedConnection; public: GetLastIds(ZrpcManager *stateClient, const QString &sid, QObject *parent = 0) : Deferred(parent) { ZrpcRequest *req = new ZrpcRequest(stateClient, this); finishedConnection = req->finished.connect(boost::bind(&GetLastIds::req_finished, this, req)); QVariantHash args; args["sid"] = sid.toUtf8(); req->start("session-get-last-ids", args); } private: void req_finished(ZrpcRequest *req) { if(req->success()) { QVariant vresult = req->result(); if(typeId(vresult) != QMetaType::QVariantHash) { setFinished(false); return; } QVariantHash result = vresult.toHash(); QHash out; QHashIterator it(result); while(it.hasNext()) { it.next(); const QVariant &i = it.value(); if(typeId(i) != QMetaType::QByteArray) { setFinished(false); return; } out.insert(it.key(), QString::fromUtf8(i.toByteArray())); } setFinished(true, QVariant::fromValue(out)); } else { setFinished(false, req->errorCondition()); } } }; Deferred *detectRulesSet(ZrpcManager *stateClient, const QList &rules, QObject *parent) { return new DetectRulesSet(stateClient, rules, parent); } Deferred *detectRulesGet(ZrpcManager *stateClient, const QString &domain, const QByteArray &path, QObject *parent) { return new DetectRulesGet(stateClient, domain, path, parent); } Deferred *createOrUpdate(ZrpcManager *stateClient, const QString &sid, const LastIds &lastIds, QObject *parent) { return new CreateOrUpdate(stateClient, sid, lastIds, parent); } Deferred *updateMany(ZrpcManager *stateClient, const QHash &sidLastIds, QObject *parent) { return new UpdateMany(stateClient, sidLastIds, parent); } Deferred *getLastIds(ZrpcManager *stateClient, const QString &sid, QObject *parent) { return new GetLastIds(stateClient, sid, parent); } } #include "sessionrequest.moc" pushpin-1.39.1/src/cpp/handler/sessionrequest.h000066400000000000000000000030351457610542000215330ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef SESSIONREQUEST_H #define SESSIONREQUEST_H #include #include #include #include "lastids.h" #include using Connection = boost::signals2::scoped_connection; class QObject; class ZrpcManager; class Deferred; class DetectRule; namespace SessionRequest { Deferred *detectRulesSet(ZrpcManager *stateClient, const QList &rules, QObject *parent = 0); Deferred *detectRulesGet(ZrpcManager *stateClient, const QString &domain, const QByteArray &path, QObject *parent = 0); Deferred *createOrUpdate(ZrpcManager *stateClient, const QString &sid, const LastIds &lastIds, QObject *parent = 0); Deferred *updateMany(ZrpcManager *stateClient, const QHash &sidLastIds, QObject *parent = 0); Deferred *getLastIds(ZrpcManager *stateClient, const QString &sid, QObject *parent = 0); } #endif pushpin-1.39.1/src/cpp/handler/variantutil.cpp000066400000000000000000000155001457610542000213340ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "variantutil.h" #include "qtcompat.h" namespace VariantUtil { void setSuccess(bool *ok, QString *errorMessage) { if(ok) *ok = true; if(errorMessage) errorMessage->clear(); } void setError(bool *ok, QString *errorMessage, const QString &msg) { if(ok) *ok = false; if(errorMessage) *errorMessage = msg; } bool isKeyedObject(const QVariant &in) { return (typeId(in) == QMetaType::QVariantHash || typeId(in) == QMetaType::QVariantMap); } QVariant createSameKeyedObject(const QVariant &in) { if(typeId(in) == QMetaType::QVariantHash) return QVariantHash(); else if(typeId(in) == QMetaType::QVariantMap) return QVariantMap(); else return QVariant(); } bool keyedObjectIsEmpty(const QVariant &in) { if(typeId(in) == QMetaType::QVariantHash) return in.toHash().isEmpty(); else if(typeId(in) == QMetaType::QVariantMap) return in.toMap().isEmpty(); else return true; } bool keyedObjectContains(const QVariant &in, const QString &name) { if(typeId(in) == QMetaType::QVariantHash) return in.toHash().contains(name); else if(typeId(in) == QMetaType::QVariantMap) return in.toMap().contains(name); else return false; } QVariant keyedObjectGetValue(const QVariant &in, const QString &name) { if(typeId(in) == QMetaType::QVariantHash) return in.toHash().value(name); else if(typeId(in) == QMetaType::QVariantMap) return in.toMap().value(name); else return QVariant(); } void keyedObjectInsert(QVariant *in, const QString &name, const QVariant &value) { if(typeId(*in) == QMetaType::QVariantHash) { QVariantHash h = in->toHash(); h.insert(name, value); *in = h; } else if(typeId(*in) == QMetaType::QVariantMap) { QVariantMap h = in->toMap(); h.insert(name, value); *in = h; } } QVariant getChild(const QVariant &in, const QString &parentName, const QString &childName, bool required, bool *ok, QString *errorMessage) { if(!isKeyedObject(in)) { QString pn = !parentName.isEmpty() ? parentName : QString("value"); setError(ok, errorMessage, QString("%1 is not an object").arg(pn)); return QVariant(); } QString pn = !parentName.isEmpty() ? parentName : QString("object"); QVariant v; if(typeId(in) == QMetaType::QVariantHash) { QVariantHash h = in.toHash(); if(!h.contains(childName)) { if(required) setError(ok, errorMessage, QString("%1 does not contain '%2'").arg(pn, childName)); else setSuccess(ok, errorMessage); return QVariant(); } v = h[childName]; } else // Map { QVariantMap m = in.toMap(); if(!m.contains(childName)) { if(required) setError(ok, errorMessage, QString("%1 does not contain '%2'").arg(pn, childName)); else setSuccess(ok, errorMessage); return QVariant(); } v = m[childName]; } setSuccess(ok, errorMessage); return v; } QVariant getKeyedObject(const QVariant &in, const QString &parentName, const QString &childName, bool required, bool *ok, QString *errorMessage) { bool ok_; QVariant v = getChild(in, parentName, childName, required, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return QVariant(); } if(!v.isValid() && !required) { setSuccess(ok, errorMessage); return QVariant(); } QString pn = !parentName.isEmpty() ? parentName : QString("object"); if(!isKeyedObject(v)) { setError(ok, errorMessage, QString("%1 contains '%2' with wrong type").arg(pn, childName)); return QVariant(); } setSuccess(ok, errorMessage); return v; } QVariantList getList(const QVariant &in, const QString &parentName, const QString &childName, bool required, bool *ok, QString *errorMessage) { bool ok_; QVariant v = getChild(in, parentName, childName, required, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return QVariantList(); } if(!v.isValid() && !required) { setSuccess(ok, errorMessage); return QVariantList(); } QString pn = !parentName.isEmpty() ? parentName : QString("object"); if(typeId(v) != QMetaType::QVariantList) { setError(ok, errorMessage, QString("%1 contains '%2' with wrong type").arg(pn, childName)); return QVariantList(); } setSuccess(ok, errorMessage); return v.toList(); } QString getString(const QVariant &in, bool *ok) { if(typeId(in) == QMetaType::QString) { if(ok) *ok = true; return in.toString(); } else if(typeId(in) == QMetaType::QByteArray) { QByteArray buf = in.toByteArray(); if(ok) *ok = true; if(!buf.isNull()) return QString::fromUtf8(buf); else return QString(); } else { if(ok) *ok = false; return QString(); } } QString getString(const QVariant &in, const QString &parentName, const QString &childName, bool required, bool *ok, QString *errorMessage) { bool ok_; QVariant v = getChild(in, parentName, childName, required, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return QString(); } if(!v.isValid() && !required) { setSuccess(ok, errorMessage); return QString(); } QString pn = !parentName.isEmpty() ? parentName : QString("object"); QString str = getString(v, &ok_); if(!ok_) { setError(ok, errorMessage, QString("%1 contains '%2' with wrong type").arg(pn, childName)); return QString(); } setSuccess(ok, errorMessage); return str; } bool convertToJsonStyleInPlace(QVariant *in) { // Hash -> Map // ByteArray (UTF-8) -> String bool changed = false; QMetaType::Type type = typeId(*in); if(type == QMetaType::QVariantHash) { QVariantMap vmap; QVariantHash vhash = in->toHash(); QHashIterator it(vhash); while(it.hasNext()) { it.next(); QVariant i = it.value(); convertToJsonStyleInPlace(&i); vmap[it.key()] = i; } *in = vmap; changed = true; } else if(type == QMetaType::QVariantList) { QVariantList vlist = in->toList(); for(int n = 0; n < vlist.count(); ++n) { QVariant i = vlist.at(n); convertToJsonStyleInPlace(&i); vlist[n] = i; } *in = vlist; changed = true; } else if(type == QMetaType::QByteArray) { QByteArray buf = in->toByteArray(); if(!buf.isNull()) *in = QString::fromUtf8(buf); else *in = QString(); changed = true; } return changed; } QVariant convertToJsonStyle(const QVariant &in) { QVariant v = in; convertToJsonStyleInPlace(&v); return v; } } pushpin-1.39.1/src/cpp/handler/variantutil.h000066400000000000000000000040171457610542000210020ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef VARIANTUTIL_H #define VARIANTUTIL_H #include namespace VariantUtil { void setSuccess(bool *ok, QString *errorMessage); void setError(bool *ok, QString *errorMessage, const QString &msg); bool isKeyedObject(const QVariant &in); QVariant createSameKeyedObject(const QVariant &in); bool keyedObjectIsEmpty(const QVariant &in); bool keyedObjectContains(const QVariant &in, const QString &name); QVariant keyedObjectGetValue(const QVariant &in, const QString &name); void keyedObjectInsert(QVariant *in, const QString &name, const QVariant &value); QVariant getChild(const QVariant &in, const QString &parentName, const QString &childName, bool required, bool *ok = 0, QString *errorMessage = 0); QVariant getKeyedObject(const QVariant &in, const QString &parentName, const QString &childName, bool required, bool *ok = 0, QString *errorMessage = 0); QVariantList getList(const QVariant &in, const QString &parentName, const QString &childName, bool required, bool *ok = 0, QString *errorMessage = 0); QString getString(const QVariant &in, bool *ok = 0); QString getString(const QVariant &in, const QString &parentName, const QString &childName, bool required, bool *ok = 0, QString *errorMessage = 0); // return true if item modified bool convertToJsonStyleInPlace(QVariant *in); QVariant convertToJsonStyle(const QVariant &in); } #endif pushpin-1.39.1/src/cpp/handler/wscontrolmessage.cpp000066400000000000000000000140441457610542000223730ustar00rootroot00000000000000/* * Copyright (C) 2016-2019 Fanout, Inc. * Copyright (C) 2024 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "wscontrolmessage.h" #include #include "qtcompat.h" #include "variantutil.h" using namespace VariantUtil; WsControlMessage WsControlMessage::fromVariant(const QVariant &in, bool *ok, QString *errorMessage) { QString pn = "grip control packet"; if(!isKeyedObject(in)) { setError(ok, errorMessage, QString("%1 is not an object").arg(pn)); return WsControlMessage(); } pn = "grip control object"; WsControlMessage out; bool ok_; QString type = getString(in, pn, "type", true, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return WsControlMessage(); } if(type == "subscribe") out.type = Subscribe; else if(type == "unsubscribe") out.type = Unsubscribe; else if(type == "detach") out.type = Detach; else if(type == "session") out.type = Session; else if(type == "set-meta") out.type = SetMeta; else if(type == "keep-alive") out.type = KeepAlive; else if(type == "send-delayed") out.type = SendDelayed; else if(type == "flush-delayed") out.type = FlushDelayed; else { setError(ok, errorMessage, QString("'type' contains unknown value: %1").arg(type)); return WsControlMessage(); } if(out.type == Subscribe || out.type == Unsubscribe) { out.channel = getString(in, pn, "channel", true, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return WsControlMessage(); } if(out.channel.isEmpty()) { setError(ok, errorMessage, QString("%1 contains 'channel' with invalid value").arg(pn)); return WsControlMessage(); } if(out.type == Subscribe) { QVariantList vfilters = getList(in, pn, "filters", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return WsControlMessage(); } foreach(const QVariant &vfilter, vfilters) { QString filter = getString(vfilter, &ok_); if(!ok_) { setError(ok, errorMessage, "filters contains value with wrong type"); return WsControlMessage(); } out.filters += filter; } } } else if(out.type == Session) { out.sessionId = getString(in, pn, "id", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return WsControlMessage(); } } else if(out.type == SetMeta) { out.metaName = getString(in, pn, "name", true, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return WsControlMessage(); } if(out.metaName.isEmpty()) { setError(ok, errorMessage, QString("%1 contains 'name' with invalid value").arg(pn)); return WsControlMessage(); } out.metaValue = getString(in, pn, "value", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return WsControlMessage(); } } else if(out.type == KeepAlive || out.type == SendDelayed) { QString typeStr = getString(in, pn, "message-type", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return WsControlMessage(); } if(!typeStr.isNull()) { if(typeStr == "text") out.messageType = Text; else if(typeStr == "binary") out.messageType = Binary; else if(typeStr == "ping") out.messageType = Ping; else if(typeStr == "pong") out.messageType = Pong; else { setError(ok, errorMessage, QString("%1 contains 'message-type' with unknown value").arg(pn)); return WsControlMessage(); } } else { // default out.messageType = Text; } if(keyedObjectContains(in, "content-bin")) { QVariant vcontentBin = keyedObjectGetValue(in, "content-bin"); if(typeId(in) == QMetaType::QVariantMap) // JSON input { if(typeId(vcontentBin) != QMetaType::QString) { setError(ok, errorMessage, QString("%1 contains 'content-bin' with wrong type").arg(pn)); return WsControlMessage(); } out.content = QByteArray::fromBase64(vcontentBin.toString().toUtf8()); } else { if(typeId(vcontentBin) != QMetaType::QByteArray) { setError(ok, errorMessage, QString("%1 contains 'content-bin' with wrong type").arg(pn)); return WsControlMessage(); } out.content = vcontentBin.toByteArray(); } if(((int)out.messageType) == -1) out.messageType = Binary; } else if(keyedObjectContains(in, "content")) { QVariant vcontent = keyedObjectGetValue(in, "content"); if(typeId(vcontent) == QMetaType::QByteArray) out.content = vcontent.toByteArray(); else if(typeId(vcontent) == QMetaType::QString) out.content = vcontent.toString().toUtf8(); else { setError(ok, errorMessage, QString("%1 contains 'content' with wrong type").arg(pn)); return WsControlMessage(); } if(((int)out.messageType) == -1) out.messageType = Text; } if(!out.content.isNull()) { if(keyedObjectContains(in, "timeout")) { QVariant vtimeout = keyedObjectGetValue(in, "timeout"); if(!canConvert(vtimeout, QMetaType::Int)) { setError(ok, errorMessage, QString("%1 contains 'timeout' with wrong type").arg(pn)); return WsControlMessage(); } out.timeout = vtimeout.toInt(); if(out.timeout < 0) { setError(ok, errorMessage, QString("%1 contains 'timeout' with invalid value").arg(pn)); return WsControlMessage(); } } } if(out.type == KeepAlive) { QString mode = getString(in, pn, "mode", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return WsControlMessage(); } if(!mode.isNull()) out.keepAliveMode = mode.toUtf8(); } } return out; } pushpin-1.39.1/src/cpp/handler/wscontrolmessage.h000066400000000000000000000026361457610542000220440ustar00rootroot00000000000000/* * Copyright (C) 2016-2019 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef WSCONTROLMESSAGE_H #define WSCONTROLMESSAGE_H #include #include class QVariant; class WsControlMessage { public: enum Type { Subscribe, Unsubscribe, Detach, Session, SetMeta, KeepAlive, SendDelayed, FlushDelayed }; enum MessageType { Text, Binary, Ping, Pong }; Type type; QString channel; QStringList filters; QString sessionId; QString metaName; QString metaValue; MessageType messageType; QByteArray content; int timeout; QByteArray keepAliveMode; WsControlMessage() : type((Type)-1), messageType((MessageType)-1), timeout(-1) { } static WsControlMessage fromVariant(const QVariant &in, bool *ok = 0, QString *errorMessage = 0); }; #endif pushpin-1.39.1/src/cpp/handler/wssession.cpp000066400000000000000000000062161457610542000210330ustar00rootroot00000000000000/* * Copyright (C) 2020 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "wssession.h" #include #include #include "log.h" #define WSCONTROL_REQUEST_TIMEOUT 8000 WsSession::WsSession(QObject *parent) : QObject(parent), nextReqId(0) { expireTimer = new QTimer(this); expireTimer->setSingleShot(true); connect(expireTimer, &QTimer::timeout, this, &WsSession::expireTimer_timeout); delayedTimer = new QTimer(this); delayedTimer->setSingleShot(true); connect(delayedTimer, &QTimer::timeout, this, &WsSession::delayedTimer_timeout); requestTimer = new QTimer(this); requestTimer->setSingleShot(true); connect(requestTimer, &QTimer::timeout, this, &WsSession::requestTimer_timeout); } WsSession::~WsSession() { expireTimer->disconnect(this); expireTimer->setParent(0); expireTimer->deleteLater(); delayedTimer->disconnect(this); delayedTimer->setParent(0); delayedTimer->deleteLater(); requestTimer->disconnect(this); requestTimer->setParent(0); requestTimer->deleteLater(); } void WsSession::refreshExpiration() { expireTimer->start(ttl * 1000); } void WsSession::flushDelayed() { if(delayedTimer->isActive()) { delayedTimer->stop(); delayedTimer_timeout(); } } void WsSession::sendDelayed(const QByteArray &type, const QByteArray &message, int timeout) { flushDelayed(); delayedType = type; delayedMessage = message; delayedTimer->start(timeout * 1000); } void WsSession::ack(int reqId) { if(pendingRequests.contains(reqId)) { pendingRequests.remove(reqId); setupRequestTimer(); } } void WsSession::setupRequestTimer() { if(!pendingRequests.isEmpty()) { // find next expiring request qint64 lowestTime = -1; QHashIterator it(pendingRequests); while(it.hasNext()) { it.next(); qint64 time = it.value(); if(lowestTime == -1 || time < lowestTime) lowestTime = time; } int until = int(lowestTime - QDateTime::currentMSecsSinceEpoch()); requestTimer->start(qMax(until, 0)); } else { requestTimer->stop(); } } void WsSession::expireTimer_timeout() { log_debug("timing out ws session: %s", qPrintable(cid)); expired(); } void WsSession::delayedTimer_timeout() { int reqId = nextReqId++; QByteArray message = delayedMessage; delayedMessage.clear(); pendingRequests[reqId] = QDateTime::currentMSecsSinceEpoch() + WSCONTROL_REQUEST_TIMEOUT; setupRequestTimer(); send(reqId, delayedType, message); } void WsSession::requestTimer_timeout() { // on error, destroy any other pending requests pendingRequests.clear(); setupRequestTimer(); error(); } pushpin-1.39.1/src/cpp/handler/wssession.h000066400000000000000000000040411457610542000204720ustar00rootroot00000000000000/* * Copyright (C) 2020 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef WSSESSION_H #define WSSESSION_H #include #include #include #include "packet/httprequestdata.h" #include using Signal = boost::signals2::signal; using Connection = boost::signals2::scoped_connection; class QTimer; class WsSession : public QObject { Q_OBJECT public: QByteArray peer; QString cid; int nextReqId; QString channelPrefix; HttpRequestData requestData; QString route; QString statsRoute; QString sid; QHash meta; QHash channelFilters; // k=channel, v=list(filters) QSet channels; QSet implicitChannels; int ttl; QByteArray keepAliveType; QByteArray keepAliveMessage; QByteArray delayedType; QByteArray delayedMessage; QHash pendingRequests; QTimer *expireTimer; QTimer *delayedTimer; QTimer *requestTimer; WsSession(QObject *parent = 0); ~WsSession(); void refreshExpiration(); void flushDelayed(); void sendDelayed(const QByteArray &type, const QByteArray &message, int timeout); void ack(int reqId); boost::signals2::signal send; Signal expired; Signal error; private: void setupRequestTimer(); private slots: void expireTimer_timeout(); void delayedTimer_timeout(); void requestTimer_timeout(); }; #endif pushpin-1.39.1/src/cpp/httpheaders.cpp000066400000000000000000000145061457610542000176750ustar00rootroot00000000000000/* * Copyright (C) 2012-2017 Fanout, Inc. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "httpheaders.h" // return position, end of string if not found, -1 on error static int findNonQuoted(const QByteArray &in, char c, int offset = 0) { bool inQuote = false; for(int n = offset; n < in.size(); ++n) { char i = in[n]; if(inQuote) { if(i == '\\') { ++n; // no character after the escape if(n >= in.size()) { return -1; } } else if(i == '\"') inQuote = false; } else { if(i == '\"') { inQuote = true; } else if(i == c) { return n; } } } // unterminated quote if(inQuote) { return -1; } return in.size(); } // search for one of many chars static int findNext(const QByteArray &in, const char *charList, int offset = 0) { int len = qstrlen(charList); for(int n = offset; n < in.size(); ++n) { char c = in[n]; for(int i = 0; i < len; ++i) { if(c == charList[i]) return n; } } return -1; } static QList headerSplit(const QByteArray &in) { QList parts; int pos = 0; while(pos < in.size()) { int end = findNonQuoted(in, ',', pos); if(end != -1) { parts += in.mid(pos, end - pos).trimmed(); if(end < in.size()) pos = end + 1; else pos = in.size(); } else { parts += in.mid(pos).trimmed(); pos = in.size(); } } return parts; } bool HttpHeaderParameters::contains(const QByteArray &key) const { for(int n = 0; n < count(); ++n) { if(qstricmp(at(n).first.data(), key.data()) == 0) return true; } return false; } QByteArray HttpHeaderParameters::get(const QByteArray &key) const { for(int n = 0; n < count(); ++n) { const HttpHeaderParameter &h = at(n); if(qstricmp(h.first.data(), key.data()) == 0) return h.second; } return QByteArray(); } bool HttpHeaders::contains(const QByteArray &key) const { for(int n = 0; n < count(); ++n) { if(qstricmp(at(n).first.data(), key.data()) == 0) return true; } return false; } QByteArray HttpHeaders::get(const QByteArray &key) const { for(int n = 0; n < count(); ++n) { const HttpHeader &h = at(n); if(qstricmp(h.first.data(), key.data()) == 0) return h.second; } return QByteArray(); } HttpHeaderParameters HttpHeaders::getAsParameters(const QByteArray &key, ParseMode mode) const { QByteArray h = get(key); if(h.isEmpty()) return HttpHeaderParameters(); return parseParameters(h, mode); } QByteArray HttpHeaders::getAsFirstParameter(const QByteArray &key) const { HttpHeaderParameters p = getAsParameters(key); if(p.isEmpty()) return QByteArray(); return p[0].first; } QList HttpHeaders::getAll(const QByteArray &key, bool split) const { QList out; for(int n = 0; n < count(); ++n) { const HttpHeader &h = at(n); if(qstricmp(h.first.data(), key.data()) == 0) { if(split) out += headerSplit(h.second); else out += h.second; } } return out; } QList HttpHeaders::getAllAsParameters(const QByteArray &key, ParseMode mode, bool split) const { QList out; foreach(const QByteArray &h, getAll(key, split)) { bool ok; HttpHeaderParameters params = parseParameters(h, mode, &ok); if(ok) out += params; } return out; } QList HttpHeaders::takeAll(const QByteArray &key, bool split) { QList out; for(int n = 0; n < count(); ++n) { const HttpHeader &h = at(n); if(qstricmp(h.first.data(), key.data()) == 0) { if(split) out += headerSplit(h.second); else out += h.second; removeAt(n); --n; // adjust position } } return out; } void HttpHeaders::removeAll(const QByteArray &key) { for(int n = 0; n < count(); ++n) { if(qstricmp(at(n).first.data(), key.data()) == 0) { removeAt(n); --n; // adjust position } } } QByteArray HttpHeaders::join(const QList &values) { QByteArray out; bool first = true; foreach(const QByteArray &val, values) { if(!first) out += ", "; out += val; first = false; } return out; } HttpHeaderParameters HttpHeaders::parseParameters(const QByteArray &in, ParseMode mode, bool *ok) { HttpHeaderParameters out; int start = 0; if(mode == NoParseFirstParameter) { int at = in.indexOf(';'); if(at != -1) { out += HttpHeaderParameter(in.mid(0, at).trimmed(), QByteArray()); start = at + 1; } else { out += HttpHeaderParameter(in.trimmed(), QByteArray()); start = in.size(); } } while(start < in.size()) { QByteArray var; QByteArray val; int at = findNext(in, "=;", start); if(at != -1) { var = in.mid(start, at - start).trimmed(); if(in[at] == '=') { ++at; if(at < in.size() && in[at] == '\"') { ++at; bool complete = false; for(int n = at; n < in.size(); ++n) { if(in[n] == '\\') { if(n + 1 >= in.size()) { if(ok) *ok = false; return HttpHeaderParameters(); } ++n; val += in[n]; } else if(in[n] == '\"') { complete = true; at = n + 1; break; } else val += in[n]; } if(!complete) { if(ok) *ok = false; return HttpHeaderParameters(); } at = in.indexOf(';', at); if(at != -1) start = at + 1; else start = in.size(); } else { int vstart = at; at = in.indexOf(';', vstart); if(at != -1) { val = in.mid(vstart, at - vstart).trimmed(); start = at + 1; } else { val = in.mid(vstart).trimmed(); start = in.size(); } } } else start = at + 1; } else { var = in.mid(start).trimmed(); start = in.size(); } out.append(HttpHeaderParameter(var, val)); } if(ok) *ok = true; return out; } pushpin-1.39.1/src/cpp/httpheaders.h000066400000000000000000000036371457610542000173450ustar00rootroot00000000000000/* * Copyright (C) 2012-2013 Fanout, Inc. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef HTTPHEADERS_H #define HTTPHEADERS_H #include #include #include typedef QPair HttpHeaderParameter; class HttpHeaderParameters : public QList { public: bool contains(const QByteArray &key) const; QByteArray get(const QByteArray &key) const; }; typedef QPair HttpHeader; class HttpHeaders : public QList { public: enum ParseMode { NoParseFirstParameter, ParseAllParameters }; bool contains(const QByteArray &key) const; QByteArray get(const QByteArray &key) const; HttpHeaderParameters getAsParameters(const QByteArray &key, ParseMode mode = NoParseFirstParameter) const; QByteArray getAsFirstParameter(const QByteArray &key) const; QList getAll(const QByteArray &key, bool split = true) const; QList getAllAsParameters(const QByteArray &key, ParseMode mode = NoParseFirstParameter, bool split = true) const; QList takeAll(const QByteArray &key, bool split = true); void removeAll(const QByteArray &key); static QByteArray join(const QList &values); static HttpHeaderParameters parseParameters(const QByteArray &in, ParseMode mode = NoParseFirstParameter, bool *ok = 0); }; #endif pushpin-1.39.1/src/cpp/httprequest.h000066400000000000000000000053471457610542000174220ustar00rootroot00000000000000/* * Copyright (C) 2012-2016 Fanout, Inc. * Copyright (C) 2023 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef HTTPREQUEST_H #define HTTPREQUEST_H #include #include #include #include "httpheaders.h" #include using Signal = boost::signals2::signal; using SignalInt = boost::signals2::signal; class HttpRequest : public QObject { Q_OBJECT public: enum ErrorCondition { ErrorGeneric, ErrorPolicy, ErrorConnect, ErrorConnectTimeout, ErrorTls, ErrorLengthRequired, ErrorDisconnected, ErrorTimeout, ErrorUnavailable, ErrorRequestTooLarge }; HttpRequest(QObject *parent = 0) : QObject(parent) {} virtual QHostAddress peerAddress() const = 0; virtual void setConnectHost(const QString &host) = 0; virtual void setConnectPort(int port) = 0; virtual void setIgnorePolicies(bool on) = 0; virtual void setTrustConnectHost(bool on) = 0; virtual void setIgnoreTlsErrors(bool on) = 0; virtual void start(const QString &method, const QUrl &uri, const HttpHeaders &headers) = 0; virtual void beginResponse(int code, const QByteArray &reason, const HttpHeaders &headers) = 0; // may call this multiple times virtual void writeBody(const QByteArray &body) = 0; virtual void endBody() = 0; virtual int bytesAvailable() const = 0; virtual int writeBytesAvailable() const = 0; virtual bool isFinished() const = 0; virtual bool isInputFinished() const = 0; virtual bool isOutputFinished() const = 0; virtual bool isErrored() const = 0; virtual ErrorCondition errorCondition() const = 0; virtual QString requestMethod() const = 0; virtual QUrl requestUri() const = 0; virtual HttpHeaders requestHeaders() const = 0; virtual int responseCode() const = 0; virtual QByteArray responseReason() const = 0; virtual HttpHeaders responseHeaders() const = 0; virtual QByteArray readBody(int size = -1) = 0; // takes from the buffer // indicates input data and/or input finished Signal readyRead; // indicates output data written and/or output finished SignalInt bytesWritten; Signal writeBytesChanged; Signal paused; Signal error; }; #endif pushpin-1.39.1/src/cpp/inspectdata.h000066400000000000000000000017361457610542000173270ustar00rootroot00000000000000/* * Copyright (C) 2012-2013 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef INSPECTDATA_H #define INSPECTDATA_H #include #include class InspectData { public: bool doProxy; QByteArray sharingKey; QByteArray sid; QHash lastIds; QVariant userData; InspectData() : doProxy(false) { } }; #endif pushpin-1.39.1/src/cpp/jwt.cpp000066400000000000000000000123251457610542000161630ustar00rootroot00000000000000/* * Copyright (C) 2012-2022 Fanout, Inc. * Copyright (C) 2023 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "jwt.h" #include #include #include #include #include namespace Jwt { EncodingKey::Private::Private() : type((KeyType)-1), raw(0) { } EncodingKey::Private::Private(JwtEncodingKey key) : type((KeyType)key.type), raw(key.key) { } EncodingKey::Private::~Private() { jwt_encoding_key_destroy(raw); } EncodingKey EncodingKey::fromSecret(const QByteArray &key) { EncodingKey k; k.d = new Private(jwt_encoding_key_from_secret((const quint8 *)key.data(), key.size())); return k; } EncodingKey EncodingKey::fromPem(const QByteArray &key) { EncodingKey k; k.d = new Private(jwt_encoding_key_from_pem((const quint8 *)key.data(), key.size())); return k; } EncodingKey EncodingKey::fromFile(const QString &fileName) { QFile f(fileName); if(!f.open(QFile::ReadOnly)) { return EncodingKey(); } QByteArray data = f.readAll(); if(data.startsWith("-----BEGIN")) return fromPem(data); else return fromSecret(QByteArray::fromHex(data.trimmed())); } EncodingKey EncodingKey::fromConfigString(const QString &s, const QDir &baseDir) { if(s.startsWith("file:")) { QString keyFile = s.mid(5); QFileInfo fi(keyFile); if(fi.isRelative()) keyFile = QFileInfo(baseDir, keyFile).filePath(); return fromFile(keyFile); } else { QByteArray secret; if(s.startsWith("base64:")) secret = QByteArray::fromBase64(s.mid(7).toUtf8()); else secret = s.toUtf8(); return fromSecret(secret); } } DecodingKey::Private::Private() : type((KeyType)-1), raw(0) { } DecodingKey::Private::Private(JwtDecodingKey key) : type((KeyType)key.type), raw(key.key) { } DecodingKey::Private::~Private() { jwt_decoding_key_destroy(raw); } DecodingKey DecodingKey::fromSecret(const QByteArray &key) { DecodingKey k; k.d = new Private(jwt_decoding_key_from_secret((const quint8 *)key.data(), key.size())); return k; } DecodingKey DecodingKey::fromPem(const QByteArray &key) { DecodingKey k; k.d = new Private(jwt_decoding_key_from_pem((const quint8 *)key.data(), key.size())); return k; } DecodingKey DecodingKey::fromFile(const QString &fileName) { QFile f(fileName); if(!f.open(QFile::ReadOnly)) { return DecodingKey(); } QByteArray data = f.readAll(); if(data.startsWith("-----BEGIN")) return fromPem(data); else return fromSecret(QByteArray::fromHex(data.trimmed())); } DecodingKey DecodingKey::fromConfigString(const QString &s, const QDir &baseDir) { if(s.startsWith("file:")) { QString keyFile = s.mid(5); QFileInfo fi(keyFile); if(fi.isRelative()) keyFile = QFileInfo(baseDir, keyFile).filePath(); return fromFile(keyFile); } else { QByteArray secret; if(s.startsWith("base64:")) secret = QByteArray::fromBase64(s.mid(7).toUtf8()); else secret = s.toUtf8(); return fromSecret(secret); } } QByteArray encodeWithAlgorithm(Algorithm alg, const QByteArray &claim, const EncodingKey &key) { char *token; if(jwt_encode((int)alg, (const char *)claim.data(), key.raw(), &token) != 0) { // error return QByteArray(); } QByteArray out = QByteArray(token); jwt_str_destroy(token); return out; } QByteArray decodeWithAlgorithm(Algorithm alg, const QByteArray &token, const DecodingKey &key) { char *claim; if(jwt_decode((int)alg, (const char *)token.data(), key.raw(), &claim) != 0) { // error return QByteArray(); } QByteArray out = QByteArray(claim); jwt_str_destroy(claim); return out; } QByteArray encode(const QVariant &claim, const EncodingKey &key) { Algorithm alg; switch(key.type()) { case Jwt::KeyType::Secret: alg = Jwt::HS256; break; case Jwt::KeyType::Ec: alg = Jwt::ES256; break; case Jwt::KeyType::Rsa: alg = Jwt::RS256; break; default: return QByteArray(); } QByteArray claimJson = QJsonDocument(QJsonObject::fromVariantMap(claim.toMap())).toJson(QJsonDocument::Compact); if(claimJson.isNull()) return QByteArray(); return encodeWithAlgorithm(alg, claimJson, key); } QVariant decode(const QByteArray &token, const DecodingKey &key) { Algorithm alg; switch(key.type()) { case Jwt::KeyType::Secret: alg = Jwt::HS256; break; case Jwt::KeyType::Ec: alg = Jwt::ES256; break; case Jwt::KeyType::Rsa: alg = Jwt::RS256; break; default: return QVariant(); } QByteArray claimJson = decodeWithAlgorithm(alg, token, key); if(claimJson.isEmpty()) return QVariant(); QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(claimJson, &error); if(error.error != QJsonParseError::NoError || !doc.isObject()) return QVariant(); return doc.object().toVariantMap(); } } pushpin-1.39.1/src/cpp/jwt.h000066400000000000000000000054101457610542000156250ustar00rootroot00000000000000/* * Copyright (C) 2012-2022 Fanout, Inc. * Copyright (C) 2023 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef JWT_H #define JWT_H #include #include #include #include #include "rust/jwt.h" class QString; namespace Jwt { enum KeyType { Secret = JWT_KEYTYPE_SECRET, Ec = JWT_KEYTYPE_EC, Rsa = JWT_KEYTYPE_RSA, }; enum Algorithm { HS256 = JWT_ALGORITHM_HS256, ES256 = JWT_ALGORITHM_ES256, RS256 = JWT_ALGORITHM_RS256, }; class EncodingKey { public: bool isNull() const { return !d; } KeyType type() const { if(d) { return d->type; } else { return (KeyType)-1; } } const void *raw() const { if(d) { return d->raw; } else { return 0; } } static EncodingKey fromSecret(const QByteArray &key); static EncodingKey fromPem(const QByteArray &key); static EncodingKey fromFile(const QString &fileName); static EncodingKey fromConfigString(const QString &s, const QDir &baseDir = QDir()); private: class Private : public QSharedData { public: KeyType type; void *raw; Private(); Private(JwtEncodingKey key); ~Private(); }; QSharedDataPointer d; }; class DecodingKey { public: bool isNull() const { return !d; } KeyType type() const { if(d) { return d->type; } else { return (KeyType)-1; } } const void *raw() const { if(d) { return d->raw; } else { return 0; } } static DecodingKey fromSecret(const QByteArray &key); static DecodingKey fromPem(const QByteArray &key); static DecodingKey fromFile(const QString &fileName); static DecodingKey fromConfigString(const QString &s, const QDir &baseDir = QDir()); private: class Private : public QSharedData { public: KeyType type; void *raw; Private(); Private(JwtDecodingKey key); ~Private(); }; QSharedDataPointer d; }; // returns token, null on error QByteArray encodeWithAlgorithm(Algorithm alg, const QByteArray &claim, const EncodingKey &key); // returns claim, null on error QByteArray decodeWithAlgorithm(Algorithm alg, const QByteArray &token, const DecodingKey &key); QByteArray encode(const QVariant &claim, const EncodingKey &key); QVariant decode(const QByteArray &token, const DecodingKey &key); } #endif pushpin-1.39.1/src/cpp/layertracker.cpp000066400000000000000000000026171457610542000200520ustar00rootroot00000000000000/* * Copyright (C) 2014 Fanout, Inc. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "layertracker.h" #include LayerTracker::LayerTracker() : plain_(0) { } void LayerTracker::reset() { plain_ = 0; items_.clear(); } void LayerTracker::addPlain(int plain) { plain_ += plain; } void LayerTracker::specifyEncoded(int encoded, int plain) { // can't specify more bytes than we have assert(plain <= plain_); plain_ -= plain; Item i; i.plain = plain; i.encoded = encoded; items_ += i; } int LayerTracker::finished(int encoded) { int plain = 0; for(QList::Iterator it = items_.begin(); it != items_.end();) { Item &i = *it; // not enough? if(encoded < i.encoded) { i.encoded -= encoded; break; } encoded -= i.encoded; plain += i.plain; it = items_.erase(it); } return plain; } pushpin-1.39.1/src/cpp/layertracker.h000066400000000000000000000017561457610542000175220ustar00rootroot00000000000000/* * Copyright (C) 2014 Fanout, Inc. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef LAYERTRACKER_H #define LAYERTRACKER_H #include class LayerTracker { public: LayerTracker(); void reset(); void addPlain(int plain); void specifyEncoded(int encoded, int plain); int finished(int encoded); private: class Item { public: int plain; int encoded; }; int plain_; QList items_; }; #endif pushpin-1.39.1/src/cpp/log.cpp000066400000000000000000000066141457610542000161440ustar00rootroot00000000000000/* * Copyright (C) 2012-2022 Fanout, Inc. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "log.h" #include #include #include #include #include #include Q_GLOBAL_STATIC(QMutex, g_mutex) static int g_level = LOG_LEVEL_DEBUG; static QElapsedTimer g_time; static QString *g_filename; static FILE *g_file; static void log(const char *s) { FILE *out; if(g_file) out = g_file; else out = stdout; fprintf(out, "%s\n", s); fflush(out); } static void log(int level, const char *fmt, va_list ap) { g_mutex()->lock(); int current_level = g_level; int elapsed; if(g_time.isValid()) elapsed = g_time.elapsed(); else elapsed = -1; g_mutex()->unlock(); if(level <= current_level) { QString str = QString::vasprintf(fmt, ap); const char *lstr; switch(level) { case LOG_LEVEL_ERROR: lstr = "ERR"; break; case LOG_LEVEL_WARNING: lstr = "WARN"; break; case LOG_LEVEL_INFO: lstr = "INFO"; break; case LOG_LEVEL_DEBUG: default: lstr = "DEBUG"; break; } QString tstr; if(elapsed != -1) { QTime t(0, 0); t = t.addMSecs(elapsed); tstr = t.toString("HH:mm:ss.zzz"); } else { tstr = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz"); } FILE *out; if(g_file) out = g_file; else out = stdout; fprintf(out, "[%s] %s %s\n", lstr, qPrintable(tstr), qPrintable(str)); fflush(out); } } void log_startClock() { QMutexLocker locker(g_mutex()); g_time.start(); } int log_outputLevel() { QMutexLocker locker(g_mutex()); return g_level; } void log_setOutputLevel(int level) { QMutexLocker locker(g_mutex()); g_level = level; } bool log_setFile(const QString &fname) { QMutexLocker locker(g_mutex()); if(g_file) { fclose(g_file); delete g_filename; g_filename = 0; g_file = 0; } if(fname.isEmpty()) return true; FILE *f = fopen(fname.toLocal8Bit().data(), "a"); if(!f) return false; setbuf(f, NULL); g_filename = new QString(fname); g_file = f; return true; } bool log_rotate() { QMutexLocker locker(g_mutex()); if(!g_file) return true; if(!freopen(g_filename->toLocal8Bit().data(), "a", g_file)) return false; setbuf(g_file, NULL); return true; } void log(int level, const char *fmt, ...) { va_list ap; va_start(ap, fmt); log(level, fmt, ap); va_end(ap); } void log_error(const char *fmt, ...) { va_list ap; va_start(ap, fmt); log(LOG_LEVEL_ERROR, fmt, ap); va_end(ap); } void log_warning(const char *fmt, ...) { va_list ap; va_start(ap, fmt); log(LOG_LEVEL_WARNING, fmt, ap); va_end(ap); } void log_info(const char *fmt, ...) { va_list ap; va_start(ap, fmt); log(LOG_LEVEL_INFO, fmt, ap); va_end(ap); } void log_debug(const char *fmt, ...) { va_list ap; va_start(ap, fmt); log(LOG_LEVEL_DEBUG, fmt, ap); va_end(ap); } void log_raw(const char *s) { log(s); } pushpin-1.39.1/src/cpp/log.h000066400000000000000000000024401457610542000156020ustar00rootroot00000000000000/* * Copyright (C) 2012-2016 Fanout, Inc. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef LOG_H #define LOG_H #include // really simply logging stuff #define LOG_LEVEL_ERROR 0 #define LOG_LEVEL_WARNING 1 #define LOG_LEVEL_INFO 2 #define LOG_LEVEL_DEBUG 3 void log_startClock(); int log_outputLevel(); void log_setOutputLevel(int level); bool log_setFile(const QString &fname); bool log_rotate(); void log(int level, const char *fmt, ...); void log_error(const char *fmt, ...); void log_warning(const char *fmt, ...); void log_info(const char *fmt, ...); void log_debug(const char *fmt, ...); // log without prefixing or anything. useful for forwarding log data void log_raw(const char *line); #endif pushpin-1.39.1/src/cpp/logutil.cpp000066400000000000000000000120601457610542000170320ustar00rootroot00000000000000/* * Copyright (C) 2017-2022 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "logutil.h" #include #include #include "qtcompat.h" #include "tnetstring.h" #include "log.h" #define MAX_DATA_LENGTH 1000 #define MAX_CONTENT_LENGTH 1000 namespace LogUtil { static QString trim(const QString &in, int max) { if(in.length() > max && max >= 7) return in.mid(0, max / 2) + "..." + in.mid(in.length() - (max / 2) + 3); else return in; } static QByteArray trim(const QByteArray &in, int max) { if(in.size() > max && max >= 7) return in.mid(0, max / 2) + "..." + in.mid(in.size() - (max / 2) + 3); else return in; } static QString makeLastIdsStr(const HttpHeaders &headers) { QString out; bool first = true; foreach(const HttpHeaderParameters ¶ms, headers.getAllAsParameters("Grip-Last")) { if(!first) out += ' '; out += QString("#%1=%2").arg(QString::fromUtf8(params[0].first), QString::fromUtf8(params.get("last-id"))); first = false; } return out; } static void logPacket(int level, const QString &message, const QVariant &data = QVariant(), int dataMax = -1, const QByteArray &content = QByteArray(), int contentMax = -1) { QString out = message; if(data.isValid()) { out += ' ' + trim(TnetString::variantToString(data, -1), dataMax); } if(!content.isNull()) { out += ' ' + QString::number(content.size()) + ' '; QByteArray buf = trim(content, contentMax); out += TnetString::variantToString(QVariant(buf), -1); } log(level, "%s", qPrintable(out)); } static void logPacket(int level, const QVariant &data, const char *fmt, va_list ap) { logPacket(level, QString::vasprintf(fmt, ap), data, MAX_DATA_LENGTH); } static void logPacket(int level, const QByteArray &content, const char *fmt, va_list ap) { logPacket(level, QString::vasprintf(fmt, ap), QVariant(), -1, content, MAX_CONTENT_LENGTH); } static void logPacket(int level, const QVariant &data, const QString &contentField, const char *fmt, va_list ap) { QVariant meta; QByteArray content; if(typeId(data) == QMetaType::QVariantHash) { // extract content. meta is the remaining data QVariantHash hdata = data.toHash(); content = hdata.value(contentField).toByteArray(); hdata.remove(contentField); meta = hdata; } else { // if data isn't a hash, then we can't extract content, so // the meta part will be the entire data meta = data; } logPacket(level, QString::vasprintf(fmt, ap), meta, MAX_DATA_LENGTH, content, MAX_CONTENT_LENGTH); } void logVariant(int level, const QVariant &data, const char *fmt, ...) { va_list ap; va_start(ap, fmt); logPacket(level, data, fmt, ap); va_end(ap); } void logByteArray(int level, const QByteArray &content, const char *fmt, ...) { va_list ap; va_start(ap, fmt); logPacket(level, content, fmt, ap); va_end(ap); } void logVariantWithContent(int level, const QVariant &data, const QString &contentField, const char *fmt, ...) { va_list ap; va_start(ap, fmt); logPacket(level, data, contentField, fmt, ap); va_end(ap); } void logRequest(int level, const RequestData &data, const Config &config) { QString msg = QString("%1 %2").arg(data.requestData.method, data.requestData.uri.toString(QUrl::FullyEncoded)); if(!data.targetStr.isEmpty()) msg += QString(" -> %1").arg(data.targetStr); if(data.requestData.uri.scheme() != "http" && data.requestData.uri.scheme() != "https" && data.targetOverHttp) msg += "[http]"; if(config.fromAddress && !data.fromAddress.isNull()) msg += QString(" from=%1").arg(data.fromAddress.toString()); QUrl ref = QUrl(QString::fromUtf8(data.requestData.headers.get("Referer"))); if(!ref.isEmpty()) msg += QString(" ref=%1").arg(ref.toString(QUrl::FullyEncoded)); if(!data.routeId.isEmpty()) msg += QString(" route=%1").arg(data.routeId); if(data.status == LogUtil::Response) { msg += QString(" code=%1 %2").arg(QString::number(data.responseData.code), QString::number(data.responseBodySize)); } else if(data.status == LogUtil::Accept) { msg += " accept"; } else { msg += " error"; } if(data.retry) msg += " retry"; if(data.sharedBy) msg += QString::asprintf(" shared=%p", data.sharedBy); if(config.userAgent) { QString userAgent = data.requestData.headers.get("User-Agent"); if(!userAgent.isEmpty()) msg += QString(" ua=%1").arg(userAgent); } QString lastIdsStr = makeLastIdsStr(data.requestData.headers); if(!lastIdsStr.isEmpty()) msg += ' ' + lastIdsStr; log(level, "%s", qPrintable(msg)); } } pushpin-1.39.1/src/cpp/logutil.h000066400000000000000000000033541457610542000165050ustar00rootroot00000000000000/* * Copyright (C) 2017 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef LOGUTIL_H #define LOGUTIL_H #include #include "packet/httprequestdata.h" #include "packet/httpresponsedata.h" namespace LogUtil { enum RequestStatus { Response, Accept, Error }; class RequestData { public: QString routeId; RequestStatus status; HttpRequestData requestData; HttpResponseData responseData; int responseBodySize; QString targetStr; bool targetOverHttp; bool retry; void *sharedBy; QHostAddress fromAddress; RequestData() : status(Response), responseBodySize(-1), targetOverHttp(false), retry(false), sharedBy(0) { } }; class Config { public: bool fromAddress; bool userAgent; Config() : fromAddress(false), userAgent(false) { } }; void logVariant(int level, const QVariant &data, const char *fmt, ...); void logByteArray(int level, const QByteArray &content, const char *fmt, ...); void logVariantWithContent(int level, const QVariant &data, const QString &contentField, const char *fmt, ...); void logRequest(int level, const RequestData &data, const Config &config = Config()); } #endif pushpin-1.39.1/src/cpp/m2adapter/000077500000000000000000000000001457610542000165275ustar00rootroot00000000000000pushpin-1.39.1/src/cpp/m2adapter/m2adapterapp.cpp000066400000000000000000002243251457610542000216230ustar00rootroot00000000000000/* * Copyright (C) 2013-2022 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "m2adapterapp.h" #include #include #include #include #include #include #include #include #include "qzmqsocket.h" #include "qzmqvalve.h" #include "qtcompat.h" #include "processquit.h" #include "tnetstring.h" #include "m2requestpacket.h" #include "m2responsepacket.h" #include "zhttprequestpacket.h" #include "zhttpresponsepacket.h" #include "bufferlist.h" #include "log.h" #include "layertracker.h" #include "logutil.h" #include "config.h" #define DEFAULT_HWM 101000 #define STATUS_INTERVAL 250 #define REFRESH_INTERVAL 1000 #define M2_CONNECTION_EXPIRE 120000 #define ZHTTP_EXPIRE 60000 #define CONTROL_REQUEST_EXPIRE 30000 #define ZHTTP_CANCEL_RATE 100 #define M2_CONNECTION_SHOULD_PROCESS (M2_CONNECTION_EXPIRE * 3 / 4) #define M2_CONNECTION_MUST_PROCESS (M2_CONNECTION_EXPIRE * 4 / 5) #define M2_REFRESH_BUCKETS (M2_CONNECTION_SHOULD_PROCESS / REFRESH_INTERVAL) #define ZHTTP_SHOULD_PROCESS (ZHTTP_EXPIRE * 3 / 4) #define ZHTTP_MUST_PROCESS (ZHTTP_EXPIRE * 4 / 5) #define ZHTTP_REFRESH_BUCKETS (ZHTTP_SHOULD_PROCESS / REFRESH_INTERVAL) #define ZHTTP_CANCEL_PER_REFRESH (ZHTTP_CANCEL_RATE * 1000 / REFRESH_INTERVAL) // make sure this is not larger than Mongrel2's DELIVER_OUTSTANDING_MSGS #define M2_PENDING_MAX 16 // make sure this is not larger than Mongrel2's limits.handler_targets #define M2_HANDLER_TARGETS_MAX 128 // this doesn't have to match the peer, but we'll set a reasonable number #define ZHTTP_IDS_MAX 128 //#define CONTROL_PORT_DEBUG static void trimlist(QStringList *list) { for(int n = 0; n < list->count(); ++n) { if((*list)[n].isEmpty()) { list->removeAt(n); --n; // adjust position } } } static bool validateHost(const QByteArray &in) { for(int n = 0; n < in.size(); ++n) { if(in[n] == '/') return false; } return true; } static QByteArray createResponseHeader(int code, const QByteArray &reason, const HttpHeaders &headers) { QByteArray out = "HTTP/1.1 " + QByteArray::number(code) + ' ' + reason + "\r\n"; foreach(const HttpHeader &h, headers) out += h.first + ": " + h.second + "\r\n"; out += "\r\n"; return out; } static QByteArray makeChunkHeader(int size) { return QByteArray::number(size, 16).toUpper() + "\r\n"; } static QByteArray makeChunkFooter() { return "\r\n"; } static bool isErrorPacket(const ZhttpResponsePacket &packet) { return (packet.type == ZhttpResponsePacket::Error || packet.type == ZhttpResponsePacket::Cancel); } static void writeBigEndian(char *dest, quint64 value, int bytes) { for(int n = 0; n < bytes; ++n) dest[n] = (char)((value >> ((bytes - 1 - n) * 8)) & 0xff); } static QByteArray makeWsHeader(bool fin, int opcode, quint64 size) { quint8 b1 = 0; if(fin) b1 |= 0x80; b1 |= (opcode & 0x0f); if(size < 126) { QByteArray out(2, 0); out[0] = (char)b1; out[1] = (char)size; return out; } else if(size < 65536) { QByteArray out(4, 0); out[0] = (char)b1; out[1] = (char)126; writeBigEndian(out.data() + 2, size, 2); return out; } else { QByteArray out(10, 0); out[0] = (char)b1; out[1] = (char)127; writeBigEndian(out.data() + 2, size, 8); return out; } } enum CommandLineParseResult { CommandLineOk, CommandLineError, CommandLineVersionRequested, CommandLineHelpRequested }; class ArgsData { public: QString configFile; QString logFile; int logLevel; ArgsData() : logLevel(-1) { } }; static CommandLineParseResult parseCommandLine(QCommandLineParser *parser, ArgsData *args, QString *errorMessage) { parser->setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions); const QCommandLineOption configFileOption("config", "Config file.", "file"); parser->addOption(configFileOption); const QCommandLineOption logFileOption("logfile", "File to log to.", "file"); parser->addOption(logFileOption); const QCommandLineOption logLevelOption("loglevel", "Log level (default: 2).", "x"); parser->addOption(logLevelOption); const QCommandLineOption verboseOption("verbose", "Verbose output. Same as --loglevel=3."); parser->addOption(verboseOption); const QCommandLineOption helpOption = parser->addHelpOption(); const QCommandLineOption versionOption = parser->addVersionOption(); if(!parser->parse(QCoreApplication::arguments())) { *errorMessage = parser->errorText(); return CommandLineError; } if(parser->isSet(versionOption)) return CommandLineVersionRequested; if(parser->isSet(helpOption)) return CommandLineHelpRequested; if(parser->isSet(configFileOption)) args->configFile = parser->value(configFileOption); if(parser->isSet(logFileOption)) args->logFile = parser->value(logFileOption); if(parser->isSet(logLevelOption)) { bool ok; int x = parser->value(logLevelOption).toInt(&ok); if(!ok || x < 0) { *errorMessage = "error: loglevel must be greater than or equal to 0"; return CommandLineError; } args->logLevel = x; } if(parser->isSet(verboseOption)) args->logLevel = 3; return CommandLineOk; } class M2AdapterApp::Private : public QObject { Q_OBJECT public: enum Mode { Http, WebSocket }; class ControlPort { public: enum State { Disabled, Idle, ExpectingResponse }; QZmq::Socket *sock; State state; bool works; int reqStartTime; ControlPort() : sock(0), state(Disabled), works(false), reqStartTime(-1) { } }; // can be used for either m2 or zhttp typedef QPair Rid; class Session; class M2PendingOutItem { public: enum Type { Headers, Response, Frame, Close }; Type type; BufferList data; bool chunked; int contentSize; M2PendingOutItem(Type _type) : type(_type), chunked(false), contentSize(0) { } }; class M2Connection { public: int identIndex; QByteArray id; int confirmedBytesWritten; int packetsPending; // count of packets sent to m2 not yet ack'd Session *session; bool isNew; bool continuation; LayerTracker bodyTracker; LayerTracker packetTracker; QList pendingOutItems; // packets yet to send bool flowControl; bool waitForAllWritten; bool outCreditsEnabled; int outCredits; quint64 subIdBase; qint64 lastRefresh; int refreshBucket; M2Connection() : confirmedBytesWritten(0), packetsPending(0), session(0), isNew(false), continuation(false), flowControl(false), waitForAllWritten(false), outCreditsEnabled(false), outCredits(0), subIdBase(0) { } bool canWrite() const { if(!flowControl) return true; if(outCreditsEnabled) { if(outCredits > 0) return true; } else { // if we aren't using outCredits, then limit pending packets // to hardcoded m2 value if(packetsPending < M2_PENDING_MAX) return true; } return false; } }; class Session { public: Mode mode; qint64 lastActive; QByteArray errorCondition; QByteArray acceptToken; // for websocket bool downClosed; // for websocket bool upClosed; // for websockets QString method; bool responseHeadersOnly; // HEAD, 204, 304 qint64 lastRefresh; int refreshBucket; bool pendingCancel; // m2 stuff M2Connection *conn; bool persistent; bool allowChunked; bool respondKeepAlive; bool respondClose; bool chunked; int readCount; BufferList pendingIn; QList pendingInPackets; bool inFinished; // zhttp stuff QByteArray id; QByteArray zhttpAddress; bool sentResponseHeader; int outSeq; int inSeq; int pendingInCredits; bool inHandoff; bool multi; Session() : lastActive(-1), downClosed(false), upClosed(false), responseHeadersOnly(false), lastRefresh(-1), pendingCancel(false), persistent(false), allowChunked(false), respondKeepAlive(false), respondClose(false), chunked(false), readCount(0), inFinished(false), sentResponseHeader(false), outSeq(0), inSeq(0), pendingInCredits(0), inHandoff(false), multi(false) { } }; M2AdapterApp *q; ArgsData args; QByteArray zhttpInstanceId; QByteArray zwsInstanceId; QZmq::Socket *m2_in_sock; QZmq::Socket *m2_out_sock; QZmq::Socket *zhttp_in_sock; QZmq::Socket *zhttp_out_sock; QZmq::Socket *zhttp_out_stream_sock; QZmq::Socket *zws_in_sock; QZmq::Socket *zws_out_sock; QZmq::Socket *zws_out_stream_sock; QZmq::Valve *m2_in_valve; QZmq::Valve *zhttp_in_valve; QZmq::Valve *zws_in_valve; QList m2_send_idents; QHash m2ConnectionsByRid; QHash sessionsByM2Rid; QHash sessionsByZhttpRid; QHash sessionsByZwsRid; QMap, M2Connection*> m2ConnectionsByLastRefresh; QSet m2ConnectionRefreshBuckets[M2_REFRESH_BUCKETS]; int currentM2RefreshBucket; QMap, Session*> sessionsByLastRefresh; QSet sessionRefreshBuckets[ZHTTP_REFRESH_BUCKETS]; int currentSessionRefreshBucket; QMap, Session*> sessionsByLastActive; int zhttpCancelMeter; QSet sessionsToCancel; int m2_client_buffer; int maxSessions; int zhttpConnectPort; int zwsConnectPort; bool ignorePolicies; QList controlPorts; QElapsedTimer time; QTimer *statusTimer; QTimer *refreshTimer; Connection quitConnection; Connection hupConnection; map rrConnection; Connection m2InValveConnection; Connection zhttpInValveConnection; Connection zwsInValveConnection; Private(M2AdapterApp *_q) : QObject(_q), q(_q), m2_in_sock(0), m2_out_sock(0), zhttp_in_sock(0), zhttp_out_sock(0), zhttp_out_stream_sock(0), zws_in_sock(0), zws_out_sock(0), zws_out_stream_sock(0), m2_in_valve(0), zhttp_in_valve(0), zws_in_valve(0), currentM2RefreshBucket(0), currentSessionRefreshBucket(0), zhttpCancelMeter(0) { quitConnection = ProcessQuit::instance()->quit.connect(boost::bind(&Private::doQuit, this)); hupConnection = ProcessQuit::instance()->hup.connect(boost::bind(&M2AdapterApp::Private::reload, this)); statusTimer = new QTimer(this); connect(statusTimer, &QTimer::timeout, this, &Private::status_timeout); refreshTimer = new QTimer(this); connect(refreshTimer, &QTimer::timeout, this, &Private::refresh_timeout); time.start(); } ~Private() { qDeleteAll(sessionsByZhttpRid); qDeleteAll(sessionsByZwsRid); qDeleteAll(m2ConnectionsByRid); } void start() { QCoreApplication::setApplicationName("m2adapter"); QCoreApplication::setApplicationVersion(Config::get().version); QCommandLineParser parser; parser.setApplicationDescription("Mongrel2 <-> ZHTTP adapter."); QString errorMessage; switch(parseCommandLine(&parser, &args, &errorMessage)) { case CommandLineOk: break; case CommandLineError: fprintf(stderr, "%s\n\n%s", qPrintable(errorMessage), qPrintable(parser.helpText())); q->quit(1); return; case CommandLineVersionRequested: printf("%s %s\n", qPrintable(QCoreApplication::applicationName()), qPrintable(QCoreApplication::applicationVersion())); q->quit(0); return; case CommandLineHelpRequested: parser.showHelp(); Q_UNREACHABLE(); } if(!init()) { q->quit(1); return; } m2_in_valve->open(); if(zhttp_in_valve) zhttp_in_valve->open(); if(zws_in_valve) zws_in_valve->open(); statusTimer->setInterval(STATUS_INTERVAL); refreshTimer->setInterval(REFRESH_INTERVAL); refreshTimer->start(); log_info("started"); } bool init() { if(args.logLevel != -1) log_setOutputLevel(args.logLevel); else log_setOutputLevel(LOG_LEVEL_INFO); if(!args.logFile.isEmpty()) { if(!log_setFile(args.logFile)) { log_error("failed to open log file: %s", qPrintable(args.logFile)); return false; } } log_info("starting..."); QString configFile = args.configFile; if(configFile.isEmpty()) configFile = QDir(Config::get().configDir).filePath("m2adapter.conf"); // QSettings doesn't inform us if the config file doesn't exist, so do that ourselves { QFile file(configFile); if(!file.open(QIODevice::ReadOnly)) { log_error("failed to open %s, and --config not passed", qPrintable(configFile)); return false; } } QSettings settings(configFile, QSettings::IniFormat); QStringList m2_in_specs = settings.value("m2_in_specs").toStringList(); trimlist(&m2_in_specs); QStringList m2_out_specs = settings.value("m2_out_specs").toStringList(); trimlist(&m2_out_specs); QStringList str_m2_send_idents = settings.value("m2_send_idents").toStringList(); trimlist(&str_m2_send_idents); QStringList m2_control_specs = settings.value("m2_control_specs").toStringList(); trimlist(&m2_control_specs); bool zhttp_connect = settings.value("zhttp_connect").toBool(); QStringList zhttp_in_specs = settings.value("zhttp_in_specs").toStringList(); trimlist(&zhttp_in_specs); QStringList zhttp_out_specs = settings.value("zhttp_out_specs").toStringList(); trimlist(&zhttp_out_specs); QStringList zhttp_out_stream_specs = settings.value("zhttp_out_stream_specs").toStringList(); trimlist(&zhttp_out_stream_specs); bool zws_connect = settings.value("zws_connect").toBool(); QStringList zws_in_specs = settings.value("zws_in_specs").toStringList(); trimlist(&zws_in_specs); QStringList zws_out_specs = settings.value("zws_out_specs").toStringList(); trimlist(&zws_out_specs); QStringList zws_out_stream_specs = settings.value("zws_out_stream_specs").toStringList(); trimlist(&zws_out_stream_specs); zhttpConnectPort = settings.value("zhttp_connect_port", -1).toInt(); zwsConnectPort = settings.value("zws_connect_port", -1).toInt(); ignorePolicies = settings.value("ignore_policies").toBool(); m2_client_buffer = settings.value("m2_client_buffer").toInt(); if(m2_client_buffer <= 0) m2_client_buffer = 200000; maxSessions = settings.value("max_open_requests", -1).toInt(); m2_send_idents.clear(); foreach(const QString &s, str_m2_send_idents) m2_send_idents += s.toUtf8(); if(m2_in_specs.isEmpty() || m2_out_specs.isEmpty() || m2_control_specs.isEmpty()) { log_error("must set m2_in_specs, m2_out_specs, and m2_control_specs"); return false; } if(m2_send_idents.count() != m2_control_specs.count()) { log_error("m2_control_specs must have the same count as m2_send_idents"); return false; } if((!zhttp_in_specs.isEmpty() || !zhttp_out_specs.isEmpty() || !zhttp_out_stream_specs.isEmpty()) && (zhttp_in_specs.isEmpty() || zhttp_out_specs.isEmpty() || zhttp_out_stream_specs.isEmpty())) { log_error("if zhttp is used, must set all of zhttp_in_specs, zhttp_out_specs, and zhttp_out_stream_specs"); return false; } if((!zws_in_specs.isEmpty() || !zws_out_specs.isEmpty() || !zws_out_stream_specs.isEmpty()) && (zws_in_specs.isEmpty() || zws_out_specs.isEmpty() || zws_out_stream_specs.isEmpty())) { log_error("if zws is used, must set all of zws_in_specs, zws_out_specs, and zws_out_stream_specs"); return false; } if(zhttp_in_specs.isEmpty() || zws_in_specs.isEmpty()) { log_error("must set zhttp_* and/or zws_* specs"); return false; } QByteArray pidStr = QByteArray::number(QCoreApplication::applicationPid()); zhttpInstanceId = "m2zhttp_" + pidStr; zwsInstanceId = "m2zws_" + pidStr; m2_in_sock = new QZmq::Socket(QZmq::Socket::Pull, this); m2_in_sock->setHwm(DEFAULT_HWM); foreach(const QString &spec, m2_in_specs) { log_info("m2_in connect %s", qPrintable(spec)); m2_in_sock->connectToAddress(spec); } m2_in_valve = new QZmq::Valve(m2_in_sock, this); m2InValveConnection = m2_in_valve->readyRead.connect(boost::bind(&Private::m2_in_readyRead, this, boost::placeholders::_1)); m2_out_sock = new QZmq::Socket(QZmq::Socket::Pub, this); m2_out_sock->setShutdownWaitTime(0); m2_out_sock->setHwm(DEFAULT_HWM); m2_out_sock->setWriteQueueEnabled(false); foreach(const QString &spec, m2_out_specs) { log_info("m2_out connect %s", qPrintable(spec)); m2_out_sock->connectToAddress(spec); } for(int n = 0; n < m2_control_specs.count(); ++n) { const QString &spec = m2_control_specs[n]; QZmq::Socket *sock = new QZmq::Socket(QZmq::Socket::Dealer, this); sock->setShutdownWaitTime(0); sock->setHwm(1); // queue up 1 outstanding request at most sock->setWriteQueueEnabled(false); rrConnection[sock] = sock->readyRead.connect(boost::bind(&Private::m2_control_readyRead, this, sock)); log_info("m2_control connect %s:%s", m2_send_idents[n].data(), qPrintable(spec)); sock->connectToAddress(spec); ControlPort controlPort; controlPort.sock = sock; controlPorts += controlPort; } if(!zhttp_in_specs.isEmpty()) { zhttp_in_sock = new QZmq::Socket(QZmq::Socket::Sub, this); zhttp_in_sock->setHwm(DEFAULT_HWM); zhttp_in_sock->setShutdownWaitTime(0); zhttp_in_sock->subscribe(zhttpInstanceId + ' '); if(zhttp_connect) { foreach(const QString &spec, zhttp_in_specs) { log_info("zhttp_in connect %s", qPrintable(spec)); zhttp_in_sock->connectToAddress(spec); } } else { log_info("zhttp_in bind %s", qPrintable(zhttp_in_specs[0])); if(!zhttp_in_sock->bind(zhttp_in_specs[0])) { log_error("unable to bind to zhttp_in spec: %s", qPrintable(zhttp_in_specs[0])); return false; } } zhttp_in_valve = new QZmq::Valve(zhttp_in_sock, this); zhttpInValveConnection = zhttp_in_valve->readyRead.connect(boost::bind(&Private::zhttp_in_readyRead, this, boost::placeholders::_1)); zhttp_out_sock = new QZmq::Socket(QZmq::Socket::Push, this); zhttp_out_sock->setShutdownWaitTime(0); zhttp_out_sock->setHwm(DEFAULT_HWM); if(zhttp_connect) { foreach(const QString &spec, zhttp_out_specs) { log_info("zhttp_out connect %s", qPrintable(spec)); zhttp_out_sock->connectToAddress(spec); } } else { log_info("zhttp_out bind %s", qPrintable(zhttp_out_specs[0])); if(!zhttp_out_sock->bind(zhttp_out_specs[0])) { log_error("unable to bind to zhttp_out spec: %s", qPrintable(zhttp_out_specs[0])); return false; } } zhttp_out_stream_sock = new QZmq::Socket(QZmq::Socket::Router, this); zhttp_out_stream_sock->setShutdownWaitTime(0); zhttp_out_stream_sock->setHwm(DEFAULT_HWM); if(zhttp_connect) { foreach(const QString &spec, zhttp_out_stream_specs) { log_info("zhttp_out_stream connect %s", qPrintable(spec)); zhttp_out_stream_sock->connectToAddress(spec); } } else { log_info("zhttp_out_stream bind %s", qPrintable(zhttp_out_stream_specs[0])); if(!zhttp_out_stream_sock->bind(zhttp_out_stream_specs[0])) { log_error("unable to bind to zhttp_out_stream spec: %s", qPrintable(zhttp_out_stream_specs[0])); return false; } } } if(!zws_in_specs.isEmpty()) { zws_in_sock = new QZmq::Socket(QZmq::Socket::Sub, this); zws_in_sock->setHwm(DEFAULT_HWM); zws_in_sock->subscribe(zwsInstanceId + ' '); if(zws_connect) { foreach(const QString &spec, zws_in_specs) { log_info("zws_in connect %s", qPrintable(spec)); zws_in_sock->connectToAddress(spec); } } else { log_info("zws_in bind %s", qPrintable(zws_in_specs[0])); if(!zws_in_sock->bind(zws_in_specs[0])) { log_error("unable to bind to zws_in spec: %s", qPrintable(zws_in_specs[0])); return false; } } zws_in_valve = new QZmq::Valve(zws_in_sock, this); zwsInValveConnection = zws_in_valve->readyRead.connect(boost::bind(&Private::zws_in_readyRead, this, boost::placeholders::_1)); zws_out_sock = new QZmq::Socket(QZmq::Socket::Push, this); zws_out_sock->setShutdownWaitTime(0); zws_out_sock->setHwm(DEFAULT_HWM); if(zws_connect) { foreach(const QString &spec, zws_out_specs) { log_info("zws_out connect %s", qPrintable(spec)); zws_out_sock->connectToAddress(spec); } } else { log_info("zws_out bind %s", qPrintable(zws_out_specs[0])); if(!zws_out_sock->bind(zws_out_specs[0])) { log_error("unable to bind to zws_out spec: %s", qPrintable(zws_out_specs[0])); return false; } } zws_out_stream_sock = new QZmq::Socket(QZmq::Socket::Router, this); zws_out_stream_sock->setShutdownWaitTime(0); zws_out_stream_sock->setHwm(DEFAULT_HWM); if(zws_connect) { foreach(const QString &spec, zws_out_stream_specs) { log_info("zws_out_stream connect %s", qPrintable(spec)); zws_out_stream_sock->connectToAddress(spec); } } else { log_info("zws_out_stream bind %s", qPrintable(zws_out_stream_specs[0])); if(!zws_out_stream_sock->bind(zws_out_stream_specs[0])) { log_error("unable to bind to zws_out_stream spec: %s", qPrintable(zws_out_stream_specs[0])); return false; } } } return true; } void removeConnection(M2Connection *conn) { m2ConnectionRefreshBuckets[conn->refreshBucket].remove(conn); m2ConnectionsByLastRefresh.remove(QPair(conn->lastRefresh, conn)); m2ConnectionsByRid.remove(Rid(m2_send_idents[conn->identIndex], conn->id)); } void unlinkConnection(Session *s) { if(s->conn) { s->conn->session = 0; // unlink the M2Connection so that it may be reused if(s->conn->packetsPending > 0 || !s->conn->pendingOutItems.isEmpty()) s->conn->waitForAllWritten = true; sessionsByM2Rid.remove(Rid(m2_send_idents[s->conn->identIndex], s->conn->id)); s->conn = 0; } } int smallestM2RefreshBucket() { int best = -1; int bestSize = 0; for(int n = 0; n < M2_REFRESH_BUCKETS; ++n) { if(best == -1 || m2ConnectionRefreshBuckets[n].count() < bestSize) { best = n; bestSize = m2ConnectionRefreshBuckets[n].count(); } } return best; } int smallestSessionRefreshBucket() { int best = -1; int bestSize = 0; for(int n = 0; n < ZHTTP_REFRESH_BUCKETS; ++n) { if(best == -1 || sessionRefreshBuckets[n].count() < bestSize) { best = n; bestSize = sessionRefreshBuckets[n].count(); } } return best; } void removeSession(Session *s) { unlinkConnection(s); sessionsToCancel -= s; if(s->lastRefresh >= 0) { QPair k(s->lastRefresh, s); if(sessionsByLastRefresh.contains(k)) { sessionRefreshBuckets[s->refreshBucket].remove(s); sessionsByLastRefresh.remove(k); } } sessionsByLastActive.remove(QPair(s->lastActive, s)); if(s->mode == Http) sessionsByZhttpRid.remove(Rid(zhttpInstanceId, s->id)); else // WebSocket sessionsByZwsRid.remove(Rid(zwsInstanceId, s->id)); } void destroySession(Session *s) { removeSession(s); delete s; } void queueCancelSession(Session *s) { assert(!s->zhttpAddress.isEmpty()); unlinkConnection(s); s->pendingCancel = true; sessionsToCancel += s; } void destroySessionAndErrorConnection(Session *s) { M2Connection *conn = s->conn; destroySession(s); if(conn) m2_writeErrorClose(conn); } void m2_out_write(const M2ResponsePacket &packet) { QByteArray buf = packet.toByteArray(); if(log_outputLevel() >= LOG_LEVEL_DEBUG) LogUtil::logByteArray(LOG_LEVEL_DEBUG, buf, "m2: OUT"); m2_out_sock->write(QList() << buf); } void m2_control_write(int index, const QByteArray &cmd, const QVariantHash &args) { QVariantList vlist; vlist += cmd; vlist += args; QByteArray buf = TnetString::fromVariant(vlist); #ifdef CONTROL_PORT_DEBUG log_debug("m2: OUT control %s %s", m2_send_idents[index].data(), buf.data()); #endif QList message; message += QByteArray(); message += buf; controlPorts[index].sock->write(message); } void m2_writeCtl(M2Connection *conn, const QVariant &args) { M2ResponsePacket mresp; mresp.sender = m2_send_idents[conn->identIndex]; mresp.id = "X " + conn->id; QVariantList parts; parts += QByteArray("ctl"); parts += args; mresp.data = TnetString::fromVariant(parts); m2_out_write(mresp); } void m2_writeCtlMany(const QByteArray &sender, const QList &connIds, const QVariant &args) { assert(!connIds.isEmpty()); M2ResponsePacket mresp; mresp.sender = sender; mresp.id = "X"; foreach(const QByteArray &id, connIds) mresp.id += " " + id; QVariantList parts; parts += QByteArray("ctl"); parts += args; mresp.data = TnetString::fromVariant(parts); m2_out_write(mresp); } void m2_writeCtlCancel(const QByteArray &sender, const QByteArray &id) { M2ResponsePacket mresp; mresp.sender = sender; mresp.id = "X " + id; QVariantHash args; args["cancel"] = true; QVariantList parts; parts += QByteArray("ctl"); parts += args; mresp.data = TnetString::fromVariant(parts); m2_out_write(mresp); } void m2_writeCtlCancel(M2Connection *conn) { m2_writeCtlCancel(m2_send_idents[conn->identIndex], conn->id); removeConnection(conn); delete conn; } // contentSize = packet.data.size() - framing overhead void m2_writeData(M2Connection *conn, const M2ResponsePacket &packet, int contentSize) { if(conn->outCreditsEnabled) conn->outCredits -= packet.data.size(); conn->bodyTracker.addPlain(contentSize); conn->bodyTracker.specifyEncoded(packet.data.size(), contentSize); ++(conn->packetsPending); conn->packetTracker.addPlain(1); conn->packetTracker.specifyEncoded(packet.data.size(), 1); m2_out_write(packet); } void m2_queueHeaders(M2Connection *conn, const QByteArray &headerData) { // only try writing if this item would be next bool tryWrite = conn->pendingOutItems.isEmpty(); M2PendingOutItem item(M2PendingOutItem::Headers); item.data += headerData; conn->pendingOutItems += item; if(tryWrite) m2_tryWriteQueued(conn); } void m2_queueResponse(M2Connection *conn, const QByteArray &data, bool chunked) { // skip if the result would send no bytes if(data.isEmpty() && !chunked) return; // only try writing if this item would be next bool tryWrite = conn->pendingOutItems.isEmpty(); M2PendingOutItem *item = 0; if(!conn->pendingOutItems.isEmpty()) { M2PendingOutItem &last = conn->pendingOutItems.last(); bool lastIsZeroChunk = (last.chunked && last.data.isEmpty()); // see if we can merge with the previous item if(last.type == M2PendingOutItem::Response && last.chunked == chunked && !lastIsZeroChunk) item = &last; } if(!item) { conn->pendingOutItems += M2PendingOutItem(M2PendingOutItem::Response); item = &(conn->pendingOutItems.last()); } item->data += data; item->chunked = chunked; if(tryWrite) m2_tryWriteQueued(conn); } void m2_queueFrame(M2Connection *conn, const QByteArray &data, int contentSize) { // only try writing if this item would be next bool tryWrite = conn->pendingOutItems.isEmpty(); M2PendingOutItem item(M2PendingOutItem::Frame); item.data += data; item.contentSize = contentSize; conn->pendingOutItems += item; if(tryWrite) m2_tryWriteQueued(conn); } void m2_queueClose(M2Connection *conn) { // only try writing if this item would be next bool tryWrite = conn->pendingOutItems.isEmpty(); conn->pendingOutItems += M2PendingOutItem(M2PendingOutItem::Close); if(tryWrite) m2_tryWriteQueued(conn); } // return true if connection was deleted as a result of writing queued items bool m2_tryWriteQueued(M2Connection *conn) { while(!conn->pendingOutItems.isEmpty() && conn->canWrite()) { M2PendingOutItem *item = &conn->pendingOutItems.first(); if(item->type == M2PendingOutItem::Headers) { M2ResponsePacket packet; packet.sender = m2_send_idents[conn->identIndex]; packet.id = conn->id; packet.data = item->data.take(); conn->pendingOutItems.removeFirst(); m2_writeData(conn, packet, 0); if(!conn->flowControl) handleConnectionBytesWritten(conn, packet.data.size(), true); } else if(item->type == M2PendingOutItem::Response) { // only write what we're allowed to int maxSize; if(conn->outCreditsEnabled) { if(item->chunked) maxSize = conn->outCredits - 16; // make room for chunked header else maxSize = conn->outCredits; } else { maxSize = 200000; // some reasonable max } if(maxSize <= 0) { // can't write at this time break; } QByteArray data = item->data.take(maxSize); int contentSize = data.size(); M2ResponsePacket packet; packet.sender = m2_send_idents[conn->identIndex]; packet.id = conn->id; if(item->chunked) packet.data = makeChunkHeader(data.size()) + data + makeChunkFooter(); else packet.data = data; if(item->data.isEmpty()) conn->pendingOutItems.removeFirst(); m2_writeData(conn, packet, contentSize); if(!conn->flowControl) handleConnectionBytesWritten(conn, packet.data.size(), true); } else if(item->type == M2PendingOutItem::Frame) { M2ResponsePacket packet; packet.sender = m2_send_idents[conn->identIndex]; packet.id = conn->id; packet.data = item->data.take(); int contentSize = item->contentSize; conn->pendingOutItems.removeFirst(); m2_writeData(conn, packet, contentSize); if(!conn->flowControl) handleConnectionBytesWritten(conn, packet.data.size(), true); } else if(item->type == M2PendingOutItem::Close) { conn->pendingOutItems.removeFirst(); m2_writeClose(conn); // this will delete the connection return true; } } return false; } void m2_writeClose(const QByteArray &sender, const QByteArray &id) { M2ResponsePacket mresp; mresp.sender = sender; mresp.id = id; mresp.data = ""; m2_out_write(mresp); } void m2_writeClose(M2Connection *conn) { m2_writeClose(m2_send_idents[conn->identIndex], conn->id); removeConnection(conn); delete conn; } void m2_writeErrorClose(const QByteArray &sender, const QByteArray &id) { // same as closing. in the future we may want to send something interesting first. m2_writeClose(sender, id); } void m2_writeErrorClose(M2Connection *conn) { // same as closing. in the future we may want to send something interesting first. m2_writeClose(conn); } void zhttp_out_write(Mode mode, const ZhttpRequestPacket &packet) { const char *logprefix = (mode == Http ? "zhttp" : "zws"); QVariant vpacket = packet.toVariant(); QByteArray buf = QByteArray("T") + TnetString::fromVariant(vpacket); if(log_outputLevel() >= LOG_LEVEL_DEBUG) LogUtil::logVariantWithContent(LOG_LEVEL_DEBUG, vpacket, "body", "%s: OUT", logprefix); if(mode == Http) zhttp_out_sock->write(QList() << buf); else // WebSocket zws_out_sock->write(QList() << buf); } void zhttp_out_write(Mode mode, const ZhttpRequestPacket &packet, const QByteArray &instanceAddress) { const char *logprefix = (mode == Http ? "zhttp" : "zws"); QVariant vpacket = packet.toVariant(); QByteArray buf = QByteArray("T") + TnetString::fromVariant(vpacket); if(log_outputLevel() >= LOG_LEVEL_DEBUG) LogUtil::logVariantWithContent(LOG_LEVEL_DEBUG, vpacket, "body", "%s: OUT instance=%s", logprefix, instanceAddress.data()); QList message; message += instanceAddress; message += QByteArray(); message += buf; if(mode == Http) zhttp_out_stream_sock->write(message); else // WebSocket zws_out_stream_sock->write(message); } void zhttp_out_writeFirst(Session *s, const ZhttpRequestPacket &packet) { ZhttpRequestPacket out = packet; out.from = (s->mode == Http ? zhttpInstanceId : zwsInstanceId); out.ids += ZhttpRequestPacket::Id(s->id, (s->outSeq)++); zhttp_out_write(s->mode, out); } void zhttp_out_write(Session *s, const ZhttpRequestPacket &packet) { assert(!s->zhttpAddress.isEmpty()); ZhttpRequestPacket out = packet; out.from = (s->mode == Http ? zhttpInstanceId : zwsInstanceId); out.ids += ZhttpRequestPacket::Id(s->id, (s->outSeq)++); zhttp_out_write(s->mode, out, s->zhttpAddress); } void handleControlResponse(int index, const QVariant &data) { #ifdef CONTROL_PORT_DEBUG if(log_outputLevel() >= LOG_LEVEL_DEBUG) log_debug("m2: IN control %s %s", m2_send_idents[index].data(), qPrintable(TnetString::variantToString(data))); #endif if(typeId(data) != QMetaType::QVariantHash) return; QVariantHash vhash = data.toHash(); if(!vhash.contains("rows")) return; QVariant rows = vhash["rows"]; // once we get at least one successful response then we flag the port as working if(!controlPorts[index].works) { controlPorts[index].works = true; log_debug("control port index=%d works", index); } QSet ids; foreach(const QVariant &row, rows.toList()) { if(typeId(row) != QMetaType::QVariantList) break; QVariantList vlist = row.toList(); QByteArray id = vlist[0].toByteArray(); int bytes_written = vlist[7].toInt(); ids += id; M2Connection *conn = m2ConnectionsByRid.value(Rid(m2_send_idents[index], id)); if(!conn || !conn->flowControl || conn->outCreditsEnabled) continue; if(bytes_written > conn->confirmedBytesWritten) { int written = bytes_written - conn->confirmedBytesWritten; conn->confirmedBytesWritten = bytes_written; handleConnectionBytesWritten(conn, written, true); // if we had any pending writes to make, now's the time. // note: this might delete the connection but that's fine m2_tryWriteQueued(conn); } } // any connections missing? QList gone; QHashIterator it(m2ConnectionsByRid); while(it.hasNext()) { it.next(); M2Connection *conn = it.value(); if(conn->identIndex == index) { // only check for missing connections that aren't flagged if(!conn->isNew) { if(!ids.contains(conn->id)) gone += conn; } else { // clear the flag so the connection gets processed next time conn->isNew = false; } } } foreach(M2Connection *conn, gone) { log_debug("m2: %s id=%s disconnected", m2_send_idents[conn->identIndex].data(), conn->id.data()); if(conn->session) endSession(conn->session, "disconnected"); removeConnection(conn); delete conn; } } // return true if connection was deleted as a result of handling bytes written void handleConnectionBytesWritten(M2Connection *conn, int written, bool giveCredits) { int bodyWritten = conn->bodyTracker.finished(written); int packetsWritten = conn->packetTracker.finished(written); conn->packetsPending -= packetsWritten; if(conn->waitForAllWritten && conn->packetsPending == 0 && conn->pendingOutItems.isEmpty()) conn->waitForAllWritten = false; if(conn->session && bodyWritten > 0) { Session *s = conn->session; // update lastActive qint64 now = QDateTime::currentMSecsSinceEpoch(); sessionsByLastActive.remove(QPair(s->lastActive, s)); s->lastActive = now; sessionsByLastActive.insert(QPair(s->lastActive, s), s); handleSessionBodyWritten(s, bodyWritten, giveCredits); } } void handleSessionBodyWritten(Session *s, int written, bool giveCredits) { s->pendingInCredits += written; log_debug("request id=%s written %d%s", s->id.data(), written, s->conn->flowControl ? "" : " (no flow control)"); if(s->inHandoff) return; // address could be empty here if we're handling write of non-sequenced response if(giveCredits && !s->zhttpAddress.isEmpty()) { ZhttpRequestPacket zreq; zreq.type = ZhttpRequestPacket::Credit; zreq.credits = s->pendingInCredits; s->pendingInCredits = 0; zhttp_out_write(s, zreq); } } void endSession(Session *s, const QByteArray &errorCondition = QByteArray()) { // if we are in handoff or haven't received a worker ack, then queue the state if(s->inHandoff || s->zhttpAddress.isEmpty()) { if(!errorCondition.isEmpty()) s->errorCondition = errorCondition; // keep the session around unlinkConnection(s); } else { if(sessionsToCancel.isEmpty() && zhttpCancelMeter < ZHTTP_CANCEL_PER_REFRESH) { ++zhttpCancelMeter; ZhttpRequestPacket zreq; if(!errorCondition.isEmpty()) { zreq.type = ZhttpRequestPacket::Error; zreq.condition = "disconnected"; } else zreq.type = ZhttpRequestPacket::Cancel; zhttp_out_write(s, zreq); destroySession(s); } else { queueCancelSession(s); } } } void handleZhttpIn(Mode mode, const QList &message) { const char *logprefix = (mode == Http ? "zhttp" : "zws"); if(message.count() != 1) { log_warning("%s: received message with parts != 1, skipping", logprefix); return; } int at = message[0].indexOf(' '); if(at == -1) { log_warning("%s: received message with invalid format, skipping", logprefix); return; } QByteArray dataRaw = message[0].mid(at + 1); if(dataRaw.length() < 1 || dataRaw[0] != 'T') { log_warning("%s: received message with invalid format (missing type), skipping", logprefix); return; } QVariant data = TnetString::toVariant(dataRaw.mid(1)); if(data.isNull()) { log_warning("%s: received message with invalid format (tnetstring parse failed), skipping", logprefix); return; } if(log_outputLevel() >= LOG_LEVEL_DEBUG) LogUtil::logVariantWithContent(LOG_LEVEL_DEBUG, data, "body", "%s: IN", logprefix); ZhttpResponsePacket zresp; if(!zresp.fromVariant(data)) { log_warning("%s: received message with invalid format (parse failed), skipping", logprefix); return; } foreach(const ZhttpResponsePacket::Id &id, zresp.ids) handleZhttpIn(logprefix, mode, id.id, id.seq, zresp); } void handleZhttpIn(const char *logprefix, Mode mode, const QByteArray &id, int seq, const ZhttpResponsePacket &zresp) { Session *s; if(mode == Http) s = sessionsByZhttpRid.value(Rid(zhttpInstanceId, id)); else // WebSocket s = sessionsByZwsRid.value(Rid(zwsInstanceId, id)); if(!s) { log_debug("%s: received message for unknown request id, canceling", logprefix); // if this was not an error packet, send cancel if(!isErrorPacket(zresp) && !zresp.from.isEmpty()) { ZhttpRequestPacket zreq; zreq.from = (mode == Http ? zhttpInstanceId : zwsInstanceId); zreq.ids += ZhttpRequestPacket::Id(id); zreq.type = ZhttpRequestPacket::Cancel; zhttp_out_write(mode, zreq, zresp.from); } return; } // mode will always match here assert(s->mode == mode); if(s->inSeq == 0) { // are we expecting a sequence of packets after the first? if((!isErrorPacket(zresp) && zresp.type != ZhttpResponsePacket::Data) || (zresp.type == ZhttpResponsePacket::Data && zresp.more)) { // sequence must have from address if(zresp.from.isEmpty()) { log_warning("%s: received first response of sequence with no from address, canceling", logprefix); destroySessionAndErrorConnection(s); return; } s->zhttpAddress = zresp.from; if(seq != 0) { log_warning("%s: received first response of sequence without valid seq, canceling", logprefix); ZhttpRequestPacket zreq; zreq.type = ZhttpRequestPacket::Cancel; zhttp_out_write(s, zreq); destroySessionAndErrorConnection(s); return; } } else { // if not sequenced, then there might be a from address if(!zresp.from.isEmpty()) s->zhttpAddress = zresp.from; // if not sequenced, but seq is provided, then it must be 0 if(seq != -1 && seq != 0) { log_warning("%s: received response out of sequence (got=%d, expected=-1,0), canceling", logprefix, seq); if(!s->zhttpAddress.isEmpty()) { ZhttpRequestPacket zreq; zreq.type = ZhttpRequestPacket::Cancel; zhttp_out_write(s, zreq); } destroySessionAndErrorConnection(s); return; } } } else { if(seq != -1 && seq != s->inSeq) { log_warning("%s: received response out of sequence (got=%d, expected=%d), canceling", logprefix, seq, s->inSeq); ZhttpRequestPacket zreq; zreq.type = ZhttpRequestPacket::Cancel; zhttp_out_write(s, zreq); destroySessionAndErrorConnection(s); return; } // if a new from address is provided, update our copy if(!zresp.from.isEmpty()) s->zhttpAddress = zresp.from; } // only bump sequence if seq was provided if(seq != -1) ++(s->inSeq); qint64 now = QDateTime::currentMSecsSinceEpoch(); if(s->lastRefresh < 0 && !s->zhttpAddress.isEmpty()) { // once we have the peer's address, set up refresh s->lastRefresh = now; sessionsByLastRefresh.insert(QPair(s->lastRefresh, s), s); s->refreshBucket = smallestSessionRefreshBucket(); sessionRefreshBuckets[s->refreshBucket] += s; } // update lastActive sessionsByLastActive.remove(QPair(s->lastActive, s)); s->lastActive = now; sessionsByLastActive.insert(QPair(s->lastActive, s), s); if(s->pendingCancel) return; // a session without a connection is just waiting to report error if(!s->conn) { // if we were in handoff, it's okay to send right now since we'd // be clearing the handoff state later on in this method anyway if(!s->zhttpAddress.isEmpty()) { ZhttpRequestPacket zreq; if(!s->errorCondition.isEmpty()) { zreq.type = ZhttpRequestPacket::Error; zreq.condition = s->errorCondition; } else zreq.type = ZhttpRequestPacket::Cancel; zhttp_out_write(s, zreq); } destroySession(s); return; } // if peer supports multi feature then flag it on the session bool multiWasTurnedOn = false; if(!s->multi && zresp.multi) { s->multi = true; multiWasTurnedOn = true; } if(s->inHandoff) { // receiving any message means handoff is complete s->inHandoff = false; // refresh would have already been set up once if we are here assert(s->lastRefresh >= 0); sessionsByLastRefresh.insert(QPair(s->lastRefresh, s), s); s->refreshBucket = smallestSessionRefreshBucket(); sessionRefreshBuckets[s->refreshBucket] += s; // in order to have been in a handoff state, we would have // had to receive a from address sometime earlier, so it // should be safe to call zhttp_out_write with session. if(multiWasTurnedOn) { // acknowledge the feature ZhttpRequestPacket zreq; zreq.type = ZhttpRequestPacket::KeepAlive; zreq.multi = true; zhttp_out_write(s, zreq); } if(s->mode == Http) { if(!s->pendingIn.isEmpty()) { ZhttpRequestPacket zreq; zreq.type = ZhttpRequestPacket::Data; // send credits too, if needed (though this probably can't happen, // since http data flows only in one direction at a time. we // can't have pending request body data while at the same // time be acking received response body data). if(s->pendingInCredits > 0) { zreq.credits = s->pendingInCredits; s->pendingInCredits = 0; } zreq.body = s->pendingIn.take(); zreq.more = !s->inFinished; zhttp_out_write(s, zreq); } } else // WebSocket { while(!s->pendingInPackets.isEmpty()) { ZhttpRequestPacket zreq = s->pendingInPackets.takeFirst(); // send credits too, if needed if(zreq.type == ZhttpRequestPacket::Data && s->pendingInCredits > 0) { zreq.credits = s->pendingInCredits; s->pendingInCredits = 0; } zhttp_out_write(s, zreq); } } // if we didn't send credits as part of a data packet, we'll do them now if(s->pendingInCredits > 0) { ZhttpRequestPacket zreq; zreq.type = ZhttpRequestPacket::Credit; zreq.credits = s->pendingInCredits; s->pendingInCredits = 0; zhttp_out_write(s, zreq); } } if(zresp.type == ZhttpResponsePacket::Data) { log_debug("zhttp: id=%s response data size=%d%s", s->id.data(), zresp.body.size(), zresp.more ? " M" : ""); // data packet may have credits if(zresp.credits > 0) { QVariantHash args; args["credits"] = zresp.credits; m2_writeCtl(s->conn, args); } if(s->mode == Http) { bool firstDataPacket = !s->sentResponseHeader; // respond with data if we have body data or this is the first packet if(!zresp.body.isEmpty() || firstDataPacket) { if(firstDataPacket) { // use flow control if the control port works and the response is more than one packet if((s->conn->outCreditsEnabled || controlPorts[s->conn->identIndex].works) && zresp.more) s->conn->flowControl = true; else s->conn->flowControl = false; s->sentResponseHeader = true; if(zresp.more && !zresp.headers.contains("Content-Length")) { if(s->allowChunked) { s->chunked = true; } else { // disable persistence s->persistent = false; s->respondKeepAlive = false; } } if(s->method == "HEAD" || (zresp.code == 204 || zresp.code == 304)) s->responseHeadersOnly = true; HttpHeaders headers = zresp.headers; QList connHeaders = headers.takeAll("Connection"); foreach(const QByteArray &h, connHeaders) headers.removeAll(h); headers.removeAll("Transfer-Encoding"); connHeaders.clear(); if(s->respondKeepAlive) connHeaders += "Keep-Alive"; if(s->respondClose) connHeaders += "close"; if(!s->responseHeadersOnly) { if(s->chunked) { connHeaders += "Transfer-Encoding"; headers += HttpHeader("Transfer-Encoding", "chunked"); } else if(!zresp.more && !headers.contains("Content-Length")) { headers += HttpHeader("Content-Length", QByteArray::number(zresp.body.size())); } } if(!connHeaders.isEmpty()) headers += HttpHeader("Connection", HttpHeaders::join(connHeaders)); log_info("OUT %s id=%s code=%d %d%s", m2_send_idents[s->conn->identIndex].data(), s->conn->id.data(), zresp.code, zresp.body.size(), zresp.more ? " M": ""); m2_queueHeaders(s->conn, createResponseHeader(zresp.code, zresp.reason, headers)); } if(!zresp.body.isEmpty()) { if(s->responseHeadersOnly) { log_warning("%s: received unexpected response body, canceling", logprefix); bool persistent = s->persistent; // cancel and destroy session M2Connection *conn = s->conn; if(!s->zhttpAddress.isEmpty()) { ZhttpRequestPacket zreq; zreq.type = ZhttpRequestPacket::Cancel; zhttp_out_write(s, zreq); } destroySession(s); if(!persistent) m2_queueClose(conn); return; } m2_queueResponse(s->conn, zresp.body, s->chunked); } if(!zresp.more && s->chunked) { // send closing chunk m2_queueResponse(s->conn, QByteArray(), true); } } else { if(!zresp.more && s->chunked) { // send closing chunk m2_queueResponse(s->conn, QByteArray(), true); } } if(!zresp.more) { bool persistent = s->persistent; M2Connection *conn = s->conn; destroySession(s); if(!persistent) m2_queueClose(conn); } } else // WebSocket { if(!s->sentResponseHeader) { s->sentResponseHeader = true; if(s->conn->outCreditsEnabled || controlPorts[s->conn->identIndex].works) s->conn->flowControl = true; else s->conn->flowControl = false; HttpHeaders headers = zresp.headers; QList connHeaders = headers.takeAll("Connection"); foreach(const QByteArray &h, connHeaders) headers.removeAll(h); headers.removeAll("Transfer-Encoding"); headers.removeAll("Upgrade"); headers.removeAll("Sec-WebSocket-Accept"); headers += HttpHeader("Upgrade", "websocket"); headers += HttpHeader("Connection", "Upgrade"); headers += HttpHeader("Sec-WebSocket-Accept", s->acceptToken); QByteArray reason; if(!zresp.reason.isEmpty()) reason = zresp.reason; else reason = "Switching Protocols"; log_info("OUT %s id=%s code=%d 0 M", m2_send_idents[s->conn->identIndex].data(), s->conn->id.data(), zresp.code); m2_queueHeaders(s->conn, createResponseHeader(101, reason, headers)); } else { int opcode; if(s->conn->continuation) { opcode = 0; } else { if(zresp.contentType == "binary") opcode = 2; else // text opcode = 1; } s->conn->continuation = zresp.more; QByteArray frame = makeWsHeader(!zresp.more, opcode, zresp.body.size()) + zresp.body; m2_queueFrame(s->conn, frame, zresp.body.size()); } } } else if(zresp.type == ZhttpResponsePacket::Error) { log_debug("%s: id=%s error condition=%s", logprefix, s->id.data(), zresp.condition.data()); if(s->mode == WebSocket && zresp.condition == "rejected") { HttpHeaders headers = zresp.headers; QList connHeaders = headers.takeAll("Connection"); foreach(const QByteArray &h, connHeaders) headers.removeAll(h); headers.removeAll("Transfer-Encoding"); if(!headers.contains("Content-Length")) headers += HttpHeader("Content-Length", QByteArray::number(zresp.body.size())); connHeaders.clear(); // if HTTP/1.1, include "Connection: close" if(s->allowChunked) connHeaders += "close"; if(!connHeaders.isEmpty()) headers += HttpHeader("Connection", HttpHeaders::join(connHeaders)); log_info("OUT %s id=%s code=%d %d", m2_send_idents[s->conn->identIndex].data(), s->conn->id.data(), zresp.code, zresp.body.size()); m2_queueHeaders(s->conn, createResponseHeader(zresp.code, zresp.reason, headers)); m2_queueResponse(s->conn, zresp.body, false); M2Connection *conn = s->conn; destroySession(s); m2_queueClose(conn); } else destroySessionAndErrorConnection(s); } else if(zresp.type == ZhttpResponsePacket::Credit) { if(zresp.credits > 0) { QVariantHash args; args["credits"] = zresp.credits; m2_writeCtl(s->conn, args); } } else if(zresp.type == ZhttpResponsePacket::KeepAlive) { // nothing to do } else if(zresp.type == ZhttpResponsePacket::Cancel) { destroySessionAndErrorConnection(s); } else if(zresp.type == ZhttpResponsePacket::HandoffStart) { s->inHandoff = true; sessionRefreshBuckets[s->refreshBucket].remove(s); sessionsByLastRefresh.remove(QPair(s->lastRefresh, s)); // whoever picks up after handoff can turn this on s->multi = false; ZhttpRequestPacket zreq; zreq.type = ZhttpRequestPacket::HandoffProceed; zhttp_out_write(s, zreq); } else if(zresp.type == ZhttpResponsePacket::Close || zresp.type == ZhttpResponsePacket::Ping || zresp.type == ZhttpResponsePacket::Pong) { int opcode; if(zresp.type == ZhttpResponsePacket::Close) { opcode = 8; s->upClosed = true; } else if(zresp.type == ZhttpResponsePacket::Ping) opcode = 9; else // Pong opcode = 10; QByteArray data; if(zresp.type == ZhttpResponsePacket::Close) { data.resize(2 + zresp.body.size()); writeBigEndian(data.data(), zresp.code != -1 ? zresp.code : 1000, 2); if(!zresp.body.isEmpty()) { memcpy(data.data() + 2, zresp.body.data(), zresp.body.size()); } } else { data = zresp.body; } QByteArray frame = makeWsHeader(true, opcode, data.size()) + data; m2_queueFrame(s->conn, frame, data.size()); if(s->downClosed && s->upClosed) { M2Connection *conn = s->conn; destroySession(s); m2_queueClose(conn); } } else { log_warning("%s: id=%s unsupported type: %d", logprefix, s->id.data(), (int)zresp.type); } } void refreshM2Connections(qint64 now) { QHash > connIdListBySender; // process the current bucket const QSet &bucket = m2ConnectionRefreshBuckets[currentM2RefreshBucket]; foreach(M2Connection *conn, bucket) { // move to the end QPair k(conn->lastRefresh, conn); m2ConnectionsByLastRefresh.remove(k); conn->lastRefresh = now; m2ConnectionsByLastRefresh.insert(QPair(conn->lastRefresh, conn), conn); if(!connIdListBySender.contains(conn->identIndex)) connIdListBySender.insert(conn->identIndex, QList()); QList &connIdList = connIdListBySender[conn->identIndex]; connIdList += conn->id; // if we're at max, send out now if(connIdList.count() >= M2_HANDLER_TARGETS_MAX) { QVariantHash args; args["keep-alive"] = true; m2_writeCtlMany(m2_send_idents[conn->identIndex], connIdList, args); connIdList.clear(); connIdListBySender.remove(conn->identIndex); } } // process any others qint64 threshold = now - M2_CONNECTION_MUST_PROCESS; while(!m2ConnectionsByLastRefresh.isEmpty()) { QMap, M2Connection*>::iterator it = m2ConnectionsByLastRefresh.begin(); M2Connection *conn = it.value(); if(conn->lastRefresh > threshold) break; // move to the end m2ConnectionsByLastRefresh.erase(it); conn->lastRefresh = now; m2ConnectionsByLastRefresh.insert(QPair(conn->lastRefresh, conn), conn); if(!connIdListBySender.contains(conn->identIndex)) connIdListBySender.insert(conn->identIndex, QList()); QList &connIdList = connIdListBySender[conn->identIndex]; connIdList += conn->id; // if we're at max, send out now if(connIdList.count() >= M2_HANDLER_TARGETS_MAX) { QVariantHash args; args["keep-alive"] = true; m2_writeCtlMany(m2_send_idents[conn->identIndex], connIdList, args); connIdList.clear(); connIdListBySender.remove(conn->identIndex); } } // send last packet QHashIterator > cit(connIdListBySender); while(cit.hasNext()) { cit.next(); int index = cit.key(); const QList &connIdList = cit.value(); if(!connIdList.isEmpty()) { QVariantHash args; args["keep-alive"] = true; m2_writeCtlMany(m2_send_idents[index], connIdList, args); } } ++currentM2RefreshBucket; if(currentM2RefreshBucket >= M2_REFRESH_BUCKETS) currentM2RefreshBucket = 0; } void refreshSessions(qint64 now) { QHash > sessionListBySender[2]; // index corresponds to mode // process the current bucket const QSet &bucket = sessionRefreshBuckets[currentSessionRefreshBucket]; foreach(Session *s, bucket) { assert(!s->inHandoff && !s->zhttpAddress.isEmpty()); // move to the end QPair k(s->lastRefresh, s); sessionsByLastRefresh.remove(k); s->lastRefresh = now; sessionsByLastRefresh.insert(QPair(s->lastRefresh, s), s); if(s->multi) { if(!sessionListBySender[s->mode].contains(s->zhttpAddress)) sessionListBySender[s->mode].insert(s->zhttpAddress, QList()); QList &sessionList = sessionListBySender[s->mode][s->zhttpAddress]; sessionList += s; // if we're at max, send out now if(sessionList.count() >= ZHTTP_IDS_MAX) { ZhttpRequestPacket zreq; zreq.from = (s->mode == Http ? zhttpInstanceId : zwsInstanceId); foreach(Session *i, sessionList) zreq.ids += ZhttpRequestPacket::Id(i->id, (i->outSeq)++); zreq.type = ZhttpRequestPacket::KeepAlive; zhttp_out_write(s->mode, zreq, s->zhttpAddress); sessionList.clear(); sessionListBySender[s->mode].remove(s->zhttpAddress); } } else { // session doesn't support sending with multiple ids ZhttpRequestPacket zreq; zreq.from = (s->mode == Http ? zhttpInstanceId : zwsInstanceId); zreq.ids += ZhttpRequestPacket::Id(s->id, (s->outSeq)++); zreq.type = ZhttpRequestPacket::KeepAlive; zhttp_out_write(s->mode, zreq, s->zhttpAddress); } } // process any others qint64 threshold = now - ZHTTP_MUST_PROCESS; while(!sessionsByLastRefresh.isEmpty()) { QMap, Session*>::iterator it = sessionsByLastRefresh.begin(); Session *s = it.value(); if(s->lastRefresh > threshold) break; assert(!s->inHandoff && !s->zhttpAddress.isEmpty()); // move to the end sessionsByLastRefresh.erase(it); s->lastRefresh = now; sessionsByLastRefresh.insert(QPair(s->lastRefresh, s), s); if(s->multi) { if(!sessionListBySender[s->mode].contains(s->zhttpAddress)) sessionListBySender[s->mode].insert(s->zhttpAddress, QList()); QList &sessionList = sessionListBySender[s->mode][s->zhttpAddress]; sessionList += s; // if we're at max, send out now if(sessionList.count() >= ZHTTP_IDS_MAX) { ZhttpRequestPacket zreq; zreq.from = (s->mode == Http ? zhttpInstanceId : zwsInstanceId); foreach(Session *i, sessionList) zreq.ids += ZhttpRequestPacket::Id(i->id, (i->outSeq)++); zreq.type = ZhttpRequestPacket::KeepAlive; zhttp_out_write(s->mode, zreq, s->zhttpAddress); sessionList.clear(); sessionListBySender[s->mode].remove(s->zhttpAddress); } } else { // session doesn't support sending with multiple ids ZhttpRequestPacket zreq; zreq.from = (s->mode == Http ? zhttpInstanceId : zwsInstanceId); zreq.ids += ZhttpRequestPacket::Id(s->id, (s->outSeq)++); zreq.type = ZhttpRequestPacket::KeepAlive; zhttp_out_write(s->mode, zreq, s->zhttpAddress); } } // send last packets for(int n = 0; n < 2; ++n) { Mode mode = (Mode)n; QHashIterator > sit(sessionListBySender[n]); while(sit.hasNext()) { sit.next(); const QByteArray &zhttpAddress = sit.key(); const QList &sessionList = sit.value(); if(!sessionList.isEmpty()) { ZhttpRequestPacket zreq; zreq.from = (mode == Http ? zhttpInstanceId : zwsInstanceId); foreach(Session *s, sessionList) zreq.ids += ZhttpRequestPacket::Id(s->id, (s->outSeq)++); zreq.type = ZhttpRequestPacket::KeepAlive; zhttp_out_write(mode, zreq, zhttpAddress); } } } ++currentSessionRefreshBucket; if(currentSessionRefreshBucket >= ZHTTP_REFRESH_BUCKETS) currentSessionRefreshBucket = 0; } void expireSessions(qint64 now) { qint64 threshold = now - ZHTTP_EXPIRE; while(!sessionsByLastActive.isEmpty()) { QMap, Session*>::iterator it = sessionsByLastActive.begin(); Session *s = it.value(); if(s->lastActive > threshold) break; log_warning("timing out request %s", s->id.data()); destroySessionAndErrorConnection(s); } } void cancelSessions() { int sent = zhttpCancelMeter; if(zhttpCancelMeter > ZHTTP_CANCEL_PER_REFRESH) zhttpCancelMeter -= ZHTTP_CANCEL_PER_REFRESH; else zhttpCancelMeter = 0; QHash > sessionListBySender[2]; // index corresponds to mode while(!sessionsToCancel.isEmpty() && sent < ZHTTP_CANCEL_PER_REFRESH) { QSet::iterator it = sessionsToCancel.begin(); Session *s = (*it); sessionsToCancel.erase(it); if(s->multi) { if(!sessionListBySender[s->mode].contains(s->zhttpAddress)) sessionListBySender[s->mode].insert(s->zhttpAddress, QList()); QList &sessionList = sessionListBySender[s->mode][s->zhttpAddress]; sessionList += s; // if we're at max, send out now if(sessionList.count() >= ZHTTP_IDS_MAX) { ZhttpRequestPacket zreq; zreq.from = (s->mode == Http ? zhttpInstanceId : zwsInstanceId); foreach(Session *i, sessionList) zreq.ids += ZhttpRequestPacket::Id(i->id, (i->outSeq)++); zreq.type = ZhttpRequestPacket::Cancel; zhttp_out_write(s->mode, zreq, s->zhttpAddress); Mode mode = s->mode; QByteArray zhttpAddress = s->zhttpAddress; foreach(Session *s, sessionList) destroySession(s); sessionList.clear(); sessionListBySender[mode].remove(zhttpAddress); } } else { // session doesn't support sending with multiple ids ZhttpRequestPacket zreq; zreq.from = (s->mode == Http ? zhttpInstanceId : zwsInstanceId); zreq.ids += ZhttpRequestPacket::Id(s->id, (s->outSeq)++); zreq.type = ZhttpRequestPacket::Cancel; zhttp_out_write(s->mode, zreq, s->zhttpAddress); destroySession(s); } ++sent; } // send last packets for(int n = 0; n < 2; ++n) { Mode mode = (Mode)n; QHashIterator > sit(sessionListBySender[n]); while(sit.hasNext()) { sit.next(); const QByteArray &zhttpAddress = sit.key(); const QList &sessionList = sit.value(); if(!sessionList.isEmpty()) { ZhttpRequestPacket zreq; zreq.from = (mode == Http ? zhttpInstanceId : zwsInstanceId); foreach(Session *s, sessionList) zreq.ids += ZhttpRequestPacket::Id(s->id, (s->outSeq)++); zreq.type = ZhttpRequestPacket::Cancel; zhttp_out_write(mode, zreq, zhttpAddress); } foreach(Session *s, sessionList) destroySession(s); } } } void m2_in_readyRead(const QList &message) { if(message.count() != 1) { log_warning("m2: received message with parts != 1, skipping"); return; } if(log_outputLevel() >= LOG_LEVEL_DEBUG) LogUtil::logByteArray(LOG_LEVEL_DEBUG, message[0], "m2: IN"); M2RequestPacket mreq; if(!mreq.fromByteArray(message[0])) { log_warning("m2: received message with invalid format, skipping"); return; } if(mreq.type == M2RequestPacket::Disconnect) { log_debug("m2: %s id=%s disconnected", mreq.sender.data(), mreq.id.data()); Rid rid(mreq.sender, mreq.id); M2Connection *conn = m2ConnectionsByRid.value(rid); if(!conn) return; if(conn->session) endSession(conn->session); removeConnection(conn); delete conn; return; } qint64 now = QDateTime::currentMSecsSinceEpoch(); Rid m2Rid(mreq.sender, mreq.id); M2Connection *conn = m2ConnectionsByRid.value(m2Rid); if(!conn) { if(mreq.version.isEmpty()) { if(mreq.type == M2RequestPacket::HttpRequest || mreq.type == M2RequestPacket::WebSocketHandshake) { log_warning("m2: id=%s no version on initial packet", mreq.id.data()); } m2_writeCtlCancel(mreq.sender, mreq.id); return; } if(mreq.version != "HTTP/1.0" && mreq.version != "HTTP/1.1") { log_error("m2: id=%s unknown version: %s", mreq.id.data(), mreq.version.data()); m2_writeCtlCancel(mreq.sender, mreq.id); return; } int index = -1; for(int n = 0; n < m2_send_idents.count(); ++n) { if(m2_send_idents[n] == mreq.sender) { index = n; break; } } if(index == -1) { log_error("m2: id=%s unknown send_ident [%s]", mreq.id.data(), mreq.sender.data()); return; } if(sessionsByM2Rid.contains(m2Rid)) { log_warning("m2: received duplicate request id=%s, skipping", mreq.id.data()); m2_writeCtlCancel(mreq.sender, mreq.id); return; } conn = new M2Connection; conn->identIndex = index; conn->id = mreq.id; if(mreq.downloadCredits >= 0) { conn->outCreditsEnabled = true; conn->outCredits += mreq.downloadCredits; } else { if(controlPorts[index].state == ControlPort::Disabled) { log_debug("activating control port index=%d", index); controlPorts[index].state = ControlPort::Idle; if(!statusTimer->isActive()) statusTimer->start(); } // if we were in the middle of requesting control info when this // http request arrived, then there's a chance the control // response won't account for this request (for example if the // control response was generated and was in the middle of being // delivered when this http request arrived). we'll flag the // connection as "new" in this case, so in the control response // handler we know to skip over it until the next control // request. if(controlPorts[index].state == ControlPort::ExpectingResponse) conn->isNew = true; } m2ConnectionsByRid.insert(m2Rid, conn); conn->lastRefresh = now; m2ConnectionsByLastRefresh.insert(QPair(conn->lastRefresh, conn), conn); conn->refreshBucket = smallestM2RefreshBucket(); m2ConnectionRefreshBuckets[conn->refreshBucket] += conn; } else { // if packet contained credits, handle them now if(conn->outCreditsEnabled && mreq.downloadCredits > 0) { conn->outCredits += mreq.downloadCredits; handleConnectionBytesWritten(conn, mreq.downloadCredits, true); // if we had any pending writes to make, now's the time bool connDeleted = m2_tryWriteQueued(conn); if(connDeleted) return; } // if the packet only held credits, then there's nothing else to do if(mreq.type == M2RequestPacket::Credits) return; } bool requestBodyMore = false; if(mreq.type == M2RequestPacket::HttpRequest && mreq.uploadStreamOffset >= 0 && !mreq.uploadStreamDone) requestBodyMore = true; Session *s = sessionsByM2Rid.value(m2Rid); if(!s) { if(mreq.type == M2RequestPacket::HttpRequest && mreq.uploadStreamOffset > 0) { log_warning("m2: id=%s stream offset > 0 but session unknown", mreq.id.data()); m2_writeCtlCancel(conn); return; } if(mreq.type != M2RequestPacket::HttpRequest && mreq.type != M2RequestPacket::WebSocketHandshake) { log_warning("m2: received unexpected starting packet type: %d", (int)mreq.type); m2_writeCtlCancel(conn); return; } QByteArray scheme; if(mreq.type == M2RequestPacket::HttpRequest) { if(mreq.scheme == "https") scheme = "https"; else scheme = "http"; } else // WebSocketHandshake { if(mreq.scheme == "https" || mreq.scheme == "wss") scheme = "wss"; else scheme = "ws"; } QByteArray host = mreq.headers.get("Host"); if(host.isEmpty()) host = "localhost"; if(!validateHost(host)) { log_warning("m2: invalid host [%s]", host.data()); m2_writeErrorClose(conn); return; } if(!mreq.uri.startsWith('/')) { log_warning("m2: invalid uri [%s]", mreq.uri.data()); m2_writeErrorClose(conn); return; } QByteArray uriRaw = scheme + "://" + host + mreq.uri; QUrl uri = QUrl::fromEncoded(uriRaw, QUrl::TolerantMode); if(!uri.isValid()) { log_warning("m2: invalid constructed uri: [%s]", uriRaw.data()); m2_writeErrorClose(conn); return; } if(maxSessions >= 0 && sessionsByZhttpRid.count() + sessionsByZwsRid.count() >= maxSessions) { log_warning("m2: max open sessions reached (%d), refusing new session", maxSessions); m2_writeErrorClose(conn); return; } s = new Session; s->conn = conn; s->conn->session = s; s->id = m2_send_idents[conn->identIndex] + '_' + conn->id + '_' + QByteArray::number((conn->subIdBase)++, 16); s->method = mreq.method; if(mreq.type == M2RequestPacket::HttpRequest) { s->mode = Http; if(mreq.version == "HTTP/1.0") { if(mreq.headers.getAll("Connection").contains("Keep-Alive")) { s->persistent = true; s->respondKeepAlive = true; } } else if(mreq.version == "HTTP/1.1") { s->allowChunked = true; if(mreq.headers.getAll("Connection").contains("close")) s->respondClose = true; else s->persistent = true; } s->readCount += mreq.body.size(); if(!requestBodyMore) s->inFinished = true; } else // WebSocketHandshake { s->mode = WebSocket; s->acceptToken = mreq.body; } sessionsByM2Rid.insert(m2Rid, s); qint64 now = QDateTime::currentMSecsSinceEpoch(); s->lastActive = now; sessionsByLastActive.insert(QPair(s->lastActive, s), s); if(mreq.type == M2RequestPacket::HttpRequest) sessionsByZhttpRid.insert(Rid(zhttpInstanceId, s->id), s); else // WebSocketHandshake sessionsByZwsRid.insert(Rid(zwsInstanceId, s->id), s); log_info("IN %s id=%s %s %s", m2_send_idents[s->conn->identIndex].data(), s->conn->id.data(), s->mode == Http ? qPrintable(mreq.method) : "GET", uri.toEncoded().data()); ZhttpRequestPacket zreq; zreq.type = ZhttpRequestPacket::Data; if(conn->outCreditsEnabled) zreq.credits = conn->outCredits; else zreq.credits = m2_client_buffer; zreq.uri = uri; zreq.headers = mreq.headers; zreq.peerAddress = mreq.remoteAddress; if(mreq.type == M2RequestPacket::HttpRequest && zhttpConnectPort != -1) zreq.connectPort = zhttpConnectPort; else if(zwsConnectPort != -1) // WebSocketHandshake zreq.connectPort = zwsConnectPort; if(ignorePolicies) zreq.ignorePolicies = true; if(mreq.type == M2RequestPacket::HttpRequest) { zreq.stream = true; zreq.method = mreq.method; zreq.body = mreq.body; zreq.more = !s->inFinished; } zreq.multi = true; zhttp_out_writeFirst(s, zreq); } else { assert(s->conn == conn); if(mreq.type != M2RequestPacket::HttpRequest && mreq.type != M2RequestPacket::WebSocketFrame) { log_warning("m2: received unexpected subsequent packet type: %d", (int)mreq.type); endSession(s); m2_writeCtlCancel(conn); return; } if(mreq.type == M2RequestPacket::HttpRequest) { int offset = 0; if(mreq.uploadStreamOffset > 0) offset = mreq.uploadStreamOffset; if(offset != s->readCount) { log_warning("m2: %s id=%s unexpected stream offset (got=%d, expected=%d)", m2_send_idents[s->conn->identIndex].data(), mreq.id.data(), offset, s->readCount); endSession(s); m2_writeCtlCancel(conn); return; } s->readCount += mreq.body.size(); if(!requestBodyMore) s->inFinished = true; } if(s->zhttpAddress.isEmpty()) { log_error("m2: %s id=%s multiple packets from m2 before response from zhttp", m2_send_idents[s->conn->identIndex].data(), mreq.id.data()); endSession(s); m2_writeCtlCancel(conn); return; } if(mreq.type == M2RequestPacket::HttpRequest) { if(s->inHandoff) { s->pendingIn += mreq.body; } else { ZhttpRequestPacket zreq; zreq.type = ZhttpRequestPacket::Data; zreq.body = mreq.body; zreq.more = !s->inFinished; zhttp_out_write(s, zreq); } } else // WebSocketFrame { int opcode = mreq.frameFlags & 0x0f; if(opcode != 1 && opcode != 2 && opcode != 8 && opcode != 9 && opcode != 10) { log_warning("m2: %s id=%s unsupported ws opcode: %d", m2_send_idents[s->conn->identIndex].data(), mreq.id.data(), opcode); endSession(s); m2_writeCtlCancel(conn); return; } if(s->downClosed) { log_debug("m2: %s id=%s ignoring frame after close", m2_send_idents[s->conn->identIndex].data(), mreq.id.data()); return; } ZhttpRequestPacket zreq; if(opcode == 1 || opcode == 2) { zreq.type = ZhttpRequestPacket::Data; if(opcode == 2) zreq.contentType = "binary"; zreq.body = mreq.body; } else if(opcode == 8) { zreq.type = ZhttpRequestPacket::Close; if(mreq.body.size() >= 2) { int hi = (unsigned char)mreq.body[0]; int lo = (unsigned char)mreq.body[1]; zreq.code = (hi << 8) + lo; zreq.body = mreq.body.mid(2); } s->downClosed = true; } else if(opcode == 9) { zreq.type = ZhttpRequestPacket::Ping; zreq.body = mreq.body; } else // 10 { zreq.type = ZhttpRequestPacket::Pong; zreq.body = mreq.body; } if(s->inHandoff) { s->pendingInPackets += zreq; } else { zhttp_out_write(s, zreq); if(s->downClosed && s->upClosed) { destroySession(s); // we aren't in handoff so this is safe m2_queueClose(conn); } } } } } void m2_control_readyRead(QZmq::Socket *sock) { int index = -1; for(int n = 0; n < controlPorts.count(); ++n) { if(controlPorts[n].sock == sock) { index = n; break; } } assert(index != -1); ControlPort &c = controlPorts[index]; while(sock->canRead()) { QList message = sock->read(); if(message.count() != 2) { log_warning("m2: received control response with parts != 2, skipping"); continue; } QVariant data = TnetString::toVariant(message[1]); if(data.isNull()) { log_warning("m2: received control response with invalid format (tnetstring parse failed), skipping"); continue; } if(c.state != ControlPort::ExpectingResponse) { log_warning("m2: received unexpected control response, skipping"); continue; } handleControlResponse(index, data); bool needControlPort = false; QHashIterator it(m2ConnectionsByRid); while(it.hasNext()) { it.next(); M2Connection *conn = it.value(); if(!conn->outCreditsEnabled) needControlPort = true; } if(needControlPort) { c.state = ControlPort::Idle; } else { log_debug("deactivating control port index=%d", index); c.state = ControlPort::Disabled; bool allDisabled = true; foreach(const ControlPort &i, controlPorts) { if(i.state != ControlPort::Disabled) { allDisabled = false; break; } } if(allDisabled) statusTimer->stop(); } c.reqStartTime = -1; } } void zhttp_in_readyRead(const QList &message) { handleZhttpIn(Http, message); } void zws_in_readyRead(const QList &message) { handleZhttpIn(WebSocket, message); } private slots: void status_timeout() { int now = time.elapsed(); for(int n = 0; n < controlPorts.count(); ++n) { ControlPort &c = controlPorts[n]; if(c.state == ControlPort::Disabled) continue; // if idle or expired, make request if(c.state == ControlPort::Idle || (c.state == ControlPort::ExpectingResponse && c.reqStartTime + CONTROL_REQUEST_EXPIRE <= now)) { // query m2 for connection info (to track bytes written) QVariantHash cmdArgs; cmdArgs["what"] = QByteArray("net"); c.state = ControlPort::ExpectingResponse; c.reqStartTime = now; m2_control_write(n, "status", cmdArgs); } } } void refresh_timeout() { qint64 now = QDateTime::currentMSecsSinceEpoch(); refreshM2Connections(now); refreshSessions(now); expireSessions(now); cancelSessions(); } void reload() { log_info("reloading"); log_rotate(); } void doQuit() { log_info("stopping..."); // remove the handler, so if we get another signal then we crash out ProcessQuit::cleanup(); log_info("stopped"); q->quit(0); } }; M2AdapterApp::M2AdapterApp(QObject *parent) : QObject(parent) { d = new Private(this); } M2AdapterApp::~M2AdapterApp() { delete d; } void M2AdapterApp::start() { d->start(); } #include "m2adapterapp.moc" pushpin-1.39.1/src/cpp/m2adapter/m2adapterapp.h000066400000000000000000000021661457610542000212650ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef M2ADAPTERAPP_H #define M2ADAPTERAPP_H #include #include using std::map; using SignalInt = boost::signals2::signal; using Connection = boost::signals2::scoped_connection; class M2AdapterApp : public QObject { Q_OBJECT public: M2AdapterApp(QObject *parent = 0); ~M2AdapterApp(); void start(); SignalInt quit; private: class Private; friend class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/m2adapter/m2adaptermain.cpp000066400000000000000000000024031457610542000217560ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include #include #include "m2adapterapp.h" class M2AdapterAppMain { public: M2AdapterApp *app; void start() { app = new M2AdapterApp; app->quit.connect(boost::bind(&M2AdapterAppMain::app_quit, this, boost::placeholders::_1)); app->start(); } void app_quit(int returnCode) { delete app; QCoreApplication::exit(returnCode); } }; extern "C" { int m2adapter_main(int argc, char **argv) { QCoreApplication qapp(argc, argv); M2AdapterAppMain appMain; QTimer::singleShot(0, [&appMain]() {appMain.start();}); return qapp.exec(); } } pushpin-1.39.1/src/cpp/m2adapter/m2requestpacket.cpp000066400000000000000000000160511457610542000223550ustar00rootroot00000000000000/* * Copyright (C) 2012-2013 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "m2requestpacket.h" #include #include #include #include "qtcompat.h" #include "tnetstring.h" static bool isAllCaps(const QString &s) { for(int n = 0; n < s.length(); ++n) { QChar c = s[n]; // non-letters are allowed, so what we really check against is // lowercase if(c.isLower()) return false; } return true; } static QString makeMixedCaseHeader(const QString &s) { QString out; for(int n = 0; n < s.length(); ++n) { QChar c = s[n]; if(n == 0 || (n - 1 >= 0 && s[n - 1] == '-')) out += c.toUpper(); else out += c; } return out; } M2RequestPacket::M2RequestPacket() : type((Type)-1), uploadDone(false), uploadStreamOffset(-1), uploadStreamDone(false), downloadCredits(-1), frameFlags(0) { } bool M2RequestPacket::fromByteArray(const QByteArray &in) { int start = 0; int end = in.indexOf(' '); if(end == -1) return false; sender = in.mid(start, end - start); start = end + 1; end = in.indexOf(' ', start); if(end == -1) return false; id = in.mid(start, end - start); start = end + 1; end = in.indexOf(' ', start); if(end == -1) return false; start = end + 1; TnetString::Type htype; int offset, size; if(!TnetString::check(in, start, &htype, &offset, &size)) return false; if(htype != TnetString::Hash && htype != TnetString::ByteArray) return false; bool ok; QVariant vheaders = TnetString::toVariant(in, start, htype, offset, size, &ok); if(!ok) return false; QSet skipHeaders; skipHeaders += "x-mongrel2-upload-start"; skipHeaders += "x-mongrel2-upload-done"; headers.clear(); // will store full headers QMap m2headers; // single-value map for easy processing if(htype == TnetString::Hash) { QVariantMap headersMap = vheaders.toMap(); QMapIterator vit(headersMap); while(vit.hasNext()) { vit.next(); QString key = vit.key(); QVariant val = vit.value(); if(typeId(val) == QMetaType::QByteArray) { QByteArray ba = val.toByteArray(); m2headers[key] = ba; if(!isAllCaps(key) && !skipHeaders.contains(key)) headers += HttpHeader(makeMixedCaseHeader(key).toLatin1(), ba); } else if(typeId(val) == QMetaType::QVariantList) { QVariantList vl = val.toList(); if(vl.isEmpty()) return false; if(typeId(vl[0]) != QMetaType::QByteArray) return false; m2headers[key] = vl[0].toByteArray(); if(!isAllCaps(key) && !skipHeaders.contains(key)) { QByteArray name = makeMixedCaseHeader(key).toLatin1(); foreach(const QVariant &v, vl) { if(typeId(v) != QMetaType::QByteArray) return false; headers += HttpHeader(name, v.toByteArray()); } } } else return false; } } else // ByteArray { QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(vheaders.toByteArray(), &error); if(error.error != QJsonParseError::NoError || !doc.isObject()) return false; QVariantMap headersMap = doc.object().toVariantMap(); QMapIterator vit(headersMap); while(vit.hasNext()) { vit.next(); QString key = vit.key(); QVariant val = vit.value(); if(typeId(val) == QMetaType::QString) { QByteArray ba = val.toString().toUtf8(); m2headers[key] = ba; if(!isAllCaps(key) && !skipHeaders.contains(key)) headers += HttpHeader(makeMixedCaseHeader(key).toLatin1(), ba); } else if(typeId(val) == QMetaType::QVariantList) { QVariantList vl = val.toList(); if(vl.isEmpty()) return false; if(typeId(vl[0]) != QMetaType::QString) return false; m2headers[key] = vl[0].toString().toUtf8(); if(!isAllCaps(key) && !skipHeaders.contains(key)) { QByteArray name = makeMixedCaseHeader(key).toLatin1(); foreach(const QVariant &v, vl) { if(typeId(v) != QMetaType::QString) return false; headers += HttpHeader(name, v.toString().toUtf8()); } } } else return false; } } start = offset + size + 1; TnetString::Type btype; if(!TnetString::check(in, start, &btype, &offset, &size)) return false; if(btype != TnetString::ByteArray) return false; body = TnetString::toByteArray(in, start, offset, size, &ok); if(!ok) return false; scheme = m2headers.value("URL_SCHEME"); version = m2headers.value("VERSION"); QByteArray m2method = m2headers.value("METHOD"); if(m2headers.contains("DOWNLOAD_CREDITS")) downloadCredits = m2headers.value("DOWNLOAD_CREDITS").toInt(); if(m2method == "JSON") { QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(body, &error); if(error.error != QJsonParseError::NoError || !doc.isObject()) return false; QVariantMap data = doc.object().toVariantMap(); if(!data.contains("type") || typeId(data["type"]) != QMetaType::QString) return false; QString jtype = data["type"].toString(); if(jtype == "disconnect") type = Disconnect; else if(jtype == "credits") type = Credits; else return false; return true; } QByteArray m2RemoteAddr = m2headers.value("REMOTE_ADDR"); method = QString::fromLatin1(m2method); uri = m2headers.value("URI"); remoteAddress = QHostAddress(); if(!m2RemoteAddr.isEmpty()) remoteAddress = QHostAddress(QString::fromLatin1(m2RemoteAddr)); if(m2method == "WEBSOCKET_HANDSHAKE") { type = WebSocketHandshake; return true; } else if(m2method == "WEBSOCKET") { type = WebSocketFrame; QByteArray flagsStr = m2headers.value("FLAGS"); frameFlags = flagsStr.toInt(&ok, 16); return ok; } type = HttpRequest; QByteArray uploadStartRaw = m2headers.value("x-mongrel2-upload-start"); QByteArray uploadDoneRaw = m2headers.value("x-mongrel2-upload-done"); if(!uploadDoneRaw.isEmpty()) { // these headers must match for the packet to be valid. not // sure why mongrel2 can't enforce this for us but whatever if(uploadStartRaw != uploadDoneRaw) return false; uploadFile = QString::fromUtf8(uploadDoneRaw); uploadDone = true; } else if(!uploadStartRaw.isEmpty()) { uploadFile = QString::fromUtf8(uploadStartRaw); } QByteArray uploadStreamRaw = m2headers.value("UPLOAD_STREAM"); QByteArray uploadStreamDoneRaw = m2headers.value("UPLOAD_STREAM_DONE"); if(!uploadStreamRaw.isEmpty()) uploadStreamOffset = uploadStreamRaw.toInt(); if(!uploadStreamDoneRaw.isEmpty() && uploadStreamDoneRaw != "0") uploadStreamDone = true; return true; } pushpin-1.39.1/src/cpp/m2adapter/m2requestpacket.h000066400000000000000000000025621457610542000220240ustar00rootroot00000000000000/* * Copyright (C) 2012-2013 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef M2REQUESTPACKET_H #define M2REQUESTPACKET_H #include #include #include "httpheaders.h" class M2RequestPacket { public: enum Type { HttpRequest, WebSocketHandshake, // body will contain accept token WebSocketFrame, Disconnect, Credits }; QByteArray sender; QByteArray id; Type type; QHostAddress remoteAddress; QByteArray scheme; QByteArray version; QString method; QByteArray uri; HttpHeaders headers; QByteArray body; QString uploadFile; bool uploadDone; int uploadStreamOffset; bool uploadStreamDone; int downloadCredits; int frameFlags; M2RequestPacket(); bool fromByteArray(const QByteArray &in); }; #endif pushpin-1.39.1/src/cpp/m2adapter/m2responsepacket.cpp000066400000000000000000000016341457610542000225240ustar00rootroot00000000000000/* * Copyright (C) 2012-2013 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "m2responsepacket.h" #include "tnetstring.h" M2ResponsePacket::M2ResponsePacket() { } QByteArray M2ResponsePacket::toByteArray() const { return sender + ' ' + TnetString::fromByteArray(id) + ' ' + data; } pushpin-1.39.1/src/cpp/m2adapter/m2responsepacket.h000066400000000000000000000016501457610542000221670ustar00rootroot00000000000000/* * Copyright (C) 2012-2013 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef M2RESPONSEPACKET_H #define M2RESPONSEPACKET_H #include class M2ResponsePacket { public: QByteArray sender; QByteArray id; QByteArray data; M2ResponsePacket(); QByteArray toByteArray() const; }; #endif pushpin-1.39.1/src/cpp/m2adapter/main.h000066400000000000000000000001461457610542000176250ustar00rootroot00000000000000#ifndef M2ADAPTER_MAIN_H #define M2ADAPTER_MAIN_H int m2adapter_main(int argc, char **argv); #endif pushpin-1.39.1/src/cpp/packet/000077500000000000000000000000001457610542000161175ustar00rootroot00000000000000pushpin-1.39.1/src/cpp/packet/httprequestdata.h000066400000000000000000000016231457610542000215140ustar00rootroot00000000000000/* * Copyright (C) 2012-2013 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef HTTPREQUESTDATA_H #define HTTPREQUESTDATA_H #include "../httpheaders.h" #include class HttpRequestData { public: QString method; QUrl uri; HttpHeaders headers; QByteArray body; }; #endif pushpin-1.39.1/src/cpp/packet/httpresponsedata.h000066400000000000000000000016611457610542000216640ustar00rootroot00000000000000/* * Copyright (C) 2012-2013 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef HTTPRESPONSEDATA_H #define HTTPRESPONSEDATA_H #include "../httpheaders.h" class HttpResponseData { public: int code; QByteArray reason; HttpHeaders headers; QByteArray body; HttpResponseData() : code(-1) { } }; #endif pushpin-1.39.1/src/cpp/packet/retryrequestpacket.cpp000066400000000000000000000211061457610542000225710ustar00rootroot00000000000000/* * Copyright (C) 2012-2023 Fanout, Inc. * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "retryrequestpacket.h" #include "qtcompat.h" RetryRequestPacket::RetryRequestPacket() : haveInspectInfo(false), retrySeq(-1) { } QVariant RetryRequestPacket::toVariant() const { QVariantHash obj; QVariantList vrequests; foreach(const Request &r, requests) { QVariantHash vrequest; QVariantHash vrid; vrid["sender"] = r.rid.first; vrid["id"] = r.rid.second; vrequest["rid"] = vrid; if(r.https) vrequest["https"] = true; if(!r.peerAddress.isNull()) vrequest["peer-address"] = r.peerAddress.toString().toUtf8(); if(r.debug) vrequest["debug"] = true; if(r.autoCrossOrigin) vrequest["auto-cross-origin"] = true; if(!r.jsonpCallback.isEmpty()) vrequest["jsonp-callback"] = r.jsonpCallback; if(r.jsonpExtendedResponse) vrequest["jsonp-extended-response"] = true; if(r.unreportedTime > 0) vrequest["unreported-time"] = r.unreportedTime; vrequest["in-seq"] = r.inSeq; vrequest["out-seq"] = r.outSeq; vrequest["out-credits"] = r.outCredits; if(r.userData.isValid()) vrequest["user-data"] = r.userData; vrequests += vrequest; } obj["requests"] = vrequests; QVariantHash vrequestData; vrequestData["method"] = requestData.method.toLatin1(); vrequestData["uri"] = requestData.uri.toEncoded(); QVariantList vheaders; foreach(const HttpHeader &h, requestData.headers) { QVariantList vheader; vheader += h.first; vheader += h.second; vheaders += QVariant(vheader); } vrequestData["headers"] = vheaders; vrequestData["body"] = requestData.body; obj["request-data"] = vrequestData; if(haveInspectInfo) { QVariantHash vinspect; vinspect["no-proxy"] = !inspectInfo.doProxy; if(!inspectInfo.sharingKey.isEmpty()) vinspect["sharing-key"] = inspectInfo.sharingKey; if(!inspectInfo.sid.isEmpty()) vinspect["sid"] = inspectInfo.sid; if(!inspectInfo.lastIds.isEmpty()) { QVariantHash vlastIds; QHashIterator it(inspectInfo.lastIds); while(it.hasNext()) { it.next(); vlastIds[QString::fromUtf8(it.key())] = it.value(); } vinspect["last-ids"] = vlastIds; } if(inspectInfo.userData.isValid()) vinspect["user-data"] = inspectInfo.userData; obj["inspect"] = vinspect; } if(!route.isEmpty()) obj["route"] = route; if(retrySeq >= 0) obj["retry-seq"] = retrySeq; return obj; } bool RetryRequestPacket::fromVariant(const QVariant &in) { if(typeId(in) != QMetaType::QVariantHash) return false; QVariantHash obj = in.toHash(); if(!obj.contains("requests") || typeId(obj["requests"]) != QMetaType::QVariantList) return false; requests.clear(); foreach(const QVariant &i, obj["requests"].toList()) { if(typeId(i) != QMetaType::QVariantHash) return false; QVariantHash vrequest = i.toHash(); Request r; if(!vrequest.contains("rid") || typeId(vrequest["rid"]) != QMetaType::QVariantHash) return false; QVariantHash vrid = vrequest["rid"].toHash(); QByteArray sender, id; if(!vrid.contains("sender") || typeId(vrid["sender"]) != QMetaType::QByteArray) return false; sender = vrid["sender"].toByteArray(); if(!vrid.contains("id") || typeId(vrid["id"]) != QMetaType::QByteArray) return false; id = vrid["id"].toByteArray(); r.rid = Rid(sender, id); if(vrequest.contains("https")) { if(typeId(vrequest["https"]) != QMetaType::Bool) return false; r.https = vrequest["https"].toBool(); } if(vrequest.contains("peer-address")) { if(typeId(vrequest["peer-address"]) != QMetaType::QByteArray) return false; r.peerAddress = QHostAddress(QString::fromUtf8(vrequest["peer-address"].toByteArray())); } if(vrequest.contains("debug")) { if(typeId(vrequest["debug"]) != QMetaType::Bool) return false; r.debug = vrequest["debug"].toBool(); } if(vrequest.contains("auto-cross-origin")) { if(typeId(vrequest["auto-cross-origin"]) != QMetaType::Bool) return false; r.autoCrossOrigin = vrequest["auto-cross-origin"].toBool(); } if(vrequest.contains("jsonp-callback")) { if(typeId(vrequest["jsonp-callback"]) != QMetaType::QByteArray) return false; r.jsonpCallback = vrequest["jsonp-callback"].toByteArray(); if(vrequest.contains("jsonp-extended-response")) { if(typeId(vrequest["jsonp-extended-response"]) != QMetaType::Bool) return false; r.jsonpExtendedResponse = vrequest["jsonp-extended-response"].toBool(); } } if(vrequest.contains("unreported-time")) { if(!canConvert(vrequest["unreported-time"], QMetaType::Int)) return false; r.unreportedTime = vrequest["unreported-time"].toInt(); } if(!vrequest.contains("in-seq") || !canConvert(vrequest["in-seq"], QMetaType::Int)) return false; r.inSeq = vrequest["in-seq"].toInt(); if(!vrequest.contains("out-seq") || !canConvert(vrequest["out-seq"], QMetaType::Int)) return false; r.outSeq = vrequest["out-seq"].toInt(); if(!vrequest.contains("out-credits") || !canConvert(vrequest["out-credits"], QMetaType::Int)) return false; r.outCredits = vrequest["out-credits"].toInt(); if(vrequest.contains("user-data")) r.userData = vrequest["user-data"]; requests += r; } if(!obj.contains("request-data") || typeId(obj["request-data"]) != QMetaType::QVariantHash) return false; QVariantHash vrequestData = obj["request-data"].toHash(); if(!vrequestData.contains("method") || typeId(vrequestData["method"]) != QMetaType::QByteArray) return false; requestData.method = QString::fromLatin1(vrequestData["method"].toByteArray()); if(!vrequestData.contains("uri") || typeId(vrequestData["uri"]) != QMetaType::QByteArray) return false; requestData.uri = QUrl::fromEncoded(vrequestData["uri"].toByteArray(), QUrl::StrictMode); requestData.headers.clear(); if(vrequestData.contains("headers")) { if(typeId(vrequestData["headers"]) != QMetaType::QVariantList) return false; foreach(const QVariant &i, vrequestData["headers"].toList()) { QVariantList list = i.toList(); if(list.count() != 2) return false; if(typeId(list[0]) != QMetaType::QByteArray || typeId(list[1]) != QMetaType::QByteArray) return false; requestData.headers += QPair(list[0].toByteArray(), list[1].toByteArray()); } } if(!vrequestData.contains("body") || typeId(vrequestData["body"]) != QMetaType::QByteArray) return false; requestData.body = vrequestData["body"].toByteArray(); if(obj.contains("inspect")) { if(typeId(obj["inspect"]) != QMetaType::QVariantHash) return false; QVariantHash vinspect = obj["inspect"].toHash(); if(!vinspect.contains("no-proxy") || typeId(vinspect["no-proxy"]) != QMetaType::Bool) return false; inspectInfo.doProxy = !vinspect["no-proxy"].toBool(); inspectInfo.sharingKey.clear(); if(vinspect.contains("sharing-key")) { if(typeId(vinspect["sharing-key"]) != QMetaType::QByteArray) return false; inspectInfo.sharingKey = vinspect["sharing-key"].toByteArray(); } if(vinspect.contains("sid")) { if(typeId(vinspect["sid"]) != QMetaType::QByteArray) return false; inspectInfo.sid = vinspect["sid"].toByteArray(); } if(vinspect.contains("last-ids")) { if(typeId(vinspect["last-ids"]) != QMetaType::QVariantHash) return false; QVariantHash vlastIds = vinspect["last-ids"].toHash(); QHashIterator it(vlastIds); while(it.hasNext()) { it.next(); if(typeId(it.value()) != QMetaType::QByteArray) return false; QByteArray key = it.key().toUtf8(); QByteArray val = it.value().toByteArray(); inspectInfo.lastIds.insert(key, val); } } inspectInfo.userData = vinspect["user-data"]; haveInspectInfo = true; } if(obj.contains("route")) { if(typeId(obj["route"]) != QMetaType::QByteArray) return false; route = obj["route"].toByteArray(); } if(obj.contains("retry-seq")) { if(!canConvert(obj["retry-seq"], QMetaType::Int)) return false; retrySeq = obj["retry-seq"].toInt(); } return true; } pushpin-1.39.1/src/cpp/packet/retryrequestpacket.h000066400000000000000000000035151457610542000222420ustar00rootroot00000000000000/* * Copyright (C) 2012-2023 Fanout, Inc. * Copyright (C) 2023 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef RETRYREQUESTPACKET_H #define RETRYREQUESTPACKET_H #include #include #include "httprequestdata.h" class RetryRequestPacket { public: typedef QPair Rid; class Request { public: Rid rid; bool https; QHostAddress peerAddress; bool debug; bool autoCrossOrigin; QByteArray jsonpCallback; bool jsonpExtendedResponse; int unreportedTime; // zhttp int inSeq; int outSeq; int outCredits; QVariant userData; Request() : https(false), debug(false), autoCrossOrigin(false), jsonpExtendedResponse(false), unreportedTime(-1), inSeq(-1), outSeq(-1), outCredits(-1) { } }; class InspectInfo { public: bool doProxy; QByteArray sharingKey; QByteArray sid; QHash lastIds; QVariant userData; InspectInfo() : doProxy(false) { } }; QList requests; HttpRequestData requestData; bool haveInspectInfo; InspectInfo inspectInfo; QByteArray route; int retrySeq; RetryRequestPacket(); QVariant toVariant() const; bool fromVariant(const QVariant &in); }; #endif pushpin-1.39.1/src/cpp/packet/statspacket.cpp000066400000000000000000000257641457610542000211670ustar00rootroot00000000000000/* * Copyright (C) 2014-2023 Fanout, Inc. * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "statspacket.h" #include "qtcompat.h" static bool tryGetInt(const QVariantHash &obj, const QString &name, int *result) { if(obj.contains(name)) { if(!canConvert(obj[name], QMetaType::Int)) return false; *result = obj[name].toInt(); } return true; } QVariant StatsPacket::toVariant() const { QVariantHash obj; if(!from.isEmpty()) obj["from"] = from; if(!route.isEmpty()) obj["route"] = route; if(type == Activity) { int x = count; if(x < 0) x = 0; obj["count"] = x; } else if(type == Message) { obj["channel"] = channel; if(!itemId.isNull()) obj["item-id"] = itemId; int x = count; if(x < 0) x = 0; obj["count"] = x; if(blocks >= 0) obj["blocks"] = blocks; obj["transport"] = transport; } else if(type == Connected || type == Disconnected) { obj["id"] = connectionId; if(type == Connected) { if(connectionType == WebSocket) obj["type"] = QByteArray("ws"); else // Http obj["type"] = QByteArray("http"); if(!peerAddress.isNull()) obj["peer-address"] = peerAddress.toString().toUtf8(); if(ssl) obj["ssl"] = true; obj["ttl"] = ttl; } else // Disconnected { obj["unavailable"] = true; } } else if(type == Subscribed || type == Unsubscribed) { obj["mode"] = mode; obj["channel"] = channel; if(type == Subscribed) { obj["ttl"] = ttl; if(subscribers >= 0) obj["subscribers"] = subscribers; } else // Unsubscribed { obj["unavailable"] = true; } } else if(type == Report) { if(connectionsMax != -1) obj["connections"] = connectionsMax; if(connectionsMinutes != -1) obj["minutes"] = connectionsMinutes; if(messagesReceived != -1) obj["received"] = messagesReceived; if(messagesSent != -1) obj["sent"] = messagesSent; if(httpResponseMessagesSent != -1) obj["http-response-sent"] = httpResponseMessagesSent; if(blocksReceived >= 0) obj["blocks-received"] = blocksReceived; if(blocksSent >= 0) obj["blocks-sent"] = blocksSent; if(duration >= 0) obj["duration"] = duration; if(clientHeaderBytesReceived >= 0) obj["client-header-bytes-received"] = clientHeaderBytesReceived; if(clientHeaderBytesSent >= 0) obj["client-header-bytes-sent"] = clientHeaderBytesSent; if(clientContentBytesReceived >= 0) obj["client-content-bytes-received"] = clientContentBytesReceived; if(clientContentBytesSent >= 0) obj["client-content-bytes-sent"] = clientContentBytesSent; if(clientMessagesReceived >= 0) obj["client-messages-received"] = clientMessagesReceived; if(clientMessagesSent >= 0) obj["client-messages-sent"] = clientMessagesSent; if(serverHeaderBytesReceived >= 0) obj["server-header-bytes-received"] = serverHeaderBytesReceived; if(serverHeaderBytesSent >= 0) obj["server-header-bytes-sent"] = serverHeaderBytesSent; if(serverContentBytesReceived >= 0) obj["server-content-bytes-received"] = serverContentBytesReceived; if(serverContentBytesSent >= 0) obj["server-content-bytes-sent"] = serverContentBytesSent; if(serverMessagesReceived >= 0) obj["server-messages-received"] = serverMessagesReceived; if(serverMessagesSent >= 0) obj["server-messages-sent"] = serverMessagesSent; } else if(type == Counts) { if(requestsReceived > 0) obj["requests-received"] = requestsReceived; } else // ConnectionsMax { obj["max"] = qMax(connectionsMax, 0); obj["ttl"] = qMax(ttl, 0); if(retrySeq >= 0) obj["retry-seq"] = retrySeq; } return obj; } bool StatsPacket::fromVariant(const QByteArray &_type, const QVariant &in) { if(typeId(in) != QMetaType::QVariantHash) return false; QVariantHash obj = in.toHash(); if(obj.contains("from")) { if(typeId(obj["from"]) != QMetaType::QByteArray) return false; from = obj["from"].toByteArray(); } if(obj.contains("route")) { if(typeId(obj["route"]) != QMetaType::QByteArray) return false; route = obj["route"].toByteArray(); } if(_type == "activity") { type = Activity; if(!obj.contains("count") || !canConvert(obj["count"], QMetaType::Int)) return false; count = obj["count"].toInt(); if(count < 0) return false; } else if(_type == "message") { type = Message; if(!obj.contains("channel") || typeId(obj["channel"]) != QMetaType::QByteArray) return false; channel = obj["channel"].toByteArray(); if(obj.contains("item-id")) { if(typeId(obj["item-id"]) != QMetaType::QByteArray) return false; itemId = obj["item-id"].toByteArray(); } if(!obj.contains("count") || !canConvert(obj["count"], QMetaType::Int)) return false; count = obj["count"].toInt(); if(count < 0) return false; if(obj.contains("blocks")) { if(!canConvert(obj["blocks"], QMetaType::Int)) return false; blocks = obj["blocks"].toInt(); } if(!obj.contains("transport") || typeId(obj["transport"]) != QMetaType::QByteArray) return false; transport = obj["transport"].toByteArray(); } else if(_type == "conn") { if(!obj.contains("id") || typeId(obj["id"]) != QMetaType::QByteArray) return false; connectionId = obj["id"].toByteArray(); type = Connected; if(obj.contains("unavailable")) { if(typeId(obj["unavailable"]) != QMetaType::Bool) return false; if(obj["unavailable"].toBool()) type = Disconnected; } if(type == Connected) { if(!obj.contains("type") || typeId(obj["type"]) != QMetaType::QByteArray) return false; QByteArray typeStr = obj["type"].toByteArray(); if(typeStr == "ws") connectionType = WebSocket; else if(typeStr == "http") connectionType = Http; else return false; if(obj.contains("peer-address")) { if(typeId(obj["peer-address"]) != QMetaType::QByteArray) return false; QByteArray peerAddressStr = obj["peer-address"].toByteArray(); if(!peerAddress.setAddress(QString::fromUtf8(peerAddressStr))) return false; } if(obj.contains("ssl")) { if(typeId(obj["ssl"]) != QMetaType::Bool) return false; ssl = obj["ssl"].toBool(); } if(!obj.contains("ttl") || !canConvert(obj["ttl"], QMetaType::Int)) return false; ttl = obj["ttl"].toInt(); if(ttl < 0) return false; } } else if(_type == "sub") { if(!obj.contains("mode") || typeId(obj["mode"]) != QMetaType::QByteArray) return false; mode = obj["mode"].toByteArray(); if(!obj.contains("channel") || typeId(obj["channel"]) != QMetaType::QByteArray) return false; channel = obj["channel"].toByteArray(); type = Subscribed; if(obj.contains("unavailable")) { if(typeId(obj["unavailable"]) != QMetaType::Bool) return false; if(obj["unavailable"].toBool()) type = Unsubscribed; } if(type == Subscribed) { if(!obj.contains("ttl") || !canConvert(obj["ttl"], QMetaType::Int)) return false; ttl = obj["ttl"].toInt(); if(ttl < 0) return false; if(obj.contains("subscribers")) { if(!canConvert(obj["subscribers"], QMetaType::Int)) return false; subscribers = obj["subscribers"].toInt(); if(subscribers < 0) return false; } } } else if(_type == "report") { type = Report; if(obj.contains("connections")) { if(!canConvert(obj["connections"], QMetaType::Int)) return false; connectionsMax = obj["connections"].toInt(); } if(obj.contains("minutes")) { if(!canConvert(obj["minutes"], QMetaType::Int)) return false; connectionsMinutes = obj["minutes"].toInt(); } if(obj.contains("received")) { if(!canConvert(obj["received"], QMetaType::Int)) return false; messagesReceived = obj["received"].toInt(); } if(obj.contains("sent")) { if(!canConvert(obj["sent"], QMetaType::Int)) return false; messagesSent = obj["sent"].toInt(); } if(obj.contains("http-response-sent")) { if(!canConvert(obj["http-response-sent"], QMetaType::Int)) return false; httpResponseMessagesSent = obj["http-response-sent"].toInt(); } if(obj.contains("blocks-received")) { if(!canConvert(obj["blocks-received"], QMetaType::Int)) return false; blocksReceived = obj["blocks-received"].toInt(); } if(obj.contains("blocks-sent")) { if(!canConvert(obj["blocks-sent"], QMetaType::Int)) return false; blocksSent = obj["blocks-sent"].toInt(); } if(obj.contains("duration")) { if(!canConvert(obj["duration"], QMetaType::Int)) return false; duration = obj["duration"].toInt(); } if(!tryGetInt(obj, "client-header-bytes-received", &clientHeaderBytesReceived)) return false; if(!tryGetInt(obj, "client-header-bytes-sent", &clientHeaderBytesSent)) return false; if(!tryGetInt(obj, "client-content-bytes-received", &clientContentBytesReceived)) return false; if(!tryGetInt(obj, "client-content-bytes-sent", &clientContentBytesSent)) return false; if(!tryGetInt(obj, "client-messages-received", &clientMessagesReceived)) return false; if(!tryGetInt(obj, "client-messages-sent", &clientMessagesSent)) return false; if(!tryGetInt(obj, "server-header-bytes-received", &serverHeaderBytesReceived)) return false; if(!tryGetInt(obj, "server-header-bytes-sent", &serverHeaderBytesSent)) return false; if(!tryGetInt(obj, "server-content-bytes-received", &serverContentBytesReceived)) return false; if(!tryGetInt(obj, "server-content-bytes-sent", &serverContentBytesSent)) return false; if(!tryGetInt(obj, "server-messages-received", &serverMessagesReceived)) return false; if(!tryGetInt(obj, "server-messages-sent", &serverMessagesSent)) return false; } else if(_type == "counts") { type = Counts; if(obj.contains("requests-received")) { if(!canConvert(obj["requests-received"], QMetaType::Int)) return false; int x = obj["requests-received"].toInt(); if(x < 0) return false; requestsReceived = x; } } else if(_type == "conn-max") { type = ConnectionsMax; if(!obj.contains("max") || !canConvert(obj["max"], QMetaType::Int)) return false; int x = obj["max"].toInt(); if(x < 0) return false; connectionsMax = x; if(!obj.contains("ttl") || !canConvert(obj["ttl"], QMetaType::Int)) return false; x = obj["ttl"].toInt(); if(x < 0) return false; ttl = x; if(obj.contains("retry-seq")) { if(!canConvert(obj["retry-seq"], QMetaType::LongLong)) return false; int x = obj["retry-seq"].toLongLong(); if(x < 0) return false; retrySeq = x; } } else return false; return true; } pushpin-1.39.1/src/cpp/packet/statspacket.h000066400000000000000000000062471457610542000206270ustar00rootroot00000000000000/* * Copyright (C) 2014-2022 Fanout, Inc. * Copyright (C) 2023 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef STATSPACKET_H #define STATSPACKET_H #include #include #include class StatsPacket { public: enum Type { Activity, Message, Connected, Disconnected, Subscribed, Unsubscribed, Report, Counts, ConnectionsMax, }; enum ConnectionType { Http, WebSocket }; Type type; QByteArray from; QByteArray route; qint64 retrySeq; // connections max int count; // activity, message QByteArray connectionId; // connected, disconnected ConnectionType connectionType; // connected QHostAddress peerAddress; // connected bool ssl; // connected int ttl; // connected, subscribed, connections max QByteArray mode; // subscribed, unsubscribed QByteArray channel; // message, subscribed, unsubscribed QByteArray itemId; // message QByteArray transport; // message int blocks; // message int subscribers; // subscribed int connectionsMax; // report, connections max int connectionsMinutes; // report int messagesReceived; // report int messagesSent; // report int httpResponseMessagesSent; // report int blocksReceived; // report int blocksSent; // report int duration; // report int requestsReceived; // counts int clientHeaderBytesReceived; // report int clientHeaderBytesSent; // report int clientContentBytesReceived; // report int clientContentBytesSent; // report int clientMessagesReceived; // report int clientMessagesSent; // report int serverHeaderBytesReceived; // report int serverHeaderBytesSent; // report int serverContentBytesReceived; // report int serverContentBytesSent; // report int serverMessagesReceived; // report int serverMessagesSent; // report StatsPacket() : type((Type)-1), retrySeq(-1), count(-1), connectionType((ConnectionType)-1), ssl(false), ttl(-1), blocks(-1), subscribers(-1), connectionsMax(-1), connectionsMinutes(-1), messagesReceived(-1), messagesSent(-1), httpResponseMessagesSent(-1), blocksReceived(-1), blocksSent(-1), duration(-1), requestsReceived(-1), clientHeaderBytesReceived(-1), clientHeaderBytesSent(-1), clientContentBytesReceived(-1), clientContentBytesSent(-1), clientMessagesReceived(-1), clientMessagesSent(-1), serverHeaderBytesReceived(-1), serverHeaderBytesSent(-1), serverContentBytesReceived(-1), serverContentBytesSent(-1), serverMessagesReceived(-1), serverMessagesSent(-1) { } QVariant toVariant() const; bool fromVariant(const QByteArray &type, const QVariant &in); }; #endif pushpin-1.39.1/src/cpp/packet/wscontrolpacket.cpp000066400000000000000000000232661457610542000220560ustar00rootroot00000000000000/* * Copyright (C) 2014-2022 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "wscontrolpacket.h" #include #include "qtcompat.h" // FIXME: rewrite packet class using this code? /*class WsControlPacket { public: class Message { public: enum Type { Here, Gone, Cancel, Grip }; Type type; QString cid; QString channelPrefix; // here only QByteArray message; // grip only }; QString channelPrefix; QList messages; static WsControlPacket fromVariant(const QVariant &in, bool *ok = 0, QString *errorMessage = 0) { QString pn = "wscontrol packet"; if(!isKeyedObject(in)) { setError(ok, errorMessage, QString("%1 is not an object").arg(pn)); return WsControlPacket(); } pn = "wscontrol object"; bool ok_; QVariantList vitems = getList(in, pn, "items", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return WsControlPacket(); } WsControlPacket out; foreach(const QVariant &vitem, vitems) { Message msg; pn = "wscontrol item"; QString type = getString(vitem, pn, "type", true, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return WsControlPacket(); } if(type == "here") msg.type = Message::Here; else if(type == "gone") msg.type = Message::Gone; else if(type == "cancel") msg.type = Message::Cancel; else if(type == "grip") msg.type = Message::Grip; else { setError(ok, errorMessage, QString("'type' contains unknown value: %1").arg(type)); return WsControlPacket(); } msg.cid = getString(vitem, pn, "cid", true, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return WsControlPacket(); } msg.uri = QUrl::fromEncoded(getString(vitem, pn, "uri", false, &ok_, errorMessage).toUtf8(), QUrl::StrictMode); if(!ok_) { if(ok) *ok = false; return WsControlPacket(); } msg.channelPrefix = getString(vitem, pn, "channel-prefix", false, &ok_, errorMessage); if(!ok_) { if(ok) *ok = false; return WsControlPacket(); } if(msg.type == Message::Grip) { if(!keyedObjectContains(vitem, "message")) { setError(ok, errorMessage, QString("'%1' does not contain 'message'").arg(pn)); return WsControlPacket(); } QVariant vmessage = keyedObjectGetValue(vitem, "message"); if(vmessage.type() != QVariant::ByteArray) { setError(ok, errorMessage, QString("'%1' contains 'message' with wrong type").arg(pn)); return WsControlPacket(); } msg.message = vmessage.toByteArray(); } out.messages += msg; } setSuccess(ok, errorMessage); return out; } };*/ QVariant WsControlPacket::toVariant() const { QVariantHash obj; obj["from"] = from; QVariantList vitems; foreach(const Item &item, items) { QVariantHash vitem; vitem["cid"] = item.cid; QByteArray typeStr; switch(item.type) { case Item::Here: typeStr = "here"; break; case Item::KeepAlive: typeStr = "keep-alive"; break; case Item::Gone: typeStr = "gone"; break; case Item::Grip: typeStr = "grip"; break; case Item::KeepAliveSetup: typeStr = "keep-alive-setup"; break; case Item::Cancel: typeStr = "cancel"; break; case Item::Send: typeStr = "send"; break; case Item::NeedKeepAlive: typeStr = "need-keep-alive"; break; case Item::Subscribe: typeStr = "subscribe"; break; case Item::Refresh: typeStr = "refresh"; break; case Item::Close: typeStr = "close"; break; case Item::Detach: typeStr = "detach"; break; case Item::Ack: typeStr = "ack"; break; default: assert(0); } vitem["type"] = typeStr; if(!item.requestId.isEmpty()) vitem["req-id"] = item.requestId; if(!item.uri.isEmpty()) vitem["uri"] = item.uri.toEncoded(); if(!item.contentType.isEmpty()) vitem["content-type"] = item.contentType; if(!item.message.isNull()) vitem["message"] = item.message; if(item.queue) vitem["queue"] = true; if(item.code >= 0) vitem["code"] = item.code; if(!item.reason.isEmpty()) vitem["reason"] = item.reason; if(!item.route.isEmpty()) vitem["route"] = item.route; if(item.separateStats) vitem["separate-stats"] = true; if(!item.channelPrefix.isEmpty()) vitem["channel-prefix"] = item.channelPrefix; if(!item.channel.isEmpty()) vitem["channel"] = item.channel; if(item.ttl >= 0) vitem["ttl"] = item.ttl; if(item.timeout >= 0) vitem["timeout"] = item.timeout; if(!item.keepAliveMode.isEmpty()) vitem["keep-alive-mode"] = item.keepAliveMode; vitems += vitem; } obj["items"] = vitems; return obj; } bool WsControlPacket::fromVariant(const QVariant &in) { if(typeId(in) != QMetaType::QVariantHash) return false; QVariantHash obj = in.toHash(); if(!obj.contains("from") || typeId(obj["from"]) != QMetaType::QByteArray) return false; from = obj["from"].toByteArray(); if(!obj.contains("items") || typeId(obj["items"]) != QMetaType::QVariantList) return false; QVariantList vitems = obj["items"].toList(); items.clear(); foreach(const QVariant &v, vitems) { if(typeId(v) != QMetaType::QVariantHash) return false; QVariantHash vitem = v.toHash(); Item item; if(!vitem.contains("cid") || typeId(vitem["cid"]) != QMetaType::QByteArray) return false; item.cid = vitem["cid"].toByteArray(); if(!vitem.contains("type") || typeId(vitem["type"]) != QMetaType::QByteArray) return false; QByteArray typeStr = vitem["type"].toByteArray(); if(typeStr == "here") item.type = Item::Here; else if(typeStr == "keep-alive") item.type = Item::KeepAlive; else if(typeStr == "gone") item.type = Item::Gone; else if(typeStr == "grip") item.type = Item::Grip; else if(typeStr == "keep-alive-setup") item.type = Item::KeepAliveSetup; else if(typeStr == "cancel") item.type = Item::Cancel; else if(typeStr == "send") item.type = Item::Send; else if(typeStr == "need-keep-alive") item.type = Item::NeedKeepAlive; else if(typeStr == "subscribe") item.type = Item::Subscribe; else if(typeStr == "refresh") item.type = Item::Refresh; else if(typeStr == "close") item.type = Item::Close; else if(typeStr == "detach") item.type = Item::Detach; else if(typeStr == "ack") item.type = Item::Ack; else return false; if(vitem.contains("req-id")) { if(typeId(vitem["req-id"]) != QMetaType::QByteArray) return false; item.requestId = vitem["req-id"].toByteArray(); } if(vitem.contains("uri")) { if(typeId(vitem["uri"]) != QMetaType::QByteArray) return false; item.uri = QUrl::fromEncoded(vitem["uri"].toByteArray(), QUrl::StrictMode); } if(vitem.contains("content-type")) { if(typeId(vitem["content-type"]) != QMetaType::QByteArray) return false; QByteArray contentType = vitem["content-type"].toByteArray(); if(!contentType.isEmpty()) item.contentType = contentType; } if(vitem.contains("message")) { if(typeId(vitem["message"]) != QMetaType::QByteArray) return false; item.message = vitem["message"].toByteArray(); } if(vitem.contains("queue")) { if(typeId(vitem["queue"]) != QMetaType::Bool) return false; item.queue = vitem["queue"].toBool(); } if(vitem.contains("code")) { if(!canConvert(vitem["code"], QMetaType::Int)) return false; item.code = vitem["code"].toInt(); } if(vitem.contains("reason")) { if(typeId(vitem["reason"]) != QMetaType::QByteArray) return false; item.reason = vitem["reason"].toByteArray(); } if(vitem.contains("route")) { if(typeId(vitem["route"]) != QMetaType::QByteArray) return false; QByteArray route = vitem["route"].toByteArray(); if(!route.isEmpty()) item.route = route; } if(vitem.contains("separate-stats")) { if(typeId(vitem["separate-stats"]) != QMetaType::Bool) return false; item.separateStats = vitem["separate-stats"].toBool(); } if(vitem.contains("channel-prefix")) { if(typeId(vitem["channel-prefix"]) != QMetaType::QByteArray) return false; QByteArray channelPrefix = vitem["channel-prefix"].toByteArray(); if(!channelPrefix.isEmpty()) item.channelPrefix = channelPrefix; } if(vitem.contains("channel")) { if(typeId(vitem["channel"]) != QMetaType::QByteArray) return false; QByteArray channel = vitem["channel"].toByteArray(); if(!channel.isEmpty()) item.channel = channel; } if(vitem.contains("ttl")) { if(!canConvert(vitem["ttl"], QMetaType::Int)) return false; item.ttl = vitem["ttl"].toInt(); if(item.ttl < 0) item.ttl = 0; } if(vitem.contains("timeout")) { if(!canConvert(vitem["timeout"], QMetaType::Int)) return false; item.timeout = vitem["timeout"].toInt(); if(item.timeout < 0) item.timeout = 0; } if(vitem.contains("keep-alive-mode")) { if(!canConvert(vitem["keep-alive-mode"], QMetaType::QByteArray)) return false; QByteArray keepAliveMode = vitem["keep-alive-mode"].toByteArray(); if(!keepAliveMode.isEmpty()) item.keepAliveMode = keepAliveMode; } items += item; } return true; } pushpin-1.39.1/src/cpp/packet/wscontrolpacket.h000066400000000000000000000031641457610542000215160ustar00rootroot00000000000000/* * Copyright (C) 2014-2022 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef WSCONTROLPACKET_H #define WSCONTROLPACKET_H #include #include #include #include class WsControlPacket { public: class Item { public: enum Type { Here, KeepAlive, Gone, Grip, NeedKeepAlive, Subscribe, Cancel, Send, KeepAliveSetup, Refresh, Close, Detach, Ack }; QByteArray cid; Type type; QByteArray requestId; QUrl uri; QByteArray contentType; QByteArray message; bool queue; int code; QByteArray reason; QByteArray route; bool separateStats; QByteArray channelPrefix; QByteArray channel; int ttl; int timeout; QByteArray keepAliveMode; Item() : type((Type)-1), queue(false), code(-1), separateStats(false), ttl(-1), timeout(-1) { } }; QByteArray from; QList items; QVariant toVariant() const; bool fromVariant(const QVariant &in); }; #endif pushpin-1.39.1/src/cpp/packet/zrpcrequestpacket.cpp000066400000000000000000000033411457610542000224030ustar00rootroot00000000000000/* * Copyright (C) 2014 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "zrpcrequestpacket.h" #include "qtcompat.h" QVariant ZrpcRequestPacket::toVariant() const { QVariantHash obj; if(!from.isEmpty()) obj["from"] = from; if(!id.isEmpty()) obj["id"] = id; obj["method"] = method.toUtf8(); if(!args.isEmpty()) obj["args"] = args; return obj; } bool ZrpcRequestPacket::fromVariant(const QVariant &in) { if(typeId(in) != QMetaType::QVariantHash) return false; QVariantHash obj = in.toHash(); if(obj.contains("from")) { if(typeId(obj["from"]) != QMetaType::QByteArray) return false; from = obj["from"].toByteArray(); } if(obj.contains("id")) { if(typeId(obj["id"]) != QMetaType::QByteArray) return false; id = obj["id"].toByteArray(); } if(!obj.contains("method") || typeId(obj["method"]) != QMetaType::QByteArray) return false; method = QString::fromUtf8(obj["method"].toByteArray()); if(obj.contains("args")) { if(typeId(obj["args"]) != QMetaType::QVariantHash) return false; args = obj["args"].toHash(); } return true; } pushpin-1.39.1/src/cpp/packet/zrpcrequestpacket.h000066400000000000000000000017751457610542000220610ustar00rootroot00000000000000/* * Copyright (C) 2014 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef ZRPCREQUESTPACKET_H #define ZRPCREQUESTPACKET_H #include #include class ZrpcRequestPacket { public: QByteArray from; QByteArray id; QString method; QVariantHash args; QVariant toVariant() const; bool fromVariant(const QVariant &in); }; #endif pushpin-1.39.1/src/cpp/packet/zrpcresponsepacket.cpp000066400000000000000000000040001457610542000225420ustar00rootroot00000000000000/* * Copyright (C) 2014 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "zrpcresponsepacket.h" #include "qtcompat.h" QVariant ZrpcResponsePacket::toVariant() const { QVariantHash obj; if(!id.isEmpty()) obj["id"] = id; obj["success"] = success; if(success) { if(typeId(value) == QMetaType::QString) obj["value"] = value.toString().toUtf8(); else obj["value"] = value; } else { obj["condition"] = condition; if(value.isValid()) { if(typeId(value) == QMetaType::QString) obj["value"] = value.toString().toUtf8(); else obj["value"] = value; } } return obj; } bool ZrpcResponsePacket::fromVariant(const QVariant &in) { if(typeId(in) != QMetaType::QVariantHash) return false; QVariantHash obj = in.toHash(); if(obj.contains("id")) { if(typeId(obj["id"]) != QMetaType::QByteArray) return false; id = obj["id"].toByteArray(); } if(!obj.contains("success") || typeId(obj["success"]) != QMetaType::Bool) return false; success = obj["success"].toBool(); value.clear(); condition.clear(); if(success) { if(!obj.contains("value")) return false; value = obj["value"]; } else { if(!obj.contains("condition") || typeId(obj["condition"]) != QMetaType::QByteArray) return false; condition = obj["condition"].toByteArray(); if(obj.contains("value")) value = obj["value"]; } return true; } pushpin-1.39.1/src/cpp/packet/zrpcresponsepacket.h000066400000000000000000000020151457610542000222130ustar00rootroot00000000000000/* * Copyright (C) 2014 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef ZRPCRESPONSEPACKET_H #define ZRPCRESPONSEPACKET_H #include #include class ZrpcResponsePacket { public: QByteArray id; bool success; QVariant value; QByteArray condition; ZrpcResponsePacket() : success(false) { } QVariant toVariant() const; bool fromVariant(const QVariant &in); }; #endif pushpin-1.39.1/src/cpp/processquit.cpp000066400000000000000000000124321457610542000177370ustar00rootroot00000000000000/* * Copyright (C) 2006 Justin Karneges * Copyright (C) 2017 Fanout, Inc. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "processquit.h" #ifndef NO_IRISNET # include "irisnetglobal_p.h" #endif #ifdef QT_GUI_LIB # include #endif #ifdef Q_OS_WIN # include #endif #ifdef Q_OS_UNIX # include # include #endif namespace { // safeobj stuff, from qca void releaseAndDeleteLater(QObject *owner, QObject *obj) { obj->disconnect(owner); obj->setParent(0); obj->deleteLater(); } class SafeSocketNotifier : public QObject { Q_OBJECT public: Connection activatedConnection; SafeSocketNotifier(int socket, QSocketNotifier::Type type, QObject *parent = 0) : QObject(parent) { sn = new QSocketNotifier(socket, type, this); connect(sn, &QSocketNotifier::activated, this, &SafeSocketNotifier::doActivated); } ~SafeSocketNotifier() { sn->setEnabled(false); releaseAndDeleteLater(this, sn); } bool isEnabled() const { return sn->isEnabled(); } int socket() const { return sn->socket(); } QSocketNotifier::Type type() const { return sn->type(); } public slots: void setEnabled(bool enable) { sn->setEnabled(enable); } public: SignalInt activated; private: QSocketNotifier *sn; void doActivated(int sock) { activated(sock); } }; } #ifndef NO_IRISNET namespace XMPP { #endif Q_GLOBAL_STATIC(QMutex, pq_mutex) static ProcessQuit *g_pq = 0; inline bool is_gui_app() { #ifdef QT_GUI_LIB return (QApplication::type() != QApplication::Tty); #else return false; #endif } class ProcessQuit::Private : public QObject { Q_OBJECT public: ProcessQuit *q; Connection activatedConnection; bool done; #ifdef Q_OS_WIN bool use_handler; #endif #ifdef Q_OS_UNIX int sig_pipe[2]; SafeSocketNotifier *sig_notifier; #endif Private(ProcessQuit *_q) : QObject(_q), q(_q) { done = false; #ifdef Q_OS_WIN use_handler = !is_gui_app(); if(use_handler) SetConsoleCtrlHandler((PHANDLER_ROUTINE)winHandler, TRUE); #endif #ifdef Q_OS_UNIX if(pipe(sig_pipe) == -1) { // no support then return; } sig_notifier = new SafeSocketNotifier(sig_pipe[0], QSocketNotifier::Read, this); activatedConnection = sig_notifier->activated.connect(boost::bind(&Private::sig_activated, this, boost::placeholders::_1)); unixWatchAdd(SIGINT); unixWatchAdd(SIGHUP); unixWatchAdd(SIGTERM); #endif } ~Private() { #ifdef Q_OS_WIN if(use_handler) SetConsoleCtrlHandler((PHANDLER_ROUTINE)winHandler, FALSE); #endif #ifdef Q_OS_UNIX unixWatchRemove(SIGINT); unixWatchRemove(SIGHUP); unixWatchRemove(SIGTERM); activatedConnection.disconnect(); delete sig_notifier; close(sig_pipe[0]); close(sig_pipe[1]); #endif } #ifdef Q_OS_WIN static BOOL winHandler(DWORD ctrlType) { Q_UNUSED(ctrlType); QMetaObject::invokeMethod(g_pq->d, "ctrl_ready", Qt::QueuedConnection); return TRUE; } #endif #ifdef Q_OS_UNIX static void unixHandler(int sig) { Q_UNUSED(sig); unsigned char c = 0; if(sig == SIGHUP) c = 1; if(::write(g_pq->d->sig_pipe[1], &c, 1) == -1) { // TODO: error handling? return; } } void unixWatchAdd(int sig) { struct sigaction sa; sigaction(sig, NULL, &sa); // if the signal is ignored, don't take it over. this is // recommended by the glibc manual if(sa.sa_handler == SIG_IGN) return; sigemptyset(&(sa.sa_mask)); sa.sa_flags = 0; sa.sa_handler = unixHandler; sigaction(sig, &sa, 0); } void unixWatchRemove(int sig) { struct sigaction sa; sigaction(sig, NULL, &sa); // ignored means we skipped it earlier, so we should // skip it again if(sa.sa_handler == SIG_IGN) return; sigemptyset(&(sa.sa_mask)); sa.sa_flags = 0; sa.sa_handler = SIG_DFL; sigaction(sig, &sa, 0); } #endif void sig_activated(int) { #ifdef Q_OS_UNIX unsigned char c; if(::read(sig_pipe[0], &c, 1) == -1) { // TODO: error handling? return; } if(c == 1) // SIGHUP { q->hup(); return; } do_emit(); #endif } public slots: void ctrl_ready() { #ifdef Q_OS_WIN do_emit(); #endif } private: void do_emit() { // only signal once if(!done) { done = true; q->quit(); } } }; ProcessQuit::ProcessQuit(QObject *parent) :QObject(parent) { d = new Private(this); } ProcessQuit::~ProcessQuit() { delete d; } ProcessQuit *ProcessQuit::instance() { QMutexLocker locker(pq_mutex()); if(!g_pq) { g_pq = new ProcessQuit; g_pq->moveToThread(QCoreApplication::instance()->thread()); #ifndef NO_IRISNET irisNetAddPostRoutine(cleanup); #endif } return g_pq; } void ProcessQuit::reset() { QMutexLocker locker(pq_mutex()); if(g_pq) g_pq->d->done = false; } void ProcessQuit::cleanup() { delete g_pq; g_pq = 0; } #ifndef NO_IRISNET } #endif #include "processquit.moc" pushpin-1.39.1/src/cpp/processquit.h000066400000000000000000000100431457610542000174000ustar00rootroot00000000000000/* * Copyright (C) 2006 Justin Karneges * Copyright (C) 2017 Fanout, Inc. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef PROCESSQUIT_H #define PROCESSQUIT_H #include using Signal = boost::signals2::signal; using SignalInt = boost::signals2::signal; using Connection = boost::signals2::scoped_connection; #ifdef NO_IRISNET # include # define IRISNET_EXPORT #else # include "irisnetglobal.h" #endif #ifndef NO_IRISNET namespace XMPP { #endif /** \brief Listens for termination requests ProcessQuit listens for requests to terminate the application process. On Unix platforms, these are the signals SIGINT, SIGHUP, and SIGTERM. On Windows, these are the console control events for Ctrl+C, console window close, and system shutdown. For Windows GUI programs, ProcessQuit has no effect. For GUI programs, ProcessQuit is not a substitute for QSessionManager. The only safe way to handle termination of a GUI program in the usual way is to use QSessionManager. However, ProcessQuit does give additional benefit to Unix GUI programs that might be terminated unconventionally, so it can't hurt to support both. When a termination request is received, the application should exit gracefully, and generally without user interaction. Otherwise, it is at risk of being terminated outside of its control. For example, if a Windows console application does not exit after just a few seconds of attempting to close the console window, Windows will display a prompt to the user asking if the process should be ended immediately. Using ProcessQuit is easy, and it usually amounts to a single line: \code myapp.connect(ProcessQuit::instance(), SIGNAL(quit()), SLOT(do_quit())); \endcode Calling instance() returns a pointer to the global ProcessQuit instance, which will be created if necessary. The quit() signal is emitted when a request to terminate is received. The quit() signal is only emitted once, future termination requests are ignored. Call reset() to allow the quit() signal to be emitted again. */ class IRISNET_EXPORT ProcessQuit : public QObject { Q_OBJECT public: /** \brief Returns the global ProcessQuit instance If the global instance does not exist yet, it will be created, and the termination handlers will be installed. \sa cleanup */ static ProcessQuit *instance(); /** \brief Allows the quit() signal to be emitted again ProcessQuit only emits the quit() signal once, so that if a user repeatedly presses Ctrl-C or sends SIGTERM, your shutdown slot will not be called multiple times. This is normally the desired behavior, but if you are ignoring the termination request then you may want to allow future notifications. Calling this function will allow the quit() signal to be emitted again, if a new termination request arrives. \sa quit */ static void reset(); /** \brief Frees all resources used by ProcessQuit This function will free any resources used by ProcessQuit, including the global instance, and the termination handlers will be uninstalled (reverted to default). Future termination requests will cause the application to exit abruptly. \note You normally do not need to call this function directly. When IrisNet cleans up, it will be called. \sa instance */ static void cleanup(); Signal quit; Signal hup; private: class Private; friend class Private; Private *d; ProcessQuit(QObject *parent = 0); ~ProcessQuit(); }; #ifndef NO_IRISNET } #endif #endif pushpin-1.39.1/src/cpp/proxy/000077500000000000000000000000001457610542000160315ustar00rootroot00000000000000pushpin-1.39.1/src/cpp/proxy/acceptdata.h000066400000000000000000000041171457610542000202760ustar00rootroot00000000000000/* * Copyright (C) 2012-2023 Fanout, Inc. * Copyright (C) 2023 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef ACCEPTDATA_H #define ACCEPTDATA_H #include #include "httpheaders.h" #include "packet/httprequestdata.h" #include "packet/httpresponsedata.h" #include "inspectdata.h" #include "zhttprequest.h" class AcceptData { public: class Request { public: ZhttpRequest::Rid rid; bool https; QHostAddress peerAddress; QHostAddress logicalPeerAddress; bool debug; bool isRetry; bool autoCrossOrigin; QByteArray jsonpCallback; bool jsonpExtendedResponse; int unreportedTime; // zhttp int responseCode; int inSeq; int outSeq; int outCredits; QVariant userData; Request() : https(false), debug(false), isRetry(false), autoCrossOrigin(false), jsonpExtendedResponse(false), unreportedTime(-1), responseCode(-1), inSeq(-1), outSeq(-1), outCredits(-1) { } }; QList requests; HttpRequestData requestData; HttpRequestData origRequestData; bool haveInspectData; InspectData inspectData; bool haveResponse; HttpResponseData response; QByteArray route; bool separateStats; QByteArray channelPrefix; QList channels; bool trusted; // whether a trusted target was used bool useSession; bool responseSent; QVariantList connMaxPackets; AcceptData() : haveInspectData(false), haveResponse(false), separateStats(false), useSession(false), responseSent(false) { } }; #endif pushpin-1.39.1/src/cpp/proxy/acceptrequest.cpp000066400000000000000000000171131457610542000214100ustar00rootroot00000000000000/* * Copyright (C) 2015-2023 Fanout, Inc. * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "acceptrequest.h" #include "qtcompat.h" #include "acceptdata.h" static QVariant acceptDataToVariant(const AcceptData &adata) { QVariantHash obj; { QVariantList vrequests; foreach(const AcceptData::Request &r, adata.requests) { QVariantHash vrequest; QVariantHash vrid; vrid["sender"] = r.rid.first; vrid["id"] = r.rid.second; vrequest["rid"] = vrid; if(r.https) vrequest["https"] = true; if(!r.peerAddress.isNull()) vrequest["peer-address"] = r.peerAddress.toString().toUtf8(); if(!r.logicalPeerAddress.isNull()) vrequest["logical-peer-address"] = r.logicalPeerAddress.toString().toUtf8(); if(r.debug) vrequest["debug"] = true; if(r.isRetry) vrequest["is-retry"] = true; if(r.autoCrossOrigin) vrequest["auto-cross-origin"] = true; if(!r.jsonpCallback.isEmpty()) { vrequest["jsonp-callback"] = r.jsonpCallback; if(r.jsonpExtendedResponse) vrequest["jsonp-extended-response"] = true; } if(r.unreportedTime > 0) vrequest["unreported-time"] = r.unreportedTime; if(r.responseCode != -1) vrequest["response-code"] = r.responseCode; vrequest["in-seq"] = r.inSeq; vrequest["out-seq"] = r.outSeq; vrequest["out-credits"] = r.outCredits; if(r.userData.isValid()) vrequest["user-data"] = r.userData; vrequests += vrequest; } obj["requests"] = vrequests; } { const HttpRequestData &requestData = adata.requestData; QVariantHash vrequestData; vrequestData["method"] = requestData.method.toLatin1(); vrequestData["uri"] = requestData.uri.toEncoded(); QVariantList vheaders; foreach(const HttpHeader &h, requestData.headers) { QVariantList vheader; vheader += h.first; vheader += h.second; vheaders += QVariant(vheader); } vrequestData["headers"] = vheaders; vrequestData["body"] = requestData.body; obj["request-data"] = vrequestData; } { const HttpRequestData &requestData = adata.origRequestData; QVariantHash vrequestData; vrequestData["method"] = requestData.method.toLatin1(); vrequestData["uri"] = requestData.uri.toEncoded(); QVariantList vheaders; foreach(const HttpHeader &h, requestData.headers) { QVariantList vheader; vheader += h.first; vheader += h.second; vheaders += QVariant(vheader); } vrequestData["headers"] = vheaders; vrequestData["body"] = requestData.body; obj["orig-request-data"] = vrequestData; } if(adata.haveInspectData) { QVariantHash vinspect; vinspect["no-proxy"] = !adata.inspectData.doProxy; if(!adata.inspectData.sharingKey.isEmpty()) vinspect["sharing-key"] = adata.inspectData.sharingKey; if(!adata.inspectData.sid.isEmpty()) vinspect["sid"] = adata.inspectData.sid; if(!adata.inspectData.lastIds.isEmpty()) { QVariantHash vlastIds; QHashIterator it(adata.inspectData.lastIds); while(it.hasNext()) { it.next(); vlastIds[QString::fromUtf8(it.key())] = it.value(); } vinspect["last-ids"] = vlastIds; } if(adata.inspectData.userData.isValid()) vinspect["user-data"] = adata.inspectData.userData; obj["inspect"] = vinspect; } if(adata.haveResponse) { QVariantHash vresponse; vresponse["code"] = adata.response.code; vresponse["reason"] = adata.response.reason; QVariantList vheaders; foreach(const HttpHeader &h, adata.response.headers) { QVariantList vheader; vheader += h.first; vheader += h.second; vheaders += QVariant(vheader); } vresponse["headers"] = vheaders; vresponse["body"] = adata.response.body; obj["response"] = vresponse; } if(!adata.route.isEmpty()) obj["route"] = adata.route; if(adata.separateStats) obj["separate-stats"] = true; if(!adata.channelPrefix.isEmpty()) obj["channel-prefix"] = adata.channelPrefix; if(!adata.channels.isEmpty()) { QVariantList vchannels; foreach(const QByteArray &channel, adata.channels) vchannels += channel; obj["channels"] = vchannels; } if(adata.trusted) obj["trusted"] = true; if(adata.useSession) obj["use-session"] = true; if(adata.responseSent) obj["response-sent"] = true; if(!adata.connMaxPackets.isEmpty()) obj["conn-max"] = adata.connMaxPackets; return obj; } static AcceptRequest::ResponseData convertResult(const QVariant &in, bool *ok) { AcceptRequest::ResponseData out; if(typeId(in) != QMetaType::QVariantHash) { *ok = false; return AcceptRequest::ResponseData(); } QVariantHash obj = in.toHash(); if(obj.contains("accepted")) { if(typeId(obj["accepted"]) != QMetaType::Bool) { *ok = false; return AcceptRequest::ResponseData(); } out.accepted = obj["accepted"].toBool(); } if(obj.contains("response")) { if(typeId(obj["response"]) != QMetaType::QVariantHash) { *ok = false; return AcceptRequest::ResponseData(); } QVariantHash vresponse = obj["response"].toHash(); if(vresponse.contains("code")) { if(!canConvert(vresponse["code"], QMetaType::Int)) { *ok = false; return AcceptRequest::ResponseData(); } out.response.code = vresponse["code"].toInt(); } if(vresponse.contains("reason")) { if(typeId(vresponse["reason"]) != QMetaType::QByteArray) { *ok = false; return AcceptRequest::ResponseData(); } out.response.reason = vresponse["reason"].toByteArray(); } if(vresponse.contains("headers")) { if(typeId(vresponse["headers"]) != QMetaType::QVariantList) { *ok = false; return AcceptRequest::ResponseData(); } foreach(const QVariant &i, vresponse["headers"].toList()) { QVariantList list = i.toList(); if(list.count() != 2) { *ok = false; return AcceptRequest::ResponseData(); } if(typeId(list[0]) != QMetaType::QByteArray || typeId(list[1]) != QMetaType::QByteArray) { *ok = false; return AcceptRequest::ResponseData(); } out.response.headers += QPair(list[0].toByteArray(), list[1].toByteArray()); } } if(vresponse.contains("body")) { if(typeId(vresponse["body"]) != QMetaType::QByteArray) { *ok = false; return AcceptRequest::ResponseData(); } out.response.body = vresponse["body"].toByteArray(); } } *ok = true; return out; } class AcceptRequest::Private : public QObject { Q_OBJECT public: AcceptRequest *q; ResponseData result; Private(AcceptRequest *_q) : QObject(_q), q(_q) { } }; AcceptRequest::AcceptRequest(ZrpcManager *manager, QObject *parent) : ZrpcRequest(manager, parent) { d = new Private(this); } AcceptRequest::~AcceptRequest() { delete d; } AcceptRequest::ResponseData AcceptRequest::result() const { return d->result; } void AcceptRequest::start(const AcceptData &adata) { ZrpcRequest::start("accept", acceptDataToVariant(adata).toHash()); } void AcceptRequest::onSuccess() { bool ok; d->result = convertResult(ZrpcRequest::result(), &ok); if(!ok) { setError(ErrorFormat); return; } } #include "acceptrequest.moc" pushpin-1.39.1/src/cpp/proxy/acceptrequest.h000066400000000000000000000024231457610542000210530ustar00rootroot00000000000000/* * Copyright (C) 2015 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef ACCEPTREQUEST_H #define ACCEPTREQUEST_H #include #include "packet/httpresponsedata.h" #include "zrpcrequest.h" class AcceptData; class ZrpcManager; class AcceptRequest : public ZrpcRequest { Q_OBJECT public: class ResponseData { public: bool accepted; HttpResponseData response; ResponseData() : accepted(false) { } }; AcceptRequest(ZrpcManager *manager, QObject *parent = 0); ~AcceptRequest(); ResponseData result() const; void start(const AcceptData &adata); protected: virtual void onSuccess(); private: class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/proxy/app.cpp000066400000000000000000000503171457610542000173230ustar00rootroot00000000000000/* * Copyright (C) 2012-2022 Fanout, Inc. * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "app.h" #include #include #include #include #include #include #include "processquit.h" #include "rtimer.h" #include "log.h" #include "settings.h" #include "xffrule.h" #include "domainmap.h" #include "engine.h" #include "config.h" using Connection = boost::signals2::scoped_connection; static void trimlist(QStringList *list) { for(int n = 0; n < list->count(); ++n) { if((*list)[n].isEmpty()) { list->removeAt(n); --n; // adjust position } } } static XffRule parse_xffRule(const QStringList &in) { XffRule out; foreach(const QString &s, in) { if(s.startsWith("truncate:")) { bool ok; int x = s.mid(9).toInt(&ok); if(!ok) return out; out.truncate = x; } else if(s == "append") out.append = true; } return out; } static QString suffixSpec(const QString &s, int i) { if(s.startsWith("ipc:")) return s + QString("-%1").arg(i); return s; } static QStringList suffixSpecs(const QStringList &l, int i) { if(l.count() == 1 && l[0].startsWith("ipc:")) return QStringList() << (l[0] + QString("-%1").arg(i)); return l; } enum CommandLineParseResult { CommandLineOk, CommandLineError, CommandLineVersionRequested, CommandLineHelpRequested }; class ArgsData { public: QString configFile; QString logFile; int logLevel; QString ipcPrefix; QStringList routeLines; bool quietCheck; ArgsData() : logLevel(-1), quietCheck(false) { } }; static CommandLineParseResult parseCommandLine(QCommandLineParser *parser, ArgsData *args, QString *errorMessage) { parser->setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions); const QCommandLineOption configFileOption("config", "Config file.", "file"); parser->addOption(configFileOption); const QCommandLineOption logFileOption("logfile", "File to log to.", "file"); parser->addOption(logFileOption); const QCommandLineOption logLevelOption("loglevel", "Log level (default: 2).", "x"); parser->addOption(logLevelOption); const QCommandLineOption verboseOption("verbose", "Verbose output. Same as --loglevel=3."); parser->addOption(verboseOption); const QCommandLineOption ipcPrefixOption("ipc-prefix", "Override ipc_prefix config option.", "prefix"); parser->addOption(ipcPrefixOption); const QCommandLineOption routeOption("route", "Add route (overrides routes file).", "line"); parser->addOption(routeOption); const QCommandLineOption quietCheckOption("quiet-check", "Log update checks in Zurl as debug level."); parser->addOption(quietCheckOption); const QCommandLineOption helpOption = parser->addHelpOption(); const QCommandLineOption versionOption = parser->addVersionOption(); if(!parser->parse(QCoreApplication::arguments())) { *errorMessage = parser->errorText(); return CommandLineError; } if(parser->isSet(versionOption)) return CommandLineVersionRequested; if(parser->isSet(helpOption)) return CommandLineHelpRequested; if(parser->isSet(configFileOption)) args->configFile = parser->value(configFileOption); if(parser->isSet(logFileOption)) args->logFile = parser->value(logFileOption); if(parser->isSet(logLevelOption)) { bool ok; int x = parser->value(logLevelOption).toInt(&ok); if(!ok || x < 0) { *errorMessage = "error: loglevel must be greater than or equal to 0"; return CommandLineError; } args->logLevel = x; } if(parser->isSet(verboseOption)) args->logLevel = 3; if(parser->isSet(ipcPrefixOption)) args->ipcPrefix = parser->value(ipcPrefixOption); if(parser->isSet(routeOption)) { foreach(const QString &r, parser->values(routeOption)) args->routeLines += r; } if(parser->isSet(quietCheckOption)) args->quietCheck = true; return CommandLineOk; } class EngineWorker : public QObject { Q_OBJECT public: EngineWorker(const Engine::Configuration &config, DomainMap *domainMap) : QObject(), config_(config), engine_(new Engine(domainMap, this)) { } Signal started; Signal stopped; Signal error; public slots: void start() { if(!engine_->start(config_)) { delete engine_; engine_ = 0; error(); return; } started(); } void stop() { delete engine_; engine_ = 0; stopped(); } void routesChanged() { if(engine_) engine_->routesChanged(); } private: Engine::Configuration config_; Engine *engine_; }; class EngineThread : public QThread { Q_OBJECT public: QMutex m; QWaitCondition w; Engine::Configuration config; DomainMap *domainMap; EngineWorker *worker; EngineThread(const Engine::Configuration &_config, DomainMap *_domainMap) : config(_config), domainMap(_domainMap), worker(0) { } ~EngineThread() { stop(); wait(); } bool start() { setObjectName("proxy-worker-" + QString::number(config.id)); QMutexLocker locker(&m); QThread::start(); w.wait(&m); return (bool)worker; } void stop() { QMutexLocker locker(&m); if(worker) QMetaObject::invokeMethod(worker, "stop", Qt::QueuedConnection); } void routesChanged() { QMutexLocker locker(&m); if(worker) QMetaObject::invokeMethod(worker, "routesChanged", Qt::QueuedConnection); } virtual void run() { // will unlock during exec m.lock(); worker = new EngineWorker(config, domainMap); Connection startedConnection = worker->started.connect(boost::bind(&EngineThread::worker_started, this)); Connection stoppedConnection = worker->stopped.connect(boost::bind(&EngineThread::worker_stopped, this)); Connection errorConnection = worker->error.connect(boost::bind(&EngineThread::worker_error, this)); QMetaObject::invokeMethod(worker, "start", Qt::QueuedConnection); exec(); // ensure deferred deletes are processed QCoreApplication::instance()->sendPostedEvents(); // deinit here, after all event loop activity has completed RTimer::deinit(); } private: void worker_started() { log_debug("worker %d: started", config.id); // unblock start() w.wakeOne(); m.unlock(); } void worker_stopped() { delete worker; worker = 0; log_debug("worker %d: stopped", config.id); quit(); } void worker_error() { delete worker; worker = 0; quit(); // unblock start() w.wakeOne(); m.unlock(); } }; class App::Private : public QObject { Q_OBJECT public: App *q; ArgsData args; DomainMap *domainMap; std::list threads; Connection quitConnection; Connection hupConnection; Connection changedConnection; Private(App *_q) : QObject(_q), q(_q), domainMap(0) { quitConnection = ProcessQuit::instance()->quit.connect(boost::bind(&Private::doQuit, this)); hupConnection = ProcessQuit::instance()->hup.connect(boost::bind(&App::Private::reload, this)); } void start() { QCoreApplication::setApplicationName("pushpin-proxy"); QCoreApplication::setApplicationVersion(Config::get().version); QCommandLineParser parser; parser.setApplicationDescription("Pushpin proxy component."); QString errorMessage; switch(parseCommandLine(&parser, &args, &errorMessage)) { case CommandLineOk: break; case CommandLineError: fprintf(stderr, "%s\n\n%s", qPrintable(errorMessage), qPrintable(parser.helpText())); q->quit(1); return; case CommandLineVersionRequested: printf("%s %s\n", qPrintable(QCoreApplication::applicationName()), qPrintable(QCoreApplication::applicationVersion())); q->quit(0); return; case CommandLineHelpRequested: parser.showHelp(); Q_UNREACHABLE(); } if(args.logLevel != -1) log_setOutputLevel(args.logLevel); else log_setOutputLevel(LOG_LEVEL_INFO); if(!args.logFile.isEmpty()) { if(!log_setFile(args.logFile)) { log_error("failed to open log file: %s", qPrintable(args.logFile)); q->quit(1); return; } } log_info("starting..."); QString configFile = args.configFile; if(configFile.isEmpty()) configFile = QDir(Config::get().configDir).filePath("pushpin.conf"); // QSettings doesn't inform us if the config file doesn't exist, so do that ourselves { QFile file(configFile); if(!file.open(QIODevice::ReadOnly)) { log_error("failed to open %s, and --config not passed", qPrintable(configFile)); q->quit(0); return; } } QDir configDir = QFileInfo(configFile).absoluteDir(); Settings settings(configFile); if(!args.ipcPrefix.isEmpty()) settings.setIpcPrefix(args.ipcPrefix); QStringList services = settings.value("runner/services").toStringList(); int workerCount = settings.value("proxy/workers", 1).toInt(); QStringList condure_in_specs = settings.value("proxy/condure_in_specs").toStringList(); trimlist(&condure_in_specs); QStringList condure_in_stream_specs = settings.value("proxy/condure_in_stream_specs").toStringList(); trimlist(&condure_in_stream_specs); QStringList condure_out_specs = settings.value("proxy/condure_out_specs").toStringList(); trimlist(&condure_out_specs); QStringList m2a_in_specs = settings.value("proxy/m2a_in_specs").toStringList(); trimlist(&m2a_in_specs); QStringList m2a_in_stream_specs = settings.value("proxy/m2a_in_stream_specs").toStringList(); trimlist(&m2a_in_stream_specs); QStringList m2a_out_specs = settings.value("proxy/m2a_out_specs").toStringList(); trimlist(&m2a_out_specs); QStringList condure_client_out_specs = settings.value("proxy/condure_client_out_specs").toStringList(); trimlist(&condure_client_out_specs); QStringList condure_client_out_stream_specs = settings.value("proxy/condure_client_out_stream_specs").toStringList(); trimlist(&condure_client_out_stream_specs); QStringList condure_client_in_specs = settings.value("proxy/condure_client_in_specs").toStringList(); trimlist(&condure_client_in_specs); QStringList zurl_out_specs = settings.value("proxy/zurl_out_specs").toStringList(); trimlist(&zurl_out_specs); QStringList zurl_out_stream_specs = settings.value("proxy/zurl_out_stream_specs").toStringList(); trimlist(&zurl_out_stream_specs); QStringList zurl_in_specs = settings.value("proxy/zurl_in_specs").toStringList(); trimlist(&zurl_in_specs); QString handler_inspect_spec = settings.value("proxy/handler_inspect_spec").toString(); QString handler_accept_spec = settings.value("proxy/handler_accept_spec").toString(); QString handler_retry_in_spec = settings.value("proxy/handler_retry_in_spec").toString(); QStringList handler_ws_control_init_specs = settings.value("proxy/handler_ws_control_init_specs").toStringList(); trimlist(&handler_ws_control_init_specs); QStringList handler_ws_control_stream_specs = settings.value("proxy/handler_ws_control_stream_specs").toStringList(); trimlist(&handler_ws_control_stream_specs); QString stats_spec = settings.value("proxy/stats_spec").toString(); QString command_spec = settings.value("proxy/command_spec").toString(); QStringList intreq_in_specs = settings.value("proxy/intreq_in_specs").toStringList(); trimlist(&intreq_in_specs); QStringList intreq_in_stream_specs = settings.value("proxy/intreq_in_stream_specs").toStringList(); trimlist(&intreq_in_stream_specs); QStringList intreq_out_specs = settings.value("proxy/intreq_out_specs").toStringList(); trimlist(&intreq_out_specs); bool ok; int ipcFileMode = settings.value("proxy/ipc_file_mode", -1).toString().toInt(&ok, 8); int sessionsMax = settings.value("proxy/max_open_requests", -1).toInt(); QString routesFile = settings.value("proxy/routesfile").toString(); bool debug = settings.value("proxy/debug").toBool(); bool autoCrossOrigin = settings.value("proxy/auto_cross_origin").toBool(); bool acceptXForwardedProtocol = settings.value("proxy/accept_x_forwarded_protocol").toBool(); QString setXForwardedProtocol = settings.value("proxy/set_x_forwarded_protocol").toString(); bool setXfProto = (setXForwardedProtocol == "true" || setXForwardedProtocol == "proto-only"); bool setXfProtocol = (setXForwardedProtocol == "true"); XffRule xffRule = parse_xffRule(settings.value("proxy/x_forwarded_for").toStringList()); XffRule xffTrustedRule = parse_xffRule(settings.value("proxy/x_forwarded_for_trusted").toStringList()); QStringList origHeadersNeedMarkStr = settings.value("proxy/orig_headers_need_mark").toStringList(); trimlist(&origHeadersNeedMarkStr); bool acceptPushpinRoute = settings.value("proxy/accept_pushpin_route").toBool(); QByteArray cdnLoop = settings.value("proxy/cdn_loop").toString().toUtf8(); bool logFrom = settings.value("proxy/log_from").toBool(); bool logUserAgent = settings.value("proxy/log_user_agent").toBool(); QByteArray sigIss = settings.value("proxy/sig_iss", "pushpin").toString().toUtf8(); Jwt::EncodingKey sigKey = Jwt::EncodingKey::fromConfigString(settings.value("proxy/sig_key").toString(), configDir); Jwt::DecodingKey upstreamKey = Jwt::DecodingKey::fromConfigString(settings.value("proxy/upstream_key").toString(), configDir); QString sockJsUrl = settings.value("proxy/sockjs_url").toString(); QString updatesCheck = settings.value("proxy/updates_check").toString(); QString organizationName = settings.value("proxy/organization_name").toString(); int clientMaxconn = settings.value("runner/client_maxconn", 50000).toInt(); bool statsConnectionSend = settings.value("global/stats_connection_send", true).toBool(); int statsConnectionTtl = settings.value("global/stats_connection_ttl", 120).toInt(); int statsConnectionsMaxTtl = settings.value("proxy/stats_connections_max_ttl", 60).toInt(); int statsReportInterval = settings.value("proxy/stats_report_interval", 10).toInt(); QString prometheusPort = settings.value("proxy/prometheus_port").toString(); QString prometheusPrefix = settings.value("proxy/prometheus_prefix").toString(); QList origHeadersNeedMark; foreach(const QString &s, origHeadersNeedMarkStr) origHeadersNeedMark += s.toUtf8(); // if routesfile is a relative path, then use it relative to the config file location QFileInfo fi(routesFile); if(fi.isRelative()) routesFile = QFileInfo(configDir, routesFile).filePath(); if(!(!condure_in_specs.isEmpty() && !condure_in_stream_specs.isEmpty() && !condure_out_specs.isEmpty()) && !(!m2a_in_specs.isEmpty() && !m2a_in_stream_specs.isEmpty() && !m2a_out_specs.isEmpty())) { log_error("must set condure_in_specs, condure_in_stream_specs, and condure_out_specs, or m2a_in_specs, m2a_in_stream_specs, and m2a_out_specs"); q->quit(0); return; } if(!(!condure_client_out_specs.isEmpty() && !condure_client_out_stream_specs.isEmpty() && !condure_client_in_specs.isEmpty()) && !(!zurl_out_specs.isEmpty() && !zurl_out_stream_specs.isEmpty() && !zurl_in_specs.isEmpty())) { log_error("must set condure_client_out_specs, condure_client_out_stream_specs, and condure_client_in_specs, or zurl_out_specs, zurl_out_stream_specs, and zurl_in_specs"); q->quit(0); return; } if(updatesCheck == "true") updatesCheck = "check"; // sessionsMax should not exceed clientMaxconn if(sessionsMax >= 0) sessionsMax = qMin(sessionsMax, clientMaxconn); else sessionsMax = clientMaxconn; if(!args.routeLines.isEmpty()) { domainMap = new DomainMap(this); foreach(const QString &line, args.routeLines) domainMap->addRouteLine(line); } else domainMap = new DomainMap(routesFile, this); changedConnection = domainMap->changed.connect(boost::bind(&Private::domainMap_changed, this)); Engine::Configuration config; config.appVersion = Config::get().version; config.clientId = "pushpin-proxy_" + QByteArray::number(QCoreApplication::applicationPid()); if(!services.contains("mongrel2") && (!condure_in_specs.isEmpty() || !condure_in_stream_specs.isEmpty() || !condure_out_specs.isEmpty())) { config.serverInSpecs = condure_in_specs; config.serverInStreamSpecs = condure_in_stream_specs; config.serverOutSpecs = condure_out_specs; } else { config.serverInSpecs = m2a_in_specs; config.serverInStreamSpecs = m2a_in_stream_specs; config.serverOutSpecs = m2a_out_specs; } if(!services.contains("zurl") && (!condure_client_out_specs.isEmpty() || !condure_client_out_stream_specs.isEmpty() || !condure_client_in_specs.isEmpty())) { config.clientOutSpecs = condure_client_out_specs; config.clientOutStreamSpecs = condure_client_out_stream_specs; config.clientInSpecs = condure_client_in_specs; } else { config.clientOutSpecs = zurl_out_specs; config.clientOutStreamSpecs = zurl_out_stream_specs; config.clientInSpecs = zurl_in_specs; } config.inspectSpec = handler_inspect_spec; config.acceptSpec = handler_accept_spec; config.retryInSpec = handler_retry_in_spec; config.wsControlInitSpecs = handler_ws_control_init_specs; config.wsControlStreamSpecs = handler_ws_control_stream_specs; config.statsSpec = stats_spec; config.commandSpec = command_spec; config.intServerInSpecs = intreq_in_specs; config.intServerInStreamSpecs = intreq_in_stream_specs; config.intServerOutSpecs = intreq_out_specs; config.ipcFileMode = ipcFileMode; config.sessionsMax = sessionsMax / workerCount; config.debug = debug; config.autoCrossOrigin = autoCrossOrigin; config.acceptXForwardedProto = acceptXForwardedProtocol; config.setXForwardedProto = setXfProto; config.setXForwardedProtocol = setXfProtocol; config.xffUntrustedRule = xffRule; config.xffTrustedRule = xffTrustedRule; config.origHeadersNeedMark = origHeadersNeedMark; config.acceptPushpinRoute = acceptPushpinRoute; config.cdnLoop = cdnLoop; config.logFrom = logFrom; config.logUserAgent = logUserAgent; config.sigIss = sigIss; config.sigKey = sigKey; config.upstreamKey = upstreamKey; config.sockJsUrl = sockJsUrl; config.updatesCheck = updatesCheck; config.organizationName = organizationName; config.quietCheck = args.quietCheck; config.statsConnectionSend = statsConnectionSend; config.statsConnectionTtl = statsConnectionTtl; config.statsConnectionsMaxTtl = statsConnectionsMaxTtl; config.statsReportInterval = statsReportInterval; config.prometheusPort = prometheusPort; config.prometheusPrefix = prometheusPrefix; for(int n = 0; n < workerCount; ++n) { Engine::Configuration wconfig = config; wconfig.id = n; if(workerCount > 1) { wconfig.clientId += '-' + QByteArray::number(n); wconfig.inspectSpec = suffixSpec(wconfig.inspectSpec, n); wconfig.acceptSpec = suffixSpec(wconfig.acceptSpec, n); wconfig.retryInSpec = suffixSpec(wconfig.retryInSpec, n); wconfig.wsControlInitSpecs = suffixSpecs(wconfig.wsControlInitSpecs, n); wconfig.wsControlStreamSpecs = suffixSpecs(wconfig.wsControlStreamSpecs, n); wconfig.statsSpec = suffixSpec(wconfig.statsSpec, n); wconfig.commandSpec = suffixSpec(wconfig.commandSpec, n); wconfig.intServerInSpecs = suffixSpecs(wconfig.intServerInSpecs, n); wconfig.intServerInStreamSpecs = suffixSpecs(wconfig.intServerInStreamSpecs, n); wconfig.intServerOutSpecs = suffixSpecs(wconfig.intServerOutSpecs, n); } EngineThread *t = new EngineThread(wconfig, domainMap); if(!t->start()) { delete t; for(EngineThread *t : threads) delete t; threads.clear(); q->quit(0); return; } threads.push_back(t); } log_info("started"); } private slots: void reload() { log_info("reloading"); log_rotate(); domainMap->reload(); } void domainMap_changed() { for(EngineThread *t : threads) t->routesChanged(); } void doQuit() { log_info("stopping..."); // remove the handler, so if we get another signal then we crash out ProcessQuit::cleanup(); for(EngineThread *t : threads) t->stop(); for(EngineThread *t : threads) delete t; threads.clear(); log_info("stopped"); q->quit(0); } }; App::App(QObject *parent) : QObject(parent) { d = new Private(this); } App::~App() { delete d; } void App::start() { d->start(); } #include "app.moc" pushpin-1.39.1/src/cpp/proxy/app.h000066400000000000000000000020711457610542000167620ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef APP_H #define APP_H #include #include using SignalInt = boost::signals2::signal; using Connection = boost::signals2::scoped_connection; class App : public QObject { Q_OBJECT public: App(QObject *parent = 0); ~App(); void start(); SignalInt quit; private: class Private; friend class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/proxy/connectionmanager.cpp000066400000000000000000000044651457610542000222400ustar00rootroot00000000000000/* * Copyright (C) 2015-2017 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "connectionmanager.h" #include #include #include "uuidutil.h" class ConnectionManager::Private { public: class Item { public: QPair rid; WebSocket *sock; QByteArray cid; WsProxySession *proxy; Item() : sock(0), proxy(0) { } }; QHash itemsBySock; QHash itemsByCid; Private() { } ~Private() { clearItemsBySock(); } void clearItemsBySock() { qDeleteAll(itemsBySock); itemsBySock.clear(); itemsByCid.clear(); } }; ConnectionManager::ConnectionManager() { d = new Private; } ConnectionManager::~ConnectionManager() { delete d; } QByteArray ConnectionManager::addConnection(WebSocket *sock) { assert(!d->itemsBySock.contains(sock)); Private::Item *i = new Private::Item; i->sock = sock; i->cid = UuidUtil::createUuid(); d->itemsBySock[i->sock] = i; d->itemsByCid[i->cid] = i; return i->cid; } QByteArray ConnectionManager::getConnection(WebSocket *sock) const { Private::Item *i = d->itemsBySock.value(sock); if(!i) return QByteArray(); return i->cid; } void ConnectionManager::removeConnection(WebSocket *sock) { Private::Item *i = d->itemsBySock.value(sock); assert(i); d->itemsBySock.remove(sock); d->itemsByCid.remove(i->cid); delete i; } WsProxySession *ConnectionManager::getProxyForConnection(const QByteArray &cid) const { Private::Item *i = d->itemsByCid.value(cid); if(!i) return 0; return i->proxy; } void ConnectionManager::setProxyForConnection(WebSocket *sock, WsProxySession *proxy) { Private::Item *i = d->itemsBySock.value(sock); assert(i); i->proxy = proxy; } pushpin-1.39.1/src/cpp/proxy/connectionmanager.h000066400000000000000000000024131457610542000216740ustar00rootroot00000000000000/* * Copyright (C) 2015-2017 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef CONNECTIONMANAGER_H #define CONNECTIONMANAGER_H #include #include class WebSocket; class WsProxySession; class ConnectionManager { public: ConnectionManager(); ~ConnectionManager(); // returns cid QByteArray addConnection(WebSocket *sock); // returns cid or empty QByteArray getConnection(WebSocket *sock) const; void removeConnection(WebSocket *sock); WsProxySession *getProxyForConnection(const QByteArray &cid) const; void setProxyForConnection(WebSocket *sock, WsProxySession *proxy); private: class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/proxy/domainmap.cpp000066400000000000000000000445341457610542000205140ustar00rootroot00000000000000/* * Copyright (C) 2012-2022 Fanout, Inc. * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "domainmap.h" #include #include #include #include #include #include #include #include #include #include #include #include "log.h" #include "routesfile.h" class DomainMap::Worker : public QObject { Q_OBJECT public: enum AddRuleResult { AddRuleOk, AddRuleNoDomainOrId, AddRuleDuplicate, }; class Rule { public: QString domain; int proto; // -1=unspecified, 0=http, 1=websocket QByteArray pathBeg; int ssl; // -1=unspecified, 0=no, 1=yes QByteArray id; bool explicitId; // if the id was provided by the user QByteArray sigIss; Jwt::EncodingKey sigKey; QByteArray prefix; bool origHeaders; QString asHost; int pathRemove; QByteArray pathPrepend; bool debug; bool autoCrossOrigin; JsonpConfig jsonpConfig; bool session; QByteArray sockJsPath; QByteArray sockJsAsPath; HttpHeaders headers; bool grip; QList targets; Rule() : proto(-1), ssl(-1), explicitId(false), origHeaders(false), pathRemove(0), debug(false), autoCrossOrigin(false), session(false), grip(true) { } // checks only the condition, not sig/targets bool compare(const Rule &other) const { return (proto == other.proto && ssl == other.ssl && pathBeg == other.pathBeg); } inline bool matchProto(Protocol reqProto) const { return ((proto == 0 && reqProto == Http) || (proto == 1 && reqProto == WebSocket)); } inline bool matchSsl(bool reqSsl) const { return ((ssl == 0 && !reqSsl) || (ssl == 1 && reqSsl)); } bool isMatch(Protocol reqProto, bool reqSsl, const QByteArray &reqPath) const { return ((proto == -1 || matchProto(reqProto)) && (ssl == -1 || matchSsl(reqSsl)) && (pathBeg.isEmpty() || reqPath.startsWith(pathBeg))); } bool isMoreSpecificMatch(const Rule &other, Protocol reqProto, bool reqSsl, const QByteArray &reqPath) const { // have to at least be a match if(!isMatch(reqProto, reqSsl, reqPath)) return false; // now let's see if we're a better match if(other.proto == -1 && proto != -1) return true; else if(other.proto != -1 && proto == -1) return false; if(pathBeg.size() > other.pathBeg.size()) return true; if(other.ssl == -1 && ssl != -1) return true; return false; } QByteArray idFromCondition() const { QString domainStr; if(!domain.isEmpty()) domainStr = domain; else domainStr = "*"; QString protoStr; if(proto == 0) protoStr = "http"; else if(proto == 1) protoStr = "ws"; else protoStr = "*"; QString sslStr; if(ssl == 0) sslStr = "ssl"; else if(ssl == 1) sslStr = "no-ssl"; else sslStr = "*"; QString pathBegStr; if(!pathBeg.isEmpty()) pathBegStr = pathBeg; else pathBegStr = "*"; return (domainStr + ',' + protoStr + ',' + sslStr + ',' + pathBegStr).toUtf8(); } Entry toEntry() const { Entry e; e.pathBeg = pathBeg; e.id = id; e.sigIss = sigIss; e.sigKey = sigKey; e.prefix = prefix; e.origHeaders = origHeaders; e.asHost = asHost; e.pathRemove = pathRemove; e.pathPrepend = pathPrepend; e.debug = debug; e.autoCrossOrigin = autoCrossOrigin; e.jsonpConfig = jsonpConfig; e.session = session; e.sockJsPath = sockJsPath; e.sockJsAsPath = sockJsAsPath; e.headers = headers; e.separateStats = explicitId; e.grip = grip; e.targets = targets; return e; } }; QMutex m; QString fileName; QList allRules; QHash< QString, QList > rulesByDomain; QHash rulesById; QTimer t; QFileSystemWatcher watcher; Worker() : t(this), watcher(this) { connect(&t, &QTimer::timeout, this, &Worker::doReload); t.setSingleShot(true); } void reload() { QFile file(fileName); if(!file.open(QFile::ReadOnly)) { log_warning("unable to open routes file: %s", qPrintable(fileName)); return; } QDir fileDir = QFileInfo(fileName).absoluteDir(); QList all; QHash< QString, QList > domainMap; QHash idMap; QTextStream ts(&file); for(int lineNum = 1; !ts.atEnd(); ++lineNum) { QString line = ts.readLine(); Rule r; if(!parseRouteLine(line, fileName, lineNum, fileDir, &r)) { // parseRouteLine will have logged a message if needed continue; } if(r.id.isEmpty()) r.id = r.idFromCondition(); AddRuleResult ret = addRule(r, &all, &domainMap, &idMap); if(ret != AddRuleOk) { if(ret == AddRuleNoDomainOrId) log_warning("%s:%d condition has no domain or id", qPrintable(fileName), lineNum); else // AddRuleDuplicate log_warning("%s:%d skipping duplicate condition", qPrintable(fileName), lineNum); continue; } } log_debug("routes by domain:"); QHashIterator< QString, QList > it(domainMap); while(it.hasNext()) { it.next(); const QString &domain = it.key(); const QList &rules = it.value(); foreach(const Rule &r, rules) { QStringList tstr; foreach(const Target &t, r.targets) { if(t.type == Target::Test) tstr += "test"; else if(t.type == Target::Custom) tstr += t.zhttpRoute.baseSpec; else // Default tstr += t.connectHost + ';' + QString::number(t.connectPort); } if(!domain.isEmpty()) log_debug(" %s: %s", qPrintable(domain), qPrintable(tstr.join(" "))); else log_debug(" (default): %s", qPrintable(tstr.join(" "))); } } // atomically replace the map m.lock(); allRules = all; rulesByDomain = domainMap; rulesById = idMap; m.unlock(); log_info("routes loaded with %d entries", allRules.count()); QMetaObject::invokeMethod(this, "doChanged", Qt::QueuedConnection); } // mutex must be locked when calling this method bool addRouteLine(const QString &line) { Rule r; if(!parseRouteLine(line, "", 1, QDir::current(), &r)) return false; if(addRule(r, &allRules, &rulesByDomain, &rulesById) != AddRuleOk) return false; return true; } Signal started; Signal changed; public slots: void doChanged() { changed(); } void start() { if(!fileName.isEmpty()) { connect(&watcher, &QFileSystemWatcher::fileChanged, this, &Worker::fileChanged); watcher.addPath(fileName); reload(); } started(); } void fileChanged(const QString &path) { Q_UNUSED(path); // inotify tends to give us extra events so let's hang around a // little bit before reloading if(!t.isActive()) { log_info("routes file changed, reloading"); t.start(1000); } } void doReload() { // in case the file was not changed, but overwritten by a different // file, re-arm watcher. if(!fileName.isEmpty()) { watcher.addPath(fileName); } reload(); } private: static bool parseRouteLine(const QString &line, const QString &fileName, int lineNum, const QDir &fileDir, Rule *rule) { bool ok; QString errmsg; QList sections = RoutesFile::parseLine(line, &ok, &errmsg); if(!ok) { log_warning("%s:%d: %s", qPrintable(fileName), lineNum, qPrintable(errmsg)); return false; } if(sections.isEmpty()) { // nothing. could happen if line is blank or commented out return false; } if(sections.count() < 2) { log_warning("%s:%d: must specify condition and at least one target", qPrintable(fileName), lineNum); return false; } QString val = sections[0].value; QMultiHash props = sections[0].props; Rule r; if(val.isEmpty()) r.domain = QString(); // null means unspecified else if(val == "*") r.domain = QString(""); // empty means wildcard else r.domain = val; // non-empty means exact match r.jsonpConfig.mode = JsonpConfig::Extended; if(props.contains("proto")) { val = props.value("proto"); if(val == "http") r.proto = 0; else if(val == "ws") r.proto = 1; else { log_warning("%s:%d: proto must be set to 'http' or 'ws'", qPrintable(fileName), lineNum); return false; } } if(props.contains("ssl")) { val = props.value("ssl"); if(val == "yes") r.ssl = 1; else if(val == "no") r.ssl = 0; else { log_warning("%s:%d: ssl must be set to 'yes' or 'no'", qPrintable(fileName), lineNum); return false; } } if(props.contains("id")) { r.id = props.value("id").toUtf8(); r.explicitId = true; } if(props.contains("path_beg")) { QString pathBeg = props.value("path_beg"); if(pathBeg.isEmpty()) { log_warning("%s:%d: path_beg cannot be empty", qPrintable(fileName), lineNum); return false; } r.pathBeg = pathBeg.toUtf8(); } if(props.contains("sig_iss")) { r.sigIss = props.value("sig_iss").toUtf8(); } if(props.contains("sig_key")) { r.sigKey = Jwt::EncodingKey::fromConfigString(props.value("sig_key"), fileDir); } if(props.contains("prefix")) { r.prefix = props.value("prefix").toUtf8(); } if(props.contains("orig_headers")) { r.origHeaders = true; } if(props.contains("as_host")) { r.asHost = props.value("as_host"); } if(props.contains("path_rem")) { r.pathRemove = props.value("path_rem").toInt(); } if(props.contains("replace_beg")) { r.pathRemove = r.pathBeg.length(); r.pathPrepend = props.value("replace_beg").toUtf8(); } if(props.contains("debug")) r.debug = true; if(props.contains("aco")) r.autoCrossOrigin = true; if(props.contains("jsonp_mode")) { val = props.value("jsonp_mode"); if(val == "basic") r.jsonpConfig.mode = JsonpConfig::Basic; else if(val == "extended") r.jsonpConfig.mode = JsonpConfig::Extended; else { log_warning("%s:%d: jsonp_mode must be set to 'basic' or 'extended'", qPrintable(fileName), lineNum); return false; } } if(props.contains("jsonp_cb")) r.jsonpConfig.callbackParam = props.value("jsonp_cb").toUtf8(); if(props.contains("jsonp_body")) r.jsonpConfig.bodyParam = props.value("jsonp_body").toUtf8(); if(props.contains("jsonp_defcb")) r.jsonpConfig.defaultCallback = props.value("jsonp_defcb").toUtf8(); if(r.jsonpConfig.mode == JsonpConfig::Basic) r.jsonpConfig.defaultMethod = "POST"; else // Extended r.jsonpConfig.defaultMethod = "GET"; if(props.contains("jsonp_defmethod")) r.jsonpConfig.defaultMethod = props.value("jsonp_defmethod"); if(props.contains("session")) r.session = true; if(props.contains("sockjs")) r.sockJsPath = props.value("sockjs").toUtf8(); if(props.contains("sockjs_as_path")) r.sockJsAsPath = props.value("sockjs_as_path").toUtf8(); if(props.contains("header")) { foreach(const QString &s, props.values("header")) { int at = s.indexOf(':'); if(at < 1) { log_warning("%s:%d: header must use format 'name:value'", qPrintable(fileName), lineNum); return false; } QByteArray name = s.mid(0, at).toUtf8(); QByteArray value = s.mid(at + 1).toUtf8(); // trim left side of value int n = 0; while(n < value.length() && value[n] == ' ') { ++n; } if(n > 0) value = value.mid(n); r.headers += HttpHeader(name, value); } } if(props.contains("no_grip")) r.grip = false; ok = true; for(int n = 1; n < sections.count(); ++n) { QString val = sections[n].value; QMultiHash props = sections[n].props; Target target; if(val == "test") { target.type = Target::Test; } else if(val.startsWith("zhttp/")) { target.type = Target::Custom; target.zhttpRoute.baseSpec = val.mid(6); } else if(val.startsWith("zhttpreq/")) { target.type = Target::Custom; target.zhttpRoute.baseSpec = val.mid(9); target.zhttpRoute.req = true; } else { target.type = Target::Default; int at = val.indexOf(':'); if(at == -1) { log_warning("%s:%d: target bad format", qPrintable(fileName), lineNum); ok = false; break; } QString sport = val.mid(at + 1); int port = sport.toInt(&ok); if(!ok || port < 1 || port > 65535) { log_warning("%s:%d: target invalid port", qPrintable(fileName), lineNum); ok = false; break; } target.connectHost = val.mid(0, at); target.connectPort = port; } if(props.contains("ssl")) target.ssl = true; if(props.contains("untrusted")) target.trusted = false; else target.trusted = true; if(props.contains("trust_connect_host")) target.trustConnectHost = true; if(props.contains("insecure")) target.insecure = true; if(props.contains("host")) target.host = props.value("host"); if(props.contains("sub")) { foreach(const QString &s, props.values("sub")) { if(!s.isEmpty()) target.subscriptions += s; } } if(props.contains("over_http")) target.overHttp = true; if(props.contains("one_event")) target.oneEvent = true; if(props.contains("ipc_file_mode")) { bool ok_; int x = props.value("ipc_file_mode").toInt(&ok_, 8); if(ok_ && x >= 0) target.zhttpRoute.ipcFileMode = x; } r.targets += target; } if(!ok) return false; *rule = r; return true; } static AddRuleResult addRule(const Rule &r, QList *all, QHash< QString,QList > *domainMap, QHash *idMap) { if(r.domain.isNull() && r.id.isEmpty()) return AddRuleNoDomainOrId; bool addByDomain = false; bool addById = false; if(!r.domain.isNull()) { if(domainMap->contains(r.domain)) { QList *rules = &((*domainMap)[r.domain]); bool found = false; foreach(const Rule &b, *rules) { if(b.compare(r)) { found = true; break; } } if(found) return AddRuleDuplicate; } addByDomain = true; } if(!r.id.isEmpty()) { if(!idMap->contains(r.id)) { addById = true; } else { // mark the key as unusable idMap->insert(r.id, Rule()); } } *all += r; if(addByDomain) { if(!domainMap->contains(r.domain)) domainMap->insert(r.domain, QList()); QList *rules = &((*domainMap)[r.domain]); *rules += r; } if(addById) { idMap->insert(r.id, r); } return AddRuleOk; } }; class DomainMap::Thread : public QThread { Q_OBJECT public: QString fileName; Worker *worker; QMutex m; QWaitCondition w; ~Thread() { quit(); wait(); } void start() { QMutexLocker locker(&m); QThread::start(); w.wait(&m); } virtual void run() { worker = new Worker; worker->fileName = fileName; Connection startedConnection = worker->started.connect(boost::bind(&Thread::worker_started, this)); QMetaObject::invokeMethod(worker, "start", Qt::QueuedConnection); exec(); startedConnection.disconnect(); delete worker; } public: void worker_started() { QMutexLocker locker(&m); w.wakeOne(); } }; class DomainMap::Private : public QObject { Q_OBJECT public: DomainMap *q; Thread *thread; Connection changedConnection; Private(DomainMap *_q) : QObject(_q), q(_q), thread(0) { } ~Private() { changedConnection.disconnect(); delete thread; } void start(const QString &fileName = QString()) { thread = new Thread; thread->fileName = fileName; thread->start(); // worker guaranteed to exist after starting changedConnection = thread->worker->changed.connect(boost::bind(&Private::workerChanged, this)); } private: // NOTE: must be thread-safe. called from separate thread void workerChanged() { QMetaObject::invokeMethod(this, "doChanged", Qt::QueuedConnection); } private slots: void doChanged() { q->changed(); } }; DomainMap::DomainMap(QObject *parent) : QObject(parent) { d = new Private(this); d->start(); } DomainMap::DomainMap(const QString &fileName, QObject *parent) : QObject(parent) { d = new Private(this); d->start(fileName); } DomainMap::~DomainMap() { delete d; } void DomainMap::reload() { QMetaObject::invokeMethod(d->thread->worker, "doReload", Qt::QueuedConnection); } bool DomainMap::isIdShared(const QString &id) const { QMutexLocker locker(&d->thread->worker->m); if(!d->thread->worker->rulesById.contains(id)) return false; const Worker::Rule *r = &d->thread->worker->rulesById[id]; return r->id.isEmpty(); } DomainMap::Entry DomainMap::entry(Protocol proto, bool ssl, const QString &domain, const QByteArray &path) const { QMutexLocker locker(&d->thread->worker->m); const QList *rules; QString empty(""); if(d->thread->worker->rulesByDomain.contains(domain)) rules = &d->thread->worker->rulesByDomain[domain]; else if(d->thread->worker->rulesByDomain.contains(empty)) rules = &d->thread->worker->rulesByDomain[empty]; else return Entry(); const Worker::Rule *best = 0; foreach(const Worker::Rule &r, *rules) { if((!best && r.isMatch(proto, ssl, path)) || (best && r.isMoreSpecificMatch(*best, proto, ssl, path))) { best = &r; } } if(!best) return Entry(); assert(!best->targets.isEmpty()); return best->toEntry(); } DomainMap::Entry DomainMap::entry(const QString &id) const { QMutexLocker locker(&d->thread->worker->m); if(!d->thread->worker->rulesById.contains(id)) return Entry(); const Worker::Rule *r = &d->thread->worker->rulesById[id]; // this can happen if there were duplicate route IDs if(r->id.isEmpty()) return Entry(); return r->toEntry(); } QList DomainMap::zhttpRoutes() const { QMutexLocker locker(&d->thread->worker->m); QList out; foreach(const Worker::Rule &r, d->thread->worker->allRules) { foreach(const Target &t, r.targets) { if(!t.zhttpRoute.isNull() && !out.contains(t.zhttpRoute)) out += t.zhttpRoute; } } return out; } bool DomainMap::addRouteLine(const QString &line) { QMutexLocker locker(&d->thread->worker->m); return d->thread->worker->addRouteLine(line); } #include "domainmap.moc" pushpin-1.39.1/src/cpp/proxy/domainmap.h000066400000000000000000000075761457610542000201660ustar00rootroot00000000000000/* * Copyright (C) 2012-2022 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef DOMAINMAP_H #define DOMAINMAP_H #include #include #include #include #include "httpheaders.h" #include "jwt.h" #include using Signal = boost::signals2::signal; using Connection = boost::signals2::scoped_connection; // this class offers fast access to the routes file. the table is maintained // by a background thread so that file access doesn't cause blocking. class DomainMap : public QObject { Q_OBJECT public: class JsonpConfig { public: enum Mode { Basic, Extended }; Mode mode; QByteArray callbackParam; QByteArray bodyParam; QByteArray defaultCallback; QString defaultMethod; JsonpConfig() : mode(Extended) { } }; enum Protocol { Http, WebSocket }; class ZhttpRoute { public: QString baseSpec; bool req; int ipcFileMode; ZhttpRoute() : req(false), ipcFileMode(-1) { } bool isNull() const { return baseSpec.isEmpty(); } bool operator==(const ZhttpRoute &other) const { // only compare spec return (baseSpec == other.baseSpec); } }; class Target { public: enum Type { Default, Custom, Test }; Type type; QString connectHost; int connectPort; ZhttpRoute zhttpRoute; bool ssl; // use https bool trusted; // bypass zurl access policies bool trustConnectHost; // verify cert against target host bool insecure; // ignore server certificate validity QString host; // override input host QStringList subscriptions; // implicit subscriptions bool overHttp; // use websocket-over-http protocol bool oneEvent; // send one event at a time with overHttp Target() : type(Default), connectPort(-1), ssl(false), trusted(false), trustConnectHost(false), insecure(false), overHttp(false), oneEvent(false) { } }; class Entry { public: QByteArray id; QByteArray pathBeg; QByteArray sigIss; Jwt::EncodingKey sigKey; QByteArray prefix; bool origHeaders; QString asHost; int pathRemove; QByteArray pathPrepend; bool debug; bool autoCrossOrigin; JsonpConfig jsonpConfig; bool session; QByteArray sockJsPath; QByteArray sockJsAsPath; HttpHeaders headers; bool separateStats; bool grip; QList targets; bool isNull() const { return targets.isEmpty(); } QByteArray statsRoute() const { if(separateStats) return id; else return QByteArray(); // global stats } Entry() : origHeaders(false), pathRemove(0), debug(false), autoCrossOrigin(false), session(false), separateStats(false), grip(true) { } }; DomainMap(QObject *parent = 0); DomainMap(const QString &fileName, QObject *parent = 0); ~DomainMap(); // shouldn't really ever need to call this, but it's here in case the // underlying file watching doesn't work void reload(); bool isIdShared(const QString &id) const; Entry entry(Protocol proto, bool ssl, const QString &domain, const QByteArray &path) const; Entry entry(const QString &id) const; QList zhttpRoutes() const; bool addRouteLine(const QString &line); Signal changed; private: class Private; friend class Private; Private *d; class Thread; class Worker; }; #endif pushpin-1.39.1/src/cpp/proxy/engine.cpp000066400000000000000000000665441457610542000200210ustar00rootroot00000000000000/* * Copyright (C) 2012-2023 Fanout, Inc. * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "engine.h" #include #include "qzmqsocket.h" #include "qzmqvalve.h" #include "qzmqreqmessage.h" #include "tnetstring.h" #include "packet/httpresponsedata.h" #include "packet/retryrequestpacket.h" #include "packet/statspacket.h" #include "packet/zrpcrequestpacket.h" #include "qtcompat.h" #include "rtimer.h" #include "log.h" #include "inspectdata.h" #include "zhttpmanager.h" #include "zhttprequest.h" #include "zwebsocket.h" #include "websocketoverhttp.h" #include "domainmap.h" #include "zroutes.h" #include "zrpcmanager.h" #include "zrpcrequest.h" #include "zrpcchecker.h" #include "wscontrolmanager.h" #include "requestsession.h" #include "proxysession.h" #include "wsproxysession.h" #include "statsmanager.h" #include "connectionmanager.h" #include "zutil.h" #include "sockjsmanager.h" #include "sockjssession.h" #include "updater.h" #include "logutil.h" #define DEFAULT_HWM 1000 class Engine::Private : public QObject { Q_OBJECT public: class ProxyItem { public: bool shared; QByteArray key; ProxySession *ps; ProxyItem() : shared(false), ps(0) { } }; class WsProxyItem { public: WsProxySession *ps; WsProxyItem() : ps(0) { } }; struct RequestSessionConnections { Connection inspectedConnection; Connection inspectErrorConnection; Connection finishedConnection; Connection finishedByAcceptConnection; }; struct ProxySessionConnections { Connection addNotAllowedConnection; Connection finishedConnection; Connection reqSessionDestroyedConnection; }; Engine *q; bool destroying; DomainMap *domainMap; Configuration config; ZhttpManager *zhttpIn; ZhttpManager *intZhttpIn; ZRoutes *zroutes; ZrpcManager *inspect; std::unique_ptr wsControl; ZrpcChecker *inspectChecker; StatsManager *stats; ZrpcManager *command; ZrpcManager *accept; QZmq::Socket *handler_retry_in_sock; QZmq::Valve *handler_retry_in_valve; QSet requestSessions; QHash proxyItemsByKey; QHash proxyItemsBySession; QHash wsProxyItemsBySession; SockJsManager *sockJsManager; ConnectionManager connectionManager; Updater *updater; LogUtil::Config logConfig; Connection cmdReqReadyConnection; Connection sessionReadyConnection; Connection requestReadyConnection; Connection socketReadyConnection; Connection iRequestReadyConnection; map reqSessionConnectionMap; map proxySessionConnectionMap; Connection connMaxConnection; Connection rrConnection; Private(Engine *_q, DomainMap *_domainMap) : QObject(_q), q(_q), destroying(false), domainMap(_domainMap), zhttpIn(0), intZhttpIn(0), zroutes(0), inspect(0), inspectChecker(0), stats(0), command(0), accept(0), handler_retry_in_sock(0), handler_retry_in_valve(0), sockJsManager(0), updater(0) { } ~Private() { destroying = true; // need to delete all objects that may have connections before // deleting zhttpmanagers/zroutes delete updater; QHashIterator it(proxyItemsBySession); while(it.hasNext()) { it.next(); delete it.key(); delete it.value(); } proxyItemsBySession.clear(); proxyItemsByKey.clear(); QHashIterator wit(wsProxyItemsBySession); while(wit.hasNext()) { wit.next(); delete wit.key(); delete wit.value(); } wsProxyItemsBySession.clear(); foreach(RequestSession *rs, requestSessions){ reqSessionConnectionMap.erase(rs); delete rs; } requestSessions.clear(); // may have background connections delete sockJsManager; sockJsManager = 0; WebSocketOverHttp::clearDisconnectManager(); // need to make sure this is deleted before inspect manager delete inspectChecker; inspectChecker = 0; } bool start(const Configuration &_config) { config = _config; // up to 10 timers per session RTimer::init(config.sessionsMax * 10); logConfig.fromAddress = config.logFrom; logConfig.userAgent = config.logUserAgent; WebSocketOverHttp::setMaxManagedDisconnects(config.sessionsMax); zhttpIn = new ZhttpManager(this); requestReadyConnection = zhttpIn->requestReady.connect(boost::bind(&Private::zhttpIn_requestReady, this)); socketReadyConnection = zhttpIn->socketReady.connect(boost::bind(&Private::zhttpIn_socketReady, this)); zhttpIn->setInstanceId(config.clientId); zhttpIn->setServerInSpecs(config.serverInSpecs); zhttpIn->setServerInStreamSpecs(config.serverInStreamSpecs); zhttpIn->setServerOutSpecs(config.serverOutSpecs); if(!config.intServerInSpecs.isEmpty() && !config.intServerInStreamSpecs.isEmpty() && !config.intServerOutSpecs.isEmpty()) { intZhttpIn = new ZhttpManager(this); intZhttpIn->setBind(true); intZhttpIn->setIpcFileMode(config.ipcFileMode); iRequestReadyConnection = intZhttpIn->requestReady.connect(boost::bind(&Private::intZhttpIn_requestReady, this)); intZhttpIn->setInstanceId(config.clientId); intZhttpIn->setServerInSpecs(config.intServerInSpecs); intZhttpIn->setServerInStreamSpecs(config.intServerInStreamSpecs); intZhttpIn->setServerOutSpecs(config.intServerOutSpecs); } zroutes = new ZRoutes(this); zroutes->setInstanceId(config.clientId); zroutes->setDefaultOutSpecs(config.clientOutSpecs); zroutes->setDefaultOutStreamSpecs(config.clientOutStreamSpecs); zroutes->setDefaultInSpecs(config.clientInSpecs); sockJsManager = new SockJsManager(config.sockJsUrl, this); sessionReadyConnection = sockJsManager->sessionReady.connect(boost::bind(&Private::sockjs_sessionReady, this)); if(!config.inspectSpec.isEmpty()) { inspect = new ZrpcManager(this); inspect->setBind(true); inspect->setIpcFileMode(config.ipcFileMode); if(!inspect->setClientSpecs(QStringList() << config.inspectSpec)) { // zrpcmanager logs error return false; } inspect->setTimeout(config.inspectTimeout); inspectChecker = new ZrpcChecker(this); } if(!config.acceptSpec.isEmpty()) { accept = new ZrpcManager(this); accept->setInstanceId(config.clientId); accept->setBind(true); accept->setIpcFileMode(config.ipcFileMode); if(!accept->setClientSpecs(QStringList() << config.acceptSpec)) { // zrpcmanager logs error return false; } // there's no acceptTimeout config option so we'll reuse inspectTimeout accept->setTimeout(config.inspectTimeout); } if(!config.retryInSpec.isEmpty()) { handler_retry_in_sock = new QZmq::Socket(QZmq::Socket::Router, this); handler_retry_in_sock->setIdentity(config.clientId); handler_retry_in_sock->setHwm(DEFAULT_HWM); QString errorMessage; if(!ZUtil::setupSocket(handler_retry_in_sock, config.retryInSpec, true, config.ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } handler_retry_in_valve = new QZmq::Valve(handler_retry_in_sock, this); rrConnection = handler_retry_in_valve->readyRead.connect(boost::bind(&Private::handler_retry_in_readyRead, this, boost::placeholders::_1)); } if(handler_retry_in_valve) handler_retry_in_valve->open(); if(!config.wsControlInitSpecs.isEmpty() && !config.wsControlStreamSpecs.isEmpty()) { wsControl = std::make_unique(); wsControl->setIdentity(config.clientId); wsControl->setIpcFileMode(config.ipcFileMode); if(!wsControl->setInitSpecs(config.wsControlInitSpecs)) { log_error("unable to bind to handler_ws_control_init_specs: %s", qPrintable(config.wsControlInitSpecs.join(", "))); return false; } if(!wsControl->setStreamSpecs(config.wsControlStreamSpecs)) { log_error("unable to bind to handler_ws_control_stream_specs: %s", qPrintable(config.wsControlStreamSpecs.join(", "))); return false; } } if(!config.statsSpec.isEmpty() || !config.prometheusPort.isEmpty()) { stats = new StatsManager(config.sessionsMax, 0, this); connMaxConnection = stats->connMax.connect(boost::bind(&Private::stats_connMax, this, boost::placeholders::_1)); stats->setInstanceId(config.clientId); stats->setIpcFileMode(config.ipcFileMode); stats->setConnectionSendEnabled(config.statsConnectionSend); stats->setConnectionsMaxSendEnabled(!config.statsConnectionSend); stats->setConnectionTtl(config.statsConnectionTtl); stats->setConnectionsMaxTtl(config.statsConnectionsMaxTtl); stats->setReportInterval(config.statsReportInterval); if(!config.statsSpec.isEmpty()) { if(!stats->setSpec(config.statsSpec)) { // statsmanager logs error return false; } } if(!config.prometheusPort.isEmpty()) { stats->setPrometheusPrefix(config.prometheusPrefix); if(!stats->setPrometheusPort(config.prometheusPort)) { log_error("unable to bind to prometheus port: %s", qPrintable(config.prometheusPort)); return false; } } } if(!config.commandSpec.isEmpty()) { command = new ZrpcManager(this); command->setBind(true); command->setIpcFileMode(config.ipcFileMode); cmdReqReadyConnection = command->requestReady.connect(boost::bind(&Private::command_requestReady, this)); if(!command->setServerSpecs(QStringList() << config.commandSpec)) { // zrpcmanager logs error return false; } } if(!config.appVersion.isEmpty() && (config.updatesCheck == "check" || config.updatesCheck == "report")) { updater = new Updater(config.updatesCheck == "report" ? Updater::ReportMode : Updater::CheckMode, config.quietCheck, config.appVersion, config.organizationName, zroutes->defaultManager(), this); } // init zroutes routesChanged(); return true; } void routesChanged() { // connect to new zhttp targets, disconnect from old zroutes->setup(domainMap->zhttpRoutes()); } void doProxy(RequestSession *rs, const InspectData *idata = 0) { DomainMap::Entry route = rs->route(); // we'll always have a route assert(!route.isNull()); bool sharable = (idata && !idata->sharingKey.isEmpty() && rs->haveCompleteRequestBody()); ProxySession *ps = 0; if(sharable) { log_debug("need to proxy with sharing key: %s", idata->sharingKey.data()); ProxyItem *i = proxyItemsByKey.value(idata->sharingKey); if(i) ps = i->ps; } if(!ps) { log_debug("creating proxysession for id=%s", rs->rid().second.data()); ps = new ProxySession(zroutes, accept, logConfig, stats); // TODO: use callbacks for performance proxySessionConnectionMap[ps] = { ps->addNotAllowed.connect(boost::bind(&Private::ps_addNotAllowed, this, ps)), ps->finished.connect(boost::bind(&Private::ps_finished, this, ps)), ps->requestSessionDestroyed.connect(boost::bind(&Private::ps_requestSessionDestroyed, this, boost::placeholders::_1, boost::placeholders::_2)) }; ps->setRoute(route); ps->setDefaultSigKey(config.sigIss, config.sigKey); ps->setAcceptXForwardedProtocol(config.acceptXForwardedProto); ps->setUseXForwardedProtocol(config.setXForwardedProto, config.setXForwardedProtocol); ps->setXffRules(config.xffUntrustedRule, config.xffTrustedRule); ps->setOrigHeadersNeedMark(config.origHeadersNeedMark); ps->setAcceptPushpinRoute(config.acceptPushpinRoute); ps->setCdnLoop(config.cdnLoop); ps->setProxyInitialResponseEnabled(true); if(idata) ps->setInspectData(*idata); ProxyItem *i = new ProxyItem; i->ps = ps; proxyItemsBySession.insert(i->ps, i); if(sharable) { i->shared = true; i->key = idata->sharingKey; proxyItemsByKey.insert(i->key, i); } } else log_debug("reusing proxysession"); // proxysession will take it from here // TODO: use callbacks for performance reqSessionConnectionMap.erase(rs); ps->add(rs); } void doProxySocket(WebSocket *sock, const DomainMap::Entry &route) { QByteArray cid = connectionManager.addConnection(sock); WsProxySession *ps = new WsProxySession(zroutes, &connectionManager, logConfig, stats, wsControl.get()); ps->finishedByPassthroughCallback().add(Private::wsps_finishedByPassthrough_cb, this); connectionManager.setProxyForConnection(sock, ps); ps->setDebugEnabled(config.debug || route.debug); ps->setDefaultSigKey(config.sigIss, config.sigKey); ps->setDefaultUpstreamKey(config.upstreamKey); ps->setAcceptXForwardedProtocol(config.acceptXForwardedProto); ps->setUseXForwardedProtocol(config.setXForwardedProto, config.setXForwardedProtocol); ps->setXffRules(config.xffUntrustedRule, config.xffTrustedRule); ps->setOrigHeadersNeedMark(config.origHeadersNeedMark); ps->setAcceptPushpinRoute(config.acceptPushpinRoute); ps->setCdnLoop(config.cdnLoop); WsProxyItem *i = new WsProxyItem; i->ps = ps; wsProxyItemsBySession.insert(i->ps, i); // after this call, ps->logicalClientAddress() will be valid ps->start(sock, cid, route); if(stats) { stats->addConnection(cid, ps->statsRoute(), StatsManager::WebSocket, ps->logicalClientAddress(), sock->requestUri().scheme() == "wss", false); stats->addActivity(ps->statsRoute()); stats->addRequestsReceived(1); } } bool canTake() { // don't accept new sessions during shutdown if(destroying) return false; // don't accept new sessions if we're servicing maximum int curSessions = requestSessions.count() + wsProxyItemsBySession.count(); if(curSessions >= config.sessionsMax) return false; return true; } bool isXForwardedProtocolTls(const HttpHeaders &headers) { QByteArray xfp = headers.get("X-Forwarded-Proto"); if(xfp.isEmpty()) xfp = headers.get("X-Forwarded-Protocol"); return (!xfp.isEmpty() && (xfp == "https" || xfp == "wss")); } void tryTakeRequest() { if(!canTake()) return; // prioritize external requests over internal requests ZhttpRequest *req = zhttpIn->takeNextRequest(); if(!req) { if(intZhttpIn) req = intZhttpIn->takeNextRequest(); if(!req) return; } QString routeId; bool preferInternal = false; bool autoShare = false; QVariant passthroughData = req->passthroughData(); if(passthroughData.isValid()) { // passthrough request, from handler const QVariantHash data = passthroughData.toHash(); // there is always a route routeId = QString::fromUtf8(data["route"].toByteArray()); if(data.contains("prefer-internal")) preferInternal = data["prefer-internal"].toBool(); if(data.contains("auto-share")) autoShare = data["auto-share"].toBool(); } else { // regular request if(config.acceptXForwardedProto && isXForwardedProtocolTls(req->requestHeaders())) req->setIsTls(true); if(config.acceptPushpinRoute) routeId = QString::fromUtf8(req->requestHeaders().get("Pushpin-Route")); } RequestSession *rs = new RequestSession(config.id, domainMap, sockJsManager, inspect, inspectChecker, accept, stats); if(passthroughData.isValid() && !preferInternal) { // passthrough request with preferInternal=false. in this case, // set up a direct route, using some settings from the original // route DomainMap::Entry originalRoute; if(!routeId.isEmpty() && !domainMap->isIdShared(routeId)) originalRoute = domainMap->entry(routeId); const QVariantHash data = passthroughData.toHash(); DomainMap::Entry route; // use sig settings from the original route, if available if(!originalRoute.isNull()) { route.sigIss = originalRoute.sigIss; route.sigKey = originalRoute.sigKey; } DomainMap::Target target; QUrl uri = req->requestUri(); bool isHttps = (uri.scheme() == "https"); target.connectHost = uri.host(); target.connectPort = uri.port(isHttps ? 443 : 80); target.ssl = isHttps; target.trusted = data["trusted"].toBool(); route.targets += target; rs->setRoute(route); } else { // regular request (with or without a route ID), or a passthrough // request with preferInternal=true. in that case, use domainmap // for lookup, with route ID if available rs->setRouteId(routeId); } if(!passthroughData.isValid()) { // these only make sense on regular requests rs->setDebugEnabled(config.debug); rs->setAutoCrossOrigin(config.autoCrossOrigin); rs->setPrefetchSize(config.inspectPrefetch); rs->setDefaultUpstreamKey(config.upstreamKey); rs->setXffRules(config.xffUntrustedRule, config.xffTrustedRule); } rs->setAutoShare(autoShare); // TODO: use callbacks for performance reqSessionConnectionMap[rs] = { rs->inspected.connect(boost::bind(&Private::rs_inspected, this, boost::placeholders::_1, rs)), rs->inspectError.connect(boost::bind(&Private::rs_inspectError, this, rs)), rs->finished.connect(boost::bind(&Private::rs_finished, this, rs)), rs->finishedByAccept.connect(boost::bind(&Private::rs_finishedByAccept, this, rs)) }; requestSessions += rs; rs->start(req); } void tryTakeSocket() { if(!canTake()) return; ZWebSocket *sock = zhttpIn->takeNextSocket(); if(!sock) return; if(config.acceptXForwardedProto && isXForwardedProtocolTls(sock->requestHeaders())) sock->setIsTls(true); QUrl requestUri = sock->requestUri(); log_debug("worker %d: IN ws id=%s, %s", config.id, sock->rid().second.data(), requestUri.toEncoded().data()); bool isSecure = (requestUri.scheme() == "wss"); QString host = requestUri.host(); QByteArray encPath = requestUri.path(QUrl::FullyEncoded).toUtf8(); QString routeId; if(config.acceptPushpinRoute) routeId = QString::fromUtf8(sock->requestHeaders().get("Pushpin-Route")); // look up the route DomainMap::Entry route; if(!routeId.isEmpty() && !domainMap->isIdShared(routeId)) route = domainMap->entry(routeId); else route = domainMap->entry(DomainMap::WebSocket, isSecure, host, encPath); // before we do anything else, see if this is a sockjs request if(!route.isNull() && !route.sockJsPath.isEmpty() && encPath.startsWith(route.sockJsPath)) { sockJsManager->giveSocket(sock, route.sockJsPath.length(), route.sockJsAsPath, route); return; } log_debug("creating wsproxysession for zws id=%s", sock->rid().second.data()); doProxySocket(sock, route); } void tryTakeSockJsSession() { if(!canTake()) return; SockJsSession *sock = sockJsManager->takeNext(); if(!sock) return; log_debug("IN sockjs obj=%p %s", sock, sock->requestUri().toEncoded().data()); log_debug("creating wsproxysession for sockjs=%p", sock); doProxySocket(sock, sock->route()); } void tryTakeNext() { tryTakeRequest(); tryTakeSocket(); tryTakeSockJsSession(); } void logFinished(RequestSession *rs, bool accepted = false) { HttpResponseData resp = rs->responseData(); LogUtil::RequestData rd; DomainMap::Entry route = rs->route(); // only log route id if explicitly set if(route.separateStats) rd.routeId = route.id; if(accepted) { rd.status = LogUtil::Accept; } else if(resp.code != -1) { rd.status = LogUtil::Response; rd.responseData = resp; rd.responseBodySize = rs->responseBodySize(); } else { rd.status = LogUtil::Error; } rd.requestData = rs->requestData(); rd.fromAddress = rs->logicalPeerAddress(); LogUtil::logRequest(LOG_LEVEL_INFO, rd, logConfig); } private: void zhttpIn_requestReady() { tryTakeNext(); } void zhttpIn_socketReady() { tryTakeNext(); } void intZhttpIn_requestReady() { tryTakeNext(); } void sockjs_sessionReady() { tryTakeNext(); } void rs_inspectError(RequestSession *rs) { // default action is to proxy without sharing doProxy(rs); } void rs_inspected(const InspectData &idata, RequestSession *rs) { // if we get here, then the request must be proxied. if it was to be directly // accepted, then finishedByAccept would have been emitted instead assert(idata.doProxy); doProxy(rs, &idata); } void rs_finished(RequestSession *rs) { if(!rs->isSockJs()) logFinished(rs); requestSessions.remove(rs); reqSessionConnectionMap.erase(rs); delete rs; tryTakeNext(); } void rs_finishedByAccept(RequestSession *rs) { logFinished(rs, true); requestSessions.remove(rs); reqSessionConnectionMap.erase(rs); delete rs; tryTakeNext(); } void ps_addNotAllowed(ProxySession *ps) { ProxyItem *i = proxyItemsBySession.value(ps); assert(i); // no more sharing for this session if(i->shared) { i->shared = false; proxyItemsByKey.remove(i->key); } } void ps_finished(ProxySession *ps) { ProxyItem *i = proxyItemsBySession.value(ps); assert(i); proxySessionConnectionMap.erase(ps); if(i->shared) proxyItemsByKey.remove(i->key); proxyItemsBySession.remove(i->ps); delete i; delete ps; tryTakeNext(); } void ps_requestSessionDestroyed(RequestSession *rs, bool accept) { requestSessions.remove(rs); rs->setAccepted(accept); tryTakeNext(); } static void wsps_finishedByPassthrough_cb(void *data, std::tuple value) { Q_UNUSED(value); Private *self = (Private *)data; self->wsps_finishedByPassthrough(std::get<0>(value)); } void wsps_finishedByPassthrough(WsProxySession *ps) { WsProxyItem *i = wsProxyItemsBySession.value(ps); assert(i); if(stats) stats->removeConnection(ps->cid(), false); wsProxyItemsBySession.remove(i->ps); delete i; ps->finishedByPassthroughCallback().remove(this); ps->deleteLater(); tryTakeNext(); } private: void handler_retry_in_readyRead(const QList &message) { QZmq::ReqMessage req(message); if(req.content().count() != 1) { log_warning("retry: received message with parts != 1, skipping"); return; } bool ok; QVariant data = TnetString::toVariant(req.content()[0], 0, &ok); if(!ok) { log_warning("retry: received message with invalid format (tnetstring parse failed), skipping"); return; } if(log_outputLevel() >= LOG_LEVEL_DEBUG) log_debug("retry: IN %s", qPrintable(TnetString::variantToString(data, -1))); RetryRequestPacket p; if(!p.fromVariant(data)) { log_warning("retry: received message with invalid format (parse failed), skipping"); return; } log_debug("IN (retry) %s %s", qPrintable(p.requestData.method), p.requestData.uri.toEncoded().data()); InspectData idata; if(p.haveInspectInfo) { idata.doProxy = p.inspectInfo.doProxy; idata.sharingKey = p.inspectInfo.sharingKey; idata.sid = p.inspectInfo.sid; idata.lastIds = p.inspectInfo.lastIds; idata.userData = p.inspectInfo.userData; } foreach(const RetryRequestPacket::Request &req, p.requests) { ZhttpRequest::ServerState ss; ss.rid = ZhttpRequest::Rid(req.rid.first, req.rid.second); ss.peerAddress = req.peerAddress; ss.requestMethod = p.requestData.method; ss.requestUri = p.requestData.uri; if(req.https) ss.requestUri.setScheme("https"); ss.requestHeaders = p.requestData.headers; ss.requestBody = p.requestData.body; ss.inSeq = req.inSeq; ss.outSeq = req.outSeq; ss.outCredits = req.outCredits; ss.userData = req.userData; ZhttpRequest *zhttpRequest = zhttpIn->createRequestFromState(ss); RequestSession *rs = new RequestSession(config.id, domainMap, sockJsManager, inspect, inspectChecker, accept, stats); requestSessions += rs; rs->setDefaultUpstreamKey(config.upstreamKey); rs->setXffRules(config.xffUntrustedRule, config.xffTrustedRule); if(!p.route.isEmpty()) rs->setRouteId(QString::fromUtf8(p.route)); // note: if the routing table was changed, there's a chance the request // might get a different route id this time around. this could confuse // stats processors tracking route+connection mappings. rs->startRetry(zhttpRequest, req.debug, req.autoCrossOrigin, req.jsonpCallback, req.jsonpExtendedResponse, req.unreportedTime, p.retrySeq); doProxy(rs, p.haveInspectInfo ? &idata : 0); } } void stats_connMax(const StatsPacket &packet) { if(accept->canWriteImmediately()) { ZrpcRequestPacket p; p.method = "conn-max"; p.args["conn-max"] = QVariantList() << packet.toVariant(); accept->write(p); } } void command_requestReady() { ZrpcRequest *req = command->takeNext(); if(req->method() == "conncheck") { if(!stats) { req->respondError("service-unavailable"); delete req; return; } QVariantHash args = req->args(); if(!args.contains("ids") || typeId(args["ids"]) != QMetaType::QVariantList) { req->respondError("bad-format"); delete req; return; } QVariantList vids = args["ids"].toList(); bool ok = true; QList ids; foreach(const QVariant &vid, vids) { if(typeId(vid) != QMetaType::QByteArray) { ok = false; break; } ids += vid.toByteArray(); } if(!ok) { req->respondError("bad-format"); delete req; return; } QVariantList out; foreach(const QByteArray &id, ids) { if(stats->checkConnection(id)) out += id; } req->respond(out); } else if(req->method() == "refresh") { QVariantHash args = req->args(); if(!args.contains("cid") || typeId(args["cid"]) != QMetaType::QByteArray) { req->respondError("bad-format"); delete req; return; } QByteArray cid = args["cid"].toByteArray(); WsProxySession *ps = connectionManager.getProxyForConnection(cid); if(!ps) { req->respondError("item-not-found"); delete req; return; } WebSocketOverHttp *woh = qobject_cast(ps->outSocket()); if(woh) woh->refresh(); req->respond(); } else if(req->method() == "report") { QVariantHash args = req->args(); if(!args.contains("stats") || typeId(args["stats"]) != QMetaType::QVariantHash) { req->respondError("bad-format"); delete req; return; } QVariant data = args["stats"]; StatsPacket p; if(!p.fromVariant("report", data)) { req->respondError("bad-format"); delete req; return; } if(!updater) { req->respondError("service-unavailable"); delete req; return; } int connectionsMax = qMax(p.connectionsMax, 0); int connectionsMinutes = qMax(p.connectionsMinutes, 0); int messagesReceived = qMax(p.messagesReceived, 0); int messagesSent = qMax(p.messagesSent, 0); int httpResponseMessagesSent = qMax(p.httpResponseMessagesSent, 0); Updater::Report report; report.connectionsMax = connectionsMax; report.connectionsMinutes = connectionsMinutes; report.messagesReceived = messagesReceived; report.messagesSent = messagesSent; // fanout cloud style ops calculation report.ops = connectionsMinutes + messagesReceived + messagesSent - httpResponseMessagesSent; updater->setReport(report); req->respond(); } else { req->respondError("method-not-found"); } delete req; } }; Engine::Engine(DomainMap *domainMap, QObject *parent) : QObject(parent) { d = new Private(this, domainMap); } Engine::~Engine() { delete d; } StatsManager *Engine::statsManager() const { return d->stats; } bool Engine::start(const Configuration &config) { return d->start(config); } void Engine::routesChanged() { d->routesChanged(); } #include "engine.moc" pushpin-1.39.1/src/cpp/proxy/engine.h000066400000000000000000000060471457610542000174560ustar00rootroot00000000000000/* * Copyright (C) 2012-2023 Fanout, Inc. * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef ENGINE_H #define ENGINE_H #include #include #include #include "jwt.h" #include "xffrule.h" #include #include using std::map; using Connection = boost::signals2::scoped_connection; class StatsManager; class DomainMap; class Engine : public QObject { Q_OBJECT public: class Configuration { public: int id; QString appVersion; QByteArray clientId; QStringList serverInSpecs; QStringList serverInStreamSpecs; QStringList serverOutSpecs; QStringList clientOutSpecs; QStringList clientOutStreamSpecs; QStringList clientInSpecs; QString inspectSpec; QString acceptSpec; QString retryInSpec; QStringList wsControlInitSpecs; QStringList wsControlStreamSpecs; QString statsSpec; QString commandSpec; QStringList intServerInSpecs; QStringList intServerInStreamSpecs; QStringList intServerOutSpecs; int ipcFileMode; int sessionsMax; int inspectTimeout; int inspectPrefetch; bool debug; bool autoCrossOrigin; bool acceptXForwardedProto; bool setXForwardedProto; bool setXForwardedProtocol; XffRule xffUntrustedRule; XffRule xffTrustedRule; QList origHeadersNeedMark; bool acceptPushpinRoute; QByteArray cdnLoop; bool logFrom; bool logUserAgent; QByteArray sigIss; Jwt::EncodingKey sigKey; Jwt::DecodingKey upstreamKey; QString sockJsUrl; QString updatesCheck; QString organizationName; bool quietCheck; bool statsConnectionSend; int statsConnectionTtl; int statsConnectionsMaxTtl; int statsReportInterval; QString prometheusPort; QString prometheusPrefix; Configuration() : id(0), ipcFileMode(-1), sessionsMax(-1), inspectTimeout(8000), inspectPrefetch(10000), debug(false), autoCrossOrigin(false), acceptXForwardedProto(false), setXForwardedProto(false), setXForwardedProtocol(false), acceptPushpinRoute(false), logFrom(false), logUserAgent(false), updatesCheck("check"), quietCheck(false), statsConnectionSend(false), statsConnectionTtl(-1), statsConnectionsMaxTtl(-1), statsReportInterval(-1) { } }; Engine(DomainMap *domainMap, QObject *parent = 0); ~Engine(); StatsManager *statsManager() const; bool start(const Configuration &config); void routesChanged(); private: class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/proxy/inspectrequest.cpp000066400000000000000000000066101457610542000216160ustar00rootroot00000000000000/* * Copyright (C) 2012-2015 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "inspectrequest.h" #include "packet/httprequestdata.h" #include "qtcompat.h" #include "inspectdata.h" static InspectData resultToData(const QVariant &in, bool *ok) { InspectData out; if(typeId(in) != QMetaType::QVariantHash) { *ok = false; return InspectData(); } QVariantHash obj = in.toHash(); if(!obj.contains("no-proxy") || typeId(obj["no-proxy"]) != QMetaType::Bool) { *ok = false; return InspectData(); } out.doProxy = !obj["no-proxy"].toBool(); out.sharingKey.clear(); if(obj.contains("sharing-key")) { if(typeId(obj["sharing-key"]) != QMetaType::QByteArray) { *ok = false; return InspectData(); } out.sharingKey = obj["sharing-key"].toByteArray(); } out.sid.clear(); if(obj.contains("sid")) { if(typeId(obj["sid"]) != QMetaType::QByteArray) { *ok = false; return InspectData(); } out.sid = obj["sid"].toByteArray(); } out.lastIds.clear(); if(obj.contains("last-ids")) { if(typeId(obj["last-ids"]) != QMetaType::QVariantHash) { *ok = false; return InspectData(); } QVariantHash vlastIds = obj["last-ids"].toHash(); QHashIterator it(vlastIds); while(it.hasNext()) { it.next(); if(typeId(it.value()) != QMetaType::QByteArray) { *ok = false; return InspectData(); } QByteArray key = it.key().toUtf8(); QByteArray val = it.value().toByteArray(); out.lastIds.insert(key, val); } } out.userData = obj["user-data"]; *ok = true; return out; } class InspectRequest::Private : public QObject { Q_OBJECT public: InspectRequest *q; InspectData idata; Private(InspectRequest *_q) : QObject(_q), q(_q) { } }; InspectRequest::InspectRequest(ZrpcManager *manager, QObject *parent) : ZrpcRequest(manager, parent) { d = new Private(this); } InspectRequest::~InspectRequest() { delete d; } InspectData InspectRequest::result() const { return d->idata; } void InspectRequest::start(const HttpRequestData &hdata, bool truncated, bool getSession, bool autoShare) { QVariantHash args; args["method"] = hdata.method.toLatin1(); args["uri"] = hdata.uri.toEncoded(); QVariantList vheaders; foreach(const HttpHeader &h, hdata.headers) { QVariantList vheader; vheader += h.first; vheader += h.second; vheaders += QVariant(vheader); } args["headers"] = vheaders; args["body"] = hdata.body; if(truncated) args["truncated"] = true; if(getSession) args["get-session"] = true; if(autoShare) args["auto-share"] = true; ZrpcRequest::start("inspect", args); } void InspectRequest::onSuccess() { bool ok; d->idata = resultToData(ZrpcRequest::result(), &ok); if(!ok) { setError(ErrorFormat); return; } } #include "inspectrequest.moc" pushpin-1.39.1/src/cpp/proxy/inspectrequest.h000066400000000000000000000023031457610542000212560ustar00rootroot00000000000000/* * Copyright (C) 2012-2015 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef INSPECTREQUEST_H #define INSPECTREQUEST_H #include #include "zrpcrequest.h" class HttpRequestData; class InspectData; class ZrpcManager; class InspectRequest : public ZrpcRequest { Q_OBJECT public: InspectRequest(ZrpcManager *manager, QObject *parent = 0); ~InspectRequest(); InspectData result() const; void start(const HttpRequestData &hdata, bool truncated, bool getSession, bool autoShare); protected: virtual void onSuccess(); private: class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/proxy/main.cpp000066400000000000000000000023111457610542000174560ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include #include #include "app.h" class AppMain { public: App *app; void start() { app = new App; app->quit.connect(boost::bind(&AppMain::app_quit, this, boost::placeholders::_1)); app->start(); } void app_quit(int returnCode) { delete app; QCoreApplication::exit(returnCode); } }; extern "C" { int proxy_main(int argc, char **argv) { QCoreApplication qapp(argc, argv); AppMain appMain; QTimer::singleShot(0, [&appMain]() {appMain.start();}); return qapp.exec(); } } pushpin-1.39.1/src/cpp/proxy/main.h000066400000000000000000000001321457610542000171220ustar00rootroot00000000000000#ifndef PROXY_MAIN_H #define PROXY_MAIN_H int proxy_main(int argc, char **argv); #endif pushpin-1.39.1/src/cpp/proxy/proxysession.cpp000066400000000000000000001130761457610542000213320ustar00rootroot00000000000000/* * Copyright (C) 2012-2023 Fanout, Inc. * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "proxysession.h" #include #include #include #include #include #include "packet/statspacket.h" #include "packet/httprequestdata.h" #include "packet/httpresponsedata.h" #include "qtcompat.h" #include "bufferlist.h" #include "log.h" #include "jwt.h" #include "inspectdata.h" #include "acceptdata.h" #include "zhttpmanager.h" #include "zhttprequest.h" #include "zroutes.h" #include "statusreasons.h" #include "xffrule.h" #include "requestsession.h" #include "proxyutil.h" #include "statsmanager.h" #include "acceptrequest.h" #include "testhttprequest.h" using std::map; #define MAX_ACCEPT_REQUEST_BODY 100000 // NOTE: if this value is ever changed, fix enginetest to match #define MAX_ACCEPT_RESPONSE_BODY 100000 #define MAX_INITIAL_BUFFER 100000 #define MAX_STREAM_BUFFER 100000 class ProxySession::Private : public QObject { Q_OBJECT public: enum State { Stopped, Requesting, Accepting, Responding, Responded }; class SessionItem { public: enum State { WaitingForResponse, Responding, Responded, Errored, Pausing, Paused }; RequestSession *rs; State state; bool startedResponse; bool unclean; int bytesToWrite; bool countClientReceivedBytes; bool countClientSentBytes; SessionItem() : rs(0), state(WaitingForResponse), startedResponse(false), unclean(false), bytesToWrite(0), countClientReceivedBytes(true), countClientSentBytes(true) { } }; struct RequestSessionConnections { Connection bytesWrittenConnection; Connection errorRespondingConnection; Connection pausedConnection; Connection finishedConnection; Connection headerBytesSentConnection; Connection bodyBytesSentConnection; }; struct ZhttpReqConnections { Connection readyReadConnection; Connection writeBytesChangedConnection; Connection errorConnection; }; ProxySession *q; State state; ZRoutes *zroutes; ZhttpManager *zhttpManager; RequestSession *inRequest; ZrpcManager *acceptManager; bool isHttps; DomainMap::Entry route; QList targets; DomainMap::Target target; HttpRequest *zhttpRequest; bool addAllowed; bool haveInspectData; InspectData idata; QSet acceptHeaderPrefixes; QSet acceptContentTypes; QSet sessionItems; bool shared; HttpRequestData requestData; HttpRequestData origRequestData; HttpResponseData responseData; HttpResponseData acceptResponseData; BufferList requestBody; BufferList responseBody; QHash sessionItemsBySession; QByteArray initialRequestBody; bool requestBodySent; int total; bool buffering; QByteArray defaultSigIss; Jwt::EncodingKey defaultSigKey; bool trustedClient; bool intReq; bool passthrough; bool acceptXForwardedProtocol; bool useXForwardedProto; bool useXForwardedProtocol; XffRule xffRule; XffRule xffTrustedRule; QList origHeadersNeedMark; bool acceptPushpinRoute; QByteArray cdnLoop; bool proxyInitialResponse; bool acceptAfterResponding; AcceptRequest *acceptRequest; LogUtil::Config logConfig; StatsManager *statsManager; Connection inReqReadyReadConnection; Connection inReqErrorConnection; ZhttpReqConnections zhttpReqConnections; Connection finishedConnection; map reqSessionConnectionMap; Private(ProxySession *_q, ZRoutes *_zroutes, ZrpcManager *_acceptManager, const LogUtil::Config &_logConfig, StatsManager *_statsManager) : QObject(_q), q(_q), state(Stopped), zroutes(_zroutes), zhttpManager(0), inRequest(0), acceptManager(_acceptManager), isHttps(false), zhttpRequest(0), addAllowed(true), haveInspectData(false), shared(false), requestBodySent(false), total(0), trustedClient(false), intReq(false), passthrough(false), acceptXForwardedProtocol(false), useXForwardedProto(false), useXForwardedProtocol(false), acceptPushpinRoute(false), proxyInitialResponse(false), acceptAfterResponding(false), acceptRequest(0), logConfig(_logConfig), statsManager(_statsManager) { acceptHeaderPrefixes += "Grip-"; acceptContentTypes += "application/grip-instruct"; } ~Private() { cleanup(); } void cleanup() { foreach(SessionItem *si, sessionItems) { // emitting a signal here is gross, but this way the engine cleans up the request sessions q->requestSessionDestroyed(si->rs, false); delete si->rs; delete si; } sessionItems.clear(); sessionItemsBySession.clear(); if(zhttpManager) { zroutes->removeRef(zhttpManager); zhttpManager = 0; } } void add(RequestSession *rs) { assert(addAllowed); assert(!route.isNull()); SessionItem *si = new SessionItem; si->rs = rs; si->rs->setParent(this); // a retried request already had its received bytes counted earlier if(rs->isRetry()) si->countClientReceivedBytes = false; // internal requests originate internally and should not have client bytes counted if(rs->request()->passthroughData().isValid()) { si->countClientReceivedBytes = false; si->countClientSentBytes = false; } if(!sessionItems.isEmpty()) shared = true; sessionItems += si; sessionItemsBySession.insert(rs, si); reqSessionConnectionMap[rs] = { rs->bytesWritten.connect(boost::bind(&Private::rs_bytesWritten, this, boost::placeholders::_1, rs)), rs->errorResponding.connect(boost::bind(&Private::rs_errorResponding, this, rs)), rs->paused.connect(boost::bind(&Private::rs_paused, this, rs)), rs->finished.connect(boost::bind(&Private::rs_finished, this, rs)), rs->headerBytesSent.connect(boost::bind(&Private::rs_headerBytesSent, this, boost::placeholders::_1, rs)), rs->bodyBytesSent.connect(boost::bind(&Private::rs_bodyBytesSent, this, boost::placeholders::_1, rs)) }; HttpRequestData rsRequestData = rs->requestData(); if(si->countClientReceivedBytes) { incCounter(Stats::ClientHeaderBytesReceived, ZhttpManager::estimateRequestHeaderBytes(rsRequestData.method, rsRequestData.uri, rsRequestData.headers)); incCounter(Stats::ClientContentBytesReceived, rsRequestData.body.size()); } if(state == Stopped) { isHttps = rs->isHttps(); requestData = rsRequestData; requestBody += requestData.body; requestData.body.clear(); origRequestData = requestData; if(!route.asHost.isEmpty()) ProxyUtil::applyHost(&requestData.uri, route.asHost); QByteArray path = requestData.uri.path(QUrl::FullyEncoded).toUtf8(); if(route.pathRemove > 0) path = path.mid(route.pathRemove); if(!route.pathPrepend.isEmpty()) path = route.pathPrepend + path; requestData.uri.setPath(QString::fromUtf8(path), QUrl::StrictMode); QByteArray sigIss = defaultSigIss; Jwt::EncodingKey sigKey = defaultSigKey; if(!route.sigIss.isEmpty()) sigIss = route.sigIss; if(!route.sigKey.isNull()) sigKey = route.sigKey; targets = route.targets; foreach(const HttpHeader &h, route.headers) { requestData.headers.removeAll(h.first); if(!h.second.isEmpty()) requestData.headers += HttpHeader(h.first, h.second); } if(!rs->isRetry()) { inRequest = rs; ZhttpRequest *req = inRequest->request(); inReqReadyReadConnection = req->readyRead.connect(boost::bind(&Private::inRequest_readyRead, this)); inReqErrorConnection = req->error.connect(boost::bind(&Private::inRequest_error, this)); requestBody += req->readBody(); intReq = req->passthroughData().isValid(); } trustedClient = rs->trusted(); QHostAddress clientAddress = rs->request()->peerAddress(); ProxyUtil::manipulateRequestHeaders("proxysession", q, &requestData, trustedClient, route, sigIss, sigKey, acceptXForwardedProtocol, useXForwardedProto, useXForwardedProtocol, xffTrustedRule, xffRule, origHeadersNeedMark, acceptPushpinRoute, cdnLoop, clientAddress, idata, route.grip, intReq); state = Requesting; buffering = true; if(trustedClient || !route.grip || intReq) passthrough = true; initialRequestBody = requestBody.toByteArray(); if(requestBody.size() > MAX_ACCEPT_REQUEST_BODY) { requestBody.clear(); buffering = false; } tryNextTarget(); } else if(state == Requesting) { // nothing to do, just wait around until a response comes } else if(state == Responding) { // get the session caught up with where we're at si->state = SessionItem::Responding; si->startedResponse = true; rs->startResponse(responseData.code, responseData.reason, responseData.headers); if(!responseBody.isEmpty()) { si->bytesToWrite += responseBody.size(); rs->writeResponseBody(responseBody.toByteArray()); } } } bool pendingWrites() { foreach(SessionItem *si, sessionItems) { if(si->bytesToWrite != -1 && si->bytesToWrite > 0) return true; } return false; } void tryNextTarget() { if(targets.isEmpty()) { QString msg = "Error while proxying to origin."; QStringList targetStrs; foreach(const DomainMap::Target &t, route.targets) targetStrs += ProxyUtil::targetToString(t); QString dmsg = QString("Unable to connect to any targets. Tried: %1").arg(targetStrs.join(", ")); rejectAll(502, "Bad Gateway", msg, dmsg); return; } target = targets.takeFirst(); if(target.overHttp) { // don't forward WOH requests from client unless trusted QByteArray contentType = requestData.headers.get("Content-Type"); int at = contentType.indexOf(';'); if(at != -1) contentType.truncate(at); if(contentType == "application/websocket-events" && !trustedClient) { rejectAll(403, "Forbidden", "Client not allowed to send WebSocket events directly."); return; } } QUrl uri = requestData.uri; if(target.ssl) uri.setScheme("https"); else uri.setScheme("http"); if(!target.host.isEmpty()) ProxyUtil::applyHost(&uri, target.host); if(zhttpManager) { zroutes->removeRef(zhttpManager); zhttpManager = 0; } if(target.type == DomainMap::Target::Test) { // for test route, auto-adjust path if(!route.pathBeg.isEmpty()) { int pathRemove = route.pathBeg.length(); if(route.pathBeg.endsWith('/')) --pathRemove; if(pathRemove > 0) uri.setPath(uri.path(QUrl::FullyEncoded).mid(pathRemove)); } zhttpRequest = new TestHttpRequest(this); } else { if(target.type == DomainMap::Target::Custom) { zhttpManager = zroutes->managerForRoute(target.zhttpRoute); log_debug("proxysession: %p forwarding to %s", q, qPrintable(target.zhttpRoute.baseSpec)); } else // Default { zhttpManager = zroutes->defaultManager(); log_debug("proxysession: %p forwarding to %s:%d", q, qPrintable(target.connectHost), target.connectPort); } zroutes->addRef(zhttpManager); zhttpRequest = zhttpManager->createRequest(); zhttpRequest->setParent(this); } zhttpReqConnections = { zhttpRequest->readyRead.connect(boost::bind(&Private::zhttpRequest_readyRead, this)), zhttpRequest->writeBytesChanged.connect(boost::bind(&Private::zhttpRequest_writeBytesChanged, this)), zhttpRequest->error.connect(boost::bind(&Private::zhttpRequest_error, this)) }; if(target.trusted) zhttpRequest->setIgnorePolicies(true); if(target.trustConnectHost) zhttpRequest->setTrustConnectHost(true); if(target.insecure) zhttpRequest->setIgnoreTlsErrors(true); if(target.type == DomainMap::Target::Default) { zhttpRequest->setConnectHost(target.connectHost); zhttpRequest->setConnectPort(target.connectPort); } ProxyUtil::applyHostHeader(&requestData.headers, uri); incCounter(Stats::ServerHeaderBytesSent, ZhttpManager::estimateRequestHeaderBytes(requestData.method, uri, requestData.headers)); zhttpRequest->start(requestData.method, uri, requestData.headers); requestBodySent = false; if(!initialRequestBody.isEmpty()) { incCounter(Stats::ServerContentBytesSent, initialRequestBody.size()); zhttpRequest->writeBody(initialRequestBody); } if(!inRequest || (inRequest->request()->isInputFinished() && inRequest->request()->bytesAvailable() == 0)) { // no need to track the primary request anymore if(inRequest) { inReqReadyReadConnection.disconnect(); inReqErrorConnection.disconnect(); inRequest = 0; } requestBodySent = true; zhttpRequest->endBody(); } } void tryRequestRead() { // if the state changed before input finished, then // stop reading input if(state != Requesting) return; int maxBytes = buffering ? MAX_STREAM_BUFFER : zhttpRequest->writeBytesAvailable(); // if we're not buffering, then sync to speed of server if(maxBytes == 0) return; QByteArray buf = inRequest->request()->readBody(maxBytes); if(!buf.isEmpty()) { log_debug("proxysession: %p input chunk: %d", q, buf.size()); SessionItem *si = sessionItemsBySession.value(inRequest); assert(si); if(si->countClientReceivedBytes) incCounter(Stats::ClientContentBytesReceived, buf.size()); if(buffering) { if(requestBody.size() + buf.size() > MAX_ACCEPT_REQUEST_BODY) { requestBody.clear(); buffering = false; } else requestBody += buf; } incCounter(Stats::ServerContentBytesSent, buf.size()); zhttpRequest->writeBody(buf); } if(!requestBodySent && inRequest->request()->isInputFinished() && inRequest->request()->bytesAvailable() == 0) { // no need to track the primary request anymore inReqReadyReadConnection.disconnect(); inReqErrorConnection.disconnect(); inRequest = 0; requestBodySent = true; zhttpRequest->endBody(); } } void cannotAcceptAll() { assert(state != Responding); assert(state != Responded); state = Responded; foreach(SessionItem *si, sessionItems) { if(si->state != SessionItem::Errored) { if(si->state == SessionItem::Paused) { if(si->startedResponse) si->state = SessionItem::Responding; else si->state = SessionItem::WaitingForResponse; si->rs->resume(); } assert(si->state == SessionItem::WaitingForResponse || si->state == SessionItem::Responding); if(si->state == SessionItem::WaitingForResponse) { si->state = SessionItem::Responded; si->bytesToWrite = -1; si->rs->respondCannotAccept(); } else { // if we already started responding, then only provide an // error message in debug mode if(si->rs->debugEnabled()) { // if debug enabled, append the message at the end. // this may ruin the content, but hey it's debug // mode QByteArray buf = "\n\nAccept service unavailable\n"; si->bytesToWrite += buf.size(); si->rs->writeResponseBody(buf); si->rs->endResponseBody(); } else { // if debug not enabled, then the best we can do is // disconnect si->state = SessionItem::Responded; si->unclean = true; si->bytesToWrite = -1; si->rs->endResponseBody(); } } } } } void rejectAll(int code, const QString &reason, const QString &errorMessage, const QString &debugErrorMessage) { zhttpReqConnections = ZhttpReqConnections(); // kill the active target request, if any delete zhttpRequest; zhttpRequest = 0; assert(state != Responding); assert(state != Responded); state = Responded; foreach(SessionItem *si, sessionItems) { if(si->state != SessionItem::Errored) { if(si->state == SessionItem::Paused) { if(si->startedResponse) si->state = SessionItem::Responding; else si->state = SessionItem::WaitingForResponse; si->rs->resume(); } assert(si->state == SessionItem::WaitingForResponse || si->state == SessionItem::Responding); if(si->state == SessionItem::WaitingForResponse) { si->state = SessionItem::Responded; si->bytesToWrite = -1; si->rs->respondError(code, reason, si->rs->debugEnabled() ? debugErrorMessage : errorMessage); } else // Responding { // if we already started responding, then only provide a // rejection message in debug mode if(si->rs->debugEnabled()) { // if debug enabled, append the message at the end. // this may ruin the content, but hey it's debug // mode QByteArray buf = "\n\n" + debugErrorMessage.toUtf8() + '\n'; si->bytesToWrite += buf.size(); si->rs->writeResponseBody(buf); si->rs->endResponseBody(); } else { // if debug not enabled, then the best we can do is // disconnect si->state = SessionItem::Responded; si->unclean = true; si->bytesToWrite = -1; si->rs->endResponseBody(); } } } } } void rejectAll(int code, const QString &reason, const QString &errorMessage) { rejectAll(code, reason, errorMessage, errorMessage); } void respondAll(int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body) { assert(state != Responding); assert(state != Responded); state = Responded; foreach(SessionItem *si, sessionItems) { if(si->state != SessionItem::Errored) { assert(si->state == SessionItem::WaitingForResponse); si->state = SessionItem::Responded; si->bytesToWrite = -1; si->rs->respond(code, reason, headers, body); } } } void destroyAll() { assert(state == Accepting || state == Responding); state = Responded; foreach(SessionItem *si, sessionItems) { if(si->state == SessionItem::Paused) { si->state = SessionItem::Responding; si->rs->resume(); } if(si->state == SessionItem::WaitingForResponse || si->state == SessionItem::Responding) { si->state = SessionItem::Responded; si->unclean = true; si->bytesToWrite = -1; si->rs->endResponseBody(); } } } // this method emits signals void tryResponseRead() { // if we're not buffering, then don't read (instead, sync to slowest // receiver before reading again) if(!buffering && pendingWrites()) return; QPointer self = this; bool wasAllowed = addAllowed; if(state == Accepting) { if(responseBody.size() + zhttpRequest->bytesAvailable() > MAX_ACCEPT_RESPONSE_BODY) { QByteArray gripHold = responseData.headers.get("Grip-Hold"); QByteArray gripNextLinkParam; foreach(const HttpHeaderParameters ¶ms, responseData.headers.getAllAsParameters("Grip-Link")) { if(params.count() >= 2 && params.get("rel") == "next") gripNextLinkParam = params[0].first; } bool usingBuildIdFilter = false; foreach(const HttpHeaderParameters ¶ms, responseData.headers.getAllAsParameters("Grip-Channel")) { if(params.count() >= 2) { bool found = false; for(int n = 1; n < params.count(); ++n) { if(params[n].first == "filter" && params[n].second == "build-id") { found = true; break; } } if(found) { usingBuildIdFilter = true; break; } } } if(proxyInitialResponse && (gripHold == "stream" || (gripHold.isEmpty() && !gripNextLinkParam.isEmpty())) && !usingBuildIdFilter) { // sending the initial response from the proxy means // we need to do some of the handler's job here // NOTE: if we ever need to do more than what's // below, we should consider querying the handler // to perform these things while still letting // the proxy send the response body // no content length responseData.headers.removeAll("Content-Length"); // interpret grip-status QByteArray statusHeader = responseData.headers.get("Grip-Status"); if(!statusHeader.isEmpty()) { QByteArray codeStr; QByteArray reason; int at = statusHeader.indexOf(' '); if(at != -1) { codeStr = statusHeader.mid(0, at); reason = statusHeader.mid(at + 1); } else { codeStr = statusHeader; } bool _ok; responseData.code = codeStr.toInt(&_ok); if(!_ok || responseData.code < 0 || responseData.code > 999) { // this may output a misleading error message cannotAcceptAll(); return; } if(reason.isEmpty()) reason = StatusReasons::getReason(responseData.code); responseData.reason = reason; } // strip any grip headers for(int n = 0; n < responseData.headers.count(); ++n) { const HttpHeader &h = responseData.headers[n]; bool prefixed = false; foreach(const QByteArray &hp, acceptHeaderPrefixes) { if(qstrnicmp(h.first.data(), hp.data(), hp.length()) == 0) { prefixed = true; break; } } if(prefixed) { responseData.headers.removeAt(n); --n; // adjust position } } // we'll let the proxy send normally, then accept afterwards acceptAfterResponding = true; } else { QString msg = "Error while proxying to origin."; QString dmsg = QString("GRIP instruct response too large from %1").arg(ProxyUtil::targetToString(target)); rejectAll(502, "Bad Gateway", msg, dmsg); return; } } } else if(state == Responding) { if(buffering && responseBody.size() + zhttpRequest->bytesAvailable() > MAX_INITIAL_BUFFER) { responseBody.clear(); buffering = false; addAllowed = false; } } QByteArray buf; int maxBytes = (buffering ? MAX_INITIAL_BUFFER - responseBody.size() : MAX_STREAM_BUFFER); if(maxBytes > 0) buf = zhttpRequest->readBody(maxBytes); if(!buf.isEmpty()) { incCounter(Stats::ServerContentBytesReceived, buf.size()); total += buf.size(); log_debug("proxysession: %p recv=%d, total=%d, avail=%d", q, buf.size(), total, zhttpRequest->bytesAvailable()); if(buffering) responseBody += buf; } if(state == Accepting) { if(acceptAfterResponding) startResponse(); } else if(state == Responding) { log_debug("proxysession: %p writing %d to clients", q, buf.size()); foreach(SessionItem *si, sessionItems) { assert(si->state != SessionItem::WaitingForResponse); if(si->state == SessionItem::Responding) { si->bytesToWrite += buf.size(); si->rs->writeResponseBody(buf); } } if(wasAllowed && !addAllowed) { q->addNotAllowed(); if(!self) return; } } checkIncomingResponseFinished(); } // this method emits signals void checkIncomingResponseFinished() { QPointer self = this; if(zhttpRequest->isFinished() && zhttpRequest->bytesAvailable() == 0) { log_debug("proxysession: %p response from target finished", q); if(!buffering && pendingWrites()) { log_debug("proxysession: %p still stuff left to write, though. we'll wait.", q); return; } zhttpReqConnections = ZhttpReqConnections(); delete zhttpRequest; zhttpRequest = 0; // once the entire response has been received, cut off any new adds if(addAllowed) { addAllowed = false; q->addNotAllowed(); if(!self) return; } if(state == Accepting || (state == Responding && acceptAfterResponding)) { state = Accepting; if(acceptManager) { log_debug("we have an acceptmanager"); foreach(SessionItem *si, sessionItems) { si->state = SessionItem::Pausing; si->rs->pause(); } } else { cannotAcceptAll(); } } else if(state == Responding) { foreach(SessionItem *si, sessionItems) { assert(si->state != SessionItem::WaitingForResponse); if(si->state == SessionItem::Responding) { si->state = SessionItem::Responded; si->rs->endResponseBody(); } } } } } void startResponse() { state = Responding; // don't relay these headers. their meaning is handled by // zurl and they only apply to the outgoing hop. responseData.headers.removeAll("Connection"); responseData.headers.removeAll("Keep-Alive"); responseData.headers.removeAll("Content-Encoding"); responseData.headers.removeAll("Transfer-Encoding"); foreach(SessionItem *si, sessionItems) { si->state = SessionItem::Responding; si->startedResponse = true; si->rs->startResponse(responseData.code, responseData.reason, responseData.headers); if(!responseBody.isEmpty()) { si->bytesToWrite += responseBody.size(); si->rs->writeResponseBody(responseBody.toByteArray()); } } } void logFinished(SessionItem *si, bool accepted = false) { RequestSession *rs = si->rs; HttpResponseData resp = rs->responseData(); LogUtil::RequestData rd; // only log route id if explicitly set if(route.separateStats) rd.routeId = route.id; if(accepted) { rd.status = LogUtil::Accept; } else if(resp.code != -1 && !si->unclean) { rd.status = LogUtil::Response; rd.responseData = resp; rd.responseBodySize = rs->responseBodySize(); } else { rd.status = LogUtil::Error; } rd.requestData = rs->requestData(); rd.targetStr = ProxyUtil::targetToString(target); rd.targetOverHttp = target.overHttp; rd.retry = rs->isRetry(); if(shared) rd.sharedBy = this; rd.fromAddress = rs->peerAddress(); LogUtil::logRequest(LOG_LEVEL_INFO, rd, logConfig); } void incCounter(Stats::Counter c, int count = 1) { if(statsManager) statsManager->incCounter(route.statsRoute(), c, count); } public: void inRequest_readyRead() { tryRequestRead(); } void inRequest_error() { log_warning("proxysession: %p error reading request", q); // don't take action here. do that in rs_finished } void zhttpRequest_readyRead() { log_debug("proxysession: %p data from target", q); if(state == Requesting) { responseData.code = zhttpRequest->responseCode(); responseData.reason = zhttpRequest->responseReason(); responseData.headers = zhttpRequest->responseHeaders(); QByteArray buf = zhttpRequest->readBody(MAX_INITIAL_BUFFER); incCounter(Stats::ServerHeaderBytesReceived, ZhttpManager::estimateResponseHeaderBytes(responseData.code, responseData.reason, responseData.headers)); incCounter(Stats::ServerContentBytesReceived, buf.size()); responseBody += buf; total += buf.size(); acceptResponseData = responseData; log_debug("proxysession: %p recv total: %d", q, total); bool doAccept = false; if(!passthrough) { QByteArray contentType = responseData.headers.get("Content-Type"); int at = contentType.indexOf(';'); if(at != -1) contentType = contentType.mid(0, at); if(acceptContentTypes.contains(contentType)) { doAccept = true; } else { foreach(const HttpHeader &h, responseData.headers) { foreach(const QByteArray &hp, acceptHeaderPrefixes) { if(qstrnicmp(h.first.data(), hp.data(), hp.length()) == 0) { doAccept = true; break; } } if(doAccept) break; } } } if(doAccept) { if(!buffering) { rejectAll(400, "Bad Request", "Request too large to accept GRIP instruct."); return; } state = Accepting; } else { startResponse(); } } assert(state == Accepting || state == Responding); tryResponseRead(); } void zhttpRequest_writeBytesChanged() { if(inRequest) tryRequestRead(); } void zhttpRequest_error() { ZhttpRequest::ErrorCondition e = zhttpRequest->errorCondition(); log_debug("proxysession: %p target error state=%d, condition=%d", q, (int)state, (int)e); if(state == Requesting || state == Accepting) { bool tryAgain = false; switch(e) { case ZhttpRequest::ErrorLengthRequired: rejectAll(411, "Length Required", "Must provide Content-Length header."); break; case ZhttpRequest::ErrorPolicy: rejectAll(502, "Bad Gateway", "Error while proxying to origin.", "Error: Origin host/IP blocked."); break; case ZhttpRequest::ErrorConnect: case ZhttpRequest::ErrorConnectTimeout: case ZhttpRequest::ErrorTls: // it should not be possible to get one of these errors while accepting assert(state == Requesting); tryAgain = true; break; case ZhttpRequest::ErrorTimeout: rejectAll(502, "Bad Gateway", "Error while proxying to origin.", "Error: zhttp service for route is unreachable."); break; default: rejectAll(502, "Bad Gateway", "Error while proxying to origin."); break; } if(tryAgain) tryNextTarget(); } else if(state == Responding) { // if we're already responding, then we can't reply with an error destroyAll(); } } void rs_bytesWritten(int count, RequestSession *rs) { log_debug("proxysession: %p response bytes written id=%s: %d", q, rs->rid().second.data(), count); SessionItem *si = sessionItemsBySession.value(rs); assert(si); if(si->bytesToWrite != -1) { si->bytesToWrite -= count; assert(si->bytesToWrite >= 0); } if(zhttpRequest) tryResponseRead(); } void rs_finished(RequestSession *rs) { log_debug("proxysession: %p response finished id=%s", q, rs->rid().second.data()); SessionItem *si = sessionItemsBySession.value(rs); assert(si); if(!intReq) logFinished(si); QPointer self = this; q->requestSessionDestroyed(si->rs, false); if(!self) return; ZhttpRequest *req = rs->request(); bool wasInputRequest = (req && inRequest && req == inRequest->request()); sessionItemsBySession.remove(rs); sessionItems.remove(si); reqSessionConnectionMap.erase(rs); delete rs; delete si; if(sessionItems.isEmpty()) { log_debug("proxysession: %p finished by passthrough", q); q->finished(); } else if(wasInputRequest) { // this should never happen. for there to be more than // one SessionItem, inRequest must be 0. assert(0); rejectAll(500, "Internal Server Error", "Input request failed."); } } void rs_paused(RequestSession *rs) { log_debug("proxysession: %p response paused id=%s", q, rs->rid().second.data()); SessionItem *si = sessionItemsBySession.value(rs); assert(si); assert(si->state == SessionItem::Pausing); si->state = SessionItem::Paused; bool allPaused = true; foreach(SessionItem *si, sessionItems) { if(si->state != SessionItem::Paused) { allPaused = false; break; } } if(allPaused) { assert(!acceptRequest); QByteArray sigIss = defaultSigIss; Jwt::EncodingKey sigKey = defaultSigKey; if(!route.sigIss.isEmpty()) sigIss = route.sigIss; if(!route.sigKey.isNull()) sigKey = route.sigKey; acceptResponseData.body = responseBody.take(); AcceptData adata; foreach(SessionItem *si, sessionItems) { int unreportedTime = -1; if(!statsManager->connectionSendEnabled()) unreportedTime = si->rs->unregisterConnection(); ZhttpRequest::ServerState ss = si->rs->request()->serverState(); AcceptData::Request areq; areq.rid = si->rs->rid(); areq.https = si->rs->isHttps(); areq.peerAddress = si->rs->peerAddress(); areq.logicalPeerAddress = si->rs->logicalPeerAddress(); areq.debug = si->rs->debugEnabled(); areq.isRetry = si->rs->isRetry(); areq.autoCrossOrigin = si->rs->autoCrossOrigin(); areq.jsonpCallback = si->rs->jsonpCallback(); areq.jsonpExtendedResponse = si->rs->jsonpExtendedResponse(); areq.unreportedTime = unreportedTime; areq.responseCode = ss.responseCode; areq.inSeq = ss.inSeq; areq.outSeq = ss.outSeq; areq.outCredits = ss.outCredits; areq.userData = ss.userData; adata.requests += areq; } adata.requestData = requestData; adata.requestData.body = requestBody.take(); adata.origRequestData = origRequestData; adata.origRequestData.body = adata.requestData.body; adata.haveResponse = true; adata.response = acceptResponseData; if(haveInspectData) { adata.haveInspectData = true; adata.inspectData = idata; } adata.route = route.id; adata.separateStats = route.separateStats; adata.channelPrefix = route.prefix; foreach(const QString &s, target.subscriptions) adata.channels += s.toUtf8(); adata.trusted = target.trusted; adata.useSession = route.session; adata.responseSent = acceptAfterResponding; if(!statsManager->connectionSendEnabled()) { // flush max. the count will include the connections we just unregistered adata.connMaxPackets += statsManager->getConnMaxPacket(route.statsRoute()).toVariant(); // flush max again to get the count without the connections adata.connMaxPackets += statsManager->getConnMaxPacket(route.statsRoute()).toVariant(); } acceptRequest = new AcceptRequest(acceptManager, this); finishedConnection = acceptRequest->finished.connect(boost::bind(&Private::acceptRequest_finished, this)); acceptRequest->start(adata); } } void rs_errorResponding(RequestSession *rs) { log_debug("proxysession: %p response error id=%s", q, rs->rid().second.data()); SessionItem *si = sessionItemsBySession.value(rs); assert(si); assert(si->state != SessionItem::Errored); // flag that we should stop attempting to respond si->state = SessionItem::Errored; si->bytesToWrite = -1; // don't destroy the RequestSession here. a finished signal will arrive next. } void rs_headerBytesSent(int count, RequestSession *rs) { SessionItem *si = sessionItemsBySession.value(rs); assert(si); if(si->countClientSentBytes) incCounter(Stats::ClientHeaderBytesSent, count); } void rs_bodyBytesSent(int count, RequestSession *rs) { SessionItem *si = sessionItemsBySession.value(rs); assert(si); if(si->countClientSentBytes) incCounter(Stats::ClientContentBytesSent, count); } void acceptRequest_finished() { if(acceptRequest->success()) { AcceptRequest::ResponseData rdata = acceptRequest->result(); finishedConnection.disconnect(); delete acceptRequest; acceptRequest = 0; if(rdata.accepted) { foreach(SessionItem *si, sessionItems) logFinished(si, true); // the requests were paused, so deleting them will leave the peer sessions active QList toDestroy; foreach(SessionItem *si, sessionItems) { toDestroy += si->rs; delete si; } sessionItems.clear(); sessionItemsBySession.clear(); QPointer self = this; foreach(RequestSession *rs, toDestroy) { q->requestSessionDestroyed(rs, true); reqSessionConnectionMap.erase(rs); delete rs; if(!self) return; } log_debug("proxysession: %p finished for accept", q); cleanup(); q->finished(); } else { if(acceptAfterResponding) { // wake up receivers and append foreach(SessionItem *si, sessionItems) { si->state = SessionItem::Responded; si->rs->resume(); if(rdata.response.code != -1) si->rs->writeResponseBody(rdata.response.body); si->bytesToWrite = -1; si->rs->endResponseBody(); } } else { if(rdata.response.code != -1) { // wake up receivers foreach(SessionItem *si, sessionItems) { si->state = SessionItem::WaitingForResponse; si->rs->resume(); } respondAll(rdata.response.code, rdata.response.reason, rdata.response.headers, rdata.response.body); } else { cannotAcceptAll(); } } } } else { // wake up receivers and reject if(acceptRequest->errorCondition() == ZrpcRequest::ErrorFormat && typeId(((ZrpcRequest *)acceptRequest)->result()) == QMetaType::QByteArray) { QString errorString = QString::fromUtf8(((ZrpcRequest *)acceptRequest)->result().toByteArray()); QString msg = "Error while proxying to origin."; QString dmsg = QString("Failed to parse accept instructions: %1").arg(errorString); rejectAll(502, "Bad Gateway", msg, dmsg); } else { cannotAcceptAll(); } finishedConnection.disconnect(); delete acceptRequest; acceptRequest = 0; } } }; ProxySession::ProxySession(ZRoutes *zroutes, ZrpcManager *acceptManager, const LogUtil::Config &logConfig, StatsManager *statsManager, QObject *parent) : QObject(parent) { d = new Private(this, zroutes, acceptManager, logConfig, statsManager); } ProxySession::~ProxySession() { delete d; } void ProxySession::setRoute(const DomainMap::Entry &route) { d->route = route; } void ProxySession::setDefaultSigKey(const QByteArray &iss, const Jwt::EncodingKey &key) { d->defaultSigIss = iss; d->defaultSigKey = key; } void ProxySession::setAcceptXForwardedProtocol(bool enabled) { d->acceptXForwardedProtocol = enabled; } void ProxySession::setUseXForwardedProtocol(bool protoEnabled, bool protocolEnabled) { d->useXForwardedProto = protoEnabled; d->useXForwardedProtocol = protocolEnabled; } void ProxySession::setXffRules(const XffRule &untrusted, const XffRule &trusted) { d->xffRule = untrusted; d->xffTrustedRule = trusted; } void ProxySession::setOrigHeadersNeedMark(const QList &names) { d->origHeadersNeedMark = names; } void ProxySession::setAcceptPushpinRoute(bool enabled) { d->acceptPushpinRoute = enabled; } void ProxySession::setCdnLoop(const QByteArray &value) { d->cdnLoop = value; } void ProxySession::setProxyInitialResponseEnabled(bool enabled) { d->proxyInitialResponse = enabled; } void ProxySession::setInspectData(const InspectData &idata) { d->haveInspectData = true; d->idata = idata; } void ProxySession::add(RequestSession *rs) { d->add(rs); } #include "proxysession.moc" pushpin-1.39.1/src/cpp/proxy/proxysession.h000066400000000000000000000042111457610542000207650ustar00rootroot00000000000000/* * Copyright (C) 2012-2022 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef PROXYSESSION_H #define PROXYSESSION_H #include #include "logutil.h" #include "domainmap.h" namespace Jwt { class EncodingKey; } class InspectData; class AcceptData; class ZrpcManager; class ZRoutes; class StatsManager; class XffRule; class RequestSession; #include using Signal = boost::signals2::signal; using Connection = boost::signals2::scoped_connection; class ProxySession : public QObject { Q_OBJECT public: ProxySession(ZRoutes *zroutes, ZrpcManager *acceptManager, const LogUtil::Config &logConfig, StatsManager *stats = 0, QObject *parent = 0); ~ProxySession(); void setRoute(const DomainMap::Entry &route); void setDefaultSigKey(const QByteArray &iss, const Jwt::EncodingKey &key); void setAcceptXForwardedProtocol(bool enabled); void setUseXForwardedProtocol(bool protoEnabled, bool protocolEnabled); void setXffRules(const XffRule &untrusted, const XffRule &trusted); void setOrigHeadersNeedMark(const QList &names); void setAcceptPushpinRoute(bool enabled); void setCdnLoop(const QByteArray &value); void setProxyInitialResponseEnabled(bool enabled); void setInspectData(const InspectData &idata); // takes ownership void add(RequestSession *rs); Signal addNotAllowed; // no more sharing, for whatever reason Signal finished; boost::signals2::signal requestSessionDestroyed; private: class Private; friend class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/proxy/proxyutil.cpp000066400000000000000000000207561457610542000206260ustar00rootroot00000000000000/* * Copyright (C) 2014-2022 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "proxyutil.h" #include #include #include #include "qtcompat.h" #include "log.h" #include "jwt.h" #include "inspectdata.h" static QByteArray make_token(const QByteArray &iss, const Jwt::EncodingKey &key) { QVariantMap claim; claim["iss"] = QString::fromUtf8(iss); claim["exp"] = QDateTime::currentDateTimeUtc().toSecsSinceEpoch() + 3600; return Jwt::encode(claim, key); } static bool validate_token(const QByteArray &token, const Jwt::DecodingKey &key) { QVariant claimObj = Jwt::decode(token, key); if(!claimObj.isValid() || typeId(claimObj) != QMetaType::QVariantMap) return false; QVariantMap claim = claimObj.toMap(); int exp = claim.value("exp").toInt(); if(exp <= 0 || (int)QDateTime::currentDateTimeUtc().toSecsSinceEpoch() >= exp) return false; return true; } namespace ProxyUtil { // check if the request is coming from a grip proxy already bool checkTrustedClient(const char *logprefix, void *object, const HttpRequestData &requestData, const Jwt::DecodingKey &defaultUpstreamKey) { if(!defaultUpstreamKey.isNull()) { QByteArray token = requestData.headers.get("Grip-Sig"); if(!token.isEmpty()) { if(validate_token(token, defaultUpstreamKey)) return true; log_debug("%s: %p signature present but invalid: %s", logprefix, object, token.data()); } } return false; } void manipulateRequestHeaders(const char *logprefix, void *object, HttpRequestData *requestData, bool trustedClient, const DomainMap::Entry &entry, const QByteArray &sigIss, const Jwt::EncodingKey &sigKey, bool acceptXForwardedProtocol, bool useXForwardedProto, bool useXForwardedProtocol, const XffRule &xffTrustedRule, const XffRule &xffRule, const QList &origHeadersNeedMark, bool acceptPushpinRoute, const QByteArray &cdnLoop, const QHostAddress &peerAddress, const InspectData &idata, bool gripEnabled, bool intReq) { if(trustedClient) log_debug("%s: %p passing to upstream", logprefix, object); if(!trustedClient && entry.origHeaders) { // copy headers to include magic prefix, so that the original // headers may be recovered later. if the client is trusted, // then we assume this has been done already. HttpHeaders origHeaders; for(int n = 0; n < requestData->headers.count(); ++n) { const HttpHeader &h = requestData->headers[n]; if(qstrnicmp(h.first.data(), "eb9bf0f5-", 9) == 0) { // if it's already marked, take it origHeaders += h; // remove where it lives now. we'll put it back later requestData->headers.removeAt(n); --n; // adjust position } else { // see if we require it to be marked already bool found = false; foreach(const QByteArray &i, origHeadersNeedMark) { if(qstricmp(h.first.data(), i.data()) == 0) { found = true; break; } } // if not, then add as marked if(!found) origHeaders += HttpHeader("eb9bf0f5-" + h.first, h.second); } } // now append all the orig headers to the end foreach(const HttpHeader &h, origHeaders) requestData->headers += h; } else if(!entry.origHeaders) { // if we don't want original headers, then filter them out // before proxying for(int n = 0; n < requestData->headers.count(); ++n) { const HttpHeader &h = requestData->headers[n]; if(qstrnicmp(h.first.data(), "eb9bf0f5-", 9) == 0) { requestData->headers.removeAt(n); --n; // adjust position } } } // don't relay these headers. their meaning is handled by // mongrel2 and they only apply to the incoming hop. requestData->headers.removeAll("Connection"); requestData->headers.removeAll("Keep-Alive"); requestData->headers.removeAll("Accept-Encoding"); requestData->headers.removeAll("Content-Encoding"); requestData->headers.removeAll("Transfer-Encoding"); requestData->headers.removeAll("Expect"); if(acceptPushpinRoute) requestData->headers.removeAll("Pushpin-Route"); if(!cdnLoop.isEmpty()) { QList values = requestData->headers.takeAll("CDN-Loop", true); values += cdnLoop; requestData->headers += HttpHeader("CDN-Loop", values.join(", ")); } if(!trustedClient && !intReq) { // remove all Grip- headers for(int n = 0; n < requestData->headers.count(); ++n) { if(qstrnicmp(requestData->headers[n].first.data(), "Grip-", 5) == 0) { requestData->headers.removeAt(n); --n; // adjust position } } } if(!trustedClient && gripEnabled) { applyGripSig(logprefix, object, &requestData->headers, sigIss, sigKey); requestData->headers.removeAll("Grip-Feature"); requestData->headers += HttpHeader("Grip-Feature", "status, session, link:next, filter:skip-self, filter:skip-users, filter:require-sub, filter:build-id, filter:var-subst"); if(!idata.sid.isEmpty()) { requestData->headers.removeAll("Grip-Session-Id"); requestData->headers += HttpHeader("Grip-Session-Id", idata.sid); } if(!idata.lastIds.isEmpty()) { QHashIterator it(idata.lastIds); while(it.hasNext()) { it.next(); requestData->headers += HttpHeader("Grip-Last", it.key() + "; last-id=" + it.value()); } } } if(acceptXForwardedProtocol || useXForwardedProto || useXForwardedProtocol) { requestData->headers.removeAll("X-Forwarded-Proto"); // TODO: deprecate requestData->headers.removeAll("X-Forwarded-Protocol"); } if(useXForwardedProto || useXForwardedProtocol) { QString scheme = requestData->uri.scheme(); if(scheme == "https" || scheme == "wss") { QByteArray schemeVal = scheme.toUtf8(); if(useXForwardedProto) requestData->headers += HttpHeader("X-Forwarded-Proto", schemeVal); // TODO: deprecate if(useXForwardedProtocol) requestData->headers += HttpHeader("X-Forwarded-Protocol", schemeVal); } } const XffRule *xr; if(trustedClient) xr = &xffTrustedRule; else xr = &xffRule; QList xffValues = requestData->headers.takeAll("X-Forwarded-For"); if(xr->truncate >= 0) xffValues = xffValues.mid(qMax(xffValues.count() - xr->truncate, 0)); if(xr->append) xffValues += peerAddress.toString().toUtf8(); if(!xffValues.isEmpty()) requestData->headers += HttpHeader("X-Forwarded-For", HttpHeaders::join(xffValues)); } void applyHost(QUrl *url, const QString &host) { int at = host.indexOf(':'); if(at != -1) { url->setHost(host.mid(0, at)); url->setPort(host.mid(at + 1).toInt()); } else { url->setHost(host); url->setPort(-1); } } void applyHostHeader(HttpHeaders *headers, const QUrl &uri) { QByteArray hostHeader = uri.host().toUtf8(); if(uri.port() != -1) hostHeader += ':' + QByteArray::number(uri.port()); if(headers->get("Host") != hostHeader) { headers->removeAll("Host"); headers->append(HttpHeader("Host", hostHeader)); } } void applyGripSig(const char *logprefix, void *object, HttpHeaders *headers, const QByteArray &sigIss, const Jwt::EncodingKey &sigKey) { if(!sigIss.isEmpty() && !sigKey.isNull()) { QByteArray token = make_token(sigIss, sigKey); if(!token.isEmpty()) { headers->removeAll("Grip-Sig"); headers->append(HttpHeader("Grip-Sig", token)); } else log_error("%s: %p failed to sign request", logprefix, object); } } QString targetToString(const DomainMap::Target &target) { if(target.type == DomainMap::Target::Test) return "test"; else if(target.type == DomainMap::Target::Custom) return(target.zhttpRoute.req ? "zhttpreq/" : "zhttp/") + target.zhttpRoute.baseSpec; else // Default return target.connectHost + ':' + QString::number(target.connectPort); } QHostAddress getLogicalAddress(const HttpHeaders &headers, const XffRule &xffRule, const QHostAddress &peerAddress) { QList xffValues = headers.getAll("X-Forwarded-For"); if(!xffValues.isEmpty() && xffRule.truncate != 0) { QHostAddress addr; if(addr.setAddress(QString::fromUtf8(xffValues.first()))) return addr; } return peerAddress; } } pushpin-1.39.1/src/cpp/proxy/proxyutil.h000066400000000000000000000040761457610542000202700ustar00rootroot00000000000000/* * Copyright (C) 2014-2022 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef PROXYUTIL_H #define PROXYUTIL_H #include #include #include #include "packet/httprequestdata.h" #include "domainmap.h" #include "xffrule.h" namespace Jwt { class EncodingKey; class DecodingKey; } class InspectData; namespace ProxyUtil { bool checkTrustedClient(const char *logprefix, void *object, const HttpRequestData &requestData, const Jwt::DecodingKey &defaultUpstreamKey); void manipulateRequestHeaders(const char *logprefix, void *object, HttpRequestData *requestData, bool trustedClient, const DomainMap::Entry &entry, const QByteArray &sigIss, const Jwt::EncodingKey &sigKey, bool acceptXForwardedProtocol, bool useXForwardedProto, bool useXForwardedProtocol, const XffRule &xffTrustedRule, const XffRule &xffRule, const QList &origHeadersNeedMark, bool acceptPushpinRoute, const QByteArray &cdnLoop, const QHostAddress &peerAddress, const InspectData &idata, bool gripEnabled, bool intReq); void applyHost(QUrl *url, const QString &host); void applyHostHeader(HttpHeaders *headers, const QUrl &uri); void applyGripSig(const char *logprefix, void *object, HttpHeaders *headers, const QByteArray &sigIss, const Jwt::EncodingKey &sigKey); QString targetToString(const DomainMap::Target &target); QHostAddress getLogicalAddress(const HttpHeaders &headers, const XffRule &xffRule, const QHostAddress &peerAddress); } #endif pushpin-1.39.1/src/cpp/proxy/requestsession.cpp000066400000000000000000001057121457610542000216370ustar00rootroot00000000000000/* * Copyright (C) 2012-2023 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "requestsession.h" #include #include #include #include #include #include #include #include #include "packet/httprequestdata.h" #include "packet/httpresponsedata.h" #include "qtcompat.h" #include "bufferlist.h" #include "log.h" #include "layertracker.h" #include "sockjsmanager.h" #include "inspectdata.h" #include "acceptdata.h" #include "zhttpmanager.h" #include "zrpcmanager.h" #include "zrpcchecker.h" #include "inspectrequest.h" #include "acceptrequest.h" #include "statsmanager.h" #include "cors.h" #include "proxyutil.h" #include "xffrule.h" #define MAX_SHARED_REQUEST_BODY 100000 #define MAX_ACCEPT_REQUEST_BODY 100000 static int fromHex(char c) { if(c >= '0' && c <= '9') return c - '0'; else if(c >= 'a' && c <= 'f') return c - 'a' + 10; else if(c >= 'A' && c <= 'F') return c - 'A' + 10; else return -1; } static QByteArray parsePercentEncoding(const QByteArray &in) { QByteArray out; for(int n = 0; n < in.size(); ++n) { char c = in[n]; if(c == '%') { if(n + 2 >= in.size()) return QByteArray(); int hi = fromHex(in[n + 1]); if(hi == -1) return QByteArray(); int lo = fromHex(in[n + 2]); if(lo == -1) return QByteArray(); unsigned char val = (hi << 4) + lo; out += val; n += 2; // adjust position } else out += c; } return out; } static bool validMethod(const QString &in) { if(in.isEmpty()) return false; for(int n = 0; n < in.size(); ++n) { if(!in[n].isPrint()) return false; } return true; } static QByteArray serializeJsonString(const QString &s) { QByteArray tmp = QJsonDocument(QJsonArray::fromVariantList(QVariantList() << s)).toJson(QJsonDocument::Compact); assert(tmp.length() >= 4); assert(tmp[0] == '[' && tmp[tmp.length() - 1] == ']'); assert(tmp[1] == '"' && tmp[tmp.length() - 2] == '"'); return tmp.mid(1, tmp.length() - 2); } static QByteArray ridToString(const QPair &rid) { return rid.first + ':' + rid.second; } class RequestSession::Private : public QObject { Q_OBJECT public: enum State { Stopped, Prefetching, Inspecting, Receiving, ReceivingForAccept, Accepting, WaitingForResponse, RespondingStart, Responding, RespondingInternal }; struct ZhttpReqConnections { Connection readyReadConnection; Connection pausedConnection; Connection errorConnection; Connection bytesWrittenConnection; }; RequestSession *q; int workerId; State state; ZhttpRequest::Rid rid; DomainMap *domainMap; SockJsManager *sockJsManager; ZrpcManager *inspectManager; ZrpcChecker *inspectChecker; ZrpcManager *acceptManager; StatsManager *stats; ZhttpRequest *zhttpRequest; HttpRequestData requestData; Jwt::DecodingKey defaultUpstreamKey; bool trusted; QHostAddress peerAddress; QHostAddress logicalPeerAddress; DomainMap::Entry route; QString routeId; bool debug; bool autoCrossOrigin; InspectRequest *inspectRequest; InspectData idata; AcceptRequest *acceptRequest; BufferList in; QByteArray jsonpCallback; bool jsonpExtendedResponse; HttpResponseData responseData; int responseBodySize; BufferList out; bool responseBodyFinished; bool pendingResponseUpdate; LayerTracker jsonpTracker; bool isRetry; QList jsonpExtractableHeaders; int prefetchSize; bool needPause; bool connectionRegistered; bool accepted; bool passthrough; bool autoShare; XffRule xffRule; XffRule xffTrustedRule; bool isSockJs; ZhttpReqConnections zhttpReqConnections; Connection inspectFinishedConnection; Connection acceptFinishedConnection; Private(RequestSession *_q, int _workerId, DomainMap *_domainMap = 0, SockJsManager *_sockJsManager = 0, ZrpcManager *_inspectManager = 0, ZrpcChecker *_inspectChecker = 0, ZrpcManager *_acceptManager = 0, StatsManager *_stats = 0) : QObject(_q), q(_q), workerId(_workerId), state(Stopped), domainMap(_domainMap), sockJsManager(_sockJsManager), inspectManager(_inspectManager), inspectChecker(_inspectChecker), acceptManager(_acceptManager), stats(_stats), zhttpRequest(0), trusted(false), debug(false), autoCrossOrigin(false), inspectRequest(0), acceptRequest(0), jsonpExtendedResponse(false), responseBodySize(0), responseBodyFinished(false), pendingResponseUpdate(false), isRetry(false), prefetchSize(0), needPause(false), connectionRegistered(false), accepted(false), passthrough(false), autoShare(false), isSockJs(false) { jsonpExtractableHeaders += "Cache-Control"; } ~Private() { cleanup(); } void cleanup() { if(zhttpRequest) { zhttpReqConnections = ZhttpReqConnections(); delete zhttpRequest; zhttpRequest = 0; } if(inspectRequest) { inspectFinishedConnection.disconnect(); inspectChecker->give(inspectRequest); inspectRequest = 0; } if(stats && connectionRegistered) { connectionRegistered = false; QByteArray cid = ridToString(rid); // refresh before remove, to ensure transition if(accepted) stats->refreshConnection(cid); // linger if accepted bool linger = accepted && stats->connectionSendEnabled(); stats->removeConnection(cid, linger); } state = Stopped; } void start(ZhttpRequest *req) { zhttpRequest = req; rid = req->rid(); passthrough = req->passthroughData().isValid(); requestData.method = req->requestMethod(); requestData.uri = req->requestUri(); requestData.headers = req->requestHeaders(); trusted = ProxyUtil::checkTrustedClient("requestsession", q, requestData, defaultUpstreamKey); peerAddress = req->peerAddress(); logicalPeerAddress = ProxyUtil::getLogicalAddress(requestData.headers, trusted ? xffTrustedRule : xffRule, peerAddress); log_debug("worker %d: IN id=%s, %s %s", workerId, rid.second.data(), qPrintable(requestData.method), requestData.uri.toEncoded().data()); bool isHttps = (requestData.uri.scheme() == "https"); QString host = requestData.uri.host(); if(route.isNull() && domainMap) { QByteArray encPath = requestData.uri.path(QUrl::FullyEncoded).toUtf8(); // look up the route if(!routeId.isEmpty() && !domainMap->isIdShared(routeId)) route = domainMap->entry(routeId); else route = domainMap->entry(DomainMap::Http, isHttps, host, encPath); // before we do anything else, see if this is a sockjs request if(!route.isNull() && !route.sockJsPath.isEmpty() && encPath.startsWith(route.sockJsPath)) { isSockJs = true; sockJsManager->giveRequest(zhttpRequest, route.sockJsPath.length(), route.sockJsAsPath, route); zhttpRequest = 0; QMetaObject::invokeMethod(this, "doFinished", Qt::QueuedConnection); return; } } zhttpReqConnections.pausedConnection = zhttpRequest->paused.connect(boost::bind(&Private::zhttpRequest_paused, this)); zhttpReqConnections.errorConnection = zhttpRequest->error.connect(boost::bind(&Private::zhttpRequest_error, this)); if(!route.isNull()) { if(route.debug) debug = true; if(route.autoCrossOrigin) autoCrossOrigin = true; } if(autoCrossOrigin) { DomainMap::JsonpConfig config; if(!route.isNull()) config = route.jsonpConfig; bool ok = false; QString str; tryApplyJsonp(config, &ok, &str); if(!ok) { state = WaitingForResponse; respondBadRequest(str); return; } } // NOTE: per the license, this functionality may not be removed as it // is the interface for the copyright notice if(requestData.headers.contains("Pushpin-Check")) { QString str = "Copyright (C) 2012-2023 Fanout, Inc.\n" "Copyright (C) 2023 Fastly, Inc.\n" "\n" "Pushpin is licensed under the Apache License, Version 2.0 (the \"License\");\n" "you may not use this software except in compliance with the License.\n" "You may obtain a copy of the License at\n" "\n" " http://www.apache.org/licenses/LICENSE-2.0\n" "\n" "Unless required by applicable law or agreed to in writing, software\n" "distributed under the License is distributed on an \"AS IS\" BASIS,\n" "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" "See the License for the specific language governing permissions and\n" "limitations under the License.\n"; state = WaitingForResponse; respondSuccess(str); return; } log_debug("requestsession: %p %s has %d routes", q, qPrintable(host), route.targets.count()); if(route.isNull()) { state = WaitingForResponse; respondError(502, "Bad Gateway", QString("No route for host: %1").arg(host)); return; } if(stats && !passthrough) { connectionRegistered = true; stats->addConnection(ridToString(rid), route.statsRoute(), StatsManager::Http, logicalPeerAddress, isHttps, false); stats->addActivity(route.statsRoute()); stats->addRequestsReceived(1); } state = Prefetching; zhttpReqConnections.readyReadConnection = zhttpRequest->readyRead.connect(boost::bind(&Private::zhttpRequest_readyRead, this)); processIncomingRequest(); } void startRetry(int unreportedTime, int retrySeq) { trusted = ProxyUtil::checkTrustedClient("requestsession", q, requestData, defaultUpstreamKey); peerAddress = zhttpRequest->peerAddress(); logicalPeerAddress = ProxyUtil::getLogicalAddress(requestData.headers, trusted ? xffTrustedRule : xffRule, peerAddress); zhttpReqConnections.pausedConnection = zhttpRequest->paused.connect(boost::bind(&Private::zhttpRequest_paused, this)); zhttpReqConnections.errorConnection = zhttpRequest->error.connect(boost::bind(&Private::zhttpRequest_error, this)); state = WaitingForResponse; bool isHttps = (requestData.uri.scheme() == "https"); QString host = requestData.uri.host(); QByteArray encPath = requestData.uri.path(QUrl::FullyEncoded).toUtf8(); // look up the route if(!routeId.isEmpty() && !domainMap->isIdShared(routeId)) route = domainMap->entry(routeId); else route = domainMap->entry(DomainMap::Http, isHttps, host, encPath); log_debug("requestsession: %p %s has %d routes", q, qPrintable(host), route.targets.count()); if(route.isNull()) { state = WaitingForResponse; respondError(502, "Bad Gateway", QString("No route for host: %1").arg(host)); return; } if(stats) { if(retrySeq >= 0) stats->setRetrySeq(route.statsRoute(), retrySeq); connectionRegistered = true; int reportOffset = stats->connectionSendEnabled() ? -1 : qMax(unreportedTime, 0); stats->addConnection(ridToString(rid), route.statsRoute(), StatsManager::Http, logicalPeerAddress, isHttps, false, reportOffset); stats->addActivity(route.statsRoute()); // note: we don't call addRequestsReceived here, because we're acting for an existing request } } void processIncomingRequest() { if(state == Prefetching) { if(prefetchSize > 0) in += zhttpRequest->readBody(prefetchSize - in.size()); if(in.size() >= prefetchSize || zhttpRequest->isInputFinished()) { // we've read enough body to start inspection zhttpReqConnections.readyReadConnection.disconnect(); state = Inspecting; requestData.body = in.toByteArray(); bool truncated = (!zhttpRequest->isInputFinished() || zhttpRequest->bytesAvailable() > 0); assert(!inspectRequest); if(inspectManager) { inspectRequest = new InspectRequest(inspectManager, this); if(inspectChecker->isInterfaceAvailable()) { inspectFinishedConnection = inspectRequest->finished.connect(boost::bind(&Private::inspectRequest_finished, this)); inspectChecker->watch(inspectRequest); inspectRequest->start(requestData, truncated, route.session, autoShare); } else { inspectChecker->watch(inspectRequest); inspectChecker->give(inspectRequest); inspectRequest->start(requestData, truncated, route.session, autoShare); inspectRequest = 0; } } if(!inspectRequest) { log_debug("inspect not available"); QMetaObject::invokeMethod(this, "doInspectError", Qt::QueuedConnection); } } } else if(state == Receiving) { in += zhttpRequest->readBody(MAX_SHARED_REQUEST_BODY - in.size()); if(in.size() >= MAX_SHARED_REQUEST_BODY || zhttpRequest->isInputFinished()) { // we've read as much as we can for now. if there is still // more to read, then the engine will notice this and // disallow sharing before passing to proxysession. at that // point, proxysession will read the remainder of the data zhttpReqConnections.readyReadConnection.disconnect(); state = WaitingForResponse; requestData.body = in.take(); q->inspected(idata); } } else if(state == ReceivingForAccept) { QByteArray buf = zhttpRequest->readBody(); if(in.size() + buf.size() > MAX_ACCEPT_REQUEST_BODY) { respondError(413, "Request Entity Too Large", QString("Body must not exceed %1 bytes").arg(MAX_ACCEPT_REQUEST_BODY)); return; } in += buf; if(zhttpRequest->isInputFinished()) { if(acceptManager) zhttpRequest->pause(); else respondCannotAccept(); } } } void respond(int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body) { q->startResponse(code, reason, headers); q->writeResponseBody(body); q->endResponseBody(); } void respond(int code, const QString &status, const QByteArray &body) { HttpHeaders headers; headers += HttpHeader("Content-Type", "text/plain"); headers += HttpHeader("Content-Length", QByteArray::number(body.size())); respond(code, status.toUtf8(), headers, body); } void respondError(int code, const QString &status, const QString &errorString) { respond(code, status, errorString.toUtf8() + '\n'); } void respondBadRequest(const QString &errorString) { respondError(400, "Bad Request", errorString); } void respondCannotAccept() { respondError(500, "Internal Server Error", "Accept service unavailable."); } void responseUpdate() { if(!pendingResponseUpdate) { pendingResponseUpdate = true; QMetaObject::invokeMethod(this, "doResponseUpdate", Qt::QueuedConnection); } } // returns null array on error QByteArray makeJsonpStart(int code, const QByteArray &reason, const HttpHeaders &headers) { QByteArray out = "/**/" + jsonpCallback + "("; if(jsonpExtendedResponse) { QByteArray reasonJson = serializeJsonString(QString::fromUtf8(reason)); if(reasonJson.isNull()) return QByteArray(); QVariantMap vheaders; foreach(const HttpHeader h, headers) { if(!vheaders.contains(h.first)) vheaders[h.first] = h.second; } QByteArray headersJson = QJsonDocument(QJsonObject::fromVariantMap(vheaders)).toJson(QJsonDocument::Compact); if(headersJson.isNull()) return QByteArray(); out += "{\"code\": " + QByteArray::number(code) + ", \"reason\": " + reasonJson + ", \"headers\": " + headersJson + ", \"body\": \""; } return out; } QByteArray makeJsonpBody(const QByteArray &buf) { if(jsonpExtendedResponse) { // FIXME: this assumes there isn't a partial character encoding QByteArray bodyJson = serializeJsonString(QString::fromUtf8(buf)); if(bodyJson.isNull()) return QByteArray(); assert(bodyJson.size() >= 2); return bodyJson.mid(1, bodyJson.size() - 2); } else return buf; } QByteArray makeJsonpEnd() { if(jsonpExtendedResponse) return QByteArray("\"});\n"); else return QByteArray(");\n"); } // return true if jsonp applied bool tryApplyJsonp(const DomainMap::JsonpConfig &config, bool *ok, QString *errorMessage) { *ok = true; // must be a GET if(requestData.method != "GET") return false; QString callbackParam = QString::fromUtf8(config.callbackParam); if(callbackParam.isEmpty()) callbackParam = "callback"; QString bodyParam; if(!config.bodyParam.isEmpty()) bodyParam = QString::fromUtf8(config.bodyParam); QUrl uri = requestData.uri; QUrlQuery query(uri); // two ways to activate JSON-P: // 1) callback param present // 2) default callback specified in configuration and body param present if(!query.hasQueryItem(callbackParam) && (config.defaultCallback.isEmpty() || bodyParam.isEmpty() || !query.hasQueryItem(bodyParam))) { return false; } QByteArray callback; if(query.hasQueryItem(callbackParam)) { callback = parsePercentEncoding(query.queryItemValue(callbackParam, QUrl::FullyEncoded).toUtf8()); if(callback.isEmpty()) { log_debug("requestsession: id=%s invalid callback parameter, rejecting", rid.second.data()); *ok = false; *errorMessage = "Invalid callback parameter."; return false; } query.removeAllQueryItems(callbackParam); } else callback = config.defaultCallback; QString method; if(query.hasQueryItem("_method")) { method = QString::fromLatin1(parsePercentEncoding(query.queryItemValue("_method", QUrl::FullyEncoded).toUtf8())); if(!validMethod(method)) { log_debug("requestsession: id=%s invalid _method parameter, rejecting", rid.second.data()); *ok = false; *errorMessage = "Invalid _method parameter."; return false; } query.removeAllQueryItems("_method"); } HttpHeaders headers; if(query.hasQueryItem("_headers")) { QJsonParseError e; QJsonDocument doc = QJsonDocument::fromJson(parsePercentEncoding(query.queryItemValue("_headers", QUrl::FullyEncoded).toUtf8()), &e); if(e.error != QJsonParseError::NoError || !doc.isObject()) { log_debug("requestsession: id=%s invalid _headers parameter, rejecting", rid.second.data()); *ok = false; *errorMessage = "Invalid _headers parameter."; return false; } QVariantMap headersMap = doc.object().toVariantMap(); QMapIterator vit(headersMap); while(vit.hasNext()) { vit.next(); if(typeId(vit.value()) != QMetaType::QString) { log_debug("requestsession: id=%s invalid _headers parameter, rejecting", rid.second.data()); *ok = false; *errorMessage = "Invalid _headers parameter."; return false; } QByteArray key = vit.key().toUtf8(); // ignore some headers that we explicitly set later on if(qstricmp(key.data(), "host") == 0) continue; if(qstricmp(key.data(), "accept") == 0) continue; headers += HttpHeader(key, vit.value().toString().toUtf8()); } query.removeAllQueryItems("_headers"); } QByteArray body; if(!bodyParam.isEmpty()) { if(query.hasQueryItem(bodyParam)) { body = parsePercentEncoding(query.queryItemValue(bodyParam, QUrl::FullyEncoded).toUtf8()); if(body.isNull()) { log_debug("requestsession: id=%s invalid body parameter, rejecting", rid.second.data()); *ok = false; *errorMessage = "Invalid body parameter."; return false; } headers.removeAll("Content-Type"); headers += HttpHeader("Content-Type", "application/json"); query.removeAllQueryItems(bodyParam); } } else { if(query.hasQueryItem("_body")) { body = parsePercentEncoding(query.queryItemValue("_body").toUtf8()); if(body.isNull()) { log_debug("requestsession: id=%s invalid _body parameter, rejecting", rid.second.data()); *ok = false; *errorMessage = "Invalid _body parameter."; return false; } query.removeAllQueryItems("_body"); } } uri.setQuery(query); // if we have no query items anymore, strip the '?' if(query.isEmpty()) { QByteArray tmp = uri.toEncoded(); if(tmp.length() > 0 && tmp[tmp.length() - 1] == '?') { tmp.truncate(tmp.length() - 1); uri = QUrl::fromEncoded(tmp, QUrl::StrictMode); } } if(method.isEmpty()) method = config.defaultMethod; requestData.method = method; requestData.uri = uri; QByteArray hostHeader = uri.host().toUtf8(); if(uri.port() != -1) hostHeader += ':' + QByteArray::number(uri.port()); headers += HttpHeader("Host", hostHeader); headers += HttpHeader("Accept", "*/*"); // carry over the rest of the headers foreach(const HttpHeader &h, requestData.headers) { if(qstricmp(h.first.data(), "host") == 0) continue; if(qstricmp(h.first.data(), "accept") == 0) continue; headers += h; } requestData.headers = headers; in += body; jsonpCallback = callback; jsonpExtendedResponse = (config.mode == DomainMap::JsonpConfig::Extended); return true; } public: void zhttpRequest_readyRead() { processIncomingRequest(); } void zhttpRequest_bytesWritten(int count) { QPointer self = this; if(!jsonpCallback.isEmpty()) { int actual = jsonpTracker.finished(count); if(actual > 0) q->bytesWritten(actual); } else q->bytesWritten(count); if(!self) return; if(zhttpRequest->isFinished()) { cleanup(); q->finished(); } } void zhttpRequest_paused() { if(state == ReceivingForAccept) { ZhttpRequest::ServerState ss = zhttpRequest->serverState(); AcceptData adata; AcceptData::Request areq; areq.rid = rid; areq.https = requestData.uri.scheme() == "https"; areq.peerAddress = peerAddress; areq.logicalPeerAddress = logicalPeerAddress; areq.debug = debug; areq.autoCrossOrigin = autoCrossOrigin; areq.jsonpCallback = jsonpCallback; areq.jsonpExtendedResponse = jsonpExtendedResponse; areq.inSeq = ss.inSeq; areq.outSeq = ss.outSeq; areq.outCredits = ss.outCredits; areq.userData = ss.userData; adata.requests += areq; adata.requestData = requestData; adata.requestData.body = in.take(); adata.haveInspectData = true; adata.inspectData = idata; adata.route = route.id; adata.channelPrefix = route.prefix; acceptRequest = new AcceptRequest(acceptManager, this); acceptFinishedConnection = acceptRequest->finished.connect(boost::bind(&Private::acceptRequest_finished, this)); acceptRequest->start(adata); } else { q->paused(); } } void zhttpRequest_error() { log_debug("requestsession: request error id=%s", rid.second.data()); cleanup(); q->finished(); } void inspectRequest_finished() { if(!inspectRequest->success()) { inspectFinishedConnection.disconnect(); inspectChecker->give(inspectRequest); inspectRequest = 0; doInspectError(); return; } idata = inspectRequest->result(); inspectFinishedConnection.disconnect(); inspectChecker->give(inspectRequest); inspectRequest = 0; if(!idata.doProxy) { state = ReceivingForAccept; // successful inspect indicated we should not proxy. in that case, // collect the body and accept zhttpReqConnections.readyReadConnection = zhttpRequest->readyRead.connect(boost::bind(&Private::zhttpRequest_readyRead, this)); processIncomingRequest(); } else { if(!idata.sharingKey.isEmpty()) { // a request can only be shared if we've read the entire // request body, so let's try to read it now state = Receiving; zhttpReqConnections.readyReadConnection = zhttpRequest->readyRead.connect(boost::bind(&Private::zhttpRequest_readyRead, this)); processIncomingRequest(); } else { state = WaitingForResponse; requestData.body = in.take(); q->inspected(idata); } } } void acceptRequest_finished() { if(acceptRequest->success()) { AcceptRequest::ResponseData rdata = acceptRequest->result(); acceptFinishedConnection.disconnect(); delete acceptRequest; acceptRequest = 0; if(rdata.accepted) { accepted = true; // the request was paused, so deleting it will leave the peer session active zhttpReqConnections = ZhttpReqConnections(); delete zhttpRequest; zhttpRequest = 0; cleanup(); q->finishedByAccept(); } else { if(rdata.response.code != -1) { zhttpRequest->resume(); respond(rdata.response.code, rdata.response.reason, rdata.response.headers, rdata.response.body); } else { zhttpRequest->resume(); respondCannotAccept(); } } } else { acceptFinishedConnection.disconnect(); delete acceptRequest; acceptRequest = 0; zhttpRequest->resume(); respondCannotAccept(); } } public slots: void doResponseUpdate() { pendingResponseUpdate = false; if(state == RespondingStart) { state = Responding; if(!jsonpCallback.isEmpty()) { HttpHeaders headers; if(responseBodyFinished) { QByteArray bodyRawBuf = out.take(); if(!jsonpExtendedResponse) { // trim any trailing newline before we wrap in a function call if(bodyRawBuf.endsWith("\r\n")) bodyRawBuf.truncate(bodyRawBuf.size() - 2); else if(bodyRawBuf.endsWith("\n")) bodyRawBuf.truncate(bodyRawBuf.size() - 1); } QByteArray startBuf = makeJsonpStart(responseData.code, responseData.reason, responseData.headers); QByteArray bodyBuf; QByteArray endBuf = makeJsonpEnd(); if(!startBuf.isNull()) bodyBuf = makeJsonpBody(bodyRawBuf); if(startBuf.isNull() || bodyBuf.isNull()) { state = RespondingInternal; QByteArray body = "Upstream response could not be JSON-P encoded.\n"; headers += HttpHeader("Content-Type", "text/plain"); headers += HttpHeader("Content-Length", QByteArray::number(body.size())); zhttpRequest->beginResponse(500, "Internal Server Error", headers); zhttpRequest->writeBody(body); responseBodySize += body.size(); zhttpRequest->endBody(); q->errorResponding(); return; } QByteArray buf = startBuf + bodyBuf + endBuf; headers += HttpHeader("Content-Type", "application/javascript"); headers += HttpHeader("Content-Length", QByteArray::number(buf.size())); // mirror headers in the wrapping response foreach(const HttpHeader &h, responseData.headers) { foreach(const QByteArray &eh, jsonpExtractableHeaders) { if(qstricmp(h.first.data(), eh.data()) == 0) { headers += h; break; } } } zhttpReqConnections.bytesWrittenConnection = zhttpRequest->bytesWritten.connect(boost::bind(&Private::zhttpRequest_bytesWritten, this, boost::placeholders::_1)); zhttpRequest->beginResponse(200, "OK", headers); jsonpTracker.addPlain(bodyRawBuf.size()); jsonpTracker.specifyEncoded(buf.size(), bodyRawBuf.size()); zhttpRequest->writeBody(buf); responseBodySize += buf.size(); zhttpRequest->endBody(); return; } QByteArray buf = makeJsonpStart(responseData.code, responseData.reason, responseData.headers); if(buf.isNull()) { state = RespondingInternal; QByteArray body = "Upstream response could not be JSON-P encoded.\n"; headers += HttpHeader("Content-Type", "text/plain"); headers += HttpHeader("Content-Length", QByteArray::number(body.size())); zhttpRequest->beginResponse(500, "Internal Server Error", headers); zhttpRequest->writeBody(body); responseBodySize += body.size(); zhttpRequest->endBody(); q->errorResponding(); return; } headers += HttpHeader("Content-Type", "application/javascript"); headers += HttpHeader("Transfer-Encoding", "chunked"); zhttpReqConnections.bytesWrittenConnection = zhttpRequest->bytesWritten.connect(boost::bind(&Private::zhttpRequest_bytesWritten, this, boost::placeholders::_1)); zhttpRequest->beginResponse(200, "OK", headers); jsonpTracker.specifyEncoded(buf.size(), 0); zhttpRequest->writeBody(buf); responseBodySize += buf.size(); } else { if(autoCrossOrigin) Cors::applyCorsHeaders(requestData.headers, &responseData.headers); zhttpReqConnections.bytesWrittenConnection = zhttpRequest->bytesWritten.connect(boost::bind(&Private::zhttpRequest_bytesWritten, this, boost::placeholders::_1)); zhttpRequest->beginResponse(responseData.code, responseData.reason, responseData.headers); } } if(!out.isEmpty()) { if(!jsonpCallback.isEmpty()) { QByteArray bodyRawBuf = out.take(); if(!jsonpExtendedResponse) { if(responseBodyFinished) { // trim any trailing newline before we wrap in a function call if(bodyRawBuf.endsWith("\r\n")) bodyRawBuf.truncate(bodyRawBuf.size() - 2); else if(bodyRawBuf.endsWith("\n")) bodyRawBuf.truncate(bodyRawBuf.size() - 1); } else { // response isn't finished. keep any trailing newline in the output buffer if(bodyRawBuf.endsWith("\r\n")) { bodyRawBuf.truncate(bodyRawBuf.size() - 2); out += QByteArray("\r\n"); } else if(bodyRawBuf.endsWith("\n")) { bodyRawBuf.truncate(bodyRawBuf.size() - 1); out += QByteArray("\n"); } } } QByteArray buf = makeJsonpBody(bodyRawBuf); if(buf.isNull()) { state = RespondingInternal; log_warning("requestsession: id=%s upstream response could not be JSON-P encoded", rid.second.data()); // if we error while streaming, all we can do is give up zhttpRequest->endBody(); q->errorResponding(); return; } jsonpTracker.addPlain(bodyRawBuf.size()); jsonpTracker.specifyEncoded(buf.size(), bodyRawBuf.size()); zhttpRequest->writeBody(buf); responseBodySize += buf.size(); } else { QByteArray buf = out.take(); zhttpRequest->writeBody(buf); responseBodySize += buf.size(); } } if(responseBodyFinished) { assert(!needPause); if(!jsonpCallback.isEmpty()) { QByteArray buf = makeJsonpEnd(); jsonpTracker.specifyEncoded(buf.size(), 0); zhttpRequest->writeBody(buf); responseBodySize += buf.size(); } zhttpRequest->endBody(); } else if(needPause) { needPause = false; zhttpRequest->pause(); } } void respondSuccess(const QString &message) { respond(200, "OK", message.toUtf8() + '\n'); } void doInspectError() { state = WaitingForResponse; q->inspectError(); } void doFinished() { q->finished(); } }; RequestSession::RequestSession(int workerId, DomainMap *domainMap, SockJsManager *sockJsManager, ZrpcManager *inspectManager, ZrpcChecker *inspectChecker, ZrpcManager *acceptManager, StatsManager *stats, QObject *parent) : QObject(parent) { d = new Private(this, workerId, domainMap, sockJsManager, inspectManager, inspectChecker, acceptManager, stats); } RequestSession::~RequestSession() { delete d; } bool RequestSession::isRetry() const { return d->isRetry; } bool RequestSession::isHttps() const { return d->requestData.uri.scheme() == "https"; } bool RequestSession::isSockJs() const { return d->isSockJs; }; bool RequestSession::trusted() const { return d->trusted; } QHostAddress RequestSession::peerAddress() const { return d->peerAddress; } QHostAddress RequestSession::logicalPeerAddress() const { return d->logicalPeerAddress; } ZhttpRequest::Rid RequestSession::rid() const { return d->rid; } HttpRequestData RequestSession::requestData() const { return d->requestData; } HttpResponseData RequestSession::responseData() const { return d->responseData; } int RequestSession::responseBodySize() const { return d->responseBodySize; } bool RequestSession::debugEnabled() const { return d->debug; } bool RequestSession::autoCrossOrigin() const { return d->autoCrossOrigin; } QByteArray RequestSession::jsonpCallback() const { return d->jsonpCallback; } bool RequestSession::jsonpExtendedResponse() const { return d->jsonpExtendedResponse; } bool RequestSession::haveCompleteRequestBody() const { return (d->zhttpRequest->isInputFinished() && d->zhttpRequest->bytesAvailable() == 0); } DomainMap::Entry RequestSession::route() const { return d->route; } ZhttpRequest *RequestSession::request() { return d->zhttpRequest; } void RequestSession::setDebugEnabled(bool enabled) { d->debug = enabled; } void RequestSession::setAutoCrossOrigin(bool enabled) { d->autoCrossOrigin = enabled; } void RequestSession::setPrefetchSize(int size) { d->prefetchSize = size; } void RequestSession::setRoute(const DomainMap::Entry &route) { d->route = route; } void RequestSession::setRouteId(const QString &routeId) { d->routeId = routeId; } void RequestSession::setAutoShare(bool enabled) { d->autoShare = enabled; } void RequestSession::setAccepted(bool enabled) { d->accepted = enabled; } void RequestSession::setDefaultUpstreamKey(const Jwt::DecodingKey &key) { d->defaultUpstreamKey = key; } void RequestSession::setXffRules(const XffRule &untrusted, const XffRule &trusted) { d->xffRule = untrusted; d->xffTrustedRule = trusted; } void RequestSession::start(ZhttpRequest *req) { d->start(req); } void RequestSession::startRetry(ZhttpRequest *req, bool debug, bool autoCrossOrigin, const QByteArray &jsonpCallback, bool jsonpExtendedResponse, int unreportedTime, int retrySeq) { d->isRetry = true; d->zhttpRequest = req; d->rid = req->rid(); d->debug = debug; d->autoCrossOrigin = autoCrossOrigin; d->jsonpCallback = jsonpCallback; d->jsonpExtendedResponse = jsonpExtendedResponse; d->requestData.method = req->requestMethod(); d->requestData.uri = req->requestUri(); d->requestData.headers = req->requestHeaders(); d->requestData.body = req->readBody(); d->startRetry(unreportedTime, retrySeq); } void RequestSession::pause() { assert(!d->responseBodyFinished); d->needPause = true; d->responseUpdate(); } void RequestSession::resume() { d->zhttpRequest->resume(); } void RequestSession::startResponse(int code, const QByteArray &reason, const HttpHeaders &headers) { assert(d->state == Private::ReceivingForAccept || d->state == Private::WaitingForResponse); headerBytesSent(ZhttpManager::estimateResponseHeaderBytes(code, reason, headers)); d->state = Private::RespondingStart; d->responseData.code = code; d->responseData.reason = reason; d->responseData.headers = headers; d->responseUpdate(); } void RequestSession::writeResponseBody(const QByteArray &body) { assert(d->state == Private::RespondingStart || d->state == Private::Responding); assert(!d->responseBodyFinished); bodyBytesSent(body.size()); d->out += body; d->responseUpdate(); } void RequestSession::endResponseBody() { assert(d->state == Private::RespondingStart || d->state == Private::Responding); assert(!d->responseBodyFinished); d->responseBodyFinished = true; d->responseUpdate(); } void RequestSession::respond(int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body) { d->respond(code, reason, headers, body); } void RequestSession::respondError(int code, const QString &reason, const QString &errorString) { d->respondError(code, reason, errorString); } void RequestSession::respondCannotAccept() { d->respondCannotAccept(); } int RequestSession::unregisterConnection() { if(!d->connectionRegistered) return 0; d->connectionRegistered = false; QByteArray cid = ridToString(d->rid); return d->stats->removeConnection(cid, false); } #include "requestsession.moc" pushpin-1.39.1/src/cpp/proxy/requestsession.h000066400000000000000000000073151457610542000213040ustar00rootroot00000000000000/* * Copyright (C) 2012-2023 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef REQUESTSESSION_H #define REQUESTSESSION_H #include #include "zhttprequest.h" #include "domainmap.h" #include using Signal = boost::signals2::signal; using SignalInt = boost::signals2::signal; using Connection = boost::signals2::scoped_connection; class QHostAddress; namespace Jwt { class DecodingKey; } class HttpRequestData; class HttpResponseData; class SockJsManager; class InspectData; class AcceptData; class ZrpcManager; class ZrpcChecker; class StatsManager; class XffRule; class RequestSession : public QObject { Q_OBJECT public: RequestSession(int workerId, DomainMap *domainMap, SockJsManager *sockJsManager, ZrpcManager *inspectManager, ZrpcChecker *inspectChecker, ZrpcManager *accept, StatsManager *stats, QObject *parent = 0); ~RequestSession(); bool isRetry() const; bool isHttps() const; bool isSockJs() const; bool trusted() const; QHostAddress peerAddress() const; QHostAddress logicalPeerAddress() const; ZhttpRequest::Rid rid() const; HttpRequestData requestData() const; HttpResponseData responseData() const; int responseBodySize() const; bool debugEnabled() const; bool autoCrossOrigin() const; QByteArray jsonpCallback() const; // non-empty if JSON-P is used bool jsonpExtendedResponse() const; bool haveCompleteRequestBody() const; DomainMap::Entry route() const; ZhttpRequest *request(); void setDebugEnabled(bool enabled); void setAutoCrossOrigin(bool enabled); void setPrefetchSize(int size); void setRoute(const DomainMap::Entry &route); void setRouteId(const QString &routeId); void setAutoShare(bool enabled); void setAccepted(bool enabled); void setDefaultUpstreamKey(const Jwt::DecodingKey &key); void setXffRules(const XffRule &untrusted, const XffRule &trusted); // takes ownership void start(ZhttpRequest *req); void startRetry(ZhttpRequest *req, bool debug, bool autoCrossOrigin, const QByteArray &jsonpCallback, bool jsonpExtendedResponse, int unreportedTime, int retrySeq); void pause(); void resume(); void startResponse(int code, const QByteArray &reason, const HttpHeaders &headers); void writeResponseBody(const QByteArray &body); void endResponseBody(); void respond(int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body); void respondError(int code, const QString &reason, const QString &errorString); void respondCannotAccept(); int unregisterConnection(); // return unreported time Signal inspectError; boost::signals2::signal inspected; Signal finishedByAccept; SignalInt bytesWritten; Signal paused; SignalInt headerBytesSent; SignalInt bodyBytesSent; // this signal means some error was encountered while responding and // that you should not attempt to call further response-related // methods. the object remains in an active state though, and so you // should still wait for finished() Signal errorResponding; Signal finished; private: class Private; friend class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/proxy/routesfile.cpp000066400000000000000000000120101457610542000207100ustar00rootroot00000000000000/* * Copyright (C) 2016-2022 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "routesfile.h" #include #include "log.h" namespace RoutesFile { static int findNext(const QString &str, const QString &chars, int offset = 0) { for(int n = offset; n < str.length(); ++n) { if(chars.contains(str[n])) return n; } return -1; } class LineParser { public: class Token { public: enum Type { Error, InitialValue, Prop, EndOfLine }; Type type; QString value; // error, initialvalue, prop QString name; // prop; Token() : type((Type)-1) { } Token(Type _type) : type(_type) { } }; enum State { ReadSeparator, ReadInitialValue, ReadProp }; State state_; QString str_; int index_; LineParser(const QString &line) : state_(ReadSeparator), str_(line), index_(0) { } Token nextToken() { int start = index_; QString part; while(true) { //log_debug("%d [%s] %d", (int)state_, qPrintable(str_), index_); if(state_ == ReadSeparator) { for(; index_ < str_.length(); ++index_) { if(str_[index_] != ' ') break; } if(index_ >= str_.length() || str_[index_] == '#') return Token(Token::EndOfLine); state_ = ReadInitialValue; continue; } else // ReadInitialValue, ReadProp { int at = findNext(str_, "\", #", index_); // quoted section? if(at != -1 && str_[at] == '\"') { part += str_.mid(index_, at - index_); ++at; // decode inner string for(; at < str_.length(); ++at) { if(str_[at] == '\\') { ++at; if(at >= str_.length()) { Token token(Token::Error); token.value = "unterminated escape sequence"; return token; } if(str_[at] == '\\') part += '\\'; else if(str_[at] == '\"') part += '\"'; else { Token token(Token::Error); token.value = QString("unexpected escape character: ") + str_[at]; return token; } } else if(str_[at] == '\"') { break; } else { part += str_[at]; } } if(at >= str_.length()) { Token token(Token::Error); token.value = "unterminated quoted section"; return token; } index_ = at + 1; continue; } // all other chars, or end of string, means end of initial value or prop if(at == -1) at = str_.length(); part += str_.mid(index_, at - index_); Token token; if(state_ == ReadInitialValue) { int n = part.indexOf('='); // '=' in initial value? if(n != -1) { // return empty initial value, re-read as a prop index_ = start; state_ = ReadProp; return Token(Token::InitialValue); } token.type = Token::InitialValue; token.value = part; } else // ReadProp { if(part.isEmpty()) { Token token(Token::Error); token.value = "expecting prop"; return token; } QString name; QString value; int n = part.indexOf('='); if(n != -1) { name = part.mid(0, n); value = part.mid(n + 1); } else name = part; if(name.isEmpty()) { token.type = Token::Error; token.value = "empty prop name"; return token; } token.type = Token::Prop; token.name = name; token.value = value; } if(at < str_.length() && str_[at] == ',') { ++at; state_ = ReadProp; } else // space, #, or end of line { state_ = ReadSeparator; } index_ = at; return token; } } } }; QList parseLine(const QString &line, bool *ok, QString *errorMessage) { QList out; LineParser parser(line); bool done = false; while(!done) { LineParser::Token t = parser.nextToken(); //log_debug("token: %d [%s]", (int)t.type, qPrintable(t.value)); switch(t.type) { case LineParser::Token::Error: if(ok) *ok = false; if(errorMessage) *errorMessage = t.value; return QList(); case LineParser::Token::InitialValue: { RouteSection s; s.value = t.value; out += s; } break; case LineParser::Token::Prop: assert(!out.isEmpty()); out.last().props.insert(t.name, t.value); break; case LineParser::Token::EndOfLine: done = true; break; } } if(ok) *ok = true; return out; } } pushpin-1.39.1/src/cpp/proxy/routesfile.h000066400000000000000000000017761457610542000203760ustar00rootroot00000000000000/* * Copyright (C) 2016-2022 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef ROUTESFILE_H #define ROUTESFILE_H #include #include #include namespace RoutesFile { class RouteSection { public: QString value; QMultiHash props; }; QList parseLine(const QString &line, bool *ok = 0, QString *errorMessage = 0); } #endif pushpin-1.39.1/src/cpp/proxy/sockjsmanager.cpp000066400000000000000000000435321457610542000213730ustar00rootroot00000000000000/* * Copyright (C) 2015-2022 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "sockjsmanager.h" #include #include #include #include #include #include #include #include #include #include "qtcompat.h" #include "log.h" #include "bufferlist.h" #include "zhttprequest.h" #include "zwebsocket.h" #include "sockjssession.h" using std::map; #define MAX_REQUEST_BODY 100000 const char *iframeHtmlTemplate = "\n" "\n" "\n" " \n" " \n" " \n" " \n" "\n" "\n" "

Don't panic!

\n" "

This is a SockJS hidden iframe. It's used for cross domain magic.

\n" "\n" "\n"; static QByteArray serializeJsonString(const QString &s) { QByteArray tmp = QJsonDocument(QJsonArray::fromVariantList(QVariantList() << s)).toJson(QJsonDocument::Compact); assert(tmp.length() >= 4); assert(tmp[0] == '[' && tmp[tmp.length() - 1] == ']'); assert(tmp[1] == '"' && tmp[tmp.length() - 2] == '"'); return tmp.mid(1, tmp.length() - 2); } class SockJsManager::Private : public QObject { Q_OBJECT public: class Session { public: enum Type { Http, WebSocket }; Private *owner; Type type; ZhttpRequest *req; ZWebSocket *sock; BufferList reqBody; QByteArray path; QByteArray jsonpCallback; QUrl asUri; DomainMap::Entry route; QByteArray sid; QByteArray lastPart; bool pending; SockJsSession *ext; QTimer *timer; QVariant closeValue; Session(Private *_owner) : owner(_owner), req(0), sock(0), pending(false), ext(0), timer(0) { } ~Session() { if(req) { owner->reqConnectionMap.erase(req); delete req; } owner->wsConnectionMap.erase(sock); delete sock; if(timer) { timer->disconnect(owner); timer->setParent(0); timer->deleteLater(); } } }; struct WSConnections { Connection closedConnection; Connection errorConnection; }; struct ZhttpReqConnections{ Connection readyReadConnection; Connection bytesWrittenConnection; Connection errorConnection; }; SockJsManager *q; QSet sessions; QHash sessionsByRequest; QHash sessionsBySocket; QHash sessionsById; QHash sessionsByExt; QHash sessionsByTimer; QList pendingSessions; QByteArray iframeHtml; QByteArray iframeHtmlEtag; QSet discardedRequests; map reqConnectionMap; map wsConnectionMap; Private(SockJsManager *_q, const QString &sockJsUrl) : QObject(_q), q(_q) { iframeHtml = QString(iframeHtmlTemplate).arg(sockJsUrl).toUtf8(); iframeHtmlEtag = '\"' + QCryptographicHash::hash(iframeHtml, QCryptographicHash::Md5).toHex() + '\"'; } ~Private() { qDeleteAll(discardedRequests); while(!pendingSessions.isEmpty()) removeSession(pendingSessions.takeFirst()); assert(sessions.isEmpty()); } void removeSession(Session *s) { // can't remove unless unlinked assert(!s->ext); // note: this method assumes the session has already been removed // from pendingSessions if needed if(s->req) sessionsByRequest.remove(s->req); if(s->sock) sessionsBySocket.remove(s->sock); if(!s->sid.isEmpty()) sessionsById.remove(s->sid); if(s->ext) sessionsByExt.remove(s->ext); if(s->timer) sessionsByTimer.remove(s->timer); sessions.remove(s); delete s; } void unlink(SockJsSession *ext) { Session *s = sessionsByExt.value(ext); assert(s); sessionsByExt.remove(s->ext); s->ext = 0; if(s->closeValue.isValid()) { // if there's a close value, hang around for a little bit s->timer = new QTimer(this); QObject::connect(s->timer, &QTimer::timeout, [this, timer=s->timer]() { this->timer_timeout(timer); }); s->timer->setSingleShot(true); sessionsByTimer.insert(s->timer, s); s->timer->start(5000); } else removeSession(s); } void setLinger(SockJsSession *ext, const QVariant &closeValue) { Session *s = sessionsByExt.value(ext); assert(s); s->closeValue = closeValue; } void startHandleRequest(ZhttpRequest *req, int basePathStart, const QByteArray &asPath, const DomainMap::Entry &route) { Session *s = new Session(this); s->req = req; QUrl uri = req->requestUri(); QByteArray encPath = uri.path(QUrl::FullyEncoded).toUtf8(); s->path = encPath.mid(basePathStart); QUrlQuery query(uri); QList parts = s->path.split('/'); if(!parts.isEmpty() && parts.last().startsWith("jsonp")) { if(query.hasQueryItem("callback")) { s->jsonpCallback = query.queryItemValue("callback").toUtf8(); query.removeAllQueryItems("callback"); } else if(query.hasQueryItem("c")) { s->jsonpCallback = query.queryItemValue("c").toUtf8(); query.removeAllQueryItems("c"); } } s->asUri = uri; s->asUri.setScheme((s->asUri.scheme() == "https") ? "wss" : "ws"); if(!asPath.isEmpty()) s->asUri.setPath(QString::fromUtf8(asPath), QUrl::StrictMode); else s->asUri.setPath(QString::fromUtf8(encPath.mid(0, basePathStart)), QUrl::StrictMode); s->route = route; reqConnectionMap[req] = { req->readyRead.connect(boost::bind(&Private::req_readyRead, this, req)), req->bytesWritten.connect(boost::bind(&Private::req_bytesWritten, this, boost::placeholders::_1, req)), req->error.connect(boost::bind(&Private::req_error, this, req)) }; sessions += s; sessionsByRequest.insert(s->req, s); processRequestInput(s); } void startHandleSocket(ZWebSocket *sock, int basePathStart, const QByteArray &asPath, const DomainMap::Entry &route) { Session *s = new Session(this); s->sock = sock; QByteArray encPath = sock->requestUri().path(QUrl::FullyEncoded).toUtf8(); s->path = encPath.mid(basePathStart); s->asUri = sock->requestUri(); if(!asPath.isEmpty()) s->asUri.setPath(QString::fromUtf8(asPath), QUrl::StrictMode); else s->asUri.setPath(QString::fromUtf8(encPath.mid(0, basePathStart) + "/websocket"), QUrl::StrictMode); s->route = route; wsConnectionMap[sock] = { sock->closed.connect(boost::bind(&Private::sock_closed, this, sock)), sock->error.connect(boost::bind(&Private::sock_error, this, sock)) }; sessions += s; sessionsBySocket.insert(s->sock, s); handleSocket(s); } void applyHeaders(const HttpHeaders &in, HttpHeaders *out) { *out += HttpHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"); QByteArray origin; if(in.contains("Origin")) origin = in.get("Origin"); else origin = "*"; *out += HttpHeader("Access-Control-Allow-Origin", origin); *out += HttpHeader("Access-Control-Allow-Credentials", "true"); } void respond(ZhttpRequest *req, int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body) { HttpHeaders outHeaders = headers; applyHeaders(req->requestHeaders(), &outHeaders); req->beginResponse(code, reason, outHeaders); req->writeBody(body); req->endBody(); } void respondEmpty(ZhttpRequest *req) { HttpHeaders headers; headers += HttpHeader("Content-Type", "text/plain"); // workaround FF issue. see sockjs spec. respond(req, 204, "No Content", headers, QByteArray()); } void respondOk(ZhttpRequest *req, const QVariant &data, const QByteArray &prefix = QByteArray(), const QByteArray &jsonpCallback = QByteArray()) { HttpHeaders headers; if(!jsonpCallback.isEmpty()) headers += HttpHeader("Content-Type", "application/javascript"); else headers += HttpHeader("Content-Type", "text/plain"); QByteArray body; if(data.isValid()) { QJsonDocument doc; if(typeId(data) == QMetaType::QVariantMap) doc = QJsonDocument(QJsonObject::fromVariantMap(data.toMap())); else // List doc = QJsonDocument(QJsonArray::fromVariantList(data.toList())); body = doc.toJson(QJsonDocument::Compact); } if(!prefix.isEmpty()) body.prepend(prefix); if(!jsonpCallback.isEmpty()) { QByteArray encBody = serializeJsonString(QString::fromUtf8(body)); body = "/**/" + jsonpCallback + '(' + encBody + ");\n"; } else if(!body.isEmpty()) body += "\n"; // newline is required respond(req, 200, "OK", headers, body); } void respondOk(ZhttpRequest *req, const QString &str, const QByteArray &jsonpCallback = QByteArray()) { HttpHeaders headers; if(!jsonpCallback.isEmpty()) headers += HttpHeader("Content-Type", "application/javascript"); else headers += HttpHeader("Content-Type", "text/plain"); QByteArray body; if(!jsonpCallback.isEmpty()) { QByteArray encBody = serializeJsonString(str); body = "/**/" + jsonpCallback + '(' + encBody + ");\n"; } else body = str.toUtf8(); respond(req, 200, "OK", headers, body); } void respondError(ZhttpRequest *req, int code, const QByteArray &reason, const QString &message, bool discard = false) { // if discarded, manager takes ownership of req to handle sending if(discard) { discardedRequests += req; reqConnectionMap[req] = { req->readyRead.connect(boost::bind(&Private::req_readyRead, this, req)), req->bytesWritten.connect(boost::bind(&Private::req_bytesWritten, this, boost::placeholders::_1, req)), req->error.connect(boost::bind(&Private::req_error, this, req)) }; } HttpHeaders headers; headers += HttpHeader("Content-Type", "text/plain"); respond(req, code, reason, headers, message.toUtf8() + '\n'); } void respondError(ZWebSocket *sock, int code, const QByteArray &reason, const QString &message) { HttpHeaders headers; headers += HttpHeader("Content-Type", "text/plain"); sock->respondError(code, reason, headers, message.toUtf8() + '\n'); } void processRequestInput(Session *s) { s->reqBody += s->req->readBody(MAX_REQUEST_BODY - s->reqBody.size() + 1); if(s->reqBody.size() > MAX_REQUEST_BODY) { respondError(s->req, 400, "Bad Request", "Request too large."); return; } if(s->req->isInputFinished()) handleRequest(s); } void handleRequest(Session *s) { QString method = s->req->requestMethod(); log_debug("sockjs request: path=[%s], asUri=[%s]", s->path.data(), s->asUri.toEncoded().data()); if(method == "OPTIONS") { respondEmpty(s->req); } else if(method == "GET" && s->path == "/info") { quint32 x = QRandomGenerator::global()->generate(); QVariantMap out; out["websocket"] = true; out["origins"] = QVariantList() << QString("*:*"); out["cookie_needed"] = false; out["entropy"] = x; respondOk(s->req, out); } else if(method == "GET" && s->path.startsWith("/iframe") && s->path.endsWith(".html")) { HttpHeaders headers; headers += HttpHeader("ETag", iframeHtmlEtag); QByteArray ifNoneMatch = s->req->requestHeaders().get("If-None-Match"); if(ifNoneMatch == iframeHtmlEtag) { respond(s->req, 304, "Not Modified", headers, QByteArray()); } else { headers += HttpHeader("Content-Type", "text/html; charset=UTF-8"); headers += HttpHeader("Cache-Control", "public, max-age=31536000"); respond(s->req, 200, "OK", headers, iframeHtml); } } else { QList parts = s->path.mid(1).split('/'); if(parts.count() == 3) { QByteArray sid = parts[1]; QByteArray lastPart = parts[2]; Session *existing = sessionsById.value(sid); if(existing) { if(existing->ext) { // give to external session ZhttpRequest *req = s->req; QByteArray body = s->reqBody.toByteArray(); QByteArray jsonpCallback = s->jsonpCallback; reqConnectionMap.erase(req); s->req = 0; removeSession(s); existing->ext->handleRequest(req, jsonpCallback, lastPart, body); } else { if(existing->closeValue.isValid()) { respondOk(s->req, existing->closeValue, "c", s->jsonpCallback); } else { QVariantList out; out += 2010; out += QString("Another connection still open"); respondOk(s->req, out, "c", s->jsonpCallback); } } return; } if((method == "POST" && lastPart == "xhr") || ((method == "GET" || method == "POST") && lastPart == "jsonp")) { if(lastPart == "jsonp" && s->jsonpCallback.isEmpty()) { respondError(s->req, 400, "Bad Request", "Bad Request"); return; } s->sid = sid; s->lastPart = lastPart; sessionsById.insert(s->sid, s); s->pending = true; pendingSessions += s; q->sessionReady(); return; } } respondError(s->req, 404, "Not Found", "Not Found"); } } void handleSocket(Session *s) { if(s->path == "/websocket") { s->pending = true; pendingSessions += s; q->sessionReady(); return; } else { QList parts = s->path.mid(1).split('/'); if(parts.count() == 3) { QByteArray sid = parts[1]; QByteArray lastPart = parts[2]; s->sid = sid; s->lastPart = lastPart; s->pending = true; pendingSessions += s; q->sessionReady(); return; } respondError(s->sock, 404, "Not Found", "Not Found"); } } SockJsSession *takeNext() { Session *s = 0; while(!s) { if(pendingSessions.isEmpty()) return 0; s = pendingSessions.takeFirst(); s->pending = false; if(!s->req && !s->sock) { // this means the object was a zombie. clean up and take next removeSession(s); s = 0; continue; } } s->ext = new SockJsSession; if(s->req) { assert(!s->sid.isEmpty()); assert(!s->lastPart.isEmpty()); s->ext->setupServer(q, s->req, s->jsonpCallback, s->asUri, s->sid, s->lastPart, s->reqBody.toByteArray(), s->route); reqConnectionMap.erase(s->req); sessionsByRequest.remove(s->req); s->req = 0; } else // s->sock { if(!s->sid.isEmpty()) { assert(!s->lastPart.isEmpty()); s->ext->setupServer(q, s->sock, s->asUri, s->sid, s->lastPart, s->route); } else s->ext->setupServer(q, s->sock, s->asUri, s->route); wsConnectionMap.erase(s->sock); sessionsBySocket.remove(s->sock); s->sock = 0; } sessionsByExt.insert(s->ext, s); s->ext->startServer(); return s->ext; } private: void req_readyRead(ZhttpRequest *req) { // for a request to have been discardable, we must have read the // entire input already and handed to the session assert(!discardedRequests.contains(req)); Session *s = sessionsByRequest.value(req); assert(s); processRequestInput(s); } void req_bytesWritten(int count, ZhttpRequest *req) { Q_UNUSED(count); if(discardedRequests.contains(req)) { if(req->isFinished()) { discardedRequests.remove(req); reqConnectionMap.erase(req); delete req; } return; } Session *s = sessionsByRequest.value(req); assert(s); if(req->isFinished()) { assert(!s->pending); removeSession(s); } } void req_error(ZhttpRequest *req) { if(discardedRequests.contains(req)) { discardedRequests.remove(req); reqConnectionMap.erase(req); delete req; return; } Session *s = sessionsByRequest.value(req); assert(s); if(s->pending) s->req = 0; else removeSession(s); } void sock_closed(ZWebSocket *sock) { Session *s = sessionsBySocket.value(sock); assert(s); if(s->pending) s->sock = 0; else removeSession(s); } void sock_error(ZWebSocket *sock) { Session *s = sessionsBySocket.value(sock); assert(s); if(s->pending) s->sock = 0; else removeSession(s); } private: void timer_timeout(QTimer *timer) { Session *s = sessionsByTimer.value(timer); assert(s); assert(!s->pending); removeSession(s); } }; SockJsManager::SockJsManager(const QString &sockJsUrl, QObject *parent) : QObject(parent) { d = new Private(this, sockJsUrl); } SockJsManager::~SockJsManager() { delete d; } void SockJsManager::giveRequest(ZhttpRequest *req, int basePathStart, const QByteArray &asPath, const DomainMap::Entry &route) { d->startHandleRequest(req, basePathStart, asPath, route); } void SockJsManager::giveSocket(ZWebSocket *sock, int basePathStart, const QByteArray &asPath, const DomainMap::Entry &route) { d->startHandleSocket(sock, basePathStart, asPath, route); } SockJsSession *SockJsManager::takeNext() { return d->takeNext(); } void SockJsManager::unlink(SockJsSession *sess) { d->unlink(sess); } void SockJsManager::setLinger(SockJsSession *ext, const QVariant &closeValue) { d->setLinger(ext, closeValue); } void SockJsManager::respondOk(ZhttpRequest *req, const QVariant &data, const QByteArray &prefix, const QByteArray &jsonpCallback) { d->respondOk(req, data, prefix, jsonpCallback); } void SockJsManager::respondOk(ZhttpRequest *req, const QString &str, const QByteArray &jsonpCallback) { d->respondOk(req, str, jsonpCallback); } void SockJsManager::respondError(ZhttpRequest *req, int code, const QByteArray &reason, const QString &message, bool discard) { d->respondError(req, code, reason, message, discard); } void SockJsManager::respond(ZhttpRequest *req, int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body) { d->respond(req, code, reason, headers, body); } #include "sockjsmanager.moc" pushpin-1.39.1/src/cpp/proxy/sockjsmanager.h000066400000000000000000000042661457610542000210410ustar00rootroot00000000000000/* * Copyright (C) 2015-2017 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef SOCKJSMANAGER_H #define SOCKJSMANAGER_H #include #include "domainmap.h" #include #include using Signal = boost::signals2::signal; using Connection = boost::signals2::scoped_connection; class HttpHeaders; class ZhttpRequest; class ZWebSocket; class SockJsSession; class SockJsManager : public QObject { Q_OBJECT public: SockJsManager(const QString &sockJsUrl, QObject *parent = 0); ~SockJsManager(); void giveRequest(ZhttpRequest *req, int basePathStart, const QByteArray &asPath = QByteArray(), const DomainMap::Entry &route = DomainMap::Entry()); void giveSocket(ZWebSocket *sock, int basePathStart, const QByteArray &asPath = QByteArray(), const DomainMap::Entry &route = DomainMap::Entry()); SockJsSession *takeNext(); Signal sessionReady; private: class Private; friend class Private; Private *d; friend class SockJsSession; void unlink(SockJsSession *sess); void setLinger(SockJsSession *sess, const QVariant &closeValue); void respondOk(ZhttpRequest *req, const QVariant &data, const QByteArray &prefix = QByteArray(), const QByteArray &jsonpCallback = QByteArray()); void respondOk(ZhttpRequest *req, const QString &str, const QByteArray &jsonpCallback = QByteArray()); void respondError(ZhttpRequest *req, int code, const QByteArray &reason, const QString &message, bool discard = false); void respond(ZhttpRequest *req, int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body); }; #endif pushpin-1.39.1/src/cpp/proxy/sockjssession.cpp000066400000000000000000000653711457610542000214510ustar00rootroot00000000000000/* * Copyright (C) 2015-2021 Fanout, Inc. * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "sockjssession.h" #include #include #include #include #include #include #include #include "qtcompat.h" #include "log.h" #include "bufferlist.h" #include "packet/httprequestdata.h" #include "zhttprequest.h" #include "zwebsocket.h" #include "sockjsmanager.h" using std::map; #define BUFFER_SIZE 200000 #define KEEPALIVE_TIMEOUT 25 #define UNCONNECTED_TIMEOUT 5 class SockJsSession::Private : public QObject { Q_OBJECT public: enum Mode { Http, WebSocketFramed, WebSocketPassthrough }; class RequestItem { public: enum Type { Background, Connect, Accept, Reject, Send, // data from client Receive, // data to client ReceiveClose // close to client }; ZhttpRequest *req; QByteArray jsonpCallback; Type type; bool responded; QList sendFrames; int sendBytes; int receiveFrames; int receiveBytes; RequestItem(ZhttpRequest *_req, const QByteArray &_jsonpCallback, Type _type, bool _responded = false) : req(_req), jsonpCallback(_jsonpCallback), type(_type), responded(_responded), sendBytes(0), receiveFrames(0), receiveBytes(0) { } ~RequestItem() { delete req; } }; class WriteItem { public: enum Type { Transport, User }; Type type; int size; WriteItem(Type _type, int _size = 0) : type(_type), size(_size) { } }; struct WSConnections { Connection readyReadConnection; Connection framesWrittenConnection; Connection writeBytesChangedConnection; Connection closedConnection; Connection peerClosedConnection; Connection sockErrorConnection; }; struct ReqConnections { Connection bytesWrittenConnection; Connection errorConnection; }; SockJsSession *q; SockJsManager *manager; Mode mode; QByteArray sid; DomainMap::Entry route; HttpRequestData requestData; QHostAddress peerAddress; State state; bool errored; ErrorCondition errorCondition; ZhttpRequest *initialReq; QByteArray initialJsonpCallback; QByteArray initialLastPart; QByteArray initialBody; ZhttpRequest *req; ZWebSocket *sock; bool passThrough; QList inWrappedFrames; QList inFrames; QList outFrames; int inBytes; int pendingWrittenFrames; int pendingWrittenBytes; QList pendingWrites; QHash requests; QTimer *keepAliveTimer; int closeCode; QString closeReason; bool closeSent; bool peerClosed; int peerCloseCode; QString peerCloseReason; bool updating; map reqConnectionMap; WSConnections wsConnection; Private(SockJsSession *_q) : QObject(_q), q(_q), manager(0), mode((Mode)-1), state(WebSocket::Idle), errored(false), errorCondition(WebSocket::ErrorGeneric), initialReq(0), req(0), sock(0), inBytes(0), pendingWrittenFrames(0), pendingWrittenBytes(0), closeCode(-1), closeSent(false), peerClosed(false), peerCloseCode(-1), updating(false) { keepAliveTimer = new QTimer(this); connect(keepAliveTimer, &QTimer::timeout, this, &Private::keepAliveTimer_timeout); } ~Private() { keepAliveTimer->disconnect(this); keepAliveTimer->setParent(0); keepAliveTimer->deleteLater(); cleanup(); } void removeRequestItem(RequestItem *ri) { reqConnectionMap.erase(ri->req); requests.remove(ri->req); delete ri; } RequestItem *findFirstSendRequest() { QHashIterator it(requests); while(it.hasNext()) { it.next(); RequestItem *ri = it.value(); if(ri->type == RequestItem::Send) return ri; } return 0; } void cleanup() { keepAliveTimer->stop(); if(req) { RequestItem *ri = requests.value(req); assert(ri); // detach req from RequestItem reqConnectionMap.erase(ri->req); requests.remove(ri->req); ri->req = 0; delete ri; // discard=true to let manager take over manager->respondError(req, 410, "Gone", "Session terminated", true); req = 0; } QHashIterator it(requests); while(it.hasNext()) { it.next(); delete it.value(); } requests.clear(); wsConnection = WSConnections(); delete sock; sock = 0; if(manager) { manager->unlink(q); manager = 0; } } void setup() { if(mode == Http) { req = initialReq; initialReq = 0; QByteArray jsonpCallback = initialJsonpCallback; initialJsonpCallback.clear(); // don't need these things initialLastPart.clear(); initialBody.clear(); requests.insert(req, new RequestItem(req, jsonpCallback, RequestItem::Connect)); reqConnectionMap[req] = { req->bytesWritten.connect(boost::bind(&Private::req_bytesWritten, this, boost::placeholders::_1, req)), req->error.connect(boost::bind(&Private::req_error, this, req)) }; } else { wsConnection = WSConnections{ sock->readyRead.connect(boost::bind(&Private::sock_readyRead, this)), sock->framesWritten.connect(boost::bind(&Private::sock_framesWritten, this, boost::placeholders::_1, boost::placeholders::_2)), sock->writeBytesChanged.connect(boost::bind(&Private::sock_writeBytesChanged, this)), sock->closed.connect(boost::bind(&Private::sock_closed, this)), sock->peerClosed.connect(boost::bind(&Private::sock_peerClosed, this)), sock->error.connect(boost::bind(&Private::sock_error, this)) }; } } void startServer() { state = Connecting; } void respondOk(ZhttpRequest *req, const QVariant &data, const QByteArray &prefix = QByteArray(), const QByteArray &jsonpCallback = QByteArray()) { manager->respondOk(req, data, prefix, jsonpCallback); } void respondOk(ZhttpRequest *req, const QString &str, const QByteArray &jsonpCallback = QByteArray()) { manager->respondOk(req, str, jsonpCallback); } void respondError(ZhttpRequest *req, int code, const QByteArray &reason, const QString &message) { manager->respondError(req, code, reason, message); } void respond(ZhttpRequest *req, int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body) { manager->respond(req, code, reason, headers, body); } void handleRequest(ZhttpRequest *_req, const QByteArray &jsonpCallback, const QByteArray &lastPart, const QByteArray &body) { reqConnectionMap[_req] = { _req->bytesWritten.connect(boost::bind(&Private::req_bytesWritten, this, boost::placeholders::_1, _req)), _req->error.connect(boost::bind(&Private::req_error, this, _req)) }; if(lastPart == "xhr" || lastPart == "jsonp") { if(req) { QVariantList out; out += 2010; out += QString("Another connection still open"); requests.insert(_req, new RequestItem(_req, jsonpCallback, RequestItem::Background, true)); respondOk(_req, out, "c", jsonpCallback); return; } if(peerClosed) { QVariantList out; out += 3000; out += QString("Client already closed connection"); requests.insert(_req, new RequestItem(_req, jsonpCallback, RequestItem::Background, true)); respondOk(_req, out, "c", jsonpCallback); return; } req = _req; requests.insert(req, new RequestItem(req, jsonpCallback, RequestItem::Receive)); keepAliveTimer->start(KEEPALIVE_TIMEOUT * 1000); tryWrite(); } else if(lastPart == "xhr_send" || lastPart == "jsonp_send") { // only allow one outstanding send request at a time if(findFirstSendRequest()) { requests.insert(_req, new RequestItem(_req, jsonpCallback, RequestItem::Background, true)); respondError(_req, 400, "Bad Request", "Already sending"); return; } QByteArray param; if(_req->requestMethod() == "POST") { if(lastPart == "xhr_send") { // assume json param = body; } else // jsonp_send { // assume form encoded foreach(const QByteArray &kv, body.split('&')) { int at = kv.indexOf('='); if(at == -1) continue; if(QUrl::fromPercentEncoding(kv.mid(0, at)) == "d") { param = QUrl::fromPercentEncoding(kv.mid(at + 1)).toUtf8(); break; } } } } else // GET { QUrlQuery query(_req->requestUri()); param = query.queryItemValue("d").toUtf8(); } QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(param, &error); if(error.error != QJsonParseError::NoError || !doc.isArray()) { requests.insert(_req, new RequestItem(_req, jsonpCallback, RequestItem::Background, true)); respondError(_req, 400, "Bad Request", "Payload expected"); return; } QVariantList messages = doc.array().toVariantList(); QList frames; int bytes = 0; foreach(const QVariant &vmessage, messages) { if(typeId(vmessage) != QMetaType::QString) { requests.insert(_req, new RequestItem(_req, jsonpCallback, RequestItem::Background, true)); respondError(_req, 400, "Bad Request", "Payload expected"); return; } QByteArray data = vmessage.toString().toUtf8(); if(data.size() > BUFFER_SIZE) { requests.insert(_req, new RequestItem(_req, jsonpCallback, RequestItem::Background, true)); respondError(_req, 400, "Bad Request", "Message too large"); return; } frames += Frame(Frame::Text, data, false); bytes += data.size(); } if(frames.isEmpty()) { requests.insert(_req, new RequestItem(_req, jsonpCallback, RequestItem::Background, true)); respondOk(_req, QString("ok"), jsonpCallback); return; } RequestItem *ri = new RequestItem(_req, jsonpCallback, RequestItem::Send); requests.insert(_req, ri); ri->sendFrames = frames; ri->sendBytes = bytes; tryRead(); } else { requests.insert(_req, new RequestItem(_req, jsonpCallback, RequestItem::Background, true)); respondError(_req, 404, "Not Found", "Not Found"); } } void accept(const QByteArray &reason, const HttpHeaders &headers) { if(errored) return; if(mode == Http) { assert(req); RequestItem *ri = requests.value(req); assert(ri && !ri->responded); // note: reason/headers don't have meaning with sockjs http ri->type = RequestItem::Accept; ri->responded = true; respondOk(req, QVariant(), "o", ri->jsonpCallback); } else { assert(sock); sock->respondSuccess(reason, headers); state = Connected; if(mode == WebSocketFramed) { Frame f(Frame::Text, "o", false); pendingWrites += WriteItem(WriteItem::Transport); sock->writeFrame(f); keepAliveTimer->start(KEEPALIVE_TIMEOUT * 1000); } } } void reject(int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body) { if(errored) return; if(mode == Http) { assert(req); RequestItem *ri = requests.value(req); assert(ri && !ri->responded); ri->type = RequestItem::Reject; ri->responded = true; respond(req, code, reason, headers, body); } else { assert(sock); sock->respondError(code, reason, headers, body); } } void writeFrame(const Frame &frame) { assert(state != Closing); if(mode == WebSocketPassthrough) { sock->writeFrame(frame); } else { if(frame.type != Frame::Text && frame.type != Frame::Binary) { ++pendingWrittenFrames; pendingWrittenBytes += frame.data.size(); update(); return; } if(mode == Http) { int outSize = 0; foreach(const Frame &f, outFrames) outSize += f.data.size(); if(outSize + frame.data.size() > BUFFER_SIZE) { errored = true; errorCondition = ErrorGeneric; update(); return; } outFrames += frame; tryWrite(); } else // WebSocketFramed { QVariantList messages; messages += QString::fromUtf8(frame.data); QByteArray arrayJson = QJsonDocument(QJsonArray::fromVariantList(messages)).toJson(QJsonDocument::Compact); Frame f(Frame::Text, "a" + arrayJson, false); pendingWrites += WriteItem(WriteItem::User, frame.data.size()); sock->writeFrame(f); } } } Frame readFrame() { if(mode == Http || mode == WebSocketFramed) { Frame f = inFrames.takeFirst(); inBytes -= f.data.size(); update(); return f; } else { return sock->readFrame(); } } void close(int code, const QString &reason) { assert(state != Closing); state = Closing; closeCode = code; closeReason = reason; if(mode == Http) { if(peerClosed) { state = Idle; applyLinger(); cleanup(); QMetaObject::invokeMethod(this, "doClosed", Qt::QueuedConnection); } else tryWrite(); } else { assert(sock); sock->close(closeCode, closeReason); } } void tryWrite() { if(!req || closeSent) return; RequestItem *ri = requests.value(req); assert(ri); if(ri->responded) return; QVariantList messages; int frames = 0; int bytes = 0; while(!outFrames.isEmpty()) { // find end int end = 0; for(; end < outFrames.count(); ++end) { if(!outFrames[end].more) break; } if(end >= outFrames.count()) break; Frame first = outFrames[0]; BufferList bufs; for(int n = 0; n <= end; ++n) { Frame f = outFrames.takeFirst(); ++frames; bytes += f.data.size(); bufs += f.data; } assert(first.type == Frame::Text || first.type == Frame::Binary); QByteArray data = bufs.toByteArray(); pendingWrites += WriteItem(WriteItem::User, data.size()); messages += QString::fromUtf8(data); } if(bytes > 0) { QPointer self = this; q->writeBytesChanged(); if(!self) return; } ri->receiveFrames = frames; ri->receiveBytes = bytes; if(!messages.isEmpty()) { ri->responded = true; respondOk(req, messages, "a", ri->jsonpCallback); keepAliveTimer->stop(); } else if(state == Closing) { closeSent = true; QVariant closeValue = applyLinger(); ri->type = RequestItem::ReceiveClose; ri->responded = true; respondOk(req, closeValue, "c", ri->jsonpCallback); } } bool tryRead() { QPointer self = this; if(mode == Http) { QList sendRequests; QHashIterator it(requests); while(it.hasNext()) { it.next(); RequestItem *ri = it.value(); if(ri->type == RequestItem::Send && !ri->responded) sendRequests += ri; } bool emitReadyRead = false; foreach(RequestItem *ri, sendRequests) { assert(!ri->sendFrames.isEmpty()); if(inBytes + ri->sendFrames.first().data.size() > BUFFER_SIZE) break; Frame f = ri->sendFrames.takeFirst(); ri->sendBytes -= f.data.size(); if(ri->sendFrames.isEmpty()) { assert(ri->sendBytes == 0); ri->responded = true; respondOk(ri->req, QString("ok"), ri->jsonpCallback); } inFrames += f; inBytes += f.data.size(); emitReadyRead = true; } if(emitReadyRead) { q->readyRead(); if(!self) return false; } } else if(mode == WebSocketFramed) { bool error = false; bool emitReadyRead = false; while(inBytes < BUFFER_SIZE) { int end = 0; for(; end < inWrappedFrames.count(); ++end) { if(!inWrappedFrames[end].more) break; } if(end >= inWrappedFrames.count()) { if(sock->framesAvailable() == 0) break; Frame f = sock->readFrame(); // allow a larger temporary read size due to wrapping if(f.data.size() > BUFFER_SIZE * 2) { error = true; break; } inWrappedFrames += f; continue; } int size = 0; for(int n = 0; n <= end; ++n) size += inWrappedFrames[n].data.size(); // allow a larger temporary read size due to wrapping if(size > BUFFER_SIZE * 2) { error = true; break; } Frame first = inWrappedFrames[0]; BufferList bufs; for(int n = 0; n <= end; ++n) { Frame f = inWrappedFrames.takeFirst(); bufs += f.data; } if(first.type != Frame::Text && first.type != Frame::Binary) continue; QByteArray data = bufs.toByteArray(); QJsonParseError e; QJsonDocument doc = QJsonDocument::fromJson(data, &e); if(e.error != QJsonParseError::NoError || !doc.isArray()) { error = true; break; } QVariantList messages = doc.array().toVariantList(); QList frames; int bytes = 0; foreach(const QVariant &vmessage, messages) { if(typeId(vmessage) != QMetaType::QString) { error = true; break; } data = vmessage.toString().toUtf8(); if(data.size() > BUFFER_SIZE) { error = true; break; } frames += Frame(Frame::Text, data, false); bytes += data.size(); } if(error) break; // note: inBytes may exceed BUFFER_SIZE at this point, but // it shouldn't be by more than double inFrames += frames; inBytes += bytes; emitReadyRead = true; } if(error) { state = Idle; cleanup(); q->error(); // stop signals return false; } if(emitReadyRead) { q->readyRead(); if(!self) return false; } } return true; } void update() { if(!updating) { updating = true; QMetaObject::invokeMethod(this, "doUpdate", Qt::QueuedConnection); } } void handleWritten(int count, int contentBytes) { if(mode == Http || mode == WebSocketFramed) { int newCount = 0; int newContentBytes = 0; for(int n = 0; n < count; ++n) { WriteItem i = pendingWrites.takeFirst(); if(i.type == WriteItem::User) { ++newCount; newContentBytes += i.size; } } count = newCount; contentBytes = newContentBytes; count += pendingWrittenFrames; contentBytes += pendingWrittenBytes; pendingWrittenFrames = 0; pendingWrittenBytes = 0; } q->framesWritten(count, contentBytes); } QVariant applyLinger() { QVariantList closeValue; if(closeCode != -1) closeValue += closeCode; else closeValue += 0; if(closeCode != -1 && !closeReason.isEmpty()) closeValue += closeReason; else closeValue += QString("Connection closed"); manager->setLinger(q, closeValue); return closeValue; } void req_bytesWritten(int count, ZhttpRequest *_req) { Q_UNUSED(count); RequestItem *ri = requests.value(_req); assert(ri); if(!_req->isFinished()) return; if(ri->type == RequestItem::Accept) { assert(_req == req); state = Connected; req = 0; removeRequestItem(ri); keepAliveTimer->start(UNCONNECTED_TIMEOUT * 1000); } else { if(_req == req) { req = 0; if(ri->type == RequestItem::Reject) { state = Idle; removeRequestItem(ri); cleanup(); q->closed(); return; } else if(ri->type == RequestItem::Receive) { int count = ri->receiveFrames; int contentBytes = ri->receiveBytes; removeRequestItem(ri); keepAliveTimer->start(UNCONNECTED_TIMEOUT * 1000); handleWritten(count, contentBytes); return; } else if(ri->type == RequestItem::ReceiveClose) { state = Idle; removeRequestItem(ri); cleanup(); q->closed(); return; } } removeRequestItem(ri); } } void req_error(ZhttpRequest *_req) { RequestItem *ri = requests.value(_req); assert(ri); if(ri->type == RequestItem::Connect || ri->type == RequestItem::Accept || ri->type == RequestItem::Reject || ri->type == RequestItem::Receive || ri->type == RequestItem::ReceiveClose) { assert(_req == req); // disconnect while long-polling means close, not error bool close = false; if(ri->type == RequestItem::Receive && !ri->responded) close = (_req->errorCondition() == ZhttpRequest::ErrorDisconnected); req = 0; removeRequestItem(ri); if(close && !peerClosed) { peerClosed = true; q->peerClosed(); return; } state = Idle; cleanup(); if(close) q->closed(); else q->error(); } else { removeRequestItem(ri); } } void sock_readyRead() { if(mode == WebSocketFramed) { tryRead(); } else // WebSocketPassthrough { q->readyRead(); } } void sock_framesWritten(int count, int contentBytes) { handleWritten(count, contentBytes); } void sock_writeBytesChanged() { q->writeBytesChanged(); } void sock_peerClosed() { peerCloseCode = sock->peerCloseCode(); peerCloseReason = sock->peerCloseReason(); q->peerClosed(); } void sock_closed() { peerCloseCode = sock->peerCloseCode(); peerCloseReason = sock->peerCloseReason(); state = Idle; cleanup(); q->closed(); } void sock_error() { state = Idle; errorCondition = sock->errorCondition(); cleanup(); q->error(); } private slots: void doUpdate() { updating = false; if(errored) { state = Idle; cleanup(); q->error(); return; } if(mode == Http || mode == WebSocketFramed) { if(!tryRead()) return; if(pendingWrittenFrames > 0) { int count = pendingWrittenFrames; int contentBytes = pendingWrittenBytes; pendingWrittenFrames = 0; pendingWrittenBytes = 0; q->framesWritten(count, contentBytes); } } } void doClosed() { q->closed(); } void keepAliveTimer_timeout() { assert(mode != WebSocketPassthrough); if(mode == Http) { if(req) { RequestItem *ri = requests.value(req); assert(ri && !ri->responded); ri->responded = true; respondOk(req, QVariant(), "h", ri->jsonpCallback); } else { // timeout while unconnected state = Idle; cleanup(); q->error(); } } else { assert(sock); Frame f(Frame::Text, "h", false); pendingWrites += WriteItem(WriteItem::Transport); sock->writeFrame(f); } } }; SockJsSession::SockJsSession(QObject *parent) : WebSocket(parent) { d = new Private(this); } SockJsSession::~SockJsSession() { delete d; } QByteArray SockJsSession::sid() const { return d->sid; } DomainMap::Entry SockJsSession::route() const { return d->route; } QHostAddress SockJsSession::peerAddress() const { return d->peerAddress; } void SockJsSession::setConnectHost(const QString &host) { Q_UNUSED(host); // this class is server only assert(0); } void SockJsSession::setConnectPort(int port) { Q_UNUSED(port); // this class is server only assert(0); } void SockJsSession::setIgnorePolicies(bool on) { Q_UNUSED(on); // this class is server only assert(0); } void SockJsSession::setTrustConnectHost(bool on) { Q_UNUSED(on); // this class is server only assert(0); } void SockJsSession::setIgnoreTlsErrors(bool on) { Q_UNUSED(on); // this class is server only assert(0); } void SockJsSession::start(const QUrl &uri, const HttpHeaders &headers) { Q_UNUSED(uri); Q_UNUSED(headers); // this class is server only assert(0); } void SockJsSession::respondSuccess(const QByteArray &reason, const HttpHeaders &headers) { d->accept(reason, headers); } void SockJsSession::respondError(int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body) { d->reject(code, reason, headers, body); } WebSocket::State SockJsSession::state() const { return d->state; } QUrl SockJsSession::requestUri() const { return d->requestData.uri; } HttpHeaders SockJsSession::requestHeaders() const { return d->requestData.headers; } int SockJsSession::responseCode() const { // this class is server only assert(0); return -1; } QByteArray SockJsSession::responseReason() const { // this class is server only assert(0); return QByteArray(); } HttpHeaders SockJsSession::responseHeaders() const { // this class is server only assert(0); return HttpHeaders(); } QByteArray SockJsSession::responseBody() const { // this class is server only assert(0); return QByteArray(); } int SockJsSession::framesAvailable() const { if(d->mode == Private::Http || d->mode == Private::WebSocketFramed) { return d->inFrames.count(); } else { return d->sock->framesAvailable(); } } int SockJsSession::writeBytesAvailable() const { if(d->mode == Private::WebSocketFramed || d->mode == Private::WebSocketPassthrough) { return d->sock->writeBytesAvailable(); } else { int outSize = 0; foreach(const Frame &f, d->outFrames) outSize += f.data.size(); if(outSize < BUFFER_SIZE) return BUFFER_SIZE - outSize; else return 0; } } int SockJsSession::peerCloseCode() const { return d->peerCloseCode; } QString SockJsSession::peerCloseReason() const { return d->peerCloseReason; } WebSocket::ErrorCondition SockJsSession::errorCondition() const { return d->errorCondition; } void SockJsSession::writeFrame(const Frame &frame) { d->writeFrame(frame); } WebSocket::Frame SockJsSession::readFrame() { return d->readFrame(); } void SockJsSession::close(int code, const QString &reason) { d->close(code, reason); } void SockJsSession::setupServer(SockJsManager *manager, ZhttpRequest *req, const QByteArray &jsonpCallback, const QUrl &asUri, const QByteArray &sid, const QByteArray &lastPart, const QByteArray &body, const DomainMap::Entry &route) { d->manager = manager; d->mode = Private::Http; d->sid = sid; d->requestData.uri = asUri; d->requestData.headers = req->requestHeaders(); // we're not forwarding the request content so ignore this d->requestData.headers.removeAll("Content-Length"); d->peerAddress = req->peerAddress(); d->route = route; d->initialReq = req; d->initialJsonpCallback = jsonpCallback; d->initialLastPart = lastPart; d->initialBody = body; d->setup(); } void SockJsSession::setupServer(SockJsManager *manager, ZWebSocket *sock, const QUrl &asUri, const DomainMap::Entry &route) { d->manager = manager; d->mode = Private::WebSocketPassthrough; d->requestData.uri = asUri; d->requestData.headers = sock->requestHeaders(); d->peerAddress = sock->peerAddress(); d->route = route; d->sock = sock; d->setup(); } void SockJsSession::setupServer(SockJsManager *manager, ZWebSocket *sock, const QUrl &asUri, const QByteArray &sid, const QByteArray &lastPart, const DomainMap::Entry &route) { Q_UNUSED(lastPart); d->manager = manager; d->mode = Private::WebSocketFramed; d->sid = sid; d->requestData.uri = asUri; d->requestData.headers = sock->requestHeaders(); d->peerAddress = sock->peerAddress(); d->route = route; d->sock = sock; d->setup(); } void SockJsSession::startServer() { d->startServer(); } void SockJsSession::handleRequest(ZhttpRequest *req, const QByteArray &jsonpCallback, const QByteArray &lastPart, const QByteArray &body) { d->handleRequest(req, jsonpCallback, lastPart, body); } #include "sockjssession.moc" pushpin-1.39.1/src/cpp/proxy/sockjssession.h000066400000000000000000000061311457610542000211030ustar00rootroot00000000000000/* * Copyright (C) 2015 Fanout, Inc. * Copyright (C) 2023 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef SOCKJSSESSION_H #define SOCKJSSESSION_H #include #include #include #include "httpheaders.h" #include "websocket.h" #include "domainmap.h" #include using Connection = boost::signals2::scoped_connection; class ZhttpRequest; class ZWebSocket; class SockJsManager; class SockJsSession : public WebSocket { Q_OBJECT public: ~SockJsSession(); QByteArray sid() const; DomainMap::Entry route() const; // reimplemented virtual QHostAddress peerAddress() const; virtual void setConnectHost(const QString &host); virtual void setConnectPort(int port); virtual void setIgnorePolicies(bool on); virtual void setTrustConnectHost(bool on); virtual void setIgnoreTlsErrors(bool on); virtual void start(const QUrl &uri, const HttpHeaders &headers); virtual void respondSuccess(const QByteArray &reason, const HttpHeaders &headers); virtual void respondError(int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body); virtual State state() const; virtual QUrl requestUri() const; virtual HttpHeaders requestHeaders() const; virtual int responseCode() const; virtual QByteArray responseReason() const; virtual HttpHeaders responseHeaders() const; virtual QByteArray responseBody() const; virtual int framesAvailable() const; virtual int writeBytesAvailable() const; virtual int peerCloseCode() const; virtual QString peerCloseReason() const; virtual ErrorCondition errorCondition() const; virtual void writeFrame(const Frame &frame); virtual Frame readFrame(); virtual void close(int code = -1, const QString &reason = QString()); private: class Private; friend class Private; Private *d; friend class SockJsManager; SockJsSession(QObject *parent = 0); void setupServer(SockJsManager *manager, ZhttpRequest *req, const QByteArray &jsonpCallback, const QUrl &asUri, const QByteArray &sid, const QByteArray &lastPart, const QByteArray &body, const DomainMap::Entry &route); void setupServer(SockJsManager *manager, ZWebSocket *sock, const QUrl &asUri, const DomainMap::Entry &route); void setupServer(SockJsManager *manager, ZWebSocket *sock, const QUrl &asUri, const QByteArray &sid, const QByteArray &lastPart, const DomainMap::Entry &route); void startServer(); void handleRequest(ZhttpRequest *req, const QByteArray &jsonpCallback, const QByteArray &lastPart, const QByteArray &body); }; #endif pushpin-1.39.1/src/cpp/proxy/testhttprequest.cpp000066400000000000000000000151221457610542000220260ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "testhttprequest.h" #include #include #include "log.h" #include "bufferlist.h" #include "packet/httprequestdata.h" #include "packet/httpresponsedata.h" #include "statusreasons.h" #define MAX_REQUEST_SIZE 100000 class TestHttpRequest::Private : public QObject { Q_OBJECT public: enum State { Idle, ReceivingRequest, Responding, Responded }; TestHttpRequest *q; State state; HttpRequestData request; HttpResponseData response; BufferList requestBody; bool requestBodyFinished; BufferList responseBody; ErrorCondition errorCondition; Private(TestHttpRequest *_q) : QObject(_q), q(_q), state(Idle), requestBodyFinished(false), errorCondition(ErrorGeneric) { } public slots: void doBytesWritten(int cnt){ q->bytesWritten(cnt); } void processRequest() { if(!requestBodyFinished) { response.code = 400; response.reason = StatusReasons::getReason(response.code); response.headers += HttpHeader("Content-Type", "text/plain"); responseBody += QByteArray("request too large\n"); state = Responded; q->readyRead(); return; } log_debug("processing test request: %s", qPrintable(request.uri.path())); QString path = request.uri.path(); if(path.length() >= 2 && path.endsWith('/')) path.truncate(path.length() - 1); QSet channels; QUrlQuery query(request.uri); QList > queryItems = query.queryItems(); for(int n = 0; n < queryItems.count(); ++n) { if(queryItems[n].first == "channel") channels += queryItems[n].second; } if(channels.isEmpty()) channels += "test"; if(path == "/") { response.code = 200; response.reason = StatusReasons::getReason(response.code); response.headers += HttpHeader("Content-Type", "text/plain"); responseBody += QByteArray("Hello from the Pushpin test handler!\n"); } else if(path == "/response") { response.code = 200; response.reason = StatusReasons::getReason(response.code); response.headers += HttpHeader("Content-Type", "text/plain"); response.headers += HttpHeader("Grip-Hold", "response"); response.headers += HttpHeader("Grip-Channel", QStringList(channels.values()).join(", ").toUtf8()); responseBody += QByteArray("nothing for now\n"); } else if(path == "/stream") { response.code = 200; response.reason = StatusReasons::getReason(response.code); response.headers += HttpHeader("Content-Type", "text/plain"); response.headers += HttpHeader("Grip-Hold", "stream"); response.headers += HttpHeader("Grip-Channel", QStringList(channels.values()).join(", ").toUtf8()); responseBody += QByteArray("[stream opened]\n"); } else { response.code = 404; response.reason = StatusReasons::getReason(response.code); response.headers += HttpHeader("Content-Type", "text/plain"); responseBody += QByteArray("no such test resource\n"); } state = Responded; q->readyRead(); } }; TestHttpRequest::TestHttpRequest(QObject *parent) : HttpRequest(parent) { d = new Private(this); } TestHttpRequest::~TestHttpRequest() { delete d; } QHostAddress TestHttpRequest::peerAddress() const { // this class is client only return QHostAddress(); } void TestHttpRequest::setConnectHost(const QString &host) { Q_UNUSED(host); } void TestHttpRequest::setConnectPort(int port) { Q_UNUSED(port); } void TestHttpRequest::setIgnorePolicies(bool on) { Q_UNUSED(on); } void TestHttpRequest::setTrustConnectHost(bool on) { Q_UNUSED(on); } void TestHttpRequest::setIgnoreTlsErrors(bool on) { Q_UNUSED(on); } void TestHttpRequest::start(const QString &method, const QUrl &uri, const HttpHeaders &headers) { assert(d->state == Private::Idle); d->state = Private::ReceivingRequest; d->request.method = method; d->request.uri = uri; d->request.headers = headers; } void TestHttpRequest::beginResponse(int code, const QByteArray &reason, const HttpHeaders &headers) { Q_UNUSED(code); Q_UNUSED(reason); Q_UNUSED(headers); // this class is client only assert(0); } void TestHttpRequest::writeBody(const QByteArray &body) { if(d->state == Private::ReceivingRequest) { if(d->requestBody.size() + body.size() > MAX_REQUEST_SIZE) { d->state = Private::Responding; QMetaObject::invokeMethod(d, "processRequest", Qt::QueuedConnection); return; } QByteArray buf = body.mid(0, MAX_REQUEST_SIZE - d->requestBody.size()); if(!buf.isEmpty()) { d->requestBody += buf; QMetaObject::invokeMethod(this, "doBytesWritten", Qt::QueuedConnection, Q_ARG(int, buf.size())); } } } void TestHttpRequest::endBody() { if(d->state == Private::ReceivingRequest) { d->requestBodyFinished = true; d->state = Private::Responding; QMetaObject::invokeMethod(d, "processRequest", Qt::QueuedConnection); } } int TestHttpRequest::bytesAvailable() const { return d->responseBody.size(); } int TestHttpRequest::writeBytesAvailable() const { return (MAX_REQUEST_SIZE - d->requestBody.size() + 1); } bool TestHttpRequest::isFinished() const { return (d->state == Private::Responded); } bool TestHttpRequest::isInputFinished() const { return (d->state == Private::Responded); } bool TestHttpRequest::isOutputFinished() const { return d->requestBodyFinished; } bool TestHttpRequest::isErrored() const { // this class can't fail return false; } HttpRequest::ErrorCondition TestHttpRequest::errorCondition() const { return d->errorCondition; } QString TestHttpRequest::requestMethod() const { return d->request.method; } QUrl TestHttpRequest::requestUri() const { return d->request.uri; } HttpHeaders TestHttpRequest::requestHeaders() const { return d->request.headers; } int TestHttpRequest::responseCode() const { return d->response.code; } QByteArray TestHttpRequest::responseReason() const { return d->response.reason; } HttpHeaders TestHttpRequest::responseHeaders() const { return d->response.headers; } QByteArray TestHttpRequest::readBody(int size) { return d->responseBody.take(size); } #include "testhttprequest.moc" pushpin-1.39.1/src/cpp/proxy/testhttprequest.h000066400000000000000000000041631457610542000214760ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef TESTHTTPREQUEST_H #define TESTHTTPREQUEST_H #include "httprequest.h" class TestHttpRequest : public HttpRequest { Q_OBJECT public: // pair of sender + request id typedef QPair Rid; TestHttpRequest(QObject *parent = 0); ~TestHttpRequest(); // reimplemented virtual QHostAddress peerAddress() const; virtual void setConnectHost(const QString &host); virtual void setConnectPort(int port); virtual void setIgnorePolicies(bool on); virtual void setTrustConnectHost(bool on); virtual void setIgnoreTlsErrors(bool on); virtual void start(const QString &method, const QUrl &uri, const HttpHeaders &headers); virtual void beginResponse(int code, const QByteArray &reason, const HttpHeaders &headers); virtual void writeBody(const QByteArray &body); virtual void endBody(); virtual int bytesAvailable() const; virtual int writeBytesAvailable() const; virtual bool isFinished() const; virtual bool isInputFinished() const; virtual bool isOutputFinished() const; virtual bool isErrored() const; virtual ErrorCondition errorCondition() const; virtual QString requestMethod() const; virtual QUrl requestUri() const; virtual HttpHeaders requestHeaders() const; virtual int responseCode() const; virtual QByteArray responseReason() const; virtual HttpHeaders responseHeaders() const; virtual QByteArray readBody(int size = -1); private: class Private; friend class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/proxy/testwebsocket.cpp000066400000000000000000000150101457610542000214200ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * Copyright (C) 2023 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "testwebsocket.h" #include #include #include #include #include "packet/httprequestdata.h" #include "packet/httpresponsedata.h" #include "statusreasons.h" #define BUFFER_SIZE 200000 class TestWebSocket::Private : public QObject { Q_OBJECT public: enum State { Idle, Connecting, Connected, Closing }; TestWebSocket *q; State state; HttpRequestData request; HttpResponseData response; bool gripEnabled; QList inFrames; int peerCloseCode; QString peerCloseReason; ErrorCondition errorCondition; Private(TestWebSocket *_q) : QObject(_q), q(_q), state(Idle), gripEnabled(false), peerCloseCode(-1), errorCondition(ErrorGeneric) { } public slots: void handleConnect() { QString path = request.uri.path(); if(path.length() >= 2 && path.endsWith('/')) path.truncate(path.length() - 1); QSet channels; QUrlQuery query(request.uri); QList > queryItems = query.queryItems(); for(int n = 0; n < queryItems.count(); ++n) { if(queryItems[n].first == "channel") channels += queryItems[n].second; } if(channels.isEmpty()) channels += "test"; if(path == "/ws") { state = Connected; response.code = 101; response.reason = StatusReasons::getReason(response.code); gripEnabled = false; foreach(const HttpHeaderParameters &ext, request.headers.getAllAsParameters("Sec-WebSocket-Extensions")) { if(!ext.isEmpty() && ext[0].first == "grip") { gripEnabled = true; break; } } if(gripEnabled) { response.headers += HttpHeader("Sec-WebSocket-Extensions", "grip"); foreach(const QString &channel, channels) { QJsonObject obj; obj["type"] = QString("subscribe"); obj["channel"] = channel; inFrames += Frame(Frame::Text, QByteArray("c:") + QJsonDocument(obj).toJson(), false); } } q->connected(); if(gripEnabled && !channels.isEmpty()) QMetaObject::invokeMethod(this, "doReadyRead", Qt::QueuedConnection); } else { response.code = 404; response.reason = StatusReasons::getReason(response.code); response.headers += HttpHeader("Content-Type", "text/plain"); response.body += QByteArray("no such test resource\n"); errorCondition = ErrorRejected; q->error(); } } void doReadyRead() { q->readyRead(); } void doFramesWritten(int count, int bytes) { q->framesWritten(count, bytes); } void doWriteBytesChanged() { q->writeBytesChanged(); } void handleClose() { state = Idle; q->closed(); } }; TestWebSocket::TestWebSocket(QObject *parent) : WebSocket(parent) { d = new Private(this); } TestWebSocket::~TestWebSocket() { delete d; } QHostAddress TestWebSocket::peerAddress() const { // this class is client only return QHostAddress(); } void TestWebSocket::setConnectHost(const QString &host) { Q_UNUSED(host); } void TestWebSocket::setConnectPort(int port) { Q_UNUSED(port); } void TestWebSocket::setIgnorePolicies(bool on) { Q_UNUSED(on); } void TestWebSocket::setTrustConnectHost(bool on) { Q_UNUSED(on); } void TestWebSocket::setIgnoreTlsErrors(bool on) { Q_UNUSED(on); } void TestWebSocket::start(const QUrl &uri, const HttpHeaders &headers) { d->request.uri = uri; d->request.headers = headers; d->state = Private::Connecting; QMetaObject::invokeMethod(d, "handleConnect", Qt::QueuedConnection); } void TestWebSocket::respondSuccess(const QByteArray &reason, const HttpHeaders &headers) { Q_UNUSED(reason); Q_UNUSED(headers); // this class is client only assert(0); } void TestWebSocket::respondError(int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body) { Q_UNUSED(code); Q_UNUSED(reason); Q_UNUSED(headers); Q_UNUSED(body); // this class is client only assert(0); } WebSocket::State TestWebSocket::state() const { if(d->state == Private::Idle) return Idle; else if(d->state == Private::Connecting) return Connecting; else if(d->state == Private::Connected) return Connected; else // Private::Closing return Closing; } QUrl TestWebSocket::requestUri() const { return d->request.uri; } HttpHeaders TestWebSocket::requestHeaders() const { return d->request.headers; } int TestWebSocket::responseCode() const { return d->response.code; } QByteArray TestWebSocket::responseReason() const { return d->response.reason; } HttpHeaders TestWebSocket::responseHeaders() const { return d->response.headers; } QByteArray TestWebSocket::responseBody() const { return d->response.body; } int TestWebSocket::framesAvailable() const { return d->inFrames.count(); } int TestWebSocket::writeBytesAvailable() const { int inSize = 0; foreach(const Frame &f, d->inFrames) inSize += f.data.size(); if(inSize < BUFFER_SIZE) return BUFFER_SIZE - inSize; else return 0; } int TestWebSocket::peerCloseCode() const { return d->peerCloseCode; } QString TestWebSocket::peerCloseReason() const { return d->peerCloseReason; } WebSocket::ErrorCondition TestWebSocket::errorCondition() const { return d->errorCondition; } void TestWebSocket::writeFrame(const Frame &frame) { Frame tmp = frame; if(d->gripEnabled && (frame.type == Frame::Text || frame.type == Frame::Binary)) { tmp.data = "m:" + tmp.data; } d->inFrames += tmp; QMetaObject::invokeMethod(d, "doFramesWritten", Qt::QueuedConnection, Q_ARG(int, 1), Q_ARG(int, tmp.data.size())); QMetaObject::invokeMethod(d, "doReadyRead", Qt::QueuedConnection); } WebSocket::Frame TestWebSocket::readFrame() { QMetaObject::invokeMethod(d, "doWriteBytesChanged", Qt::QueuedConnection); return d->inFrames.takeFirst(); } void TestWebSocket::close(int code, const QString &reason) { d->state = Private::Closing; d->peerCloseCode = code; d->peerCloseReason = reason; QMetaObject::invokeMethod(d, "handleClose", Qt::QueuedConnection); } #include "testwebsocket.moc" pushpin-1.39.1/src/cpp/proxy/testwebsocket.h000066400000000000000000000042301457610542000210670ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * Copyright (C) 2023 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef TESTWEBSOCKET_H #define TESTWEBSOCKET_H #include "websocket.h" class ZhttpManager; class TestWebSocket : public WebSocket { Q_OBJECT public: TestWebSocket(QObject *parent = 0); ~TestWebSocket(); // reimplemented virtual QHostAddress peerAddress() const; virtual void setConnectHost(const QString &host); virtual void setConnectPort(int port); virtual void setIgnorePolicies(bool on); virtual void setTrustConnectHost(bool on); virtual void setIgnoreTlsErrors(bool on); virtual void start(const QUrl &uri, const HttpHeaders &headers); virtual void respondSuccess(const QByteArray &reason, const HttpHeaders &headers); virtual void respondError(int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body); virtual State state() const; virtual QUrl requestUri() const; virtual HttpHeaders requestHeaders() const; virtual int responseCode() const; virtual QByteArray responseReason() const; virtual HttpHeaders responseHeaders() const; virtual QByteArray responseBody() const; virtual int framesAvailable() const; virtual int writeBytesAvailable() const; virtual int peerCloseCode() const; virtual QString peerCloseReason() const; virtual ErrorCondition errorCondition() const; virtual void writeFrame(const Frame &frame); virtual Frame readFrame(); virtual void close(int code = -1, const QString &reason = QString()); private: class Private; friend class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/proxy/updater.cpp000066400000000000000000000153611457610542000202070ustar00rootroot00000000000000/* * Copyright (C) 2015-2017 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "updater.h" #include #include #include #include #include #include #include #include #include "qtcompat.h" #include "log.h" #include "httpheaders.h" #include "zhttpmanager.h" #include "zhttprequest.h" #define CHECK_INTERVAL (24 * 60 * 60 * 1000) #define REPORT_INTERVAL (15 * 60 * 1000) #define CHECK_URL "https://updates.fanout.io/check/" #define USER_AGENT "Pushpin-Updater" #define MAX_RESPONSE_SIZE 50000 static QString getOs() { #if defined(Q_OS_MAC) return "mac"; #elif defined(Q_OS_LINUX) return "linux"; #elif defined(Q_OS_FREEBSD) return "freebsd"; #elif defined(Q_OS_NETBSD) return "netbsd"; #elif defined(Q_OS_OPENBSD) return "openbsd"; #elif defined(Q_OS_UNIX) return "unix"; #else return QString(); #endif } static QString getArch() { return QString::number(sizeof(void *) * 8); } class Updater::Private : public QObject { Q_OBJECT public: struct ReqConnections { Connection readyReadConnection; Connection errorConnection; }; Updater *q; Mode mode; bool quiet; QString currentVersion; QString org; ZhttpManager *zhttpManager; QTimer *timer; ZhttpRequest *req; Report report; QDateTime lastLogTime; ReqConnections reqConnections; Private(Updater *_q, Mode _mode, bool _quiet, const QString &_currentVersion, const QString &_org, ZhttpManager *zhttp) : QObject(_q), q(_q), mode(_mode), quiet(_quiet), currentVersion(_currentVersion), org(_org), zhttpManager(zhttp), req(0) { timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &Private::timer_timeout); timer->setInterval(mode == ReportMode ? REPORT_INTERVAL : CHECK_INTERVAL); timer->start(); report.connectionsMax = -1; // stale } ~Private() { timer->disconnect(this); timer->setParent(0); timer->deleteLater(); } void cleanupRequest() { reqConnections = ReqConnections(); delete req; req = 0; } private: void doRequest() { req = zhttpManager->createRequest(); req->setParent(this); reqConnections = { req->readyRead.connect(boost::bind(&Private::req_readyRead, this)), req->error.connect(boost::bind(&Private::req_error, this)) }; req->setIgnorePolicies(true); req->setIgnoreTlsErrors(true); req->setQuiet(quiet); QUrl url(CHECK_URL); QUrlQuery query; query.addQueryItem("package", "pushpin"); query.addQueryItem("version", currentVersion); QString os = getOs(); if(!os.isEmpty()) query.addQueryItem("os", os); QString arch = getArch(); if(!arch.isEmpty()) query.addQueryItem("arch", arch); if(!org.isEmpty()) query.addQueryItem("org", org); if(mode == ReportMode) { QString host = QHostInfo::localHostName(); QString hashedHost = QString::fromUtf8(QCryptographicHash::hash(host.toUtf8(), QCryptographicHash::Sha1).toHex()); query.addQueryItem("id", hashedHost); int cmax = (report.connectionsMax > 0 ? report.connectionsMax : 0); query.addQueryItem("cmax", QString::number(cmax)); query.addQueryItem("cminutes", QString::number(report.connectionsMinutes)); query.addQueryItem("recv", QString::number(report.messagesReceived)); query.addQueryItem("sent", QString::number(report.messagesSent)); query.addQueryItem("ops", QString::number(report.ops)); report.connectionsMax = -1; // stale report.connectionsMinutes = 0; report.messagesReceived = 0; report.messagesSent = 0; report.ops = 0; } url.setQuery(query); HttpHeaders headers; #ifdef USER_AGENT headers += HttpHeader("User-Agent", USER_AGENT); #endif log_debug("updater: checking for updates: %s", qPrintable(url.toString())); req->start("GET", url, headers); req->endBody(); } void req_readyRead() { if(req->bytesAvailable() > MAX_RESPONSE_SIZE) { log_debug("updater: check failed, response too large"); cleanupRequest(); return; } if(!req->isFinished()) return; if(req->responseCode() != 200) { log_debug("updater: check failed, response code: %d", req->responseCode()); cleanupRequest(); return; } QByteArray rawBody = req->readBody(); cleanupRequest(); QJsonParseError e; QJsonDocument doc = QJsonDocument::fromJson(rawBody, &e); if(e.error != QJsonParseError::NoError || !doc.isObject()) { log_debug("updater: check failed, unexpected response body format"); return; } log_debug("updater: check finished"); QVariantMap body = doc.object().toVariantMap(); if(body.contains("updates") && typeId(body["updates"]) == QMetaType::QVariantList) { QVariantList updates = body["updates"].toList(); if(!updates.isEmpty() && typeId(updates[0]) == QMetaType::QVariantMap) { QVariantMap update = updates[0].toMap(); QString version = update.value("version").toString(); QString link = update.value("link").toString(); QDateTime now = QDateTime::currentDateTime(); if(!version.isEmpty() && (lastLogTime.isNull() || now >= lastLogTime.addMSecs(CHECK_INTERVAL - (REPORT_INTERVAL / 2)))) { lastLogTime = now; QString msg = QString("New version of Pushpin available! version=%1").arg(version); if(!link.isEmpty()) msg += QString(" %1").arg(link); log_info("%s", qPrintable(msg)); } } } } void req_error() { log_debug("updater: check failed, req error: %d", (int)req->errorCondition()); cleanupRequest(); } void timer_timeout() { if(!req) doRequest(); } }; Updater::Updater(Mode mode, bool quiet, const QString ¤tVersion, const QString &org, ZhttpManager *zhttp, QObject *parent) : QObject(parent) { d = new Private(this, mode, quiet, currentVersion, org, zhttp); } Updater::~Updater() { delete d; } void Updater::setReport(const Report &report) { // update the current report data if(d->report.connectionsMax == -1 || report.connectionsMax > d->report.connectionsMax) d->report.connectionsMax = report.connectionsMax; d->report.connectionsMinutes += report.connectionsMinutes; d->report.messagesReceived += report.messagesReceived; d->report.messagesSent += report.messagesSent; d->report.ops += report.ops; } #include "updater.moc" pushpin-1.39.1/src/cpp/proxy/updater.h000066400000000000000000000026511457610542000176520ustar00rootroot00000000000000/* * Copyright (C) 2015 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef UPDATER_H #define UPDATER_H #include #include using Connection = boost::signals2::scoped_connection; class ZhttpManager; class Updater : public QObject { Q_OBJECT public: enum Mode { CheckMode, ReportMode }; class Report { public: int connectionsMax; int connectionsMinutes; int messagesReceived; int messagesSent; int ops; Report() : connectionsMax(0), connectionsMinutes(0), messagesReceived(0), messagesSent(0), ops(0) { } }; Updater(Mode mode, bool quiet, const QString ¤tVersion, const QString &org, ZhttpManager *zhttp, QObject *parent = 0); ~Updater(); void setReport(const Report &report); private: class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/proxy/websocketoverhttp.cpp000066400000000000000000000573371457610542000223360ustar00rootroot00000000000000/* * Copyright (C) 2014-2022 Fanout, Inc. * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "websocketoverhttp.h" #include #include #include #include #include "log.h" #include "bufferlist.h" #include "packet/httprequestdata.h" #include "packet/httpresponsedata.h" #include "zhttprequest.h" #include "zhttpmanager.h" #include "uuidutil.h" #define BUFFER_SIZE 200000 #define FRAME_SIZE_MAX 16384 #define RESPONSE_BODY_MAX 1000000 #define REJECT_BODY_MAX 100000 #define RETRY_TIMEOUT 1000 #define RETRY_MAX 5 #define RETRY_RAND_MAX 1000 namespace { class WsEvent { public: QByteArray type; QByteArray content; WsEvent() { } WsEvent(const QByteArray &_type, const QByteArray &_content = QByteArray()) : type(_type), content(_content) { } }; } class WebSocketOverHttp::DisconnectManager : public QObject { Q_OBJECT struct WSConnections { Connection disconnectedConnection; Connection closedConnection; Connection errorConnection; }; map wsConnectionMap; public: DisconnectManager(QObject *parent = 0) : QObject(parent) { } void addSocket(WebSocketOverHttp *sock) { sock->setParent(this); wsConnectionMap[sock] = { sock->disconnected.connect(boost::bind(&DisconnectManager::sock_disconnected, this, sock)), sock->closed.connect(boost::bind(&DisconnectManager::sock_closed, this, sock)), sock->error.connect(boost::bind(&DisconnectManager::sock_error, this, sock)) }; sock->sendDisconnect(); } int count() const { return children().count(); } private: void cleanupSocket(WebSocketOverHttp *sock) { wsConnectionMap.erase(sock); delete sock; } private: void sock_disconnected(WebSocketOverHttp *sock) { cleanupSocket(sock); } void sock_closed(WebSocketOverHttp *sock) { cleanupSocket(sock); } void sock_error(WebSocketOverHttp *sock) { cleanupSocket(sock); } }; thread_local WebSocketOverHttp::DisconnectManager *WebSocketOverHttp::g_disconnectManager = 0; thread_local int WebSocketOverHttp::g_maxManagedDisconnects = -1; static QList decodeEvents(const QByteArray &in, bool *ok = 0) { QList out; if(ok) *ok = false; int start = 0; while(start < in.size()) { int at = in.indexOf("\r\n", start); if(at == -1) return QList(); QByteArray typeLine = in.mid(start, at - start); start = at + 2; WsEvent e; at = typeLine.indexOf(' '); if(at != -1) { e.type = typeLine.mid(0, at); bool check; int clen = typeLine.mid(at + 1).toInt(&check, 16); if(!check) return QList(); e.content = in.mid(start, clen); start += clen + 2; } else { e.type = typeLine; } out += e; } if(ok) *ok = true; return out; } static QByteArray encodeEvents(const QList &events) { QByteArray out; foreach(const WsEvent &e, events) { if(!e.content.isNull()) { out += e.type + ' ' + QByteArray::number(e.content.size(), 16) + "\r\n" + e.content + "\r\n"; } else { out += e.type + "\r\n"; } } return out; } class WebSocketOverHttp::Private : public QObject { Q_OBJECT public: struct ReqConnections { Connection readyReadConnection; Connection bytesWrittenConnection; Connection errorConnection; }; WebSocketOverHttp *q; ZhttpManager *zhttpManager; QString connectHost; int connectPort; bool ignorePolicies; bool trustConnectHost; bool ignoreTlsErrors; State state; QByteArray cid; HttpRequestData requestData; HttpResponseData responseData; ErrorCondition errorCondition; ErrorCondition pendingErrorCondition; int keepAliveInterval; HttpHeaders meta; bool updating; ZhttpRequest *req; QByteArray reqBody; int reqPendingBytes; int reqFrames; int reqContentSize; bool reqClose; BufferList inBuf; QList inFrames; QList outFrames; int closeCode; QString closeReason; bool closeSent; bool peerClosing; int peerCloseCode; QString peerCloseReason; bool disconnecting; bool disconnectSent; bool updateQueued; QTimer *keepAliveTimer; QTimer *retryTimer; int retries; int maxEvents; ReqConnections reqConnections; Private(WebSocketOverHttp *_q) : QObject(_q), q(_q), connectPort(-1), ignorePolicies(false), trustConnectHost(false), ignoreTlsErrors(false), state(WebSocket::Idle), errorCondition(ErrorGeneric), pendingErrorCondition((ErrorCondition)-1), keepAliveInterval(-1), updating(false), req(0), reqPendingBytes(0), reqFrames(0), reqContentSize(0), reqClose(false), closeCode(-1), closeSent(false), peerClosing(false), peerCloseCode(-1), disconnecting(false), disconnectSent(false), updateQueued(false), retries(0), maxEvents(0) { if(!g_disconnectManager) g_disconnectManager = new DisconnectManager; keepAliveTimer = new QTimer(this); connect(keepAliveTimer, &QTimer::timeout, this, &Private::keepAliveTimer_timeout); keepAliveTimer->setSingleShot(true); retryTimer = new QTimer(this); connect(retryTimer, &QTimer::timeout, this, &Private::retryTimer_timeout); retryTimer->setSingleShot(true); } ~Private() { keepAliveTimer->disconnect(this); keepAliveTimer->setParent(0); keepAliveTimer->deleteLater(); retryTimer->disconnect(this); retryTimer->setParent(0); retryTimer->deleteLater(); } void cleanup() { keepAliveTimer->stop(); retryTimer->stop(); updating = false; disconnecting = false; updateQueued = false; reqConnections = ReqConnections(); delete req; req = 0; state = Idle; } void sanitizeRequestHeaders() { // don't forward certain headers requestData.headers.removeAll("Upgrade"); requestData.headers.removeAll("Accept"); requestData.headers.removeAll("Connection-Id"); requestData.headers.removeAll("Content-Length"); // don't forward headers starting with Meta-* for(int n = 0; n < requestData.headers.count(); ++n) { const HttpHeader &h = requestData.headers[n]; if(qstrnicmp(h.first.data(), "Meta-", 5) == 0) { requestData.headers.removeAt(n); --n; // adjust position } } } void start() { state = Connecting; if(cid.isEmpty()) cid = UuidUtil::createUuid(); if(requestData.uri.scheme() == "wss") requestData.uri.setScheme("https"); else requestData.uri.setScheme("http"); update(); } void writeFrame(const Frame &frame) { assert(state == Connected); outFrames += frame; if(needUpdate()) update(); } Frame readFrame() { return inFrames.takeFirst(); } void close(int code, const QString &reason) { assert(state != Closing); state = Closing; closeCode = code; closeReason = reason; update(); } int writeBytesAvailable() const { if(reqContentSize >= BUFFER_SIZE) return 0; int avail = BUFFER_SIZE - reqContentSize; foreach(const Frame &f, outFrames) { if(f.data.size() >= avail) return 0; avail -= f.data.size(); } return avail; } void sendDisconnect() { disconnecting = true; update(); } void refresh() { // only allow refresh requests if connected if(state == Connected && !disconnecting) { if(!updating) update(); else updateQueued = true; } } private: bool canReceive() const { int avail = 0; foreach(const Frame &f, inFrames) { avail += f.data.size(); if(avail >= BUFFER_SIZE) return false; } return true; } void appendInMessage(Frame::Type type, const QByteArray &message) { // split into frames to avoid credits issue QList frames; for(int n = 0; frames.isEmpty() || n < message.size(); n += FRAME_SIZE_MAX) { Frame::Type ftype; if(n == 0) ftype = type; else ftype = Frame::Continuation; QByteArray data = message.mid(n, FRAME_SIZE_MAX); bool more = (n + FRAME_SIZE_MAX < message.size()); frames += Frame(ftype, data, more); } foreach(const Frame &f, frames) inFrames += f; } bool canSendCompleteMessage() const { foreach(const Frame &f, outFrames) { if(!f.more) return true; } return false; } bool needUpdate() const { // always send this right away if(disconnecting && !disconnectSent) return true; if(updateQueued) return true; bool cscm = canSendCompleteMessage(); if(!cscm && writeBytesAvailable() == 0) { // write buffer maxed with incomplete message. this is // unrecoverable. update to throw error right away. return true; } // if we can't fit a response then don't update yet if(!canReceive()) return false; // have message to send or close? if(cscm || (outFrames.isEmpty() && state == Closing && !closeSent)) return true; return false; } void queueError(ErrorCondition e) { if((int)pendingErrorCondition == -1) { pendingErrorCondition = e; QMetaObject::invokeMethod(this, "doError", Qt::QueuedConnection); } } void update() { // only one request allowed at a time if(updating) return; updateQueued = false; updating = true; keepAliveTimer->stop(); // if we can't send yet but also have no room for writes, then fail if(!canSendCompleteMessage() && writeBytesAvailable() == 0) { updating = false; queueError(ErrorGeneric); return; } reqFrames = 0; reqContentSize = 0; reqClose = false; QList events; if(state == Connecting) { events += WsEvent("OPEN"); } else if(disconnecting && !disconnectSent) { events += WsEvent("DISCONNECT"); disconnectSent = true; } else { while(!outFrames.isEmpty() && reqContentSize < BUFFER_SIZE && (maxEvents <= 0 || events.count() < maxEvents)) { // make sure the next message is fully readable int takeCount = -1; for(int n = 0; n < outFrames.count(); ++n) { if(!outFrames[n].more) { takeCount = n + 1; break; } } if(takeCount < 1) break; Frame::Type ftype = Frame::Text; BufferList content; for(int n = 0; n < takeCount; ++n) { Frame f = outFrames.takeFirst(); if((n == 0 && f.type == Frame::Continuation) || (n > 0 && f.type != Frame::Continuation)) { updating = false; queueError(ErrorGeneric); return; } if(n == 0) { assert(f.type != Frame::Continuation); ftype = f.type; } content += f.data; assert(n + 1 < takeCount || !f.more); } QByteArray data = content.toByteArray(); // for compactness, we only include content on ping/pong if non-empty if(ftype == Frame::Text) events += WsEvent("TEXT", data); else if(ftype == Frame::Binary) events += WsEvent("BINARY", data); else if(ftype == Frame::Ping) events += WsEvent("PING", !data.isEmpty() ? data : QByteArray()); else if(ftype == Frame::Pong) events += WsEvent("PONG", !data.isEmpty() ? data : QByteArray()); reqFrames += takeCount; reqContentSize += content.size(); } if(state == Closing && (maxEvents <= 0 || events.count() < maxEvents)) { // if there was a partial message left, throw it away if(!outFrames.isEmpty()) { log_warning("woh: dropping partial message before close"); outFrames.clear(); } if(closeCode != -1) { QByteArray rawReason = closeReason.toUtf8(); QByteArray buf(2 + rawReason.size(), 0); buf[0] = (closeCode >> 8) & 0xff; buf[1] = closeCode & 0xff; memcpy(buf.data() + 2, rawReason.data(), rawReason.size()); events += WsEvent("CLOSE", buf); } else events += WsEvent("CLOSE"); reqClose = true; } } reqBody = encodeEvents(events); doRequest(); } void doRequest() { assert(!req); q->aboutToSendRequest(); req = zhttpManager->createRequest(); req->setParent(this); reqConnections = { req->readyRead.connect(boost::bind(&Private::req_readyRead, this)), req->bytesWritten.connect(boost::bind(&Private::req_bytesWritten, this, boost::placeholders::_1)), req->error.connect(boost::bind(&Private::req_error, this)) }; if(!connectHost.isEmpty()) req->setConnectHost(connectHost); if(connectPort != -1) req->setConnectPort(connectPort); req->setIgnorePolicies(ignorePolicies); req->setTrustConnectHost(trustConnectHost); req->setIgnoreTlsErrors(ignoreTlsErrors); req->setSendBodyAfterAcknowledgement(true); HttpHeaders headers = requestData.headers; headers += HttpHeader("Accept", "application/websocket-events"); headers += HttpHeader("Connection-Id", cid); headers += HttpHeader("Content-Type", "application/websocket-events"); headers += HttpHeader("Content-Length", QByteArray::number(reqBody.size())); foreach(const HttpHeader &h, meta) headers += HttpHeader("Meta-" + h.first, h.second); reqPendingBytes = reqBody.size(); req->start("POST", requestData.uri, headers); req->writeBody(reqBody); req->endBody(); } void req_readyRead() { if(inBuf.size() + req->bytesAvailable() > RESPONSE_BODY_MAX) { cleanup(); q->error(); return; } inBuf += req->readBody(); if(!req->isFinished()) { // if request isn't finished yet, keep waiting return; } reqBody.clear(); retries = 0; int responseCode = req->responseCode(); QByteArray responseReason = req->responseReason(); HttpHeaders responseHeaders = req->responseHeaders(); QByteArray responseBody = inBuf.take(); reqConnections = ReqConnections(); delete req; req = 0; if(state == Connecting) { // save the initial response responseData.code = responseCode; responseData.reason = responseReason; responseData.headers = responseHeaders; } QByteArray contentType = responseHeaders.get("Content-Type"); if(responseCode != 200 || contentType != "application/websocket-events") { if(state == Connecting) { errorCondition = ErrorRejected; responseData.body = responseBody.mid(0, REJECT_BODY_MAX); } else errorCondition = ErrorGeneric; cleanup(); q->error(); return; } if(responseHeaders.contains("Keep-Alive-Interval")) { bool ok; int x = responseHeaders.get("Keep-Alive-Interval").toInt(&ok); if(ok && x > 0) { if(x < 20) x = 20; keepAliveInterval = x; } else keepAliveInterval = -1; } foreach(const HttpHeader &h, responseHeaders) { if(h.first.size() >= 10 && qstrnicmp(h.first.data(), "Set-Meta-", 9) == 0) { QByteArray name = h.first.mid(9); if(meta.contains(name)) meta.removeAll(name); if(!h.second.isEmpty()) meta += HttpHeader(name, h.second); } } bool ok; QList events = decodeEvents(responseBody, &ok); if(!ok) { cleanup(); q->error(); return; } if(state == Connecting) { // server must respond with events or enable keep alive if(events.isEmpty() && keepAliveInterval == -1) { cleanup(); q->error(); return; } // first event must be OPEN if(!events.isEmpty() && events.first().type != "OPEN") { cleanup(); q->error(); return; } // correct the status code/reason responseData.code = 101; responseData.reason = "Switching Protocols"; // strip private headers from the initial response responseData.headers.removeAll("Content-Length"); responseData.headers.removeAll("Content-Type"); responseData.headers.removeAll("Keep-Alive-Interval"); for(int n = 0; n < responseData.headers.count(); ++n) { const HttpHeader &h = responseData.headers[n]; if(h.first.size() >= 10 && qstrnicmp(h.first.data(), "Set-Meta-", 9) == 0) { responseData.headers.removeAt(n); --n; // adjust position } } } if(disconnectSent) { cleanup(); q->disconnected(); return; } QPointer self = this; bool emitConnected = false; bool emitReadyRead = false; bool closed = false; bool disconnected = false; foreach(const WsEvent &e, events) { if(e.type == "OPEN") { if(state != Connecting) { disconnected = true; break; } state = Connected; emitConnected = true; } else if(e.type == "TEXT") { appendInMessage(Frame::Text, e.content); emitReadyRead = true; } else if(e.type == "BINARY") { appendInMessage(Frame::Binary, e.content); emitReadyRead = true; } else if(e.type == "PING") { appendInMessage(Frame::Ping, e.content); emitReadyRead = true; } else if(e.type == "PONG") { appendInMessage(Frame::Pong, e.content); emitReadyRead = true; } else if(e.type == "CLOSE") { peerClosing = true; if(e.content.size() >= 2) { int hi = (unsigned char)e.content[0]; int lo = (unsigned char)e.content[1]; peerCloseCode = (hi << 8) + lo; peerCloseReason = QString::fromUtf8(e.content.mid(2)); } closed = true; break; } else if(e.type == "DISCONNECT") { disconnected = true; break; } } if(emitConnected) { q->connected(); if(!self) return; } if(emitReadyRead) { q->readyRead(); if(!self) return; } if(reqFrames > 0) { q->framesWritten(reqFrames, reqContentSize); if(!self) return; } bool hadContent = reqContentSize > 0; reqFrames = 0; reqContentSize = 0; if(hadContent) { q->writeBytesChanged(); if(!self) return; } if(reqClose) closeSent = true; if(closed) { if(closeSent) { cleanup(); q->closed(); return; } else { q->peerClosed(); } } else if(closeSent && keepAliveInterval == -1) { // if there are no keep alives, then the server has only one // chance to respond to a close. if it doesn't, then // consider the connection uncleanly disconnected. disconnected = true; } if(disconnected) { cleanup(); q->error(); return; } if(reqClose && peerClosing) { cleanup(); q->closed(); return; } updating = false; if(needUpdate()) update(); else if(keepAliveInterval != -1) keepAliveTimer->start(keepAliveInterval * 1000); } void req_bytesWritten(int count) { reqPendingBytes -= count; assert(reqPendingBytes >= 0); } void req_error() { bool retry = false; ZhttpRequest::ErrorCondition reqError = req->errorCondition(); switch(reqError) { case ZhttpRequest::ErrorConnect: case ZhttpRequest::ErrorConnectTimeout: case ZhttpRequest::ErrorTls: // these errors mean the server wasn't reached at all retry = true; break; case ZhttpRequest::ErrorGeneric: case ZhttpRequest::ErrorTimeout: // these errors mean the server may have been reached, so // only retry if the request body wasn't completely sent if(reqPendingBytes > 0) retry = true; break; default: // all other errors are hard fails that shouldn't be retried break; } reqConnections = ReqConnections(); delete req; req = 0; if(retry && retries < RETRY_MAX && state != Connecting) { keepAliveTimer->stop(); int delay = RETRY_TIMEOUT; for(int n = 0; n < retries; ++n) delay *= 2; delay += (int)(QRandomGenerator::global()->generate() % RETRY_RAND_MAX); log_debug("woh: trying again in %dms", delay); ++retries; // this should still be flagged, for protection while retrying assert(updating); retryTimer->start(delay); return; } if(reqError == ZhttpRequest::ErrorConnect) errorCondition = WebSocket::ErrorConnect; else if(reqError == ZhttpRequest::ErrorConnectTimeout) errorCondition = WebSocket::ErrorConnectTimeout; else if(reqError == ZhttpRequest::ErrorTls) errorCondition = WebSocket::ErrorTls; cleanup(); q->error(); } private slots: void keepAliveTimer_timeout() { update(); } void retryTimer_timeout() { doRequest(); } void doError() { cleanup(); errorCondition = pendingErrorCondition; pendingErrorCondition = (ErrorCondition)-1; q->error(); } }; WebSocketOverHttp::WebSocketOverHttp(ZhttpManager *zhttpManager, QObject *parent) : WebSocket(parent) { d = new Private(this); d->zhttpManager = zhttpManager; } WebSocketOverHttp::WebSocketOverHttp(QObject *parent) : WebSocket(parent), d(0) { } WebSocketOverHttp::~WebSocketOverHttp() { if(d->state != Idle && parent() != g_disconnectManager && (g_maxManagedDisconnects < 0 || g_disconnectManager->count() < g_maxManagedDisconnects)) { // if we get destructed while active, clean up in the background WebSocketOverHttp *sock = new WebSocketOverHttp; sock->d = d; d->setParent(sock); d->q = sock; d = 0; g_disconnectManager->addSocket(sock); } delete d; } void WebSocketOverHttp::setConnectionId(const QByteArray &id) { d->cid = id; } void WebSocketOverHttp::setMaxEventsPerRequest(int max) { d->maxEvents = max; } void WebSocketOverHttp::refresh() { d->refresh(); } void WebSocketOverHttp::setMaxManagedDisconnects(int max) { g_maxManagedDisconnects = max; } void WebSocketOverHttp::clearDisconnectManager() { delete g_disconnectManager; g_disconnectManager = 0; } void WebSocketOverHttp::sendDisconnect() { d->sendDisconnect(); } QHostAddress WebSocketOverHttp::peerAddress() const { // this class is client only return QHostAddress(); } void WebSocketOverHttp::setConnectHost(const QString &host) { d->connectHost = host; } void WebSocketOverHttp::setConnectPort(int port) { d->connectPort = port; } void WebSocketOverHttp::setIgnorePolicies(bool on) { d->ignorePolicies = on; } void WebSocketOverHttp::setTrustConnectHost(bool on) { d->trustConnectHost = on; } void WebSocketOverHttp::setIgnoreTlsErrors(bool on) { d->ignoreTlsErrors = on; } void WebSocketOverHttp::start(const QUrl &uri, const HttpHeaders &headers) { assert(d->state == Idle); d->requestData.uri = uri; d->requestData.headers = headers; d->sanitizeRequestHeaders(); d->start(); } void WebSocketOverHttp::respondSuccess(const QByteArray &reason, const HttpHeaders &headers) { Q_UNUSED(reason); Q_UNUSED(headers); // this class is client only assert(0); } void WebSocketOverHttp::respondError(int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body) { Q_UNUSED(code); Q_UNUSED(reason); Q_UNUSED(headers); Q_UNUSED(body); // this class is client only assert(0); } WebSocket::State WebSocketOverHttp::state() const { return d->state; } QUrl WebSocketOverHttp::requestUri() const { return d->requestData.uri; } HttpHeaders WebSocketOverHttp::requestHeaders() const { return d->requestData.headers; } int WebSocketOverHttp::responseCode() const { return d->responseData.code; } QByteArray WebSocketOverHttp::responseReason() const { return d->responseData.reason; } HttpHeaders WebSocketOverHttp::responseHeaders() const { return d->responseData.headers; } QByteArray WebSocketOverHttp::responseBody() const { return d->responseData.body; } int WebSocketOverHttp::framesAvailable() const { return d->inFrames.count(); } int WebSocketOverHttp::writeBytesAvailable() const { return d->writeBytesAvailable(); } int WebSocketOverHttp::peerCloseCode() const { return d->peerCloseCode; } QString WebSocketOverHttp::peerCloseReason() const { return d->peerCloseReason; } WebSocket::ErrorCondition WebSocketOverHttp::errorCondition() const { return d->errorCondition; } void WebSocketOverHttp::writeFrame(const Frame &frame) { d->writeFrame(frame); } WebSocket::Frame WebSocketOverHttp::readFrame() { return d->readFrame(); } void WebSocketOverHttp::close(int code, const QString &reason) { d->close(code, reason); } void WebSocketOverHttp::setHeaders(const HttpHeaders &headers) { d->requestData.headers = headers; d->sanitizeRequestHeaders(); } #include "websocketoverhttp.moc" pushpin-1.39.1/src/cpp/proxy/websocketoverhttp.h000066400000000000000000000056621457610542000217750ustar00rootroot00000000000000/* * Copyright (C) 2014-2020 Fanout, Inc. * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef WEBSOCKETOVERHTTP_H #define WEBSOCKETOVERHTTP_H #include "websocket.h" #include #include using std::map; using Signal = boost::signals2::signal; using Connection = boost::signals2::scoped_connection; class ZhttpManager; class WebSocketOverHttp : public WebSocket { Q_OBJECT public: WebSocketOverHttp(ZhttpManager *zhttpManager, QObject *parent = 0); ~WebSocketOverHttp(); void setConnectionId(const QByteArray &id); void setMaxEventsPerRequest(int max); void refresh(); // disconnection management is thread local static void setMaxManagedDisconnects(int max); static void clearDisconnectManager(); // reimplemented virtual QHostAddress peerAddress() const; virtual void setConnectHost(const QString &host); virtual void setConnectPort(int port); virtual void setIgnorePolicies(bool on); virtual void setTrustConnectHost(bool on); virtual void setIgnoreTlsErrors(bool on); virtual void start(const QUrl &uri, const HttpHeaders &headers); virtual void respondSuccess(const QByteArray &reason, const HttpHeaders &headers); virtual void respondError(int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body); virtual State state() const; virtual QUrl requestUri() const; virtual HttpHeaders requestHeaders() const; virtual int responseCode() const; virtual QByteArray responseReason() const; virtual HttpHeaders responseHeaders() const; virtual QByteArray responseBody() const; virtual int framesAvailable() const; virtual int writeBytesAvailable() const; virtual int peerCloseCode() const; virtual QString peerCloseReason() const; virtual ErrorCondition errorCondition() const; virtual void writeFrame(const Frame &frame); virtual Frame readFrame(); virtual void close(int code = -1, const QString &reason = QString()); void setHeaders(const HttpHeaders &headers); Signal aboutToSendRequest; Signal disconnected; private: class DisconnectManager; friend class DisconnectManager; WebSocketOverHttp(QObject *parent = 0); void sendDisconnect(); class Private; friend class Private; Private *d; static thread_local DisconnectManager *g_disconnectManager; static thread_local int g_maxManagedDisconnects; }; #endif pushpin-1.39.1/src/cpp/proxy/wscontrolmanager.cpp000066400000000000000000000263051457610542000221300ustar00rootroot00000000000000/* * Copyright (C) 2014-2020 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "wscontrolmanager.h" #include #include #include #include #include "qzmqsocket.h" #include "qzmqvalve.h" #include "qzmqreqmessage.h" #include "log.h" #include "tnetstring.h" #include "zutil.h" #include "logutil.h" #include "wscontrolsession.h" #define DEFAULT_HWM 101000 #define REFRESH_INTERVAL 1000 #define SESSION_EXPIRE 60000 #define SESSION_SHOULD_PROCESS (SESSION_EXPIRE * 3 / 4) #define SESSION_MUST_PROCESS (SESSION_EXPIRE * 4 / 5) #define SESSION_REFRESH_BUCKETS (SESSION_SHOULD_PROCESS / REFRESH_INTERVAL) #define PACKET_ITEMS_MAX 128 class WsControlManager::Private : public QObject { Q_OBJECT public: class KeepAliveRegistration { public: WsControlSession *s; qint64 lastRefresh; int refreshBucket; }; WsControlManager *q; QByteArray identity; int ipcFileMode; QStringList initSpecs; QStringList streamSpecs; QZmq::Socket *initSock; QZmq::Socket *streamSock; QZmq::Valve *streamValve; QHash sessionsByCid; QTimer *refreshTimer; QHash keepAliveRegistrations; QMap, KeepAliveRegistration*> sessionsByLastRefresh; QSet sessionRefreshBuckets[SESSION_REFRESH_BUCKETS]; int currentSessionRefreshBucket; Connection streamValveConnection; Private(WsControlManager *_q) : QObject(_q), q(_q), ipcFileMode(-1), initSock(0), streamSock(0), streamValve(0), currentSessionRefreshBucket(0) { refreshTimer = new QTimer(this); connect(refreshTimer, &QTimer::timeout, this, &Private::refresh_timeout); } ~Private() { assert(sessionsByCid.isEmpty()); assert(keepAliveRegistrations.isEmpty()); refreshTimer->disconnect(this); refreshTimer->setParent(0); refreshTimer->deleteLater(); } bool setupInit() { delete initSock; initSock = new QZmq::Socket(QZmq::Socket::Push, this); initSock->setHwm(DEFAULT_HWM); initSock->setShutdownWaitTime(0); foreach(const QString &spec, initSpecs) { QString errorMessage; if(!ZUtil::setupSocket(initSock, spec, true, ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } } return true; } bool setupStream() { delete streamSock; streamSock = new QZmq::Socket(QZmq::Socket::Router, this); streamSock->setIdentity(identity); streamSock->setHwm(DEFAULT_HWM); streamSock->setShutdownWaitTime(0); foreach(const QString &spec, streamSpecs) { QString errorMessage; if(!ZUtil::setupSocket(streamSock, spec, true, ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } } streamValve = new QZmq::Valve(streamSock, this); streamValveConnection = streamValve->readyRead.connect(boost::bind(&Private::stream_readyRead, this, boost::placeholders::_1)); streamValve->open(); return true; } int smallestSessionRefreshBucket() { int best = -1; int bestSize = 0; for(int n = 0; n < SESSION_REFRESH_BUCKETS; ++n) { if(best == -1 || sessionRefreshBuckets[n].count() < bestSize) { best = n; bestSize = sessionRefreshBuckets[n].count(); } } return best; } void writeInit(const WsControlPacket &packet) { assert(streamSock); QVariant vpacket = packet.toVariant(); QByteArray buf = TnetString::fromVariant(vpacket); if(log_outputLevel() >= LOG_LEVEL_DEBUG) LogUtil::logVariant(LOG_LEVEL_DEBUG, vpacket, "wscontrol: OUT"); initSock->write(QList() << buf); } void writeStream(const WsControlPacket &packet, const QByteArray &instanceAddress) { assert(streamSock); QVariant vpacket = packet.toVariant(); QByteArray buf = TnetString::fromVariant(vpacket); if(log_outputLevel() >= LOG_LEVEL_DEBUG) LogUtil::logVariant(LOG_LEVEL_DEBUG, vpacket, "wscontrol: OUT to=%s", instanceAddress.data()); QList msg; msg += instanceAddress; msg += QByteArray(); msg += buf; streamSock->write(msg); } void writeInit(const WsControlPacket::Item &item) { WsControlPacket out; out.from = identity; out.items += item; writeInit(out); } void writeStream(const WsControlPacket::Item &item, const QByteArray &instanceAddress) { WsControlPacket out; out.from = identity; out.items += item; writeStream(out, instanceAddress); } void registerKeepAlive(WsControlSession *s) { if(keepAliveRegistrations.contains(s)) return; qint64 now = QDateTime::currentMSecsSinceEpoch(); KeepAliveRegistration *r = new KeepAliveRegistration; r->s = s; keepAliveRegistrations.insert(s, r); r->lastRefresh = now; sessionsByLastRefresh.insert(QPair(r->lastRefresh, r), r); r->refreshBucket = smallestSessionRefreshBucket(); sessionRefreshBuckets[r->refreshBucket] += r; setupKeepAlive(); } void unregisterKeepAlive(WsControlSession *s) { KeepAliveRegistration *r = keepAliveRegistrations.value(s); if(!r) return; sessionRefreshBuckets[r->refreshBucket].remove(r); sessionsByLastRefresh.remove(QPair(r->lastRefresh, r)); keepAliveRegistrations.remove(s); delete r; setupKeepAlive(); } void setupKeepAlive() { if(!keepAliveRegistrations.isEmpty()) { if(!refreshTimer->isActive()) refreshTimer->start(REFRESH_INTERVAL); } else refreshTimer->stop(); } private: void stream_readyRead(const QList &message) { QZmq::ReqMessage req(message); if(req.content().count() != 1) { log_warning("wscontrol: received message with parts != 1, skipping"); return; } QVariant data = TnetString::toVariant(req.content()[0]); if(data.isNull()) { log_warning("wscontrol: received message with invalid format (tnetstring parse failed), skipping"); return; } if(log_outputLevel() >= LOG_LEVEL_DEBUG) LogUtil::logVariant(LOG_LEVEL_DEBUG, data, "wscontrol: IN"); WsControlPacket p; if(!p.fromVariant(data)) { log_warning("wscontrol: received message with invalid format (parse failed), skipping"); return; } if(p.from.isEmpty()) { log_warning("wscontrol: received message with invalid from value, skipping"); return; } QPointer self = this; foreach(const WsControlPacket::Item &i, p.items) { WsControlSession *s = sessionsByCid.value(i.cid); if(!s) { log_debug("wscontrol: received item for unknown connection id, canceling"); // if this was not an error item, send cancel if(i.type != WsControlPacket::Item::Cancel) { WsControlPacket::Item out; out.cid = i.cid; out.type = WsControlPacket::Item::Cancel; writeStream(out, p.from); } continue; } s->handle(p.from, i); if(!self) return; } } private slots: void refresh_timeout() { qint64 now = QDateTime::currentMSecsSinceEpoch(); QHash packets; // process the current bucket const QSet &bucket = sessionRefreshBuckets[currentSessionRefreshBucket]; foreach(KeepAliveRegistration *r, bucket) { // move to the end QPair k(r->lastRefresh, r); sessionsByLastRefresh.remove(k); r->lastRefresh = now; sessionsByLastRefresh.insert(QPair(r->lastRefresh, r), r); QByteArray peer = r->s->peer(); if(peer.isEmpty()) continue; if(!packets.contains(peer)) { WsControlPacket packet; packet.from = identity; packets.insert(peer, packet); } WsControlPacket &packet = packets[peer]; WsControlPacket::Item i; i.cid = r->s->cid(); i.type = WsControlPacket::Item::KeepAlive; i.ttl = SESSION_EXPIRE / 1000; packet.items += i; // if we're at max, send out now if(packet.items.count() >= PACKET_ITEMS_MAX) { writeStream(packet, peer); packet.items.clear(); } } // process any others qint64 threshold = now - SESSION_MUST_PROCESS; while(!sessionsByLastRefresh.isEmpty()) { QMap, KeepAliveRegistration*>::iterator it = sessionsByLastRefresh.begin(); KeepAliveRegistration *r = it.value(); if(r->lastRefresh > threshold) break; // move to the end sessionsByLastRefresh.erase(it); r->lastRefresh = now; sessionsByLastRefresh.insert(QPair(r->lastRefresh, r), r); QByteArray peer = r->s->peer(); if(peer.isEmpty()) continue; if(!packets.contains(peer)) { WsControlPacket packet; packet.from = identity; packets.insert(peer, packet); } WsControlPacket &packet = packets[peer]; WsControlPacket::Item i; i.cid = r->s->cid(); i.type = WsControlPacket::Item::KeepAlive; i.ttl = SESSION_EXPIRE / 1000; packet.items += i; // if we're at max, send out now if(packet.items.count() >= PACKET_ITEMS_MAX) { writeStream(packet, peer); packet.items.clear(); } } // send the rest QHashIterator it(packets); while(it.hasNext()) { it.next(); const QByteArray &peer = it.key(); const WsControlPacket &packet = it.value(); if(!packet.items.isEmpty()) writeStream(packet, peer); } ++currentSessionRefreshBucket; if(currentSessionRefreshBucket >= SESSION_REFRESH_BUCKETS) currentSessionRefreshBucket = 0; } }; WsControlManager::WsControlManager() { d = std::make_unique(this); } WsControlManager::~WsControlManager() = default; void WsControlManager::setIdentity(const QByteArray &id) { d->identity = id; } void WsControlManager::setIpcFileMode(int mode) { d->ipcFileMode = mode; } bool WsControlManager::setInitSpecs(const QStringList &specs) { d->initSpecs = specs; return d->setupInit(); } bool WsControlManager::setStreamSpecs(const QStringList &specs) { d->streamSpecs = specs; return d->setupStream(); } WsControlSession *WsControlManager::createSession(const QByteArray &cid) { WsControlSession *s = new WsControlSession; s->setup(this, cid); return s; } void WsControlManager::link(WsControlSession *s, const QByteArray &cid) { d->sessionsByCid.insert(cid, s); } void WsControlManager::unlink(const QByteArray &cid) { d->sessionsByCid.remove(cid); } void WsControlManager::writeInit(const WsControlPacket::Item &item) { d->writeInit(item); } void WsControlManager::writeStream(const WsControlPacket::Item &item, const QByteArray &instanceAddress) { d->writeStream(item, instanceAddress); } void WsControlManager::registerKeepAlive(WsControlSession *s) { d->registerKeepAlive(s); } void WsControlManager::unregisterKeepAlive(WsControlSession *s) { d->unregisterKeepAlive(s); } #include "wscontrolmanager.moc" pushpin-1.39.1/src/cpp/proxy/wscontrolmanager.h000066400000000000000000000031601457610542000215670ustar00rootroot00000000000000/* * Copyright (C) 2014 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef WSCONTROLMANAGER_H #define WSCONTROLMANAGER_H #include #include #include "packet/wscontrolpacket.h" class WsControlSession; class WsControlManager : public QObject { Q_OBJECT public: WsControlManager(); ~WsControlManager(); void setIdentity(const QByteArray &id); void setIpcFileMode(int mode); bool setInitSpecs(const QStringList &specs); bool setStreamSpecs(const QStringList &specs); WsControlSession *createSession(const QByteArray &cid); private: class Private; std::unique_ptr d; friend class WsControlSession; void link(WsControlSession *s, const QByteArray &cid); void unlink(const QByteArray &cid); void writeInit(const WsControlPacket::Item &item); void writeStream(const WsControlPacket::Item &item, const QByteArray &instanceAddress); void registerKeepAlive(WsControlSession *s); void unregisterKeepAlive(WsControlSession *s); }; #endif pushpin-1.39.1/src/cpp/proxy/wscontrolsession.cpp000066400000000000000000000170201457610542000221730ustar00rootroot00000000000000/* * Copyright (C) 2014-2022 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "wscontrolsession.h" #include #include #include #include #include "wscontrolmanager.h" #define SESSION_TTL 60 #define REQUEST_TIMEOUT 8000 class WsControlSession::Private : public QObject { Q_OBJECT public: WsControlSession *q; WsControlManager *manager; int nextReqId; QList pendingItems; QHash pendingRequests; QList pendingSendEventWrites; QTimer *requestTimer; QByteArray peer; QByteArray cid; QByteArray route; bool separateStats; QByteArray channelPrefix; QUrl uri; Private(WsControlSession *_q) : QObject(_q), q(_q), manager(0), nextReqId(0), separateStats(false) { requestTimer = new QTimer(this); requestTimer->setSingleShot(true); connect(requestTimer, &QTimer::timeout, this, &Private::requestTimer_timeout); } ~Private() { cleanup(); requestTimer->setParent(0); requestTimer->disconnect(this); requestTimer->deleteLater(); } void cleanup() { if(manager) { manager->unregisterKeepAlive(q); WsControlPacket::Item i; i.type = WsControlPacket::Item::Gone; write(i); manager->unlink(cid); manager = 0; } } void start() { manager->registerKeepAlive(q); int reqId = nextReqId++; WsControlPacket::Item i; i.type = WsControlPacket::Item::Here; i.requestId = QByteArray::number(reqId); i.route = route; i.separateStats = separateStats; i.channelPrefix = channelPrefix; i.uri = uri; i.ttl = SESSION_TTL; write(i, true); } void setupRequestTimer() { if(!pendingRequests.isEmpty()) { // find next expiring request qint64 lowestTime = -1; QHashIterator it(pendingRequests); while(it.hasNext()) { it.next(); qint64 time = it.value(); if(lowestTime == -1 || time < lowestTime) lowestTime = time; } int until = int(lowestTime - QDateTime::currentMSecsSinceEpoch()); requestTimer->start(qMax(until, 0)); } else { requestTimer->stop(); } } void sendGripMessage(const QByteArray &message) { int reqId = nextReqId++; WsControlPacket::Item i; i.type = WsControlPacket::Item::Grip; i.requestId = QByteArray::number(reqId); i.message = message; pendingRequests[reqId] = QDateTime::currentMSecsSinceEpoch() + REQUEST_TIMEOUT; setupRequestTimer(); write(i); } void sendNeedKeepAlive() { WsControlPacket::Item i; i.type = WsControlPacket::Item::NeedKeepAlive; write(i); } void sendSubscribe(const QByteArray &channel) { int reqId = nextReqId++; WsControlPacket::Item i; i.type = WsControlPacket::Item::Subscribe; i.requestId = QByteArray::number(reqId); i.channel = channel; pendingRequests[reqId] = QDateTime::currentMSecsSinceEpoch() + REQUEST_TIMEOUT; setupRequestTimer(); write(i); } void write(const WsControlPacket::Item &item, bool init = false) { if(init) { WsControlPacket::Item out = item; out.cid = cid; manager->writeInit(out); } else { if(!peer.isEmpty()) { WsControlPacket::Item out = item; out.cid = cid; manager->writeStream(out, peer); } else pendingItems += item; } } void flushPending() { while(!pendingItems.isEmpty()) { WsControlPacket::Item out = pendingItems.takeFirst(); out.cid = cid; manager->writeStream(out, peer); } } void handle(const QByteArray &from, const WsControlPacket::Item &item) { peer = from; flushPending(); if(item.type != WsControlPacket::Item::Ack && !item.requestId.isEmpty()) { // ack non-sends immediately if(item.type != WsControlPacket::Item::Send) { WsControlPacket::Item i; i.type = WsControlPacket::Item::Ack; i.requestId = item.requestId; write(i); } } if(item.type == WsControlPacket::Item::Send) { WebSocket::Frame::Type type; if(item.contentType == "binary") type = WebSocket::Frame::Binary; else if(item.contentType == "ping") type = WebSocket::Frame::Ping; else if(item.contentType == "pong") type = WebSocket::Frame::Pong; else type = WebSocket::Frame::Text; // for sends, don't ack until written if(!item.requestId.isEmpty()) pendingSendEventWrites += item.requestId; else pendingSendEventWrites += QByteArray(); // placeholder q->sendEventReceived(type, item.message, item.queue); } else if(item.type == WsControlPacket::Item::KeepAliveSetup) { if(item.timeout > 0) { WsControl::KeepAliveMode mode; if(item.keepAliveMode == "interval") mode = WsControl::Interval; else // idle mode = WsControl::Idle; q->keepAliveSetupEventReceived(mode, item.timeout); } else q->keepAliveSetupEventReceived(WsControl::NoKeepAlive, -1); } else if(item.type == WsControlPacket::Item::Refresh) { q->refreshEventReceived(); } else if(item.type == WsControlPacket::Item::Close) { q->closeEventReceived(item.code, item.reason); } else if(item.type == WsControlPacket::Item::Detach) { q->detachEventReceived(); } else if(item.type == WsControlPacket::Item::Cancel) { q->cancelEventReceived(); } else if(item.type == WsControlPacket::Item::Ack) { int reqId = item.requestId.toInt(); if(pendingRequests.contains(reqId)) { pendingRequests.remove(reqId); setupRequestTimer(); } } } void sendEventWritten() { assert(!pendingSendEventWrites.isEmpty()); QByteArray requestId = pendingSendEventWrites.takeFirst(); if(!requestId.isNull()) { WsControlPacket::Item i; i.type = WsControlPacket::Item::Ack; i.requestId = requestId; write(i); } } private slots: void requestTimer_timeout() { // on error, destroy any other pending requests pendingRequests.clear(); setupRequestTimer(); q->error(); } }; WsControlSession::WsControlSession() { d = std::make_unique(this); } WsControlSession::~WsControlSession() = default; QByteArray WsControlSession::peer() const { return d->peer; } QByteArray WsControlSession::cid() const { return d->cid; } void WsControlSession::start(const QByteArray &routeId, bool separateStats, const QByteArray &channelPrefix, const QUrl &uri) { d->route = routeId; d->separateStats = separateStats; d->channelPrefix = channelPrefix; d->uri = uri; d->start(); } void WsControlSession::sendGripMessage(const QByteArray &message) { d->sendGripMessage(message); } void WsControlSession::sendNeedKeepAlive() { d->sendNeedKeepAlive(); } void WsControlSession::sendSubscribe(const QByteArray &channel) { d->sendSubscribe(channel); } void WsControlSession::setup(WsControlManager *manager, const QByteArray &cid) { d->manager = manager; d->cid = cid; d->manager->link(this, d->cid); } void WsControlSession::sendEventWritten() { d->sendEventWritten(); } void WsControlSession::handle(const QByteArray &from, const WsControlPacket::Item &item) { assert(d->manager); d->handle(from, item); } #include "wscontrolsession.moc" pushpin-1.39.1/src/cpp/proxy/wscontrolsession.h000066400000000000000000000041501457610542000216400ustar00rootroot00000000000000/* * Copyright (C) 2014-2022 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef WSCONTROLSESSION_H #define WSCONTROLSESSION_H #include #include #include "websocket.h" #include "wscontrol.h" #include "packet/wscontrolpacket.h" #include using Signal = boost::signals2::signal; class WsControlManager; class WsControlSession : public QObject { Q_OBJECT public: ~WsControlSession(); QByteArray peer() const; QByteArray cid() const; void start(const QByteArray &routeId, bool separateStats, const QByteArray &channelPrefix, const QUrl &uri); void sendGripMessage(const QByteArray &message); void sendNeedKeepAlive(); void sendSubscribe(const QByteArray &channel); // tell session that a received sendEvent has been written void sendEventWritten(); boost::signals2::signal sendEventReceived; boost::signals2::signal keepAliveSetupEventReceived; Signal refreshEventReceived; boost::signals2::signal closeEventReceived; // Use -1 for no code Signal detachEventReceived; Signal cancelEventReceived; Signal error; private: class Private; friend class Private; std::unique_ptr d; friend class WsControlManager; WsControlSession(); void setup(WsControlManager *manager, const QByteArray &cid); void handle(const QByteArray &from, const WsControlPacket::Item &item); }; #endif pushpin-1.39.1/src/cpp/proxy/wsproxysession.cpp000066400000000000000000000727531457610542000217120ustar00rootroot00000000000000/* * Copyright (C) 2014-2023 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "wsproxysession.h" #include #include #include #include #include #include #include #include "packet/httprequestdata.h" #include "log.h" #include "rtimer.h" #include "jwt.h" #include "zhttpmanager.h" #include "zwebsocket.h" #include "websocketoverhttp.h" #include "zroutes.h" #include "wscontrol.h" #include "wscontrolmanager.h" #include "wscontrolsession.h" #include "xffrule.h" #include "proxyutil.h" #include "statsmanager.h" #include "inspectdata.h" #include "connectionmanager.h" #include "testwebsocket.h" #define ACTIVITY_TIMEOUT 60000 #define KEEPALIVE_RAND_MAX 1000 class HttpExtension { public: bool isNull() const { return name.isEmpty(); } QByteArray name; QHash params; }; static int findNext(const QByteArray &in, const char *charList, int start = 0) { int len = qstrlen(charList); for(int n = start; n < in.size(); ++n) { char c = in[n]; for(int i = 0; i < len; ++i) { if(c == charList[i]) return n; } } return -1; } static QHash parseParams(const QByteArray &in, bool *ok = 0) { QHash out; int start = 0; while(start < in.size()) { QByteArray var; QByteArray val; int at = findNext(in, "=;", start); if(at != -1) { var = in.mid(start, at - start).trimmed(); if(in[at] == '=') { if(at + 1 >= in.size()) { if(ok) *ok = false; return QHash(); } ++at; if(in[at] == '\"') { ++at; bool complete = false; for(int n = at; n < in.size(); ++n) { if(in[n] == '\\') { if(n + 1 >= in.size()) { if(ok) *ok = false; return QHash(); } ++n; val += in[n]; } else if(in[n] == '\"') { complete = true; at = n + 1; break; } else val += in[n]; } if(!complete) { if(ok) *ok = false; return QHash(); } at = in.indexOf(';', at); if(at != -1) start = at + 1; else start = in.size(); } else { int vstart = at; at = in.indexOf(';', vstart); if(at != -1) { val = in.mid(vstart, at - vstart).trimmed(); start = at + 1; } else { val = in.mid(vstart).trimmed(); start = in.size(); } } } else start = at + 1; } else { var = in.mid(start).trimmed(); start = in.size(); } out[var] = val; } if(ok) *ok = true; return out; } static QByteArray getExtensionRaw(const QList &extStrings, const QByteArray &name) { foreach(const QByteArray &ext, extStrings) { int at = ext.indexOf(';'); if(at != -1) { if(ext.mid(0, at).trimmed() == name) return ext; } else { if(ext == name) return ext; } } return QByteArray(); } static HttpExtension getExtension(const QList &extStrings, const QByteArray &name) { QByteArray ext = getExtensionRaw(extStrings, name); if(ext.isNull()) return HttpExtension(); HttpExtension e; e.name = name; int at = ext.indexOf(';'); if(at != -1) { bool ok; e.params = parseParams(ext.mid(at + 1), &ok); if(!ok) return HttpExtension(); } return e; } class WsProxySession::Private : public QObject { Q_OBJECT public: enum State { Idle, Connecting, Connected, Closing }; typedef QPair QueuedFrame; struct WSConnections { Connection connectedConnection; Connection readyReadConnection; Connection writeBytesChangedConnection; Connection peerClosedConnection; Connection closedConnection; Connection errorConnection; }; struct InWSConnections { Connection readyReadConnection; Connection framesWrittenConnection; Connection writeBytesChangedConnection; Connection peerClosedConnection; Connection closedConnection; Connection errorConnection; }; struct WSProxyConnections { Connection sendEventReceivedConnection; Connection keepAliveSetupEventReceivedConnection; Connection refreshEventReceivedConnection; Connection closeEventReceivedConnection; Connection detachEventReceivedConnection; Connection cancelEventReceivedConnection; Connection errorConnection; }; WsProxySession *q; State state; ZRoutes *zroutes; ZhttpManager *zhttpManager; ConnectionManager *connectionManager; StatsManager *statsManager; WsControlManager *wsControlManager; WsControlSession *wsControl; DomainMap::Entry route; bool debug; QByteArray defaultSigIss; Jwt::EncodingKey defaultSigKey; Jwt::DecodingKey defaultUpstreamKey; bool passToUpstream; bool acceptXForwardedProtocol; bool useXForwardedProto; bool useXForwardedProtocol; XffRule xffRule; XffRule xffTrustedRule; QList origHeadersNeedMark; bool acceptPushpinRoute; QByteArray cdnLoop; HttpRequestData requestData; bool trustedClient; QHostAddress logicalClientAddress; QByteArray sigIss; Jwt::EncodingKey sigKey; WebSocket *inSock; WebSocket *outSock; QList inPendingFrames; // true means we should ack a send event int outReadInProgress; // frame type or -1 QByteArray pathBeg; QByteArray channelPrefix; QList targets; DomainMap::Target target; QHostAddress clientAddress; bool acceptGripMessages; QByteArray messagePrefix; bool detached; QDateTime activityTime; QByteArray publicCid; RTimer *keepAliveTimer; WsControl::KeepAliveMode keepAliveMode; int keepAliveTimeout; QList queuedInFrames; // frames to deliver after out read finishes LogUtil::Config logConfig; Callback> finishedByPassthroughCallback; Connection keepAliveConnection; Connection aboutToSendRequestConnection; map wsProxyConnectionMap; WSConnections outWSConnection; InWSConnections inWSConnection; Private(WsProxySession *_q, ZRoutes *_zroutes, ConnectionManager *_connectionManager, const LogUtil::Config &_logConfig, StatsManager *_statsManager, WsControlManager *_wsControlManager) : QObject(_q), q(_q), state(Idle), zroutes(_zroutes), zhttpManager(0), connectionManager(_connectionManager), statsManager(_statsManager), wsControlManager(_wsControlManager), wsControl(0), debug(false), passToUpstream(false), acceptXForwardedProtocol(false), useXForwardedProto(false), useXForwardedProtocol(false), acceptPushpinRoute(false), trustedClient(false), inSock(0), outSock(0), outReadInProgress(-1), acceptGripMessages(false), detached(false), keepAliveTimer(0), keepAliveMode(WsControl::NoKeepAlive), keepAliveTimeout(0), logConfig(_logConfig) { } ~Private() { cleanup(); } void cleanup() { cleanupKeepAliveTimer(); cleanupInSock(); outWSConnection = WSConnections(); delete outSock; outSock = 0; wsProxyConnectionMap.erase(wsControl); delete wsControl; wsControl = 0; if(zhttpManager) { zroutes->removeRef(zhttpManager); zhttpManager = 0; } } void cleanupInSock() { if(inSock) { connectionManager->removeConnection(inSock); inWSConnection = InWSConnections(); delete inSock; inSock = 0; } } void cleanupKeepAliveTimer() { if(keepAliveTimer) { keepAliveConnection.disconnect(); keepAliveTimer->setParent(0); keepAliveTimer->deleteLater(); keepAliveTimer = 0; } } void start(WebSocket *sock, const QByteArray &_publicCid, const DomainMap::Entry &entry) { assert(!inSock); state = Connecting; publicCid = _publicCid; if(statsManager) activityTime = QDateTime::currentDateTimeUtc(); inSock = sock; inSock->setParent(this); inWSConnection = InWSConnections{ inSock->readyRead.connect(boost::bind(&Private::in_readyRead, this)), inSock->framesWritten.connect(boost::bind(&Private::in_framesWritten, this, boost::placeholders::_1, boost::placeholders::_2)), inSock->writeBytesChanged.connect(boost::bind(&Private::in_writeBytesChanged, this)), inSock->peerClosed.connect(boost::bind(&Private::in_peerClosed, this)), inSock->closed.connect(boost::bind(&Private::in_closed, this)), inSock->error.connect(boost::bind(&Private::in_error, this)) }; requestData.uri = inSock->requestUri(); requestData.headers = inSock->requestHeaders(); trustedClient = ProxyUtil::checkTrustedClient("wsproxysession", q, requestData, defaultUpstreamKey); logicalClientAddress = ProxyUtil::getLogicalAddress(requestData.headers, trustedClient ? xffTrustedRule : xffRule, inSock->peerAddress()); QString host = requestData.uri.host(); route = entry; log_debug("wsproxysession: %p %s has %d routes", q, qPrintable(host), route.targets.count()); if(route.isNull()) { reject(false, 502, "Bad Gateway", QString("No route for host: %1").arg(host)); return; } incCounter(Stats::ClientHeaderBytesReceived, ZhttpManager::estimateRequestHeaderBytes("GET", requestData.uri, requestData.headers)); if(!route.asHost.isEmpty()) ProxyUtil::applyHost(&requestData.uri, route.asHost); QByteArray path = requestData.uri.path(QUrl::FullyEncoded).toUtf8(); if(route.pathRemove > 0) path = path.mid(route.pathRemove); if(!route.pathPrepend.isEmpty()) path = route.pathPrepend + path; requestData.uri.setPath(QString::fromUtf8(path), QUrl::StrictMode); sigIss = defaultSigIss; sigKey = defaultSigKey; if(!route.sigIss.isEmpty()) sigIss = route.sigIss; if(!route.sigKey.isNull()) sigKey = route.sigKey; pathBeg = route.pathBeg; channelPrefix = route.prefix; targets = route.targets; foreach(const HttpHeader &h, route.headers) { requestData.headers.removeAll(h.first); if(!h.second.isEmpty()) requestData.headers += HttpHeader(h.first, h.second); } clientAddress = inSock->peerAddress(); ProxyUtil::manipulateRequestHeaders("wsproxysession", q, &requestData, trustedClient, route, sigIss, sigKey, acceptXForwardedProtocol, useXForwardedProto, useXForwardedProtocol, xffTrustedRule, xffRule, origHeadersNeedMark, acceptPushpinRoute, cdnLoop, clientAddress, InspectData(), route.grip, false); // don't proxy extensions, as we may not know how to handle them requestData.headers.removeAll("Sec-WebSocket-Extensions"); if(route.grip) { // send grip extension requestData.headers += HttpHeader("Sec-WebSocket-Extensions", "grip"); } if(trustedClient || !route.grip) passToUpstream = true; tryNextTarget(); } void writeInFrame(const WebSocket::Frame &frame, bool fromSendEvent = false) { inPendingFrames += fromSendEvent; inSock->writeFrame(frame); incCounter(Stats::ClientContentBytesSent, frame.data.size()); if(!frame.more) incCounter(Stats::ClientMessagesSent); } void tryNextTarget() { if(targets.isEmpty()) { QString msg = "Error while proxying to origin."; QStringList targetStrs; foreach(const DomainMap::Target &t, route.targets) targetStrs += ProxyUtil::targetToString(t); QString dmsg = QString("Unable to connect to any targets. Tried: %1").arg(targetStrs.join(", ")); reject(true, 502, "Bad Gateway", msg, dmsg); return; } target = targets.takeFirst(); QUrl uri = requestData.uri; if(target.ssl) uri.setScheme("wss"); else uri.setScheme("ws"); if(!target.host.isEmpty()) ProxyUtil::applyHost(&uri, target.host); if(zhttpManager) { zroutes->removeRef(zhttpManager); zhttpManager = 0; } if(target.type == DomainMap::Target::Test) { // for test route, auto-adjust path if(!pathBeg.isEmpty()) { int pathRemove = pathBeg.length(); if(pathBeg.endsWith('/')) --pathRemove; if(pathRemove > 0) uri.setPath(uri.path(QUrl::FullyEncoded).mid(pathRemove)); } outSock = new TestWebSocket(this); } else { if(target.type == DomainMap::Target::Custom) { zhttpManager = zroutes->managerForRoute(target.zhttpRoute); log_debug("wsproxysession: %p forwarding to %s", q, qPrintable(target.zhttpRoute.baseSpec)); } else // Default { zhttpManager = zroutes->defaultManager(); log_debug("wsproxysession: %p forwarding to %s:%d", q, qPrintable(target.connectHost), target.connectPort); } zroutes->addRef(zhttpManager); if(target.overHttp) { WebSocketOverHttp *woh = new WebSocketOverHttp(zhttpManager, this); woh->setConnectionId(publicCid); if(target.oneEvent) woh->setMaxEventsPerRequest(1); aboutToSendRequestConnection = woh->aboutToSendRequest.connect(boost::bind(&Private::out_aboutToSendRequest, this, woh)); outSock = woh; } else { // websockets don't work with zhttp req mode if(zhttpManager->clientUsesReq()) { reject(false, 502, "Bad Gateway", "Error while proxying to origin.", "WebSockets cannot be used with zhttpreq target"); return; } outSock = zhttpManager->createSocket(); outSock->setParent(this); } } outWSConnection = { outSock->connected.connect(boost::bind(&Private::out_connected, this)), outSock->readyRead.connect(boost::bind(&Private::out_readyRead, this)), outSock->writeBytesChanged.connect(boost::bind(&Private::out_writeBytesChanged, this)), outSock->peerClosed.connect(boost::bind(&Private::out_peerClosed, this)), outSock->closed.connect(boost::bind(&Private::out_closed, this)), outSock->error.connect(boost::bind(&Private::out_error, this)) }; if(target.trusted) outSock->setIgnorePolicies(true); if(target.trustConnectHost) outSock->setTrustConnectHost(true); if(target.insecure) outSock->setIgnoreTlsErrors(true); if(target.type == DomainMap::Target::Default) { outSock->setConnectHost(target.connectHost); outSock->setConnectPort(target.connectPort); } ProxyUtil::applyHostHeader(&requestData.headers, uri); incCounter(Stats::ServerHeaderBytesSent, ZhttpManager::estimateRequestHeaderBytes("GET", uri, requestData.headers)); outSock->start(uri, requestData.headers); } void reject(bool proxied, int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body) { assert(state == Connecting); state = Closing; inSock->respondError(code, reason, headers, body); incCounter(Stats::ClientHeaderBytesSent, ZhttpManager::estimateResponseHeaderBytes(code, reason, headers)); logConnection(proxied, code, body.size()); } void reject(bool proxied, int code, const QString &reason, const QString &errorMessage, const QString &debugErrorMessage) { QString msg = debug ? debugErrorMessage : errorMessage; reject(proxied, code, reason.toUtf8(), HttpHeaders(), (msg + '\n').toUtf8()); } void reject(bool proxied, int code, const QString &reason, const QString &errorMessage) { reject(proxied, code, reason, errorMessage, errorMessage); } void tryReadIn() { while(inSock->framesAvailable() > 0 && ((outSock && outSock->writeBytesAvailable() > 0) || detached)) { WebSocket::Frame f = inSock->readFrame(); tryLogActivity(); incCounter(Stats::ClientContentBytesReceived, f.data.size()); if(!f.more) incCounter(Stats::ClientMessagesReceived); if(detached) continue; outSock->writeFrame(f); incCounter(Stats::ServerContentBytesSent, f.data.size()); if(!f.more) incCounter(Stats::ServerMessagesSent); } } void tryReadOut() { while(outSock->framesAvailable() > 0 && ((inSock && inSock->writeBytesAvailable() > 0) || detached)) { WebSocket::Frame f = outSock->readFrame(); tryLogActivity(); incCounter(Stats::ServerContentBytesReceived, f.data.size()); if(!f.more) incCounter(Stats::ServerMessagesReceived); if(detached && outReadInProgress == -1) continue; if(f.type == WebSocket::Frame::Text || f.type == WebSocket::Frame::Binary || f.type == WebSocket::Frame::Continuation) { // we are skipping the rest of this message if(f.type == WebSocket::Frame::Continuation && outReadInProgress == -1) continue; if(f.type != WebSocket::Frame::Continuation) outReadInProgress = (int)f.type; if(wsControl && acceptGripMessages) { if(f.type == WebSocket::Frame::Text && f.data.startsWith("c:")) { // grip messages must only be one frame if(!f.more) wsControl->sendGripMessage(f.data.mid(2)); // process else outReadInProgress = -1; // ignore rest of message } else if(f.type != WebSocket::Frame::Continuation) { if(f.data.startsWith(messagePrefix)) { f.data = f.data.mid(messagePrefix.size()); writeInFrame(f); adjustKeepAlive(); } else { log_debug("wsproxysession: dropping unprefixed message"); } } else if(f.type == WebSocket::Frame::Continuation) { assert(outReadInProgress != -1); writeInFrame(f); adjustKeepAlive(); } } else { writeInFrame(f); adjustKeepAlive(); } if(!f.more) outReadInProgress = -1; } else { // always relay non-content frames writeInFrame(f); adjustKeepAlive(); } if(outReadInProgress == -1 && !queuedInFrames.isEmpty()) { foreach(const QueuedFrame &i, queuedInFrames) writeInFrame(i.first, i.second); } } } void tryFinish() { if(!inSock && !outSock) { cleanup(); finishedByPassthroughCallback.call({q}); } } void tryLogActivity() { if(statsManager && !activityTime.isNull()) { QDateTime now = QDateTime::currentDateTimeUtc(); if(now >= activityTime.addMSecs(ACTIVITY_TIMEOUT)) { statsManager->addActivity(route.id); activityTime = activityTime.addMSecs((activityTime.msecsTo(now) / ACTIVITY_TIMEOUT) * ACTIVITY_TIMEOUT); } } } void logConnection(bool proxied, int responseCode, int responseBodySize) { LogUtil::RequestData rd; // only log route id if explicitly set if(route.separateStats) rd.routeId = route.id; if(responseCode != -1) { rd.status = LogUtil::Response; rd.responseData.code = responseCode; rd.responseBodySize = responseBodySize; } else { rd.status = LogUtil::Error; } rd.requestData.method = "GET"; rd.requestData.uri = inSock->requestUri(); rd.requestData.headers = inSock->requestHeaders(); if(proxied) { rd.targetStr = ProxyUtil::targetToString(target); rd.targetOverHttp = target.overHttp; } rd.fromAddress = logicalClientAddress; LogUtil::logRequest(LOG_LEVEL_INFO, rd, logConfig); } void setupKeepAlive() { if(keepAliveTimeout >= 0) { int timeout = keepAliveTimeout * 1000; timeout = qMax(timeout - (int)(QRandomGenerator::global()->generate() % KEEPALIVE_RAND_MAX), 0); keepAliveTimer->start(timeout); } } void adjustKeepAlive() { // if idle mode, restart the timer. else leave alone if(keepAliveTimer && keepAliveMode == WsControl::Idle) setupKeepAlive(); } void incCounter(Stats::Counter c, int count = 1) { if(statsManager) statsManager->incCounter(route.statsRoute(), c, count); } private slots: void in_readyRead() { if((outSock && outSock->state() == WebSocket::Connected) || detached) tryReadIn(); } void in_framesWritten(int count, int contentBytes) { Q_UNUSED(contentBytes); for(int n = 0; n < count; ++n) { bool fromSendEvent = inPendingFrames.takeFirst(); if(fromSendEvent) wsControl->sendEventWritten(); } } void in_writeBytesChanged() { if(!detached && outSock) tryReadOut(); } void in_peerClosed() { if(detached) { inSock->close(); } else { if(outSock) { if(outSock->state() == WebSocket::Connecting) { outWSConnection = WSConnections(); delete outSock; outSock = 0; inSock->close(); } else if(outSock->state() == WebSocket::Connected) { outSock->close(inSock->peerCloseCode(), inSock->peerCloseReason()); } } } } void in_closed() { int code = inSock->peerCloseCode(); QString reason = inSock->peerCloseReason(); cleanupInSock(); if(!detached && outSock && outSock->state() != WebSocket::Closing) outSock->close(code, reason); tryFinish(); } void in_error() { cleanupInSock(); if(!detached) { outWSConnection = WSConnections(); delete outSock; outSock = 0; } tryFinish(); } void out_connected() { log_debug("wsproxysession: %p connected", q); state = Connected; HttpHeaders headers = outSock->responseHeaders(); incCounter(Stats::ServerHeaderBytesReceived, ZhttpManager::estimateResponseHeaderBytes(101, outSock->responseReason(), headers)); // don't proxy extensions, as we may not know how to handle them QList wsExtensions = headers.takeAll("Sec-WebSocket-Extensions"); HttpExtension grip = getExtension(wsExtensions, "grip"); if(!grip.isNull() || !target.subscriptions.isEmpty()) { if(!grip.isNull()) { if(!passToUpstream) { if(grip.params.contains("message-prefix")) messagePrefix = grip.params.value("message-prefix"); else messagePrefix = "m:"; acceptGripMessages = true; log_debug("wsproxysession: %p grip enabled, message-prefix=[%s]", q, messagePrefix.data()); } else { // tell upstream to do the grip stuff headers += HttpHeader("Sec-WebSocket-Extensions", getExtensionRaw(wsExtensions, "grip")); } } if(wsControlManager) { wsControl = wsControlManager->createSession(publicCid); wsProxyConnectionMap[wsControl] = { wsControl->sendEventReceived.connect(boost::bind(&Private::wsControl_sendEventReceived, this, boost::placeholders::_1, boost::placeholders::_2, boost::placeholders::_3)), wsControl->keepAliveSetupEventReceived.connect(boost::bind(&Private::wsControl_keepAliveSetupEventReceived, this, boost::placeholders::_1, boost::placeholders::_2)), wsControl->refreshEventReceived.connect(boost::bind(&Private::wsControl_refreshEventReceived, this)), wsControl->closeEventReceived.connect(boost::bind(&Private::wsControl_closeEventReceived, this, boost::placeholders::_1, boost::placeholders::_2)), wsControl->detachEventReceived.connect(boost::bind(&Private::wsControl_detachEventReceived, this)), wsControl->cancelEventReceived.connect(boost::bind(&Private::wsControl_cancelEventReceived, this)), wsControl->error.connect(boost::bind(&Private::wsControl_error, this)) }; wsControl->start(route.id, route.separateStats, channelPrefix, inSock->requestUri()); foreach(const QString &subChannel, target.subscriptions) { log_debug("wsproxysession: %p implicit subscription to [%s]", q, qPrintable(subChannel)); wsControl->sendSubscribe(subChannel.toUtf8()); } } } inSock->respondSuccess(outSock->responseReason(), headers); incCounter(Stats::ClientHeaderBytesSent, ZhttpManager::estimateResponseHeaderBytes(101, outSock->responseReason(), headers)); logConnection(true, 101, 0); // send any pending frames tryReadIn(); } void out_readyRead() { tryReadOut(); } void out_writeBytesChanged() { if(!detached && inSock) tryReadIn(); } void out_peerClosed() { if(!detached && inSock && inSock->state() != WebSocket::Closing) inSock->close(outSock->peerCloseCode(), outSock->peerCloseReason()); } void out_closed() { int code = outSock->peerCloseCode(); QString reason = outSock->peerCloseReason(); outWSConnection = WSConnections(); delete outSock; outSock = 0; if(!detached && inSock && inSock->state() != WebSocket::Closing) inSock->close(code, reason); tryFinish(); } void out_error() { WebSocket::ErrorCondition e = outSock->errorCondition(); log_debug("wsproxysession: %p target error state=%d, condition=%d", q, (int)state, (int)e); if(detached) { outWSConnection = WSConnections(); delete outSock; outSock = 0; tryFinish(); return; } if(state == Connecting) { bool tryAgain = false; switch(e) { case WebSocket::ErrorConnect: case WebSocket::ErrorConnectTimeout: case WebSocket::ErrorTls: tryAgain = true; break; case WebSocket::ErrorRejected: reject(true, outSock->responseCode(), outSock->responseReason(), outSock->responseHeaders(), outSock->responseBody()); break; default: reject(true, 502, "Bad Gateway", "Error while proxying to origin."); break; } outWSConnection = WSConnections(); delete outSock; outSock = 0; if(tryAgain) tryNextTarget(); } else { cleanupInSock(); outWSConnection = WSConnections(); delete outSock; outSock = 0; tryFinish(); } } void out_aboutToSendRequest(WebSocketOverHttp *woh) { ProxyUtil::applyGripSig("wsproxysession", q, &requestData.headers, sigIss, sigKey); woh->setHeaders(requestData.headers); } private: void wsControl_sendEventReceived(WebSocket::Frame::Type type, const QByteArray &message, bool queue) { // this method accepts a full message, which must be typed if(type == WebSocket::Frame::Continuation) return; // if we have no socket to write to, say the data was written anyway. // this is not quite correct but better than leaving the send event // dangling if(!inSock || inSock->state() != WebSocket::Connected) { wsControl->sendEventWritten(); return; } // if queue == false, drop if we can't send right now if(!queue && (inSock->writeBytesAvailable() == 0 || outReadInProgress != -1)) { // if drop is allowed, drop is success :) wsControl->sendEventWritten(); return; } WebSocket::Frame f(type, message, false); if(outReadInProgress != -1) { queuedInFrames += QueuedFrame(f, true); } else { writeInFrame(f, true); } adjustKeepAlive(); } void wsControl_keepAliveSetupEventReceived(WsControl::KeepAliveMode mode, int timeout) { keepAliveMode = mode; if(keepAliveMode != WsControl::NoKeepAlive && timeout > 0) { keepAliveTimeout = timeout; if(!keepAliveTimer) { keepAliveTimer = new RTimer; keepAliveConnection = keepAliveTimer->timeout.connect(boost::bind(&Private::keepAliveTimer_timeout, this)); keepAliveTimer->setSingleShot(true); } setupKeepAlive(); } else { cleanupKeepAliveTimer(); } } void wsControl_refreshEventReceived() { WebSocketOverHttp *woh = qobject_cast(outSock); if(woh) woh->refresh(); } void wsControl_closeEventReceived(int code, const QByteArray &reason) { if(!detached && outSock && outSock->state() != WebSocket::Closing) outSock->close(); if(inSock && inSock->state() != WebSocket::Closing) inSock->close(code, reason); } void wsControl_detachEventReceived() { // if already detached, do nothing if(detached) return; detached = true; if(outSock && outSock->state() != WebSocket::Closing) outSock->close(); } void wsControl_cancelEventReceived() { if(outSock) { outWSConnection = WSConnections(); delete outSock; outSock = 0; } cleanupInSock(); tryFinish(); } void wsControl_error() { log_debug("wsproxysession: %p wscontrol session error", q); wsControl_cancelEventReceived(); } void keepAliveTimer_timeout() { wsControl->sendNeedKeepAlive(); if(keepAliveMode == WsControl::Interval) setupKeepAlive(); } }; WsProxySession::WsProxySession(ZRoutes *zroutes, ConnectionManager *connectionManager, const LogUtil::Config &logConfig, StatsManager *statsManager, WsControlManager *wsControlManager, QObject *parent) : QObject(parent) { d = new Private(this, zroutes, connectionManager, logConfig, statsManager, wsControlManager); } WsProxySession::~WsProxySession() { delete d; } QHostAddress WsProxySession::logicalClientAddress() const { return d->logicalClientAddress; } QByteArray WsProxySession::statsRoute() const { return d->route.statsRoute(); } QByteArray WsProxySession::cid() const { return d->publicCid; } WebSocket *WsProxySession::inSocket() const { return d->inSock; } WebSocket *WsProxySession::outSocket() const { return d->outSock; } void WsProxySession::setDebugEnabled(bool enabled) { d->debug = enabled; } void WsProxySession::setDefaultSigKey(const QByteArray &iss, const Jwt::EncodingKey &key) { d->defaultSigIss = iss; d->defaultSigKey = key; } void WsProxySession::setDefaultUpstreamKey(const Jwt::DecodingKey &key) { d->defaultUpstreamKey = key; } void WsProxySession::setAcceptXForwardedProtocol(bool enabled) { d->acceptXForwardedProtocol = enabled; } void WsProxySession::setUseXForwardedProtocol(bool protoEnabled, bool protocolEnabled) { d->useXForwardedProto = protoEnabled; d->useXForwardedProtocol = protocolEnabled; } void WsProxySession::setXffRules(const XffRule &untrusted, const XffRule &trusted) { d->xffRule = untrusted; d->xffTrustedRule = trusted; } void WsProxySession::setOrigHeadersNeedMark(const QList &names) { d->origHeadersNeedMark = names; } void WsProxySession::setAcceptPushpinRoute(bool enabled) { d->acceptPushpinRoute = enabled; } void WsProxySession::setCdnLoop(const QByteArray &value) { d->cdnLoop = value; } void WsProxySession::start(WebSocket *sock, const QByteArray &publicCid, const DomainMap::Entry &route) { d->start(sock, publicCid, route); } Callback> & WsProxySession::finishedByPassthroughCallback() { return d->finishedByPassthroughCallback; } #include "wsproxysession.moc" pushpin-1.39.1/src/cpp/proxy/wsproxysession.h000066400000000000000000000045441457610542000213500ustar00rootroot00000000000000/* * Copyright (C) 2014-2022 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef WSPROXYSESSION_H #define WSPROXYSESSION_H #include #include "callback.h" #include "logutil.h" #include "domainmap.h" #include using std::map; using Connection = boost::signals2::scoped_connection; namespace Jwt { class EncodingKey; class DecodingKey; } class WebSocket; class ZRoutes; class WsControlManager; class StatsManager; class ConnectionManager; class XffRule; class WsProxySession : public QObject { Q_OBJECT public: WsProxySession(ZRoutes *zroutes, ConnectionManager *connectionManager, const LogUtil::Config &logConfig, StatsManager *stats = 0, WsControlManager *wsControlManager = 0, QObject *parent = 0); ~WsProxySession(); QHostAddress logicalClientAddress() const; QByteArray statsRoute() const; QByteArray cid() const; WebSocket *inSocket() const; WebSocket *outSocket() const; void setDebugEnabled(bool enabled); void setDefaultSigKey(const QByteArray &iss, const Jwt::EncodingKey &key); void setDefaultUpstreamKey(const Jwt::DecodingKey &key); void setAcceptXForwardedProtocol(bool enabled); void setUseXForwardedProtocol(bool protoEnabled, bool protocolEnabled); void setXffRules(const XffRule &untrusted, const XffRule &trusted); void setOrigHeadersNeedMark(const QList &names); void setAcceptPushpinRoute(bool enabled); void setCdnLoop(const QByteArray &value); // takes ownership void start(WebSocket *sock, const QByteArray &publicCid, const DomainMap::Entry &route); // NOTE: for performance reasons we use callbacks instead of signals/slots Callback> & finishedByPassthroughCallback(); private: class Private; friend class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/proxy/xffrule.h000066400000000000000000000015261457610542000176610ustar00rootroot00000000000000/* * Copyright (C) 2013 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef XFFRULE_H #define XFFRULE_H class XffRule { public: int truncate; bool append; XffRule() : truncate(-1), append(false) { } }; #endif pushpin-1.39.1/src/cpp/proxy/zroutes.cpp000066400000000000000000000143531457610542000202560ustar00rootroot00000000000000/* * Copyright (C) 2014 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "zroutes.h" #include #include #include #include #include "log.h" static QStringList baseSpecToSpecs(const QString &baseSpec) { int at = baseSpec.indexOf("://"); assert(at != -1); at = baseSpec.indexOf(':', at + 3); if(at != -1) // probably a host:port spec { QString s = baseSpec.mid(0, at + 1); QString portStr = baseSpec.mid(at + 1); bool ok = false; int port = portStr.toInt(&ok); assert(ok); QStringList out; out += s + QString::number(port); out += s + QString::number(port + 1); out += s + QString::number(port + 2); return out; } else // probably a path spec { QStringList out; out += baseSpec + "-out"; out += baseSpec + "-out-stream"; out += baseSpec + "-in"; return out; } } class ZRoutes::Private : public QObject { Q_OBJECT public: class Item { public: QString spec; ZhttpManager *manager; int refs; bool markedForRemoval; Item(const QString &_spec, ZhttpManager *_manager) : spec(_spec), manager(_manager), refs(0), markedForRemoval(false) { } ~Item() { delete manager; } }; ZRoutes *q; QByteArray instanceId; QStringList defaultOutSpecs; QStringList defaultOutStreamSpecs; QStringList defaultInSpecs; Item *defaultItem; QHash itemsBySpec; QHash itemsByManager; QTimer *cleanupTimer; Private(ZRoutes *_q) : QObject(_q), q(_q), defaultItem(0) { cleanupTimer = new QTimer(this); connect(cleanupTimer, &QTimer::timeout, this, &Private::removeUnused); cleanupTimer->setInterval(10000); cleanupTimer->start(); } ~Private() { qDeleteAll(itemsBySpec); delete defaultItem; cleanupTimer->disconnect(this); cleanupTimer->setParent(0); cleanupTimer->deleteLater(); } Item *ensureDefaultItem() { if(!defaultItem) { ZhttpManager *manager = new ZhttpManager(this); manager->setInstanceId(instanceId); manager->setClientOutSpecs(defaultOutSpecs); manager->setClientOutStreamSpecs(defaultOutStreamSpecs); manager->setClientInSpecs(defaultInSpecs); defaultItem = new Item(QString(), manager); } return defaultItem; } Item *ensureItem(const DomainMap::ZhttpRoute &route) { Item *i = itemsBySpec.value(route.baseSpec); if(!i) { ZhttpManager *manager = new ZhttpManager(this); manager->setInstanceId(instanceId); manager->setIpcFileMode(route.ipcFileMode); manager->setBind(true); if(route.req) { manager->setClientReqSpecs(QStringList() << route.baseSpec); } else { QStringList specs = baseSpecToSpecs(route.baseSpec); manager->setClientOutSpecs(QStringList() << specs[0]); manager->setClientOutStreamSpecs(QStringList() << specs[1]); manager->setClientInSpecs(QStringList() << specs[2]); } i = new Item(route.baseSpec, manager); itemsBySpec.insert(route.baseSpec, i); itemsByManager.insert(manager, i); } return i; } void tryRemoveItem(Item *i) { if(i->refs == 0 && i->manager->connectionCount() == 0) removeItem(i); else i->markedForRemoval = true; } void removeItem(Item *i) { log_debug("zroutes: removing %s", qPrintable(i->spec)); assert(i->refs == 0 && i->manager->connectionCount() == 0); itemsBySpec.remove(i->spec); itemsByManager.remove(i->manager); delete i; } public slots: void removeUnused() { QList toRemove; QHashIterator it(itemsBySpec); while(it.hasNext()) { it.next(); Item *i = it.value(); if(i->markedForRemoval && i->refs == 0 && i->manager->connectionCount() == 0) toRemove += i; } foreach(Item *i, toRemove) removeItem(i); } }; ZRoutes::ZRoutes(QObject *parent) : QObject(parent) { d = new Private(this); } ZRoutes::~ZRoutes() { delete d; } void ZRoutes::setInstanceId(const QByteArray &id) { d->instanceId = id; } void ZRoutes::setDefaultOutSpecs(const QStringList &specs) { d->defaultOutSpecs = specs; } void ZRoutes::setDefaultOutStreamSpecs(const QStringList &specs) { d->defaultOutStreamSpecs = specs; } void ZRoutes::setDefaultInSpecs(const QStringList &specs) { d->defaultInSpecs = specs; } void ZRoutes::setup(const QList &routes) { d->ensureDefaultItem(); QList toAdd; foreach(const DomainMap::ZhttpRoute &route, routes) { if(!d->itemsBySpec.contains(route.baseSpec)) toAdd += route; } QStringList toRemove; QHashIterator it(d->itemsBySpec); while(it.hasNext()) { it.next(); const QString &spec = it.key(); bool found = false; foreach(const DomainMap::ZhttpRoute &route, routes) { if(route.baseSpec == spec) { found = true; break; } } if(!found) toRemove += spec; } foreach(const QString &spec, toRemove) { Private::Item *i = d->itemsBySpec.value(spec); assert(i); d->tryRemoveItem(i); } foreach(const DomainMap::ZhttpRoute &route, toAdd) { log_debug("zroutes: adding %s", qPrintable(route.baseSpec)); d->ensureItem(route); } } ZhttpManager *ZRoutes::defaultManager() { return d->ensureDefaultItem()->manager; } ZhttpManager *ZRoutes::managerForRoute(const DomainMap::ZhttpRoute &route) { return d->ensureItem(route)->manager; } void ZRoutes::addRef(ZhttpManager *zhttpManager) { Private::Item *i = (d->defaultItem->manager == zhttpManager ? d->defaultItem : d->itemsByManager.value(zhttpManager)); assert(i); ++(i->refs); } void ZRoutes::removeRef(ZhttpManager *zhttpManager) { Private::Item *i = (d->defaultItem->manager == zhttpManager ? d->defaultItem : d->itemsByManager.value(zhttpManager)); assert(i); assert(i->refs > 0); --(i->refs); } #include "zroutes.moc" pushpin-1.39.1/src/cpp/proxy/zroutes.h000066400000000000000000000025761457610542000177270ustar00rootroot00000000000000/* * Copyright (C) 2014 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef ZROUTES_H #define ZROUTES_H #include #include "zhttpmanager.h" #include "domainmap.h" class ZRoutes : public QObject { Q_OBJECT public: ZRoutes(QObject *parent = 0); ~ZRoutes(); void setInstanceId(const QByteArray &id); void setDefaultOutSpecs(const QStringList &specs); void setDefaultOutStreamSpecs(const QStringList &specs); void setDefaultInSpecs(const QStringList &specs); void setup(const QList &routes); ZhttpManager *defaultManager(); ZhttpManager *managerForRoute(const DomainMap::ZhttpRoute &route); void addRef(ZhttpManager *zhttpManager); void removeRef(ZhttpManager *zhttpManager); private: class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/proxy/zrpcchecker.cpp000066400000000000000000000104221457610542000210370ustar00rootroot00000000000000/* * Copyright (C) 2015 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "zrpcchecker.h" #include #include #include #include "zrpcrequest.h" #define CHECK_TIMEOUT 8 using std::map; class ZrpcChecker::Private : public QObject { Q_OBJECT public: class Item { public: ZrpcRequest *req; bool owned; Item() : req(0), owned(false) { } ~Item() { if(owned) delete req; } }; struct ZrpcReqConnections{ Connection finishedConnection; Connection destroyedConnection; }; ZrpcChecker *q; bool avail; QTimer *timer; QHash requestsByReq; map reqConnectionMap; Private(ZrpcChecker *_q) : QObject(_q), q(_q), avail(true) { timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &Private::timer_timeout); timer->setSingleShot(true); } ~Private() { cleanup(); } void cleanup() { if(timer) { timer->disconnect(this); timer->setParent(0); timer->deleteLater(); timer = 0; } QHashIterator it(requestsByReq); while(it.hasNext()) { it.next(); Item *i = it.value(); reqConnectionMap.erase(i->req); delete i; } requestsByReq.clear(); } void restartCountdown() { timer->start(CHECK_TIMEOUT * 1000); } void watch(ZrpcRequest *req) { Item *i = requestsByReq.value(req); if(i) return; // already watching reqConnectionMap[req] = { req->finished.connect(boost::bind(&Private::req_finished, this, req)), req->destroyed.connect(boost::bind(&Private::req_destroyed, this, req)) }; i = new Item; i->req = req; i->owned = false; requestsByReq.insert(req, i); // start the clock if we haven't yet if(!timer->isActive()) restartCountdown(); } void give(ZrpcRequest *req) { Item *i = requestsByReq.value(req); if(i) { // take over ownership req->setParent(this); i->owned = true; } else { // if we aren't watching (or were watching, but no // longer watching), then just delete what we were // given reqConnectionMap.erase(req); delete req; } } void handleSuccess() { avail = true; // success means we restart (or stop) the clock if(!requestsByReq.isEmpty()) restartCountdown(); else timer->stop(); } void handleError() { if(!requestsByReq.isEmpty()) { // let the clock keep running } else { // stop clock and immediately indicate unavailability timer->stop(); avail = false; } } void req_finished(ZrpcRequest *req) { Item *i = requestsByReq.value(req); assert(i); bool success = req->success(); ZrpcRequest::ErrorCondition e = req->errorCondition(); reqConnectionMap.erase(req); requestsByReq.remove(req); delete i; if(success) { handleSuccess(); } else { if(e == ZrpcRequest::ErrorTimeout || e == ZrpcRequest::ErrorUnavailable) { handleError(); } else { // any other error is fine, it means the inspector is responding handleSuccess(); } } } void req_destroyed(ZrpcRequest *req) { Item *i = requestsByReq.value(req); assert(i); reqConnectionMap.erase(req); requestsByReq.remove(req); delete i; } public slots: void timer_timeout() { avail = false; } }; ZrpcChecker::ZrpcChecker(QObject *parent) : QObject(parent) { d = new Private(this); } ZrpcChecker::~ZrpcChecker() { delete d; } bool ZrpcChecker::isInterfaceAvailable() const { return d->avail; } void ZrpcChecker::setInterfaceAvailable(bool available) { d->avail = available; } void ZrpcChecker::watch(ZrpcRequest *req) { d->watch(req); } void ZrpcChecker::give(ZrpcRequest *req) { d->give(req); } #include "zrpcchecker.moc" pushpin-1.39.1/src/cpp/proxy/zrpcchecker.h000066400000000000000000000025461457610542000205140ustar00rootroot00000000000000/* * Copyright (C) 2015 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef ZRPCCHECKER_H #define ZRPCCHECKER_H #include #include using Connection = boost::signals2::scoped_connection; class ZrpcRequest; // all requests should be passed to this class for monitoring. use // watch() to have it monitor a request, but not own it. use give() to have // this class take ownership of an already-watched request. class ZrpcChecker : public QObject { Q_OBJECT public: ZrpcChecker(QObject *parent = 0); ~ZrpcChecker(); bool isInterfaceAvailable() const; void setInterfaceAvailable(bool available); void watch(ZrpcRequest *req); void give(ZrpcRequest *req); private: class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/qtcompat.h000066400000000000000000000021021457610542000166440ustar00rootroot00000000000000/* * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include #include inline QMetaType::Type typeId(const QVariant &v) { #if QT_VERSION >= 0x060000 return (QMetaType::Type)v.typeId(); #else return (QMetaType::Type)v.type(); #endif } inline bool canConvert(const QVariant &v, QMetaType::Type type) { #if QT_VERSION >= 0x060000 return v.canConvert(QMetaType(type)); #else return v.canConvert(type); #endif } pushpin-1.39.1/src/cpp/qzmq/000077500000000000000000000000001457610542000156405ustar00rootroot00000000000000pushpin-1.39.1/src/cpp/qzmq/.gitignore000066400000000000000000000001521457610542000176260ustar00rootroot00000000000000conf.pri Makefile *.o *.moc moc_*.cpp /examples/helloclient/helloclient /examples/helloserver/helloserver pushpin-1.39.1/src/cpp/qzmq/COPYING000066400000000000000000000020431457610542000166720ustar00rootroot00000000000000Copyright (C) 2012 Justin Karneges Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pushpin-1.39.1/src/cpp/qzmq/README000066400000000000000000000015411457610542000165210ustar00rootroot00000000000000QZmq ---- Author: Justin Karneges Yet another Qt binding for ZeroMQ. It wraps the C API of libzmq. It is compatible with libzmq versions 2.x, 3.x, and 4.x. Some features: - Completely event-driven, with both read and write notifications. - For convenience, it is not necessary to create a Context explicitly. If a Socket is created without one, then a globally shared Context will be created automatically. - Some handy extra classes. For example, RepRouter makes it easy to write a REP socket server that handles multiple requests simultaneously, and Valve makes it easy to regulate reads. To build the examples: echo "LIBS += -lzmq" > conf.pri qmake && make To include the code in your project, just use the files in src. From a qmake project you can include src.pri. It's your responsibility to link to libzmq. pushpin-1.39.1/src/cpp/qzmq/examples/000077500000000000000000000000001457610542000174565ustar00rootroot00000000000000pushpin-1.39.1/src/cpp/qzmq/examples/examples.pri000066400000000000000000000002111457610542000220020ustar00rootroot00000000000000exists($$PWD/../conf.pri):include($$PWD/../conf.pri) QT -= gui QT += network INCLUDEPATH += $$PWD/../src include($$PWD/../src/src.pri) pushpin-1.39.1/src/cpp/qzmq/examples/examples.pro000066400000000000000000000000671457610542000220210ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS += helloclient helloserver pushpin-1.39.1/src/cpp/qzmq/examples/helloclient/000077500000000000000000000000001457610542000217605ustar00rootroot00000000000000pushpin-1.39.1/src/cpp/qzmq/examples/helloclient/helloclient.cpp000066400000000000000000000023141457610542000247660ustar00rootroot00000000000000#include #include #include #include "qzmqsocket.h" #include using Connection = boost::signals2::scoped_connection; class App : public QObject { Q_OBJECT private: QZmq::Socket sock; Connection rrConnection; Connection mwConnection; public: App() : sock(QZmq::Socket::Req) { } void sock_messagesWritten(int count) { printf("messages written: %d\n", count); } void sock_readyRead() { QList resp = sock.read(); printf("read: %s\n", resp[0].data()); emit quit(); } public slots: void start() { rrConnection = sock.readyRead.connect(boost::bind(&Private::sock_readyRead, this)); mwConnection = sock.messagesWritten.connect(boost::bind(&Private::sock_messagesWritten, this, boost::placeholders::_1)); sock.connectToAddress("tcp://localhost:5555"); QByteArray out = "hello"; printf("writing: %s\n", out.data()); sock.write(QList() << out); } signals: void quit(); }; int main(int argc, char **argv) { QCoreApplication qapp(argc, argv); App app; QObject::connect(&app, SIGNAL(quit()), &qapp, SLOT(quit())); QTimer::singleShot(0, &app, SLOT(start())); return qapp.exec(); } #include "helloclient.moc" pushpin-1.39.1/src/cpp/qzmq/examples/helloclient/helloclient.pro000066400000000000000000000000651457610542000250050ustar00rootroot00000000000000include(../examples.pri) SOURCES += helloclient.cpp pushpin-1.39.1/src/cpp/qzmq/examples/helloserver/000077500000000000000000000000001457610542000220105ustar00rootroot00000000000000pushpin-1.39.1/src/cpp/qzmq/examples/helloserver/helloserver.cpp000066400000000000000000000022071457610542000250470ustar00rootroot00000000000000#include #include #include #include "qzmqreqmessage.h" #include "qzmqreprouter.h" class App : public QObject { Q_OBJECT private: QZmq::RepRouter sock; void sock_messagesWritten(int count) { printf("messages written: %d\n", count); } void sock_readyRead() { QZmq::ReqMessage msg = sock.read(); if(msg.content().isEmpty()) { printf("error: received empty message\n"); return; } printf("read: %s\n", msg.content()[0].data()); QByteArray out = "world"; printf("writing: %s\n", out.data()); sock.write(msg.createReply(QList() << out)); } public slots: void start() { rrConnection = sock.readyRead.connect(boost::bind(&Private::sock_readyRead, this)); mwConnection = sock.messagesWritten.connect(boost::bind(&Private::sock_messagesWritten, this, boost::placeholders::_1)); sock.bind("tcp://*:5555"); } signals: void quit(); }; int main(int argc, char **argv) { QCoreApplication qapp(argc, argv); App app; QObject::connect(&app, SIGNAL(quit()), &qapp, SLOT(quit())); QTimer::singleShot(0, &app, SLOT(start())); return qapp.exec(); } #include "helloserver.moc" pushpin-1.39.1/src/cpp/qzmq/examples/helloserver/helloserver.pro000066400000000000000000000000651457610542000250650ustar00rootroot00000000000000include(../examples.pri) SOURCES += helloserver.cpp pushpin-1.39.1/src/cpp/qzmq/qzmq.pro000066400000000000000000000000501457610542000173450ustar00rootroot00000000000000TEMPLATE = subdirs SUBDIRS += examples pushpin-1.39.1/src/cpp/qzmq/src/000077500000000000000000000000001457610542000164275ustar00rootroot00000000000000pushpin-1.39.1/src/cpp/qzmq/src/qzmqcontext.cpp000066400000000000000000000025071457610542000215340ustar00rootroot00000000000000/* * Copyright (C) 2012 Justin Karneges * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "qzmqcontext.h" #include #include "rust/wzmq.h" namespace QZmq { Context::Context(int ioThreads) { context_ = wzmq_init(ioThreads); assert(context_); } Context::~Context() { wzmq_term(context_); } } pushpin-1.39.1/src/cpp/qzmq/src/qzmqcontext.h000066400000000000000000000025111457610542000211740ustar00rootroot00000000000000/* * Copyright (C) 2012 Justin Karneges * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef QZMQCONTEXT_H #define QZMQCONTEXT_H namespace QZmq { class Context { public: Context(int ioThreads = 1); ~Context(); // the zmq context void *context() { return context_; } private: void *context_; }; } #endif pushpin-1.39.1/src/cpp/qzmq/src/qzmqreprouter.cpp000066400000000000000000000045151457610542000221000ustar00rootroot00000000000000/* * Copyright (C) 2012 Justin Karneges * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "qzmqreprouter.h" #include "qzmqsocket.h" #include "qzmqreqmessage.h" namespace QZmq { class RepRouter::Private { public: RepRouter *q; std::unique_ptr sock; Connection mWConnection; Connection rrConnection; Private(RepRouter *_q) : q(_q) { sock = std::make_unique(Socket::Router); rrConnection = sock->readyRead.connect(boost::bind(&Private::sock_readyRead, this)); mWConnection = sock->messagesWritten.connect(boost::bind(&Private::sock_messagesWritten, this, boost::placeholders::_1)); } void sock_messagesWritten(int count) { q->messagesWritten(count); } void sock_readyRead() { q->readyRead(); } }; RepRouter::RepRouter() { d = std::make_unique(this); } RepRouter::~RepRouter() = default; void RepRouter::setShutdownWaitTime(int msecs) { d->sock->setShutdownWaitTime(msecs); } void RepRouter::connectToAddress(const QString &addr) { d->sock->connectToAddress(addr); } bool RepRouter::bind(const QString &addr) { return d->sock->bind(addr); } bool RepRouter::canRead() const { return d->sock->canRead(); } ReqMessage RepRouter::read() { return ReqMessage(d->sock->read()); } void RepRouter::write(const ReqMessage &message) { d->sock->write(message.toRawMessage()); } } pushpin-1.39.1/src/cpp/qzmq/src/qzmqreprouter.h000066400000000000000000000034641457610542000215470ustar00rootroot00000000000000/* * Copyright (C) 2012 Justin Karneges * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef QZMQREPROUTER_H #define QZMQREPROUTER_H #include #include using Signal = boost::signals2::signal; using SignalInt = boost::signals2::signal; using Connection = boost::signals2::scoped_connection; namespace QZmq { class ReqMessage; class RepRouter { public: RepRouter(); ~RepRouter(); void setShutdownWaitTime(int msecs); void connectToAddress(const QString &addr); bool bind(const QString &addr); bool canRead() const; ReqMessage read(); void write(const ReqMessage &message); Signal readyRead; SignalInt messagesWritten; private: Q_DISABLE_COPY(RepRouter) class Private; friend class Private; std::unique_ptr d; }; } #endif pushpin-1.39.1/src/cpp/qzmq/src/qzmqreqmessage.h000066400000000000000000000042051457610542000216460ustar00rootroot00000000000000/* * Copyright (C) 2012 Justin Karneges * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef QZMQREQMESSAGE_H #define QZMQREQMESSAGE_H namespace QZmq { class ReqMessage { public: ReqMessage() { } ReqMessage(const QList &headers, const QList &content) : headers_(headers), content_(content) { } ReqMessage(const QList &rawMessage) { bool collectHeaders = true; foreach(const QByteArray &part, rawMessage) { if(part.isEmpty()) { collectHeaders = false; continue; } if(collectHeaders) headers_ += part; else content_ += part; } } bool isNull() const { return headers_.isEmpty() && content_.isEmpty(); } QList headers() const { return headers_; } QList content() const { return content_; } ReqMessage createReply(const QList &content) { return ReqMessage(headers_, content); } QList toRawMessage() const { QList out; out += headers_; out += QByteArray(); out += content_; return out; } private: QList headers_; QList content_; }; } #endif pushpin-1.39.1/src/cpp/qzmq/src/qzmqsocket.cpp000066400000000000000000000345041457610542000213420ustar00rootroot00000000000000/* * Copyright (C) 2012-2020 Justin Karneges * Copyright (C) 2024 Fastly, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "qzmqsocket.h" #include #include #include #include #include #include #include #include "rust/wzmq.h" #include "qzmqcontext.h" namespace QZmq { static int get_fd(void *sock) { int fd; size_t opt_len = sizeof(fd); int ret = wzmq_getsockopt(sock, WZMQ_FD, &fd, &opt_len); assert(ret == 0); return fd; } static void set_subscribe(void *sock, const char *data, int size) { size_t opt_len = size; int ret = wzmq_setsockopt(sock, WZMQ_SUBSCRIBE, data, opt_len); assert(ret == 0); } static void set_unsubscribe(void *sock, const char *data, int size) { size_t opt_len = size; wzmq_setsockopt(sock, WZMQ_UNSUBSCRIBE, data, opt_len); // note: we ignore errors, such as unsubscribing a nonexisting filter } static void set_linger(void *sock, int value) { size_t opt_len = sizeof(value); int ret = wzmq_setsockopt(sock, WZMQ_LINGER, &value, opt_len); assert(ret == 0); } static int get_identity(void *sock, char *data, int size) { size_t opt_len = size; int ret = wzmq_getsockopt(sock, WZMQ_IDENTITY, data, &opt_len); assert(ret == 0); return (int)opt_len; } static void set_identity(void *sock, const char *data, int size) { size_t opt_len = size; int ret = wzmq_setsockopt(sock, WZMQ_IDENTITY, data, opt_len); if(ret != 0) printf("%d\n", errno); assert(ret == 0); } #if WZMQ_VERSION_MAJOR >= 4 static void set_immediate(void *sock, bool on) { int v = on ? 1 : 0; size_t opt_len = sizeof(v); int ret = wzmq_setsockopt(sock, WZMQ_IMMEDIATE, &v, opt_len); assert(ret == 0); } static void set_router_mandatory(void *sock, bool on) { int v = on ? 1 : 0; size_t opt_len = sizeof(v); int ret = wzmq_setsockopt(sock, WZMQ_ROUTER_MANDATORY, &v, opt_len); assert(ret == 0); } #else static void set_immediate(void *sock, bool on) { int v = on ? 1 : 0; size_t opt_len = sizeof(v); int ret = wzmq_setsockopt(sock, WZMQ_DELAY_ATTACH_ON_CONNECT, &v, opt_len); assert(ret == 0); } #endif #if (WZMQ_VERSION_MAJOR >= 4) || ((WZMQ_VERSION_MAJOR >= 3) && (WZMQ_VERSION_MINOR >= 2)) #define USE_MSG_IO static bool get_rcvmore(void *sock) { int more; size_t opt_len = sizeof(more); int ret = wzmq_getsockopt(sock, WZMQ_RCVMORE, &more, &opt_len); assert(ret == 0); return more ? true : false; } static int get_events(void *sock) { while(true) { int events; size_t opt_len = sizeof(events); int ret = wzmq_getsockopt(sock, WZMQ_EVENTS, &events, &opt_len); if(ret == 0) { return (int)events; } assert(errno == EINTR); } } static int get_sndhwm(void *sock) { int hwm; size_t opt_len = sizeof(hwm); int ret = wzmq_getsockopt(sock, WZMQ_SNDHWM, &hwm, &opt_len); assert(ret == 0); return (int)hwm; } static void set_sndhwm(void *sock, int value) { int v = value; size_t opt_len = sizeof(v); int ret = wzmq_setsockopt(sock, WZMQ_SNDHWM, &v, opt_len); assert(ret == 0); } static int get_rcvhwm(void *sock) { int hwm; size_t opt_len = sizeof(hwm); int ret = wzmq_getsockopt(sock, WZMQ_RCVHWM, &hwm, &opt_len); assert(ret == 0); return (int)hwm; } static void set_rcvhwm(void *sock, int value) { int v = value; size_t opt_len = sizeof(v); int ret = wzmq_setsockopt(sock, WZMQ_RCVHWM, &v, opt_len); assert(ret == 0); } static int get_hwm(void *sock) { return get_sndhwm(sock); } static void set_hwm(void *sock, int value) { set_sndhwm(sock, value); set_rcvhwm(sock, value); } static void set_tcp_keepalive(void *sock, int value) { int v = value; size_t opt_len = sizeof(v); int ret = wzmq_setsockopt(sock, WZMQ_TCP_KEEPALIVE, &v, opt_len); assert(ret == 0); } static void set_tcp_keepalive_idle(void *sock, int value) { int v = value; size_t opt_len = sizeof(v); int ret = wzmq_setsockopt(sock, WZMQ_TCP_KEEPALIVE_IDLE, &v, opt_len); assert(ret == 0); } static void set_tcp_keepalive_cnt(void *sock, int value) { int v = value; size_t opt_len = sizeof(v); int ret = wzmq_setsockopt(sock, WZMQ_TCP_KEEPALIVE_CNT, &v, opt_len); assert(ret == 0); } static void set_tcp_keepalive_intvl(void *sock, int value) { int v = value; size_t opt_len = sizeof(v); int ret = wzmq_setsockopt(sock, WZMQ_TCP_KEEPALIVE_INTVL, &v, opt_len); assert(ret == 0); } #else static bool get_rcvmore(void *sock) { qint64 more; size_t opt_len = sizeof(more); int ret = wzmq_getsockopt(sock, WZMQ_RCVMORE, &more, &opt_len); assert(ret == 0); return more ? true : false; } static int get_events(void *sock) { while(true) { quint32 events; size_t opt_len = sizeof(events); int ret = wzmq_getsockopt(sock, WZMQ_EVENTS, &events, &opt_len); if(ret == 0) { return (int)events; } assert(errno == EINTR); } } static int get_hwm(void *sock) { quint64 hwm; size_t opt_len = sizeof(hwm); int ret = wzmq_getsockopt(sock, WZMQ_HWM, &hwm, &opt_len); assert(ret == 0); return (int)hwm; } static void set_hwm(void *sock, int value) { quint64 v = value; size_t opt_len = sizeof(v); int ret = wzmq_setsockopt(sock, WZMQ_HWM, &v, opt_len); assert(ret == 0); } static int get_sndhwm(void *sock) { return get_hwm(sock); } static void set_sndhwm(void *sock, int value) { set_hwm(sock, value); } static int get_rcvhwm(void *sock) { return get_hwm(sock); } static void set_rcvhwm(void *sock, int value) { set_hwm(sock, value); } static void set_tcp_keepalive(void *sock, int value) { // not supported for this zmq version Q_UNUSED(sock); Q_UNUSED(on); } static void set_tcp_keepalive_idle(void *sock, int value) { // not supported for this zmq version Q_UNUSED(sock); Q_UNUSED(on); } static void set_tcp_keepalive_cnt(void *sock, int value) { // not supported for this zmq version Q_UNUSED(sock); Q_UNUSED(on); } static void set_tcp_keepalive_intvl(void *sock, int value) { // not supported for this zmq version Q_UNUSED(sock); Q_UNUSED(on); } #endif Q_GLOBAL_STATIC(QMutex, g_mutex) class Global { public: Context context; int refs; Global() : refs(0) { } }; static Global *global = 0; static Context *addGlobalContextRef() { QMutexLocker locker(g_mutex()); if(!global) global = new Global; ++(global->refs); return &(global->context); } static void removeGlobalContextRef() { QMutexLocker locker(g_mutex()); assert(global); assert(global->refs > 0); --(global->refs); if(global->refs == 0) { delete global; global = 0; } } class Socket::Private : public QObject { Q_OBJECT public: Socket *q; bool usingGlobalContext; Context *context; void *sock; QSocketNotifier *sn_read; bool canWrite, canRead; QList< QList > pendingWrites; int pendingWritten; QTimer *updateTimer; bool pendingUpdate; int shutdownWaitTime; bool writeQueueEnabled; Private(Socket *_q, Socket::Type type, Context *_context) : QObject(_q), q(_q), canWrite(false), canRead(false), pendingWritten(0), pendingUpdate(false), shutdownWaitTime(-1), writeQueueEnabled(true) { if(_context) { usingGlobalContext = false; context = _context; } else { usingGlobalContext = true; context = addGlobalContextRef(); } int ztype = 0; switch(type) { case Socket::Pair: ztype = WZMQ_PAIR; break; case Socket::Dealer: ztype = WZMQ_DEALER; break; case Socket::Router: ztype = WZMQ_ROUTER; break; case Socket::Req: ztype = WZMQ_REQ; break; case Socket::Rep: ztype = WZMQ_REP; break; case Socket::Push: ztype = WZMQ_PUSH; break; case Socket::Pull: ztype = WZMQ_PULL; break; case Socket::Pub: ztype = WZMQ_PUB; break; case Socket::Sub: ztype = WZMQ_SUB; break; default: assert(0); } sock = wzmq_socket(context->context(), ztype); assert(sock != NULL); sn_read = new QSocketNotifier(get_fd(sock), QSocketNotifier::Read, this); connect(sn_read, &QSocketNotifier::activated, this, &Private::sn_read_activated); sn_read->setEnabled(true); updateTimer = new QTimer(this); connect(updateTimer, SIGNAL(timeout()), SLOT(update_timeout())); updateTimer->setSingleShot(true); } ~Private() { updateTimer->disconnect(this); updateTimer->setParent(0); updateTimer->deleteLater(); set_linger(sock, shutdownWaitTime); wzmq_close(sock); if(usingGlobalContext) removeGlobalContextRef(); } void update() { if(!pendingUpdate) { pendingUpdate = true; updateTimer->start(); } } QList read() { if(canRead) { QList out; bool ok = true; do { wzmq_msg_t msg; int ret = wzmq_msg_init(&msg); assert(ret == 0); #ifdef USE_MSG_IO ret = wzmq_msg_recv(&msg, sock, WZMQ_DONTWAIT); #else ret = wzmq_recv(sock, &msg, WZMQ_NOBLOCK); #endif if(ret < 0) { ret = wzmq_msg_close(&msg); assert(ret == 0); ok = false; break; } QByteArray buf((const char *)wzmq_msg_data(&msg), wzmq_msg_size(&msg)); ret = wzmq_msg_close(&msg); assert(ret == 0); out += buf; } while(get_rcvmore(sock)); processEvents(); if((canWrite && !pendingWrites.isEmpty()) || canRead) update(); if(ok) return out; else return QList(); } else return QList(); } void write(const QList &message) { assert(!message.isEmpty()); if(writeQueueEnabled) { pendingWrites += message; if(canWrite) update(); } else { if(zmqWrite(message)) { ++pendingWritten; } processEvents(); if(pendingWritten > 0 || canRead) update(); } } // return true if flags changed bool processEvents() { int flags = get_events(sock); bool canWriteOld = canWrite; bool canReadOld = canRead; canWrite = (flags & WZMQ_POLLOUT); canRead = (flags & WZMQ_POLLIN); return (canWrite != canWriteOld || canRead != canReadOld); } bool zmqWrite(const QList &message) { for(int n = 0; n < message.count(); ++n) { const QByteArray &buf = message[n]; wzmq_msg_t msg; int ret = wzmq_msg_init_size(&msg, buf.size()); assert(ret == 0); memcpy(wzmq_msg_data(&msg), buf.data(), buf.size()); #ifdef USE_MSG_IO ret = wzmq_msg_send(&msg, sock, WZMQ_DONTWAIT | (n + 1 < message.count() ? WZMQ_SNDMORE : 0)); #else ret = wzmq_send(sock, &msg, WZMQ_NOBLOCK | (n + 1 < message.count() ? WZMQ_SNDMORE : 0)); #endif if(ret < 0) { ret = wzmq_msg_close(&msg); assert(ret == 0); return false; } ret = wzmq_msg_close(&msg); assert(ret == 0); } return true; } void tryWrite() { while(canWrite && !pendingWrites.isEmpty()) { // whether this write succeeds or not, we assume we // can't write afterwards canWrite = false; if(zmqWrite(pendingWrites.first())) { pendingWrites.removeFirst(); ++pendingWritten; } processEvents(); } } void doUpdate() { tryWrite(); if(canRead) { QPointer self = this; q->readyRead(); if(!self) return; } if(pendingWritten > 0) { int count = pendingWritten; pendingWritten = 0; q->messagesWritten(count); } } public slots: void sn_read_activated() { if(!processEvents()) return; if(pendingUpdate) { pendingUpdate = false; updateTimer->stop(); } doUpdate(); } void update_timeout() { pendingUpdate = false; doUpdate(); } }; Socket::Socket(Type type, QObject *parent) : QObject(parent) { d = new Private(this, type, 0); } Socket::Socket(Type type, Context *context, QObject *parent) : QObject(parent) { d = new Private(this, type, context); } Socket::~Socket() { delete d; } void Socket::setShutdownWaitTime(int msecs) { d->shutdownWaitTime = msecs; } void Socket::setWriteQueueEnabled(bool enable) { d->writeQueueEnabled = enable; } void Socket::subscribe(const QByteArray &filter) { set_subscribe(d->sock, filter.data(), filter.size()); } void Socket::unsubscribe(const QByteArray &filter) { set_unsubscribe(d->sock, filter.data(), filter.size()); } QByteArray Socket::identity() const { QByteArray buf(255, 0); buf.resize(get_identity(d->sock, buf.data(), buf.size())); return buf; } void Socket::setIdentity(const QByteArray &id) { set_identity(d->sock, id.data(), id.size()); } int Socket::hwm() const { return get_hwm(d->sock); } void Socket::setHwm(int hwm) { set_hwm(d->sock, hwm); } int Socket::sendHwm() const { return get_sndhwm(d->sock); } int Socket::receiveHwm() const { return get_rcvhwm(d->sock); } void Socket::setSendHwm(int hwm) { set_sndhwm(d->sock, hwm); } void Socket::setReceiveHwm(int hwm) { set_rcvhwm(d->sock, hwm); } void Socket::setImmediateEnabled(bool on) { set_immediate(d->sock, on); } void Socket::setRouterMandatoryEnabled(bool on) { set_router_mandatory(d->sock, on); } void Socket::setTcpKeepAliveEnabled(bool on) { set_tcp_keepalive(d->sock, on ? 1 : 0); } void Socket::setTcpKeepAliveParameters(int idle, int count, int interval) { set_tcp_keepalive_idle(d->sock, idle); set_tcp_keepalive_cnt(d->sock, count); set_tcp_keepalive_intvl(d->sock, interval); } void Socket::connectToAddress(const QString &addr) { int ret = wzmq_connect(d->sock, addr.toUtf8().data()); assert(ret == 0); } bool Socket::bind(const QString &addr) { int ret = wzmq_bind(d->sock, addr.toUtf8().data()); if(ret != 0) return false; return true; } bool Socket::canRead() const { return d->canRead; } bool Socket::canWriteImmediately() const { return d->canWrite; } QList Socket::read() { return d->read(); } void Socket::write(const QList &message) { d->write(message); } } #include "qzmqsocket.moc" pushpin-1.39.1/src/cpp/qzmq/src/qzmqsocket.h000066400000000000000000000066041457610542000210070ustar00rootroot00000000000000/* * Copyright (C) 2012-2015 Justin Karneges * Copyright (C) 2024 Fastly, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef QZMQSOCKET_H #define QZMQSOCKET_H #include #include using Signal = boost::signals2::signal; using SignalInt = boost::signals2::signal; using Connection = boost::signals2::scoped_connection; namespace QZmq { class Context; class Socket : public QObject { Q_OBJECT public: enum Type { Pair, Dealer, Router, Req, Rep, Push, Pull, Pub, Sub }; Socket(Type type, QObject *parent = 0); Socket(Type type, Context *context, QObject *parent = 0); ~Socket(); // 0 means drop queue and don't block, -1 means infinite (default = -1) void setShutdownWaitTime(int msecs); // if enabled, messages are queued internally until the socket is able // to accept them. the messagesWritten signal is emitted once writes // have succeeded. otherwise, messages are passed directly to // zmq_send and dropped if they can't be written. default enabled. // disabling the queue is good for socket types where the HWM has a // drop policy. enabling the queue is good when the HWM has a // blocking policy. void setWriteQueueEnabled(bool enable); void subscribe(const QByteArray &filter); void unsubscribe(const QByteArray &filter); QByteArray identity() const; void setIdentity(const QByteArray &id); // deprecated, zmq 2.x int hwm() const; void setHwm(int hwm); int sendHwm() const; int receiveHwm() const; void setSendHwm(int hwm); void setReceiveHwm(int hwm); void setImmediateEnabled(bool on); void setRouterMandatoryEnabled(bool on); void setTcpKeepAliveEnabled(bool on); void setTcpKeepAliveParameters(int idle = -1, int count = -1, int interval = -1); void connectToAddress(const QString &addr); bool bind(const QString &addr); bool canRead() const; // returns true if this object believes the next write to zmq will // succeed immediately. note that it starts out false until the // value is discovered. also note that the write could still end up // needing to be queued, if the conditions change in between. bool canWriteImmediately() const; QList read(); void write(const QList &message); Signal readyRead; SignalInt messagesWritten; private: Q_DISABLE_COPY(Socket) class Private; friend class Private; Private *d; }; } #endif pushpin-1.39.1/src/cpp/qzmq/src/qzmqvalve.cpp000066400000000000000000000053421457610542000211650ustar00rootroot00000000000000/* * Copyright (C) 2012-2020 Justin Karneges * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "qzmqvalve.h" #include #include "qzmqsocket.h" namespace QZmq { class Valve::Private : public QObject { Q_OBJECT public: Valve *q; QZmq::Socket *sock; bool isOpen; bool pendingRead; int maxReadsPerEvent; boost::signals2::scoped_connection rrConnection; Private(Valve *_q) : QObject(_q), q(_q), sock(0), isOpen(false), pendingRead(false), maxReadsPerEvent(100) { } void setup(QZmq::Socket *_sock) { sock = _sock; rrConnection = sock->readyRead.connect(boost::bind(&Private::sock_readyRead, this)); } void queueRead() { if(pendingRead) return; pendingRead = true; QMetaObject::invokeMethod(this, "queuedRead", Qt::QueuedConnection); } void tryRead() { QPointer self = this; int count = 0; while(isOpen && sock->canRead()) { if(count >= maxReadsPerEvent) { queueRead(); return; } QList msg = sock->read(); if(!msg.isEmpty()) { q->readyRead(msg); if(!self) return; } ++count; } } void sock_readyRead() { if(pendingRead) return; tryRead(); } private slots: void queuedRead() { pendingRead = false; tryRead(); } }; Valve::Valve(QZmq::Socket *sock, QObject *parent) : QObject(parent) { d = new Private(this); d->setup(sock); } Valve::~Valve() { delete d; } bool Valve::isOpen() const { return d->isOpen; } void Valve::setMaxReadsPerEvent(int max) { d->maxReadsPerEvent = max; } void Valve::open() { if(!d->isOpen) { d->isOpen = true; if(!d->pendingRead && d->sock->canRead()) d->queueRead(); } } void Valve::close() { d->isOpen = false; } } #include "qzmqvalve.moc" pushpin-1.39.1/src/cpp/qzmq/src/qzmqvalve.h000066400000000000000000000032201457610542000206230ustar00rootroot00000000000000/* * Copyright (C) 2012 Justin Karneges * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef QZMQVALVE_H #define QZMQVALVE_H #include #include using SignalList = boost::signals2::signal&)>; using Connection = boost::signals2::scoped_connection; namespace QZmq { class Socket; class Valve : public QObject { Q_OBJECT public: Valve(QZmq::Socket *sock, QObject *parent = 0); ~Valve(); bool isOpen() const; void setMaxReadsPerEvent(int max); void open(); void close(); SignalList readyRead; private: class Private; friend class Private; Private *d; }; } #endif pushpin-1.39.1/src/cpp/qzmq/src/src.pri000066400000000000000000000003571457610542000177370ustar00rootroot00000000000000HEADERS += \ $$PWD/qzmqcontext.h \ $$PWD/qzmqsocket.h \ $$PWD/qzmqvalve.h \ $$PWD/qzmqreqmessage.h \ $$PWD/qzmqreprouter.h SOURCES += \ $$PWD/qzmqcontext.cpp \ $$PWD/qzmqsocket.cpp \ $$PWD/qzmqvalve.cpp \ $$PWD/qzmqreprouter.cpp pushpin-1.39.1/src/cpp/rtimer.cpp000066400000000000000000000111241457610542000166550ustar00rootroot00000000000000/* * Copyright (C) 2021 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "rtimer.h" #include #include #include #include "timerwheel.h" #define TICK_DURATION_MS 10 #define UPDATE_TICKS_MAX 1000 #define EXPIRES_PER_CYCLE_MAX 100 static qint64 durationToTicksRoundDown(qint64 msec) { return msec / TICK_DURATION_MS; } static qint64 durationToTicksRoundUp(qint64 msec) { return (msec + TICK_DURATION_MS - 1) / TICK_DURATION_MS; } static qint64 ticksToDuration(qint64 ticks) { return ticks * TICK_DURATION_MS; } class TimerManager : public QObject { Q_OBJECT public: TimerManager(int capacity, QObject *parent = 0); int add(int msec, RTimer *r); void remove(int key); private slots: void t_timeout(); private: TimerWheel wheel_; qint64 startTime_; quint64 currentTicks_; QTimer *t_; void updateTimeout(qint64 currentTime); }; TimerManager::TimerManager(int capacity, QObject *parent) : QObject(parent), wheel_(TimerWheel(capacity)) { startTime_ = QDateTime::currentMSecsSinceEpoch(); currentTicks_ = 0; t_ = new QTimer(this); connect(t_, &QTimer::timeout, this, &TimerManager::t_timeout); t_->setSingleShot(true); } int TimerManager::add(int msec, RTimer *r) { qint64 currentTime = QDateTime::currentMSecsSinceEpoch(); // expireTime must be >= startTime_ qint64 expireTime = qMax(currentTime + msec, startTime_); qint64 expiresTicks = durationToTicksRoundUp(expireTime - startTime_); int id = wheel_.add(expiresTicks, (size_t)r); if(id >= 0) { updateTimeout(currentTime); } return id; } void TimerManager::remove(int key) { wheel_.remove(key); qint64 currentTime = QDateTime::currentMSecsSinceEpoch(); updateTimeout(currentTime); } void TimerManager::t_timeout() { qint64 currentTime = QDateTime::currentMSecsSinceEpoch(); // time must go forward if(currentTime > startTime_) { currentTicks_ = (quint64)durationToTicksRoundDown(currentTime - startTime_); wheel_.update(currentTicks_); } for(int i = 0; i < EXPIRES_PER_CYCLE_MAX; ++i) { TimerWheel::Expired expired = wheel_.takeExpired(); if(expired.key < 0) { break; } RTimer *r = (RTimer *)expired.userData; r->timerReady(); } updateTimeout(currentTime); } void TimerManager::updateTimeout(qint64 currentTime) { qint64 timeoutTicks = wheel_.timeout(); if(timeoutTicks >= 0) { // currentTime must be >= startTime_ currentTime = qMax(currentTime, startTime_); quint64 currentTicks = (quint64)durationToTicksRoundDown(currentTime - startTime_); // time must go forward currentTicks = qMax(currentTicks, currentTicks_); qint64 ticksSinceWheelUpdate = (qint64)(currentTicks - currentTicks_); // reduce the timeout by the time already elapsed timeoutTicks = qMax(timeoutTicks - ticksSinceWheelUpdate, (qint64)0); // cap the timeout so the wheel is regularly updated qint64 maxTimeoutTicks = qMax(UPDATE_TICKS_MAX - ticksSinceWheelUpdate, (qint64)0); timeoutTicks = qMin(timeoutTicks, maxTimeoutTicks); int msec = ticksToDuration(timeoutTicks); t_->start(msec); } else { t_->stop(); } } static thread_local TimerManager *g_manager = 0; RTimer::RTimer() : singleShot_(false), interval_(0), timerId_(-1) { } RTimer::~RTimer() { stop(); } bool RTimer::isActive() const { return (timerId_ >= 0); } void RTimer::setSingleShot(bool singleShot) { singleShot_ = singleShot; } void RTimer::start(int msec) { interval_ = msec; start(); } void RTimer::start() { // must call RTimer::init first assert(g_manager); stop(); int id = g_manager->add(interval_, this); assert(id >= 0); timerId_ = id; } void RTimer::stop() { if(timerId_ >= 0) { assert(g_manager); g_manager->remove(timerId_); timerId_ = -1; } } void RTimer::timerReady() { timerId_ = -1; if(!singleShot_) { start(); } timeout(); } void RTimer::init(int capacity) { assert(!g_manager); g_manager = new TimerManager(capacity); } void RTimer::deinit() { delete g_manager; g_manager = 0; } #include "rtimer.moc" pushpin-1.39.1/src/cpp/rtimer.h000066400000000000000000000025201457610542000163220ustar00rootroot00000000000000/* * Copyright (C) 2021 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef RTIMER_H #define RTIMER_H #include #include using Signal = boost::signals2::signal; class TimerManager; class RTimer : public QObject { Q_OBJECT public: RTimer(); ~RTimer(); bool isActive() const; void setSingleShot(bool singleShot); void start(int msec); void start(); void stop(); // initialization is thread local static void init(int capacity); // only call if there are no active RTimers static void deinit(); Signal timeout; private: friend class TimerManager; bool singleShot_; int interval_; int timerId_; void timerReady(); }; #endif pushpin-1.39.1/src/cpp/runner/000077500000000000000000000000001457610542000161615ustar00rootroot00000000000000pushpin-1.39.1/src/cpp/runner/condureservice.cpp000066400000000000000000000065221457610542000217120ustar00rootroot00000000000000/* * Copyright (C) 2020-2023 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "condureservice.h" #include #include #include #include #include "log.h" #include "template.h" CondureService::CondureService( const QString &name, const QString &binFile, const QString &runDir, const QString &logDir, const QString &ipcPrefix, const QString &filePrefix, int logLevel, const QString &certsDir, int clientBufferSize, int maxconn, bool allowCompression, const QList &ports, bool enableClient) { args_ += binFile; if(!logDir.isEmpty()) { setStandardOutputFile(QDir(logDir).filePath(filePrefix + name + ".log")); } if(logLevel >= 0) args_ += "--log-level=" + QString::number(logLevel); args_ += "--buffer-size=" + QString::number(clientBufferSize); args_ += "--stream-maxconn=" + QString::number(maxconn); if(allowCompression) args_ += "--compression"; if(!ports.isEmpty()) { // server mode bool usingSsl = false; foreach(const ListenPort &p, ports) { if(!p.localPath.isEmpty()) { QString arg = "--listen=" + p.localPath + ",local,stream"; if(p.mode >= 0) arg += ",mode=" + QString::number(p.mode, 8); if(!p.user.isEmpty()) arg += ",user=" + p.user; if(!p.group.isEmpty()) arg += ",group=" + p.group; args_ += arg; } else { QUrl url; url.setHost(!p.addr.isNull() ? p.addr.toString() : QString("0.0.0.0")); url.setPort(p.port); QString arg = "--listen=" + url.authority() + ",stream"; if(p.ssl) { usingSsl = true; arg += ",tls,default-cert=default_" + QString::number(p.port); } args_ += arg; } } args_ += "--zclient-stream=ipc://" + runDir + "/" + ipcPrefix + "condure"; if(usingSsl) args_ += "--tls-identities-dir=" + certsDir; } if(enableClient) { // client mode args_ += "--zserver-stream=ipc://" + runDir + "/" + ipcPrefix + "condure-client"; args_ += "--deny-out-internal"; } setName(name); setPidFile(QDir(runDir).filePath(filePrefix + name + ".pid")); } QStringList CondureService::arguments() const { return args_; } bool CondureService::hasClientMode(const QString &binFile) { QProcess proc; proc.start(binFile, QStringList() << "--help"); if(!proc.waitForFinished(-1)) { log_error("Failed to run condure: process error: %d", proc.error()); return false; } if(proc.exitStatus() != QProcess::NormalExit) { log_error("Failed to run condure: process did not exit normally"); return false; } int code = proc.exitCode(); if(proc.exitCode() != 0) { log_error("Condure returned non-zero status: %d", code); return false; } QByteArray output = proc.readAllStandardOutput(); return output.contains("--zserver-stream"); } pushpin-1.39.1/src/cpp/runner/condureservice.h000066400000000000000000000024751457610542000213620ustar00rootroot00000000000000/* * Copyright (C) 2020-2023 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef CONDURESERVICE_H #define CONDURESERVICE_H #include "service.h" #include "listenport.h" class CondureService : public Service { public: CondureService( const QString &name, const QString &binFile, const QString &runDir, const QString &logDir, const QString &ipcPrefix, const QString &filePrefix, int logLevel, const QString &certsDir, int clientBufferSize, int maxconn, bool allowCompression, const QList &ports, bool enableClient); static bool hasClientMode(const QString &binFile); // reimplemented virtual QStringList arguments() const; private: QStringList args_; }; #endif pushpin-1.39.1/src/cpp/runner/listenport.h000066400000000000000000000024041457610542000205350ustar00rootroot00000000000000/* * Copyright (C) 2020-2023 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef LISTENPORT_H #define LISTENPORT_H #include class ListenPort { public: QHostAddress addr; int port; bool ssl; QString localPath; int mode; QString user; QString group; ListenPort() : port(-1), ssl(false), mode(-1) { } ListenPort(const QHostAddress &_addr, int _port, bool _ssl, const QString &_localPath = QString(), int _mode = -1, const QString &_user = QString(), const QString &_group = QString()) : addr(_addr), port(_port), ssl(_ssl), localPath(_localPath), mode(_mode), user(_user), group(_group) { } }; #endif pushpin-1.39.1/src/cpp/runner/m2adapterservice.cpp000066400000000000000000000043171457610542000221320ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "m2adapterservice.h" #include #include #include #include "log.h" #include "template.h" M2AdapterService::M2AdapterService( const QString &binFile, const QString &configTemplateFile, const QString &runDir, const QString &logDir, const QString &ipcPrefix, const QString &filePrefix, int logLevel, const QList &ports) { args_ += binFile; args_ += "--config=" + QDir(runDir).filePath(filePrefix + "m2adapter.conf"); if(!logDir.isEmpty()) { args_ += "--logfile=" + QDir(logDir).filePath(filePrefix + "m2adapter.log"); setStandardOutputFile(QProcess::nullDevice()); } if(logLevel >= 0) args_ += "--loglevel=" + QString::number(logLevel); configTemplateFile_ = configTemplateFile; runDir_ = runDir; ipcPrefix_ = ipcPrefix; filePrefix_ = filePrefix; ports_ = ports; setName("m2a"); setPidFile(QDir(runDir).filePath(filePrefix + "m2adapter.pid")); } QStringList M2AdapterService::arguments() const { return args_; } bool M2AdapterService::acceptSighup() const { return true; } bool M2AdapterService::preStart() { QVariantList portStrs; foreach(int port, ports_) portStrs += QString::number(port); QVariantMap context; context["ports"] = portStrs; context["rundir"] = runDir_; context["ipc_prefix"] = ipcPrefix_; QString error; if(!Template::renderFile(configTemplateFile_, QDir(runDir_).filePath(filePrefix_ + "m2adapter.conf"), context, &error)) { log_error("Failed to generate m2adapter config file: %s", qPrintable(error)); return false; } return true; } pushpin-1.39.1/src/cpp/runner/m2adapterservice.h000066400000000000000000000024671457610542000216030ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef M2ADAPTERSERVICE_H #define M2ADAPTERSERVICE_H #include "service.h" class M2AdapterService : public Service { public: M2AdapterService( const QString &binFile, const QString &configTemplateFile, const QString &runDir, const QString &logDir, const QString &ipcPrefix, const QString &filePrefix, int logLevel, const QList &ports); // reimplemented virtual QStringList arguments() const; virtual bool acceptSighup() const; virtual bool preStart(); private: QStringList args_; QString configTemplateFile_; QString runDir_; QString ipcPrefix_; QString filePrefix_; QList ports_; }; #endif pushpin-1.39.1/src/cpp/runner/main.h000066400000000000000000000001351457610542000172550ustar00rootroot00000000000000#ifndef RUNNER_MAIN_H #define RUNNER_MAIN_H int runner_main(int argc, char **argv); #endif pushpin-1.39.1/src/cpp/runner/mongrel2service.cpp000066400000000000000000000125701457610542000220000ustar00rootroot00000000000000/* * Copyright (C) 2016-2022 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "mongrel2service.h" #include "tnetstring.h" #include #include #include #include #include #include "log.h" #include "template.h" Mongrel2Service::Mongrel2Service( const QString &binFile, const QString &configFile, const QString &serverName, const QString &runDir, const QString &logDir, const QString &filePrefix, int port, bool ssl, int logLevel) : logLevel_(logLevel) { args_ += binFile; args_ += configFile; args_ += serverName; setName(QString("m2 %1:%2").arg(ssl ? "https" : "http", QString::number(port))); // delete stale pid file, if any QString pidFile = QDir(runDir).filePath(filePrefix + "mongrel2_" + QString::number(port) + ".pid"); QFile::remove(pidFile); if(!logDir.isEmpty()) { setStandardOutputFile(QDir(logDir).filePath(filePrefix + "mongrel2_" + QString::number(port) + ".log")); } } bool Mongrel2Service::generateConfigFile(const QString &m2shBinFile, const QString &configTemplateFile, const QString &runDir, const QString &logDir, const QString &ipcPrefix, const QString &filePrefix, const QString &certsDir, int clientBufferSize, int maxconn, const QList &ports, int logLevel) { QVariantList vinterfaces; foreach(const ListenPort &p, ports) { if(!p.localPath.isEmpty()) { log_error("Cannot use local_ports option with mongrel2"); return false; } QVariantMap v; v["addr"] = (!p.addr.isNull() ? p.addr.toString() : QString("0.0.0.0")); v["port"] = p.port; v["ssl"] = p.ssl; vinterfaces += v; } QVariantMap context; context["interfaces"] = vinterfaces; context["rundir"] = runDir; context["logdir"] = logDir; context["loglevel"] = logLevel; context["certdir"] = certsDir; context["ipc_prefix"] = ipcPrefix; context["file_prefix"] = filePrefix; context["limits_buffer_size"] = clientBufferSize; context["disable_access_logging"] = (logLevel >= LOG_LEVEL_INFO) ? 0 : 1; context["superpoll_max_fd"] = maxconn + 1000; QString error; if(!Template::renderFile(configTemplateFile, QDir(runDir).filePath(filePrefix + "mongrel2.conf"), context, &error)) { log_error("Failed to generate mongrel2 config file: %s", qPrintable(error)); return false; } QStringList args; args << "load"; args << "-config" << QDir(runDir).filePath(filePrefix + "mongrel2.conf"); args << "-db" << QDir(runDir).filePath(filePrefix + "mongrel2.sqlite"); int ret = QProcess::execute(m2shBinFile, args); if(ret != 0) { log_error("Failed to run m2sh"); return false; } return true; } QStringList Mongrel2Service::arguments() const { return args_; } bool Mongrel2Service::acceptSighup() const { return true; } bool Mongrel2Service::alwaysLogStatus() const { return true; } QString Mongrel2Service::filterLogLine(const int level, const QString &line) const { if(level > logLevel_) { return QString(); } QString tstr = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz"); switch(level) { case LOG_LEVEL_DEBUG: return "[DEBUG] " + tstr + " " + line; case LOG_LEVEL_INFO: return "[INFO] " + tstr + " " + line; case LOG_LEVEL_WARNING: return "[WARN] " + tstr + " " + line; default: return "[ERR] " + tstr + " " + line; } } QString Mongrel2Service::formatLogLine(const QString &line) const { if(line.isEmpty()) { return line; } TnetString::Type type; int dataOffset; int dataSize; bool isTnetString = TnetString::check(qPrintable(line), 0, &type, &dataOffset, &dataSize); // if line is a valid tnet string, it most probably is an access log entry if(isTnetString) { return filterLogLine(LOG_LEVEL_INFO, line); } int at = line.indexOf('['); int end; int level; if(at == -1) { QString debugTag("DEBUG"); at = line.indexOf(debugTag); if(at == -1) { return filterLogLine(LOG_LEVEL_WARNING, "Can't parse mongrel2 log: " + line); } else { end = at + debugTag.length(); level = LOG_LEVEL_DEBUG; } } else { end = line.indexOf(']', at); if(end == -1) { return filterLogLine(LOG_LEVEL_WARNING, "Can't parse mongrel2 log: " + line); } #if QT_VERSION >= 0x060000 QStringView s = QStringView(line).mid(at + 1, end - at - 1); #else QStringRef s = line.midRef(at + 1, end - at - 1); #endif if(s.compare(QLatin1String("DEBUG")) == 0) { level = LOG_LEVEL_DEBUG; } else if(s.compare(QLatin1String("INFO")) == 0) { level = LOG_LEVEL_INFO; } else if(s.compare(QLatin1String("ERROR")) == 0) { level = LOG_LEVEL_ERROR; } else if(s.compare(QLatin1String("WARN")) == 0) { level = LOG_LEVEL_WARNING; } else { return filterLogLine(LOG_LEVEL_WARNING, "Can't parse severity: " + line); } } if(line.size() > end + 1 && line.at(end + 1) == ' ') { end++; } return filterLogLine(level, line.mid(end + 1)); } pushpin-1.39.1/src/cpp/runner/mongrel2service.h000066400000000000000000000032561457610542000214460ustar00rootroot00000000000000/* * Copyright (C) 2016-2020 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef MONGREL2SERVICE_H #define MONGREL2SERVICE_H #include "service.h" #include "listenport.h" class Mongrel2Service : public Service { Q_OBJECT public: Mongrel2Service( const QString &binFile, const QString &configFile, const QString &serverName, const QString &runDir, const QString &logDir, const QString &filePrefix, int port, bool ssl, int logLevel); static bool generateConfigFile(const QString &m2shBinFile, const QString &configTemplateFile, const QString &runDir, const QString &logDir, const QString &ipcPrefix, const QString &filePrefix, const QString &certsDir, int clientBufferSize, int maxconn, const QList &ports, int logLevel); // reimplemented virtual QStringList arguments() const; virtual bool acceptSighup() const; virtual bool alwaysLogStatus() const; virtual QString formatLogLine(const QString &line) const; private: QStringList args_; QString prefix_; int logLevel_; QString filterLogLine(int, const QString&) const; }; #endif pushpin-1.39.1/src/cpp/runner/pushpinhandlerservice.cpp000066400000000000000000000032271457610542000232760ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "pushpinhandlerservice.h" #include #include PushpinHandlerService::PushpinHandlerService( const QString &binFile, const QString &configFile, const QString &runDir, const QString &logDir, const QString &ipcPrefix, const QString &filePrefix, int portOffset, int logLevel) { args_ += binFile; args_ += "--config=" + configFile; if(!ipcPrefix.isEmpty()) args_ += "--ipc-prefix=" + ipcPrefix; if(portOffset > 0) args_ += "--port-offset=" + QString::number(portOffset); if(!logDir.isEmpty()) { args_ += "--logfile=" + QDir(logDir).filePath(filePrefix + "pushpin-handler.log"); setStandardOutputFile(QProcess::nullDevice()); } if(logLevel >= 0) args_ += "--loglevel=" + QString::number(logLevel); setName("handler"); setPidFile(QDir(runDir).filePath(filePrefix + "pushpin-handler.pid")); } QStringList PushpinHandlerService::arguments() const { return args_; } bool PushpinHandlerService::acceptSighup() const { return true; } pushpin-1.39.1/src/cpp/runner/pushpinhandlerservice.h000066400000000000000000000022611457610542000227400ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef PUSHPINHANDLERSERVICE_H #define PUSHPINHANDLERSERVICE_H #include "service.h" class PushpinHandlerService : public Service { public: PushpinHandlerService( const QString &binFile, const QString &configFile, const QString &runDir, const QString &logDir, const QString &ipcPrefix, const QString &filePrefix, int portOffset, int logLevel); // reimplemented virtual QStringList arguments() const; virtual bool acceptSighup() const; private: QStringList args_; }; #endif pushpin-1.39.1/src/cpp/runner/pushpinproxyservice.cpp000066400000000000000000000033201457610542000230340ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "pushpinproxyservice.h" #include #include PushpinProxyService::PushpinProxyService( const QString &binFile, const QString &configFile, const QString &runDir, const QString &logDir, const QString &ipcPrefix, const QString &filePrefix, int logLevel, const QStringList &routeLines, bool quietCheck) { args_ += binFile; args_ += "--config=" + configFile; if(!ipcPrefix.isEmpty()) args_ += "--ipc-prefix=" + ipcPrefix; if(!logDir.isEmpty()) { args_ += "--logfile=" + QDir(logDir).filePath(filePrefix + "pushpin-proxy.log"); setStandardOutputFile(QProcess::nullDevice()); } if(logLevel >= 0) args_ += "--loglevel=" + QString::number(logLevel); foreach(const QString &route, routeLines) args_ += "--route=" + route; if(quietCheck) args_ += "--quiet-check"; setName("proxy"); setPidFile(QDir(runDir).filePath(filePrefix + "pushpin-proxy.pid")); } QStringList PushpinProxyService::arguments() const { return args_; } bool PushpinProxyService::acceptSighup() const { return true; } pushpin-1.39.1/src/cpp/runner/pushpinproxyservice.h000066400000000000000000000023131457610542000225020ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef PUSHPINPROXYSERVICE_H #define PUSHPINPROXYSERVICE_H #include "service.h" class PushpinProxyService : public Service { public: PushpinProxyService( const QString &binFile, const QString &configFile, const QString &runDir, const QString &logDir, const QString &ipcPrefix, const QString &filePrefix, int logLevel, const QStringList &routeLines, bool quietCheck); // reimplemented virtual QStringList arguments() const; virtual bool acceptSighup() const; private: QStringList args_; }; #endif pushpin-1.39.1/src/cpp/runner/runnerapp.cpp000066400000000000000000000472511457610542000207100ustar00rootroot00000000000000/* * Copyright (C) 2016-2023 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "runnerapp.h" #include #include #include #include #include #include #include #include "processquit.h" #include "log.h" #include "settings.h" #include "listenport.h" #include "condureservice.h" #include "mongrel2service.h" #include "m2adapterservice.h" #include "zurlservice.h" #include "pushpinproxyservice.h" #include "pushpinhandlerservice.h" #include "config.h" struct ServiceConnections{ Connection startedConnection; Connection stoppedConnection; Connection logConnection; Connection errConnection; }; static void trimlist(QStringList *list) { for(int n = 0; n < list->count(); ++n) { if((*list)[n].isEmpty()) { list->removeAt(n); --n; // adjust position } } } static bool ensureDir(const QString &path) { QDir dir(path); if(dir.exists()) return true; return QDir().mkpath(dir.absolutePath()); } static QPair parsePort(const QString &s) { // if the string doesn't contain a colon character, assume it's a port // number by itself if(!s.contains(':')) return QPair(QHostAddress(), s.toInt()); // otherwise, assume it's an address:port combination // parse with QUrl in order to support bracketed IPv6 notation QUrl url{QUrl::fromUserInput(s)}; return QPair(QHostAddress(url.host()), url.port()); } QMap parseLogLevel(const QStringList &parts, QString *errorMessage) { QMap levels; foreach(const QString &part, parts) { if(part.isEmpty()) { *errorMessage = "log level component cannot be empty"; return QMap(); } int at = part.indexOf(':'); if(at != -1) { if(at == 0) { *errorMessage = "log level component name cannot be empty"; return QMap(); } QString name = part.mid(0, at); bool ok; int x = part.mid(at + 1).toInt(&ok); if(!ok || x < 0) { *errorMessage = QString("log level for service %1 must be greater than or equal to 0").arg(name); return QMap(); } levels[name] = x; } else { bool ok; int x = part.toInt(&ok); if(!ok || x < 0) { *errorMessage = "log level must be greater than or equal to 0"; return QMap(); } levels[""] = x; } } return levels; } enum CommandLineParseResult { CommandLineOk, CommandLineError, CommandLineVersionRequested, CommandLineHelpRequested }; class ArgsData { public: QString configFile; QString logFile; QMap logLevels; bool mergeOutput; QPair port; int id; QStringList routeLines; ArgsData() : mergeOutput(false), id(-1) { } }; static CommandLineParseResult parseCommandLine(QCommandLineParser *parser, ArgsData *args, QString *errorMessage) { parser->setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions); const QCommandLineOption configFileOption("config", "Config file.", "file"); parser->addOption(configFileOption); const QCommandLineOption logFileOption("logfile", "File to log to.", "file"); parser->addOption(logFileOption); const QCommandLineOption logLevelOption("loglevel", "Log level (default: 2).", "x"); parser->addOption(logLevelOption); const QCommandLineOption verboseOption("verbose", "Verbose output. Same as --loglevel=3."); parser->addOption(verboseOption); const QCommandLineOption mergeOutputOption(QStringList() << "m" << "merge-output", "Combine output of subprocesses."); parser->addOption(mergeOutputOption); const QCommandLineOption portOption("port", "Run a single HTTP server instance.", "[addr:]port"); parser->addOption(portOption); const QCommandLineOption idOption("id", "Set instance ID (needed to run multiple instances).", "x"); parser->addOption(idOption); const QCommandLineOption routeOption("route", "Add route (overrides routes file).", "line"); parser->addOption(routeOption); const QCommandLineOption helpOption = parser->addHelpOption(); const QCommandLineOption versionOption = parser->addVersionOption(); if(!parser->parse(QCoreApplication::arguments())) { *errorMessage = parser->errorText(); return CommandLineError; } if(parser->isSet(versionOption)) return CommandLineVersionRequested; if(parser->isSet(helpOption)) return CommandLineHelpRequested; if(parser->isSet(configFileOption)) args->configFile = parser->value(configFileOption); if(parser->isSet(logFileOption)) args->logFile = parser->value(logFileOption); if(parser->isSet(logLevelOption)) { QStringList parts = parser->value(logLevelOption).split(','); QMap levels = parseLogLevel(parts, errorMessage); if(levels.isEmpty()) { return CommandLineError; } args->logLevels = levels; } if(parser->isSet(verboseOption)) { args->logLevels.clear(); args->logLevels[""] = 3; } if(parser->isSet(mergeOutputOption)) args->mergeOutput = true; if(parser->isSet(portOption)) { args->port = parsePort(parser->value(portOption)); if(args->port.second < 1) { *errorMessage = "port must be greater than or equal to 1"; return CommandLineError; } } if(parser->isSet(idOption)) { bool ok; int x = parser->value(idOption).toInt(&ok); if(!ok || x < 0) { *errorMessage = "id must be greater than or equal to 0"; return CommandLineError; } args->id = x; } if(parser->isSet(routeOption)) { foreach(const QString &r, parser->values(routeOption)) args->routeLines += r; } return CommandLineOk; } class RunnerApp::Private { public: RunnerApp *q; ArgsData args; QList services; bool stopping; bool errored; Connection quitConnection; Connection hupConnection; map serviceConnectionMap; Private(RunnerApp *_q) : q(_q), stopping(false), errored(false) { quitConnection = ProcessQuit::instance()->quit.connect(boost::bind(&Private::processQuit, this)); hupConnection = ProcessQuit::instance()->hup.connect(boost::bind(&Private::reload, this)); } void start() { QCoreApplication::setApplicationName("pushpin"); QCoreApplication::setApplicationVersion(Config::get().version); QCommandLineParser parser; parser.setApplicationDescription("Reverse proxy for realtime web services."); QString errorMessage; switch(parseCommandLine(&parser, &args, &errorMessage)) { case CommandLineOk: break; case CommandLineError: fprintf(stderr, "error: %s\n\n%s", qPrintable(errorMessage), qPrintable(parser.helpText())); q->quit(1); return; case CommandLineVersionRequested: printf("%s %s\n", qPrintable(QCoreApplication::applicationName()), qPrintable(QCoreApplication::applicationVersion())); q->quit(0); return; case CommandLineHelpRequested: parser.showHelp(); Q_UNREACHABLE(); } if(!args.logFile.isEmpty()) { if(!log_setFile(args.logFile)) { log_error("failed to open log file: %s", qPrintable(args.logFile)); q->quit(1); return; } } log_info("starting..."); QStringList configFileList; if(!args.configFile.isEmpty()) { configFileList += args.configFile; } else { // ./config configFileList += QDir("config").absoluteFilePath("pushpin.conf"); // same dir as executable (NOTE: deprecated) configFileList += QDir(".").absoluteFilePath("pushpin.conf"); // ./examples/config configFileList += QDir("examples/config").absoluteFilePath("pushpin.conf"); // default configFileList += QDir(Config::get().configDir).filePath("pushpin.conf"); } QString configFile; foreach(const QString &f, configFileList) { if(QFileInfo(f).isFile()) { configFile = f; break; } } if(configFile.isEmpty()) { log_error("no configuration file found. Tried: %s", qPrintable(configFileList.join(" "))); q->quit(1); return; } // QSettings doesn't inform us if the config file can't be accessed, // so do that ourselves { QFile file(configFile); if(!file.open(QIODevice::ReadOnly)) { log_error("failed to open %s", qPrintable(configFile)); q->quit(1); return; } } if(args.configFile.isEmpty()) log_info("using config: %s", qPrintable(configFile)); Settings settings(configFile); QString exeDir = QCoreApplication::applicationDirPath(); // NOTE: libdir in config file is deprecated QString libDir = settings.value("global/libdir").toString(); if(!libDir.isEmpty()) { libDir = QDir(libDir).absoluteFilePath("runner"); } else { if(QFile::exists("src/bin/pushpin.rs")) { // running in tree libDir = QFileInfo("src/runner").absoluteFilePath(); } else { // use compiled value libDir = QDir(Config::get().libDir).absoluteFilePath("runner"); } } QString ipcPrefix = settings.value("global/ipc_prefix", "pushpin-").toString(); QString configDir = QFileInfo(configFile).dir().filePath("runner"); QStringList serviceNames = settings.value("runner/services").toStringList(); trimlist(&serviceNames); QStringList httpPortStrs = settings.value("runner/http_port").toStringList(); trimlist(&httpPortStrs); QStringList httpsPortStrs = settings.value("runner/https_ports").toStringList(); trimlist(&httpsPortStrs); QStringList localPortStrs = settings.value("runner/local_ports").toStringList(); trimlist(&localPortStrs); QString runDir; if(settings.contains("global/rundir")) { runDir = settings.value("global/rundir").toString(); } else { log_warning("rundir in [runner] section is deprecated. put in [global]"); runDir = settings.value("runner/rundir").toString(); } runDir = QDir(runDir).absolutePath(); QString logDir = settings.value("runner/logdir").toString(); logDir = QDir(logDir).absolutePath(); QMap logLevels; QStringList logLevelParts = settings.value("runner/log_level").toStringList(); if(!logLevelParts.isEmpty()) { logLevels = parseLogLevel(logLevelParts, &errorMessage); if(logLevels.isEmpty()) { fprintf(stderr, "error: %s\n", qPrintable(errorMessage)); q->quit(1); return; } } // command line overrides config file if(!args.logLevels.isEmpty()) logLevels = args.logLevels; // if default log level not provided, use info level int defaultLevel = logLevels.value("", 2); // NOTE: since we only finally set the log level here, earlier // log messages outside the default level will be lost (if any) log_setOutputLevel(logLevels.value("runner", defaultLevel)); int clientBufferSize = settings.value("runner/client_buffer_size", 8192).toInt(); int clientMaxConnections = settings.value("runner/client_maxconn", 50000).toInt(); bool allowCompression = settings.value("runner/allow_compression").toBool(); QString m2aBin = "m2adapter"; QFileInfo fi(QDir(exeDir).filePath("bin/m2adapter")); if(fi.isFile()) m2aBin = fi.canonicalFilePath(); QString condureBin = "pushpin-condure"; fi = QFileInfo(QDir(exeDir).filePath("bin/pushpin-condure")); if(fi.isFile()) condureBin = fi.canonicalFilePath(); QString proxyBin = "pushpin-proxy"; fi = QFileInfo(QDir(exeDir).filePath("bin/pushpin-proxy")); if(fi.isFile()) proxyBin = fi.canonicalFilePath(); QString handlerBin = "pushpin-handler"; fi = QFileInfo(QDir(exeDir).filePath("bin/pushpin-handler")); if(fi.isFile()) handlerBin = fi.canonicalFilePath(); if(!ensureDir(runDir)) { log_error("failed to create directory: %s", qPrintable(runDir)); q->quit(1); return; } if(!args.mergeOutput && !ensureDir(logDir)) { log_error("failed to create directory: %s", qPrintable(logDir)); q->quit(1); return; } int portOffset = 0; QString filePrefix; QList ports; if(args.port.second > 0) { // if port specified then instantiate a single http server ports += ListenPort(args.port.first, args.port.second, false); } else { foreach(const QString &httpPortStr, httpPortStrs) { QPair p = parsePort(httpPortStr); if(p.second < 0) { log_error("invalid http port: %s", qPrintable(httpPortStr)); q->quit(1); return; } ports += ListenPort(p.first, p.second, false); } foreach(const QString &httpsPortStr, httpsPortStrs) { QPair p = parsePort(httpsPortStr); if(p.second < 1) { log_error("invalid https port: %s", qPrintable(httpsPortStr)); q->quit(1); return; } ports += ListenPort(p.first, p.second, true); } foreach(const QString &localPortStr, localPortStrs) { QUrl path = QUrl::fromEncoded(localPortStr.toUtf8()); if(!path.isValid()) { log_error("invalid local port: %s", qPrintable(localPortStr)); q->quit(1); return; } QUrlQuery query(path.query()); int mode = -1; if(query.hasQueryItem("mode")) { QString modeStr = query.queryItemValue("mode"); bool ok = false; mode = modeStr.toInt(&ok, 8); if(!ok) { log_error("invalid mode: %s", qPrintable(modeStr)); q->quit(1); return; } } QString user = query.queryItemValue("user"); QString group = query.queryItemValue("group"); ports += ListenPort(QHostAddress(), 0, true, path.path(), mode, user, group); } } if(ports.isEmpty()) { log_error("no server ports configured"); q->quit(1); return; } if(args.id >= 0) { ipcPrefix = QString("p%1-").arg(args.id); portOffset = args.id * 10; filePrefix = ipcPrefix; } if(serviceNames.contains("condure") && (serviceNames.contains("mongrel2") || serviceNames.contains("m2adapter"))) { log_error("cannot enable the condure service at the same time as mongrel2 or m2adapter"); q->quit(1); return; } if(serviceNames.contains("condure")) { QString certsDir = QDir(configDir).filePath("certs"); bool useClient = false; if(!serviceNames.contains("zurl") && CondureService::hasClientMode(condureBin)) useClient = true; services += new CondureService("condure", condureBin, runDir, !args.mergeOutput ? logDir : QString(), ipcPrefix, filePrefix, logLevels.value("condure", defaultLevel), certsDir, clientBufferSize, clientMaxConnections, allowCompression, ports, useClient); } if(serviceNames.contains("mongrel2")) { QString m2Bin = "mongrel2"; if(settings.contains("runner/mongrel2_bin")) m2Bin = settings.value("runner/mongrel2_bin").toString(); QString m2shBin = "m2sh"; if(settings.contains("runner/m2sh_bin")) m2shBin = settings.value("runner/m2sh_bin").toString(); QString certsDir = QDir(configDir).filePath("certs"); if(!Mongrel2Service::generateConfigFile(m2shBin, QDir(libDir).filePath("mongrel2.conf.template"), runDir, !args.mergeOutput ? logDir : QString(), ipcPrefix, filePrefix, certsDir, clientBufferSize, clientMaxConnections, ports, logLevels.value("mongrel2", defaultLevel))) { q->quit(1); return; } foreach(const ListenPort &p, ports) services += new Mongrel2Service(m2Bin, QDir(runDir).filePath(QString("%1mongrel2.sqlite").arg(filePrefix)), "default_" + QString::number(p.port), runDir, !args.mergeOutput ? logDir : QString(), filePrefix, p.port, p.ssl, logLevels.value("mongrel2", defaultLevel)); } if(serviceNames.contains("m2adapter")) { QList portsOnly; foreach(const ListenPort &p, ports) portsOnly += p.port; services += new M2AdapterService(m2aBin, QDir(libDir).filePath("m2adapter.conf.template"), runDir, !args.mergeOutput ? logDir : QString(), ipcPrefix, filePrefix, logLevels.value("m2adapter", defaultLevel), portsOnly); } bool quietCheck = false; if(serviceNames.contains("zurl")) { QString zurlBin = "zurl"; if(settings.contains("runner/zurl_bin")) zurlBin = settings.value("runner/zurl_bin").toString(); services += new ZurlService(zurlBin, QDir(libDir).filePath("zurl.conf.template"), runDir, !args.mergeOutput ? logDir : QString(), ipcPrefix, filePrefix, logLevels.value("zurl", defaultLevel)); // when zurl is managed by pushpin, log updates checks as debug level quietCheck = true; } if(serviceNames.contains("pushpin-proxy")) services += new PushpinProxyService(proxyBin, configFile, runDir, !args.mergeOutput ? logDir : QString(), ipcPrefix, filePrefix, logLevels.value("pushpin-proxy", defaultLevel), args.routeLines, quietCheck); if(serviceNames.contains("pushpin-handler")) services += new PushpinHandlerService(handlerBin, configFile, runDir, !args.mergeOutput ? logDir : QString(), ipcPrefix, filePrefix, portOffset, logLevels.value("pushpin-handler", defaultLevel)); foreach(Service *s, services) { serviceConnectionMap[s] = { s->started.connect(boost::bind(&Private::service_started, this)), s->stopped.connect(boost::bind(&Private::service_stopped, this, s)), s->logLine.connect(boost::bind(&Private::service_logLine, this, boost::placeholders::_1, s)), s->error.connect(boost::bind(&Private::service_error, this, boost::placeholders::_1, s)) }; if(!args.mergeOutput || s->alwaysLogStatus()) log_info("starting %s", qPrintable(s->name())); s->start(); } } private: QString tryInsertPrefix(const QString &line, const QString &prefix) { if(line.startsWith('[')) { // find third space and insert the service name int at = 0; for(int n = 0; n < 3 && at != -1; ++n) { at = line.indexOf(' ', at); if(at != -1) ++at; } if(at != -1) { QString out = line; out.insert(at, prefix); return out; } } return line; } void stopAll() { foreach(Service *s, services) { if(!args.mergeOutput || s->alwaysLogStatus()) log_info("stopping %s", qPrintable(s->name())); s->stop(); } } void checkStopped() { if(services.isEmpty()) { log_info("stopped"); doQuit(); } } void doQuit() { q->quit(errored ? 1 : 0); } void service_started() { bool allStarted = true; foreach(Service *s, services) { if(!s->isStarted()) { allStarted = false; break; } } if(allStarted) log_info("started"); } void service_stopped(Service *s) { serviceConnectionMap.erase(s); services.removeAll(s); delete s; checkStopped(); } void service_logLine(const QString &line, Service *s) { QString out = tryInsertPrefix(s->formatLogLine(line), '[' + s->name() + "] "); if(!out.isEmpty()) { log_raw(qPrintable(out)); } } void service_error(const QString &error, Service *s) { log_error("%s: %s", qPrintable(s->name()), qPrintable(error)); serviceConnectionMap.erase(s); services.removeAll(s); delete s; errored = true; if(stopping) { checkStopped(); } else { // shutdown if we receive an unexpected error from any service stopping = true; stopAll(); } } void reload() { log_info("reloading"); log_rotate(); foreach(Service *s, services) { if(s->acceptSighup()) s->sendSighup(); } } void processQuit() { if(!stopping) { stopping = true; ProcessQuit::reset(); // allow user to quit again log_info("stopping..."); stopAll(); } else { qDeleteAll(services); ProcessQuit::cleanup(); // if we were already stopping, then exit immediately doQuit(); } } }; RunnerApp::RunnerApp() { d = std::make_unique(this); } RunnerApp::~RunnerApp() = default; void RunnerApp::start() { d->start(); } pushpin-1.39.1/src/cpp/runner/runnerapp.h000066400000000000000000000021041457610542000203410ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef RUNNERAPP_H #define RUNNERAPP_H #include #include using std::map; using SignalInt = boost::signals2::signal; using Connection = boost::signals2::scoped_connection; class RunnerApp { public: RunnerApp(); ~RunnerApp(); void start(); SignalInt quit; private: class Private; friend class Private; std::unique_ptr d; }; #endif pushpin-1.39.1/src/cpp/runner/runnermain.cpp000066400000000000000000000023561457610542000210510ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include #include #include "runnerapp.h" class RunnerAppMain { public: RunnerApp *app; void start() { app = new RunnerApp; app->quit.connect(boost::bind(&RunnerAppMain::app_quit, this, boost::placeholders::_1)); app->start(); } void app_quit(int returnCode) { delete app; QCoreApplication::exit(returnCode); } }; extern "C" { int runner_main(int argc, char **argv) { QCoreApplication qapp(argc, argv); RunnerAppMain appMain; QTimer::singleShot(0, [&appMain]() {appMain.start();}); return qapp.exec(); } } pushpin-1.39.1/src/cpp/runner/service.cpp000066400000000000000000000153241457610542000203320ustar00rootroot00000000000000/* * Copyright (C) 2016-2022 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "service.h" #include #include #include #include #include #include #include "log.h" #define STOP_TIMEOUT 4000 static void setupChild() { signal(SIGINT, SIG_IGN); // subprocesses hopefully respect SIG_IGN, but are not required // to. in case subprocess might reinstate a SIGINT handler, // detach from process group to ensure ctrl-c in a shell // doesn't cause SIGINT to be sent directly to subprocesses setpgid(0, 0); } class ServiceProcess : public QProcess { Q_OBJECT public: ServiceProcess(QObject *parent = 0) : QProcess(parent) { #if QT_VERSION >= 0x060000 setChildProcessModifier(setupChild); #endif } #if QT_VERSION < 0x060000 // reimplemented virtual void setupChildProcess() { setupChild(); } #endif }; class Service::Private : public QObject { Q_OBJECT public: enum State { NotStarted, Starting, Started, Stopping, Stopped }; Service *q; State state; QString name; QString outputFile; QString pidFile; QProcess *proc; bool terminateAfterStarted; bool sentKill; QTimer *timer; Private(Service *_q) : QObject(_q), q(_q), state(NotStarted), proc(0), terminateAfterStarted(false), sentKill(false) { timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &Private::timer_timeout); timer->setSingleShot(true); } ~Private() { timer->stop(); if(state == Starting || state == Started || state == Stopping) { proc->disconnect(this); proc->setParent(0); if(!sentKill) { sentKill = true; state = Stopping; log_warning("%s running while needing to exit, forcing quit", qPrintable(name)); proc->kill(); if(!pidFile.isEmpty()) QFile::remove(pidFile); } proc->waitForFinished(); } cleanup(); timer->disconnect(this); timer->setParent(0); timer->deleteLater(); } void cleanup() { timer->stop(); if(proc) { proc->disconnect(this); proc->setParent(0); proc->deleteLater(); proc = 0; } } void start() { proc = new ServiceProcess(this); connect(proc, &QProcess::started, this, &Private::proc_started); connect(proc, &QProcess::readyReadStandardOutput, this, &Private::proc_readyRead); connect(proc, static_cast(&QProcess::finished), this, &Private::proc_finished); connect(proc, static_cast(&QProcess::errorOccurred), this, &Private::proc_errorOccurred); proc->setProcessChannelMode(QProcess::MergedChannels); proc->setReadChannel(QProcess::StandardOutput); if(!outputFile.isEmpty()) proc->setStandardOutputFile(outputFile, QIODevice::Append); state = Starting; QStringList args = q->arguments(); log_debug("running: %s", qPrintable(args.join(' '))); proc->start(args[0], args.mid(1)); } void stop() { if(state == Starting) { terminateAfterStarted = true; } else if(state == Started) { doStop(); } } private: void doStop() { state = Stopping; timer->start(STOP_TIMEOUT); proc->terminate(); } bool writePidFile(const QString &file, int pid) { QFile f(file); if(!f.open(QFile::WriteOnly | QFile::Truncate)) return false; if(f.write(QByteArray::number(pid) + '\n') == -1) return false; return true; } private slots: void doError(const QString &str) { q->error(str); } void proc_started() { if(!pidFile.isEmpty()) { if(!writePidFile(pidFile, proc->processId())) log_error("failed to write pid file: %s", qPrintable(pidFile)); } state = Started; q->started(); if(terminateAfterStarted) doStop(); } void proc_readyRead() { while(proc->canReadLine()) { QByteArray line = proc->readLine(); if(!line.isEmpty() && line[line.length() - 1] == '\n') line.truncate(line.length() - 1); q->logLine(QString::fromLocal8Bit(line)); } } void proc_finished(int exitCode, QProcess::ExitStatus exitStatus) { if(!pidFile.isEmpty()) QFile::remove(pidFile); if(state != Stopping) { state = Stopped; cleanup(); q->error("Exited unexpectedly"); return; } state = Stopped; cleanup(); if(exitStatus == QProcess::CrashExit) { if(sentKill) q->stopped(); else q->error("Exited uncleanly"); return; } if(exitCode != 0) { q->error("Unexpected return code: " + QString::number(exitCode)); return; } q->stopped(); } void proc_errorOccurred(QProcess::ProcessError error) { if(error == QProcess::FailedToStart) { QString program = proc->program(); state = Stopped; cleanup(); q->error("Error running: " + program); } else { // other errors are followed by finished(), so we don't // need to handle them here } } void timer_timeout() { if(!sentKill) { sentKill = true; log_warning("%s taking too long, forcing quit", qPrintable(name)); proc->kill(); } } }; Service::Service(QObject *parent) : QObject(parent) { d = new Private(this); } Service::~Service() { delete d; } QString Service::name() const { return d->name; } bool Service::acceptSighup() const { return false; } bool Service::alwaysLogStatus() const { return false; } bool Service::isStarted() const { return (d->state != Private::NotStarted && d->state != Private::Starting); } bool Service::preStart() { // by default do nothing return true; } void Service::start() { if(!preStart()) { QString str = "Failure preparing to start"; QMetaObject::invokeMethod(this, "doError", Qt::QueuedConnection, Q_ARG(QString, str)); return; } d->start(); } void Service::postStart() { // by default do nothing } void Service::stop() { d->stop(); } void Service::postStop() { // by default do nothing } QString Service::formatLogLine(const QString &line) const { return line; } void Service::sendSighup() { if(d->proc) ::kill(d->proc->processId(), SIGHUP); } void Service::setName(const QString &name) { d->name = name; } void Service::setStandardOutputFile(const QString &file) { d->outputFile = file; } void Service::setPidFile(const QString &file) { d->pidFile = file; } #include "service.moc" pushpin-1.39.1/src/cpp/runner/service.h000066400000000000000000000032661457610542000200010ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef SERVICE_H #define SERVICE_H #include #include #include using Signal = boost::signals2::signal; using SignalStr = boost::signals2::signal; using Connection = boost::signals2::scoped_connection; class Service : public QObject { Q_OBJECT public: Service(QObject *parent = 0); ~Service(); QString name() const; virtual QStringList arguments() const = 0; virtual bool acceptSighup() const; virtual bool alwaysLogStatus() const; virtual bool isStarted() const; virtual bool preStart(); virtual void start(); virtual void postStart(); virtual void stop(); virtual void postStop(); virtual QString formatLogLine(const QString &line) const; void sendSighup(); Signal started; Signal stopped; SignalStr logLine; SignalStr error; protected: void setName(const QString &name); void setStandardOutputFile(const QString &file); void setPidFile(const QString &file); private: class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/runner/template.cpp000066400000000000000000000243401457610542000205030ustar00rootroot00000000000000/* * Copyright (C) 2016-2020 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ // NOTE: this is a basic jinja-like template engine. its abilities are // minimal, because at the time of this writing our templates aren't very // complex. if someday we need more template functionality, we should // consider throwing this code away and using a real template library. #include "template.h" #include #include #include "qtcompat.h" #include "log.h" namespace Template { class TemplateItem { public: enum Type { Root, Content, Expression, If, For }; Type type; QString data; QList children; TemplateItem() : type((Type)-1) { } TemplateItem(Type _type, const QString &_data) : type(_type), data(_data) { } }; enum ControlType { ControlNone, ControlIf, ControlFor }; static QList parseContent(const QString &content, int *pos, ControlType ctype, QString *error) { QList out; QString curContent; bool closed = false; for(int n = *pos; n < content.length(); ++n) { QChar c = content[n]; if(n + 1 < content.length() && c == '{' && (content[n + 1] == '{' || content[n + 1] == '%')) { if(!curContent.isEmpty()) { out += TemplateItem(TemplateItem::Content, curContent); curContent.clear(); } ++n; if(content[n] == '{') { if(n + 1 >= content.length()) { *error = "EOF reached while parsing directive"; return QList(); } ++n; int end = content.indexOf("}}", n); if(end == -1) { *error = "no matching }}"; return QList(); } QString s = content.mid(n, end - n).simplified(); n = end + 2; out += TemplateItem(TemplateItem::Expression, s); } else if(content[n] == '%') { if(n + 1 >= content.length()) { *error = "EOF reached while parsing directive"; return QList(); } ++n; int end = content.indexOf("%}", n); if(end == -1) { *error = "no matching %}"; return QList(); } QString s = content.mid(n, end - n).simplified(); n = end + 2; QString stype; int at = s.indexOf(' '); if(at != -1) { stype = s.mid(0, at); s = s.mid(at + 1); } else { stype = s; s.clear(); } if(stype == "if" || stype == "for") { TemplateItem t; ControlType ct; t.data = s; if(stype == "if") { t.type = TemplateItem::If; ct = ControlIf; } else // for { t.type = TemplateItem::For; ct = ControlFor; } *pos = n; QString error_; t.children = parseContent(content, pos, ct, &error_); if(!error_.isEmpty()) { *error = error_; return QList(); } out += t; n = *pos; } else if(stype == "endif" || stype == "endfor") { ControlType endType; if(stype == "endif") endType = ControlIf; else // for endType = ControlFor; if(endType != ctype) { QString expected; if(ctype == ControlIf) expected = "endif"; else expected = "endfor"; *error = QString("encountered \"%1\" while expecting \"%2\"").arg(stype, expected); return QList(); } *pos = n; closed = true; break; } else { // unknown control type *error = QString("unknown control directive \"%1\"").arg(stype); return QList(); } } --n; // adjust position } else { curContent += c; } *pos = n; } if(ctype != ControlNone && !closed) { QString ctypeStr; if(ctype == ControlIf) ctypeStr = "if"; else // ControlFor ctypeStr = "for"; *error = "directive \"%1\" not closed"; return QList(); } if(!curContent.isEmpty()) { out += TemplateItem(TemplateItem::Content, curContent); curContent.clear(); } error->clear(); return out; } // handles lookup by exact name or dot-notation for children static QVariant getVar(const QString &s, const QVariantMap &context) { int at = s.indexOf('.'); if(at != -1) { QString parent = s.mid(0, at); QString member = s.mid(at + 1); if(parent.isEmpty() || !context.contains(parent)) return QVariant(); QVariant subContext = context[parent]; if(typeId(subContext) != QMetaType::QVariantMap) return QVariant(); return getVar(member, subContext.toMap()); } else { if(!context.contains(s)) return QVariant(); return context[s]; } } static QString renderExpression(const QString &exp, const QVariantMap &context) { // for now all we support is variable lookups. no fancy expressions QVariant val = getVar(exp, context); if(!val.isValid()) return QString(); return val.toString(); } static bool evalCondition(const QString &s, const QVariantMap &context) { // for now all we support is variable test with optional negation if(s.startsWith("not ")) { return !evalCondition(s.mid(4), context); } else { QVariant val = getVar(s, context); if(typeId(val) == QMetaType::QString) return !val.toString().isEmpty(); else if(typeId(val) == QMetaType::Bool) return val.toBool(); else if(canConvert(val, QMetaType::Int)) return (val.toInt() != 0); else return false; } } static QVariantList parseFor(const QString &s, QString *iterVarName, const QVariantMap &context, QString *error) { // for now all we support is "varname in map" int at = s.indexOf(" in "); if(at == -1) { *error = "\"for\" directive must be of the form: \"for variable in container\""; return QVariantList(); } *iterVarName = s.mid(0, at); QString containerName = s.mid(at + 4); QVariant container = getVar(containerName, context); if(typeId(container) != QMetaType::QVariantList) { *error = "\"for\" container must be a list"; return QVariantList(); } return container.toList(); } static QString renderInternal(const TemplateItem &item, const QVariantMap &context, QString *error) { QString out; if(item.type == TemplateItem::Root) { foreach(const TemplateItem &i, item.children) { out += renderInternal(i, context, error); if(!error->isEmpty()) return QString(); } } else if(item.type == TemplateItem::Content) { out += item.data; } else if(item.type == TemplateItem::Expression) { out += renderExpression(item.data, context); } else if(item.type == TemplateItem::If) { if(evalCondition(item.data, context)) { foreach(const TemplateItem &i, item.children) { out += renderInternal(i, context, error); if(!error->isEmpty()) return QString(); } } } else if(item.type == TemplateItem::For) { QString iterVarName; QVariantList forItems = parseFor(item.data, &iterVarName, context, error); if(!error->isEmpty()) return QString(); for(int n = 0; n < forItems.count(); ++n) { const QVariant &forItem = forItems[n]; QVariantMap loop; loop["first"] = (n == 0); loop["last"] = (n == forItems.count() - 1); QVariantMap tmp = context; tmp[iterVarName] = forItem; tmp["loop"] = loop; foreach(const TemplateItem &i, item.children) { out += renderInternal(i, tmp, error); if(!error->isEmpty()) return QString(); } } } return out; } static void dumpItem(const TemplateItem &item, int depth = 0) { for(int n = 0; n < depth; ++n) printf(" "); if(item.type == TemplateItem::Root) { printf("root\n"); foreach(const TemplateItem &i, item.children) dumpItem(i, depth + 2); } else if(item.type == TemplateItem::Content) { printf("content: [%s]\n", qPrintable(item.data)); } else if(item.type == TemplateItem::Expression) { printf("expression: [%s]\n", qPrintable(item.data)); } else if(item.type == TemplateItem::If) { printf("if: [%s]\n", qPrintable(item.data)); foreach(const TemplateItem &i, item.children) dumpItem(i, depth + 2); } else if(item.type == TemplateItem::For) { printf("for: [%s]\n", qPrintable(item.data)); foreach(const TemplateItem &i, item.children) dumpItem(i, depth + 2); } } QString render(const QString &content, const QVariantMap &context, QString *error) { TemplateItem root; root.type = TemplateItem::Root; int pos = 0; QString error_; root.children = parseContent(content, &pos, ControlNone, &error_); if(!error_.isEmpty()) { if(error) *error = error_; return QString(); } QString result = renderInternal(root, context, &error_); if(!error_.isEmpty()) { if(error) *error = error_; return QString(); } return result; } bool renderFile(const QString &inFile, const QString &outFile, const QVariantMap &context, QString *error) { QFile in(inFile); if(!in.open(QFile::ReadOnly | QFile::Text)) { if(error) *error = QString("error reading file \"%1\"").arg(inFile); return false; } QString inFileData = QString::fromLocal8Bit(in.readAll()); in.close(); QString error_; QString outFileData = render(inFileData, context, &error_); if(outFileData.isNull()) { if(error) *error = QString("error rendering template: %1").arg(error_); return false; } QFile out(outFile); if(!out.open(QFile::WriteOnly | QFile::Truncate)) { if(error) *error = QString("error writing file \"%1\"").arg(outFile); return false; } int ret = out.write(outFileData.toLocal8Bit()); if(ret == -1) { if(error) *error = QString("error writing file \"%1\"").arg(outFile); return false; } return true; } void dumpTemplate(const QString &content) { TemplateItem root; root.type = TemplateItem::Root; int pos = 0; QString error; root.children = parseContent(content, &pos, ControlNone, &error); if(!error.isEmpty()) { printf("error parsing template: %s\n", qPrintable(error)); return; } dumpItem(root); } } pushpin-1.39.1/src/cpp/runner/template.h000066400000000000000000000020441457610542000201450ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef TEMPLATE_H #define TEMPLATE_H #include #include namespace Template { QString render(const QString &content, const QVariantMap &context, QString *error = 0); bool renderFile(const QString &inFile, const QString &outFile, const QVariantMap &context, QString *error = 0); void dumpTemplate(const QString &content); } #endif pushpin-1.39.1/src/cpp/runner/zurlservice.cpp000066400000000000000000000037751457610542000212560ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "zurlservice.h" #include #include #include "log.h" #include "template.h" ZurlService::ZurlService( const QString &binFile, const QString &configTemplateFile, const QString &runDir, const QString &logDir, const QString &ipcPrefix, const QString &filePrefix, int logLevel) { args_ += binFile; args_ += "--config=" + QDir(runDir).filePath(filePrefix + "zurl.conf"); if(!logDir.isEmpty()) { args_ += "--logfile=" + QDir(logDir).filePath(filePrefix + "zurl.log"); setStandardOutputFile(QProcess::nullDevice()); } if(logLevel >= 3) args_ += "--verbose"; else args_ += "--loglevel=" + QString::number(logLevel); configTemplateFile_ = configTemplateFile; runDir_ = runDir; ipcPrefix_ = ipcPrefix; filePrefix_ = filePrefix; setName("zurl"); setPidFile(QDir(runDir).filePath(filePrefix + "zurl.pid")); } QStringList ZurlService::arguments() const { return args_; } bool ZurlService::acceptSighup() const { return true; } bool ZurlService::preStart() { QVariantMap context; context["rundir"] = runDir_; context["ipc_prefix"] = ipcPrefix_; QString error; if(!Template::renderFile(configTemplateFile_, QDir(runDir_).filePath(filePrefix_ + "zurl.conf"), context, &error)) { log_error("Failed to generate zurl config file: %s", qPrintable(error)); return false; } return true; } pushpin-1.39.1/src/cpp/runner/zurlservice.h000066400000000000000000000023641457610542000207140ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef ZURLSERVICE_H #define ZURLSERVICE_H #include "service.h" class ZurlService : public Service { public: ZurlService( const QString &binFile, const QString &configTemplateFile, const QString &runDir, const QString &logDir, const QString &ipcPrefix, const QString &filePrefix, int logLevel); // reimplemented virtual QStringList arguments() const; virtual bool acceptSighup() const; virtual bool preStart(); private: QString configTemplateFile_; QString runDir_; QString ipcPrefix_; QString filePrefix_; QStringList args_; }; #endif pushpin-1.39.1/src/cpp/settings.cpp000066400000000000000000000077661457610542000172340ustar00rootroot00000000000000/* * Copyright (C) 2012-2022 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "settings.h" #include #include #include #include #include "qtcompat.h" #include "config.h" Settings::Settings(const QString &fileName) : include_(0), portOffset_(0) { main_ = new QSettings(fileName, QSettings::IniFormat); libdir_ = valueRaw("global/libdir").toString(); if(libdir_.isEmpty()) { if(QFile::exists("src/bin/pushpin.rs")) { // running in tree libdir_ = QFileInfo("src").absoluteFilePath(); } else { // use compiled value libdir_ = Config::get().libDir; } } rundir_ = valueRaw("global/rundir").toString(); if(rundir_.isEmpty()) { // fallback to runner section (deprecated) rundir_ = valueRaw("runner/rundir").toString(); } ipcPrefix_ = valueRaw("global/ipc_prefix", "pushpin-").toString(); portOffset_ = valueRaw("global/port_offset", 0).toInt(); QString includeFile = valueRaw("global/include").toString(); // if include is exactly "internal.conf", rewrite relative to libdir // TODO: remove this hack at next major version if(includeFile == "internal.conf") includeFile = "{libdir}/internal.conf"; includeFile = resolveVars(includeFile); if(!includeFile.isEmpty()) { // if include is a relative path, then use it relative to the config file location QFileInfo fi(includeFile); if(fi.isRelative()) includeFile = QFileInfo(QFileInfo(fileName).absoluteDir(), includeFile).filePath(); include_ = new QSettings(includeFile, QSettings::IniFormat); } } Settings::~Settings() { delete include_; delete main_; } QString Settings::resolveVars(const QString &in) const { QString out = in; out.replace("{libdir}", libdir_); out.replace("{rundir}", rundir_); out.replace("{ipc_prefix}", ipcPrefix_); // adjust tcp ports int at = 0; while(true) { at = out.indexOf("tcp://", at); if(at == -1) break; at = out.indexOf(':', at + 6); if(at == -1) break; int start = at + 1; for(at = start; at < out.length(); ++at) { if(!out[at].isDigit()) break; } bool ok; int x = out.mid(start, at - start).toInt(&ok); if(!ok) break; x += portOffset_; out.replace(start, at, QString::number(x)); } return out; } bool Settings::contains(const QString &key) const { if(main_->contains(key)) return true; if(include_) return include_->contains(key); return false; } QVariant Settings::valueRaw(const QString &key, const QVariant &defaultValue) const { if(include_) { if(main_->contains(key)) return main_->value(key); else return include_->value(key, defaultValue); } else return main_->value(key, defaultValue); } QVariant Settings::value(const QString &key, const QVariant &defaultValue) const { QVariant v = valueRaw(key, defaultValue); if(v.isValid()) { if(typeId(v) == QMetaType::QString) { v = resolveVars(v.toString()); } else if(typeId(v) == QMetaType::QStringList) { QStringList oldList = v.toStringList(); QStringList newList; foreach(QString s, oldList) newList += resolveVars(s); v = newList; } } return v; } int Settings::adjustedPort(const QString &key, int defaultValue) const { int x = value(key, QVariant(defaultValue)).toInt(); if(x > 0) x += portOffset_; return x; } void Settings::setIpcPrefix(const QString &s) { ipcPrefix_ = s; } void Settings::setPortOffset(int x) { portOffset_ = x; } pushpin-1.39.1/src/cpp/settings.h000066400000000000000000000026101457610542000166600ustar00rootroot00000000000000/* * Copyright (C) 2012-2022 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef SETTINGS_H #define SETTINGS_H #include #include class QSettings; class Settings { public: Settings(const QString &fileName); ~Settings(); bool contains(const QString &key) const; QVariant valueRaw(const QString &key, const QVariant &defaultValue = QVariant()) const; QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const; int adjustedPort(const QString &key, int defaultValue = -1) const; void setIpcPrefix(const QString &s); void setPortOffset(int x); private: QSettings *main_; QSettings *include_; QString libdir_; QString rundir_; QString ipcPrefix_; int portOffset_; QString resolveVars(const QString &in) const; }; #endif pushpin-1.39.1/src/cpp/simplehttpserver.cpp000066400000000000000000000276701457610542000210100ustar00rootroot00000000000000/* * Copyright (C) 2015-2022 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "simplehttpserver.h" #include #include #include #include #include #include #include #include "log.h" #include "httpheaders.h" class SimpleHttpRequest::Private : public QObject { Q_OBJECT public: enum State { ReadHeader, ReadBody, WriteBody, WaitForWritten, Closing }; SimpleHttpRequest *q; QIODevice *sock; State state; QByteArray inBuf; bool version1dot0; QString method; QByteArray uri; HttpHeaders reqHeaders; QByteArray reqBody; int contentLength; int pendingWritten; int maxHeadersSize; int maxBodySize; Private(SimpleHttpRequest *_q, int maxHeadersSize, int maxBodySize) : QObject(_q), q(_q), sock(0), state(ReadHeader), version1dot0(false), contentLength(0), pendingWritten(0), maxHeadersSize(maxHeadersSize), maxBodySize(maxBodySize) { } ~Private() { cleanup(); } void cleanup() { if(sock) { sock->disconnect(this); sock->setParent(0); sock->deleteLater(); sock = 0; } } void start(QTcpSocket *_sock) { QObject::connect(_sock, &QTcpSocket::readyRead, [this]() { this->sock_readyRead(); }); QObject::connect(_sock, &QTcpSocket::bytesWritten, this, [this](qint64 bytes) { this->sock_bytesWritten(bytes); }); QObject::connect(_sock, &QTcpSocket::disconnected, [this]() { this->sock_disconnected(); }); sock = _sock; sock->setParent(this); processIn(); } void start(QLocalSocket *_sock) { QObject::connect(_sock, &QLocalSocket::readyRead, [this]() { this->sock_readyRead(); }); QObject::connect(_sock, &QLocalSocket::bytesWritten, this, [this](qint64 bytes) { this->sock_bytesWritten(bytes); }); QObject::connect(_sock, &QLocalSocket::disconnected, [this]() { this->sock_disconnected(); }); sock = _sock; sock->setParent(this); processIn(); } void respond(int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body) { if(state != WriteBody) return; HttpHeaders outHeaders = headers; outHeaders.removeAll("Connection"); outHeaders.removeAll("Transfer-Encoding"); outHeaders.removeAll("Content-Length"); outHeaders += HttpHeader("Connection", "close"); outHeaders += HttpHeader("Content-Length", QByteArray::number(body.size())); QByteArray respData = "HTTP/"; if(version1dot0) respData += "1.0 "; else respData += "1.1 "; respData += QByteArray::number(code) + " " + reason + "\r\n"; foreach(const HttpHeader &h, outHeaders) respData += h.first + ": " + h.second + "\r\n"; respData += "\r\n"; respData += body; state = WaitForWritten; pendingWritten += respData.size(); sock->write(respData); } void respond(int code, const QByteArray &reason, const QString &body) { HttpHeaders headers; headers += HttpHeader("Content-Type", "text/plain"); respond(code, reason, headers, body.toUtf8()); } Signal ready; private: void respondError(int code, const QByteArray &reason, const QString &body) { state = WriteBody; respond(code, reason, body + '\n'); } void respondBadRequest(const QString &body) { respondError(400, "Bad Request", body); } void respondLengthRequired(const QString &body) { respondError(411, "Length Required", body); } bool processHeaderData(const QByteArray &headerData) { QList lines; int at = 0; while(at < headerData.size()) { int end = headerData.indexOf("\n", at); assert(end != -1); if(end > at && headerData[end - 1] == '\r') lines += headerData.mid(at, end - at - 1); else lines += headerData.mid(at, end - at); at = end + 1; } if(lines.isEmpty()) return false; QByteArray requestLine = lines[0]; at = requestLine.indexOf(' '); if(at == -1) return false; method = QString::fromLatin1(requestLine.mid(0, at)); if(method.isEmpty()) return false; ++at; int end = requestLine.indexOf(' ', at); if(end == -1) return false; uri = requestLine.mid(at, end - at); QByteArray versionStr = requestLine.mid(end + 1); if(versionStr == "HTTP/1.0") version1dot0 = true; for(int n = 1; n < lines.count(); ++n) { const QByteArray &line = lines[n]; end = line.indexOf(':'); if(end == -1) continue; // skip first space at = end + 1; if(at < line.length() && line[at] == ' ') ++at; QByteArray name = line.mid(0, end); QByteArray val = line.mid(at); reqHeaders += HttpHeader(name, val); } //log_debug("httpserver: IN method=[%s] uri=[%s] 1.1=%s", qPrintable(method), uri.data(), version1dot0 ? "no" : "yes"); //foreach(const HttpHeader &h, reqHeaders) // log_debug("httpserver: [%s] [%s]", h.first.data(), h.second.data()); log_debug("httpserver: IN %s %s", qPrintable(method), uri.data()); return true; } void processIn() { if(state == ReadHeader) { inBuf += sock->read(maxHeadersSize - inBuf.size()); // look for double newline int at = -1; int next = 0; for(int n = 0; n < inBuf.size(); ++n) { if(n + 1 < inBuf.size() && qstrncmp(inBuf.data() + n, "\n\n", 2) == 0) { at = n + 1; next = n + 2; break; } else if(n + 2 < inBuf.size() && qstrncmp(inBuf.data() + n, "\n\r\n", 3) == 0) { at = n + 1; next = n + 3; break; } } if(at != -1) { QByteArray headerData = inBuf.mid(0, at); reqBody = inBuf.mid(next); inBuf.clear(); if(!processHeaderData(headerData)) { respondBadRequest("Failed to parse request header."); return; } bool methodAssumesBody = (method != "HEAD" && method != "GET" && method != "DELETE" && method != "OPTIONS"); if(!reqHeaders.contains("Content-Length") && (reqHeaders.contains("Transfer-Encoding") || methodAssumesBody)) { respondLengthRequired("Request requires Content-Length."); return; } if(reqHeaders.contains("Content-Length")) { bool ok; contentLength = reqHeaders.get("Content-Length").toInt(&ok); if(!ok) { respondBadRequest("Bad Content-Length."); return; } if(contentLength > maxBodySize) { respondBadRequest("Request body too large."); return; } if(reqHeaders.get("Expect") == "100-continue") { QByteArray respData = "HTTP/"; if(version1dot0) respData += "1.0 "; else respData += "1.1 "; respData += "100 Continue\r\n\r\n"; pendingWritten += respData.size(); sock->write(respData); } state = ReadBody; processIn(); } else { state = WriteBody; ready(); } } else if(inBuf.size() >= maxHeadersSize) { inBuf.clear(); respondBadRequest("Request header too large."); return; } } else if(state == ReadBody) { reqBody += sock->read(maxBodySize - reqBody.size() + 1); if(reqBody.size() > contentLength) { respondBadRequest("Request body exceeded Content-Length."); return; } if(reqBody.size() == contentLength) { state = WriteBody; ready(); } } } void sock_readyRead() { if(state == ReadHeader || state == ReadBody) processIn(); } void sock_bytesWritten(qint64 bytes) { pendingWritten -= (int)bytes; assert(pendingWritten >= 0); if(state != WaitForWritten) return; if(pendingWritten == 0) { state = Closing; sock->close(); } } void sock_disconnected() { cleanup(); q->finished(); } }; SimpleHttpRequest::SimpleHttpRequest(int maxHeadersSize, int maxBodySize,QObject *parent) : QObject(parent) { d = new Private(this, maxHeadersSize, maxBodySize); } SimpleHttpRequest::~SimpleHttpRequest() { delete d; } QString SimpleHttpRequest::requestMethod() const { return d->method; } QByteArray SimpleHttpRequest::requestUri() const { return d->uri; } HttpHeaders SimpleHttpRequest::requestHeaders() const { return d->reqHeaders; } QByteArray SimpleHttpRequest::requestBody() const { return d->reqBody; } void SimpleHttpRequest::respond(int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body) { d->respond(code, reason, headers, body); } void SimpleHttpRequest::respond(int code, const QByteArray &reason, const QString &body) { d->respond(code, reason, body); } class SimpleHttpServerPrivate : public QObject { Q_OBJECT public: SimpleHttpServer *q; void *server; bool local; QSet accepting; QList pending; int maxHeadersSize; int maxBodySize; map finishedConnections; map readyConnections; SimpleHttpServerPrivate(int maxHeadersSize, int maxBodySize, SimpleHttpServer *_q) : QObject(_q), q(_q), server(0), local(false), maxHeadersSize(maxHeadersSize), maxBodySize(maxBodySize) { } ~SimpleHttpServerPrivate() { qDeleteAll(pending); qDeleteAll(accepting); } bool listen(const QHostAddress &addr, int port) { assert(!server); QTcpServer *s = new QTcpServer(this); connect(s, &QTcpServer::newConnection, this, &SimpleHttpServerPrivate::server_newConnection); if(!s->listen(addr, port)) { delete s; return false; } server = s; local = false; return true; } bool listenLocal(const QString &name) { assert(!server); QFileInfo fi(name); QString filePath = fi.absoluteFilePath(); QFile::remove(filePath); QLocalServer *s = new QLocalServer(this); connect(s, &QLocalServer::newConnection, this, &SimpleHttpServerPrivate::server_newConnection); if(!s->listen(filePath)) { delete s; return false; } server = s; local = true; return true; } private: void server_newConnection() { if(local) { QLocalSocket *sock = ((QLocalServer *)server)->nextPendingConnection(); SimpleHttpRequest *req = new SimpleHttpRequest(maxHeadersSize, maxBodySize); readyConnections[req] = req->d->ready.connect(boost::bind(&SimpleHttpServerPrivate::req_ready, this, req->d->q)); finishedConnections[req] = req->finished.connect(boost::bind(&SimpleHttpServerPrivate::req_finished, this, req)); accepting += req; req->d->start(sock); } else { QTcpSocket *sock = ((QTcpServer *)server)->nextPendingConnection(); SimpleHttpRequest *req = new SimpleHttpRequest(maxHeadersSize, maxBodySize); readyConnections[req] = req->d->ready.connect(boost::bind(&SimpleHttpServerPrivate::req_ready, this, req->d->q)); finishedConnections[req] = req->finished.connect(boost::bind(&SimpleHttpServerPrivate::req_finished, this, req)); accepting += req; req->d->start(sock); } } void req_ready(SimpleHttpRequest *req) { accepting.remove(req); pending += req; q->requestReady(); } void req_finished(SimpleHttpRequest *req) { accepting.remove(req); pending.removeAll(req); delete req; } }; SimpleHttpServer::SimpleHttpServer(int maxHeadersSize, int maxBodySize, QObject *parent) : QObject(parent) { d = new SimpleHttpServerPrivate(maxHeadersSize, maxBodySize, this); } SimpleHttpServer::~SimpleHttpServer() { delete d; } bool SimpleHttpServer::listen(const QHostAddress &addr, int port) { return d->listen(addr, port); } bool SimpleHttpServer::listenLocal(const QString &name) { return d->listenLocal(name); } SimpleHttpRequest *SimpleHttpServer::takeNext() { if(!d->pending.isEmpty()) { SimpleHttpRequest *req = d->pending.takeFirst(); d->finishedConnections.erase(req); return req; } else return 0; } #include "simplehttpserver.moc" pushpin-1.39.1/src/cpp/simplehttpserver.h000066400000000000000000000037561457610542000204540ustar00rootroot00000000000000/* * Copyright (C) 2015-2022 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef SIMPLEHTTPSERVER_H #include #include #include #include using std::map; using Signal = boost::signals2::signal; using Connection = boost::signals2::scoped_connection; class HttpHeaders; class SimpleHttpServerPrivate; class SimpleHttpRequest : public QObject { Q_OBJECT public: SimpleHttpRequest(int maxHeadersSize, int maxBodySize, QObject* parent = 0); ~SimpleHttpRequest(); QString requestMethod() const; QByteArray requestUri() const; HttpHeaders requestHeaders() const; QByteArray requestBody() const; void respond(int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body); void respond(int code, const QByteArray &reason, const QString &body); Signal finished; private: class Private; friend class Private; friend class SimpleHttpServerPrivate; Private *d; SimpleHttpRequest(QObject *parent = 0); }; class SimpleHttpServer : public QObject { Q_OBJECT public: SimpleHttpServer(int maxHeadersSize, int maxBodySize, QObject *parent = 0); ~SimpleHttpServer(); bool listen(const QHostAddress &addr, int port); bool listenLocal(const QString &name); SimpleHttpRequest *takeNext(); Signal requestReady; private: friend class SimpleHttpServerPrivate; SimpleHttpServerPrivate *d; }; #endif pushpin-1.39.1/src/cpp/stats.cpp000066400000000000000000000015601457610542000165140ustar00rootroot00000000000000/* * Copyright (C) 2022 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "stats.h" namespace Stats { void Counters::add(const Counters &other) { for(int n = 0; n < STATS_COUNTERS_MAX; ++n) inc((Counter)n, other._values[n]); } } pushpin-1.39.1/src/cpp/stats.h000066400000000000000000000041221457610542000161560ustar00rootroot00000000000000/* * Copyright (C) 2022-2023 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef STATS_H #define STATS_H #include #include #include #define STATS_COUNTERS_MAX 12 namespace Stats { // keep in sync with STATS_COUNTERS_MAX above enum Counter { ClientHeaderBytesReceived = 0, ClientHeaderBytesSent = 1, ClientContentBytesReceived = 2, ClientContentBytesSent = 3, ClientMessagesReceived = 4, ClientMessagesSent = 5, ServerHeaderBytesReceived = 6, ServerHeaderBytesSent = 7, ServerContentBytesReceived = 8, ServerContentBytesSent = 9, ServerMessagesReceived = 10, ServerMessagesSent = 11, }; class Counters { public: Counters() { reset(); } bool isEmpty() const { return _empty; } void reset() { for(int n = 0; n < STATS_COUNTERS_MAX; ++n) _values[n] = 0; _empty = true; } quint32 get(Counter c) { int index = (int)c; assert(index >= 0 && index < STATS_COUNTERS_MAX); return _values[index]; } void inc(Counter c, quint32 count = 1) { int index = (int)c; assert(index >= 0 && index < STATS_COUNTERS_MAX); _values[index] += count; if(count > 0) _empty = false; } void add(const Counters &other); private: quint32 _values[STATS_COUNTERS_MAX]; bool _empty; }; } #endif pushpin-1.39.1/src/cpp/statsmanager.cpp000066400000000000000000001441551457610542000200570ustar00rootroot00000000000000/* * Copyright (C) 2014-2023 Fanout, Inc. * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "statsmanager.h" #include #include #include #include #include #include #include #include "qzmqsocket.h" #include "timerwheel.h" #include "log.h" #include "tnetstring.h" #include "httpheaders.h" #include "simplehttpserver.h" #include "zutil.h" // make this somewhat big since PUB is lossy #define OUT_HWM 200000 #define ACTIVITY_TIMEOUT 100 #define REFRESH_INTERVAL 1000 #define EXTERNAL_CONNECTIONS_MAX_INTERVAL 10000 #define EXPIRE_MAX 10000 #define SHOULD_PROCESS_TIME(x) (x * 3 / 4) #define TICK_DURATION_MS 10 static qint64 durationToTicksRoundDown(qint64 msec) { return msec / TICK_DURATION_MS; } static qint64 durationToTicksRoundUp(qint64 msec) { return (msec + TICK_DURATION_MS - 1) / TICK_DURATION_MS; } class StatsManager::Private : public QObject { Q_OBJECT public: class TimerBase { public: enum Type { Connection, ExternalConnection, Subscription, }; Type timerType; int timerId; TimerBase() : timerId(-1) { } }; class ConnectionInfo : public TimerBase { public: QByteArray id; QByteArray routeId; ConnectionType type; QHostAddress peerAddress; bool ssl; qint64 lastRefresh; int refreshBucket; bool linger; qint64 lastReport; qint64 retrySeq; QByteArray from; // external or linger source int ttl; // external qint64 lastActive; // external ConnectionInfo() : ssl(false), lastRefresh(-1), refreshBucket(-1), linger(false), lastReport(-1), retrySeq(-1), ttl(-1), lastActive(-1) { } }; class Subscription : public TimerBase { public: QString mode; QString channel; quint32 subscriberCount; qint64 lastRefresh; int refreshBucket; bool linger; Subscription() : subscriberCount(0), lastRefresh(-1), refreshBucket(-1), linger(false) { } }; class ConnectionsMax { public: int retrySeq; quint32 currentValue; quint32 maxValue; quint32 lastSentValue; qint64 lastSentTime; ConnectionsMax() : retrySeq(-1), currentValue(0), maxValue(0), lastSentValue(0), lastSentTime(-1) { } quint32 valueToSend() { if(maxValue > lastSentValue) return maxValue; else return currentValue; } }; class ExternalConnectionsMax { public: quint64 value; qint64 expires; ExternalConnectionsMax() : value(0), expires(0) { } }; class ConnectionsMaxes { public: QHash maxes; QSet needSend; qint64 lastRefresh; ConnectionsMaxes() : lastRefresh(0) { } }; class ExternalConnectionsMaxes { public: QHash maxes; quint32 total() const { quint32 count = 0; QHashIterator it(maxes); while(it.hasNext()) { it.next(); const ExternalConnectionsMax &cm = it.value(); count += cm.value; } return count; } }; class RetryInfo { public: quint64 nextSeq; QMap connectionInfoBySeq; RetryInfo() : nextSeq(0) { } }; class Report { public: QByteArray routeId; quint32 connectionsMax; bool connectionsMaxStale; quint32 connectionsMinutes; quint32 messagesReceived; quint32 messagesSent; quint32 httpResponseMessagesSent; int blocksReceived; int blocksSent; quint32 requestsReceived; Stats::Counters counters; qint64 lastUpdate; qint64 startTime; QHash externalReports; Report() : connectionsMax(0), connectionsMaxStale(true), connectionsMinutes(0), messagesReceived(0), messagesSent(0), httpResponseMessagesSent(0), blocksReceived(-1), blocksSent(-1), requestsReceived(0), lastUpdate(-1), startTime(-1) { } bool isEmpty() const { return (connectionsMax == 0 && connectionsMinutes == 0 && messagesReceived == 0 && messagesSent == 0 && httpResponseMessagesSent == 0 && blocksReceived <= 0 && blocksSent <= 0 && requestsReceived == 0 && counters.isEmpty()); } quint32 externalConnectionsMinutes() const { quint32 count = 0; QHashIterator it(externalReports); while(it.hasNext()) { it.next(); const Report &r = it.value(); count += r.connectionsMinutes; } return count; } void addConnectionsMinutes(quint32 mins, qint64 now) { connectionsMinutes += mins; lastUpdate = now; } void addMessageReceived(int blocks, qint64 now) { ++messagesReceived; if(blocks > 0) { if(blocksReceived < 0) blocksReceived = 0; blocksReceived += blocks; } lastUpdate = now; } void addMessageSent(const QString &transport, int blocks, qint64 now) { ++messagesSent; if(transport == "http-response") ++httpResponseMessagesSent; if(blocks > 0) { if(blocksSent < 0) blocksSent = 0; blocksSent += blocks; } lastUpdate = now; } void addRequestsReceived(quint32 count, qint64 now) { requestsReceived += count; lastUpdate = now; } void incCounter(Stats::Counter c, quint32 count, qint64 now) { counters.inc(c, count); lastUpdate = now; } void addCounters(const Stats::Counters &other, qint64 now) { counters.add(other); lastUpdate = now; } }; class Counts { public: quint32 requestsReceived; Counts() : requestsReceived(0) { } bool isEmpty() { return (requestsReceived == 0); } }; class PrometheusMetric { public: enum Type { RequestReceived, ConnectionConnected, ConnectionMinute, MessageReceived, MessageSent }; Type mtype; QString name; QString type; QString help; PrometheusMetric(Type _mtype, const QString &_name, const QString &_type, const QString &_help) : mtype(_mtype), name(_name), type(_type), help(_help) { } }; typedef QPair SubscriptionKey; StatsManager *q; int connectionsMax; int subscriptionsMax; QByteArray instanceId; int ipcFileMode; QString spec; Format outputFormat; bool connectionSend; bool connectionsMaxSend; int connectionTtl; int connectionsMaxTtl; int connectionLinger; int subscriptionTtl; int subscriptionLinger; int reportInterval; QZmq::Socket *sock; SimpleHttpServer *prometheusServer; QString prometheusPrefix; QList prometheusMetrics; QHash routeActivity; QHash connectionInfoById; QHash > connectionInfoByRoute; QHash retryInfoBySource; QVector > connectionInfoRefreshBuckets; int currentConnectionInfoRefreshBucket; QHash > externalConnectionInfoByFrom; QHash > externalConnectionInfoByRoute; QHash subscriptionsByKey; QVector > subscriptionRefreshBuckets; int currentSubscriptionRefreshBucket; ConnectionsMaxes connectionsMaxes; QHash externalConnectionsMaxes; TimerWheel wheel; qint64 startTime; QHash reports; Counts combinedCounts; Report combinedReport; QTimer *activityTimer; QTimer *reportTimer; QTimer *refreshTimer; QTimer *externalConnectionsMaxTimer; Connection promServerConnection; Private(StatsManager *_q, int _connectionsMax, int _subscriptionsMax) : QObject(_q), q(_q), connectionsMax(_connectionsMax), subscriptionsMax(_subscriptionsMax), ipcFileMode(-1), outputFormat(TnetStringFormat), connectionSend(false), connectionsMaxSend(false), connectionTtl(120 * 1000), connectionsMaxTtl(60 * 1000), connectionLinger(60 * 1000), subscriptionTtl(60 * 1000), subscriptionLinger(60 * 1000), reportInterval(10 * 1000), sock(0), prometheusServer(0), currentConnectionInfoRefreshBucket(0), currentSubscriptionRefreshBucket(0), wheel(TimerWheel((_connectionsMax * 2) + _subscriptionsMax)), reportTimer(0) { activityTimer = new QTimer(this); connect(activityTimer, &QTimer::timeout, this, &Private::activity_timeout); activityTimer->setSingleShot(true); refreshTimer = new QTimer(this); connect(refreshTimer, &QTimer::timeout, this, &Private::refresh_timeout); refreshTimer->start(REFRESH_INTERVAL); externalConnectionsMaxTimer = new QTimer(this); connect(externalConnectionsMaxTimer, &QTimer::timeout, this, &Private::externalConnectionsMax_timeout); externalConnectionsMaxTimer->start(EXTERNAL_CONNECTIONS_MAX_INTERVAL); setupConnectionBuckets(); setupSubscriptionBuckets(); prometheusMetrics += PrometheusMetric(PrometheusMetric::RequestReceived, "request_received", "counter", "Number of requests received"); prometheusMetrics += PrometheusMetric(PrometheusMetric::ConnectionConnected, "connection_connected", "gauge", "Number of concurrent connections"); prometheusMetrics += PrometheusMetric(PrometheusMetric::ConnectionMinute, "connection_minute", "counter", "Number of minutes clients have been connected"); prometheusMetrics += PrometheusMetric(PrometheusMetric::MessageReceived, "message_received", "counter", "Number of messages received by the publish API"); prometheusMetrics += PrometheusMetric(PrometheusMetric::MessageSent,"message_sent", "counter", "Number of messages sent to clients"); startTime = QDateTime::currentMSecsSinceEpoch(); connectionsMaxes.lastRefresh = startTime; } ~Private() { if(activityTimer) { activityTimer->disconnect(this); activityTimer->setParent(0); activityTimer->deleteLater(); activityTimer = 0; } if(reportTimer) { reportTimer->disconnect(this); reportTimer->setParent(0); reportTimer->deleteLater(); reportTimer = 0; } if(refreshTimer) { refreshTimer->disconnect(this); refreshTimer->setParent(0); refreshTimer->deleteLater(); refreshTimer = 0; } if(externalConnectionsMaxTimer) { externalConnectionsMaxTimer->disconnect(this); externalConnectionsMaxTimer->setParent(0); externalConnectionsMaxTimer->deleteLater(); externalConnectionsMaxTimer = 0; } qDeleteAll(connectionInfoById); QMutableHashIterator > it(externalConnectionInfoByFrom); while(it.hasNext()) { it.next(); qDeleteAll(it.value()); } qDeleteAll(subscriptionsByKey); qDeleteAll(reports); } bool setupSock() { delete sock; sock = new QZmq::Socket(QZmq::Socket::Pub, this); sock->setHwm(OUT_HWM); sock->setWriteQueueEnabled(false); sock->setShutdownWaitTime(0); QString errorMessage; if(!ZUtil::setupSocket(sock, spec, true, ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } return true; } bool setPrometheusPort(const QString &portStr) { prometheusServer = new SimpleHttpServer(8192, 8192, this); promServerConnection = prometheusServer->requestReady.connect(boost::bind(&Private::prometheus_requestReady, this)); if(portStr.startsWith("ipc://")) { if(!prometheusServer->listenLocal(portStr.mid(6))) { promServerConnection.disconnect(); delete prometheusServer; return false; } } else { QHostAddress addr; int port = -1; int pos = portStr.indexOf(':'); if(pos >= 0) { addr = QHostAddress(portStr.mid(0, pos)); port = portStr.mid(pos + 1).toInt(); } else { port = portStr.toInt(); } if(!prometheusServer->listen(addr, port)) { promServerConnection.disconnect(); delete prometheusServer; return false; } } return true; } void setupConnectionBuckets() { int shouldProcessTime = SHOULD_PROCESS_TIME(connectionTtl); QVector > newBuckets(qMax(shouldProcessTime / REFRESH_INTERVAL, 1)); // rebalance (NOTE: this algorithm is not optimal) int nextBucketIndex = 0; for(int n = 0; n < connectionInfoRefreshBuckets.count(); ++n) { foreach(ConnectionInfo *c, connectionInfoRefreshBuckets[n]) { newBuckets[nextBucketIndex++] += c; if(nextBucketIndex >= newBuckets.count()) nextBucketIndex = 0; } } connectionInfoRefreshBuckets = newBuckets; currentConnectionInfoRefreshBucket = 0; } void setupSubscriptionBuckets() { int shouldProcessTime = SHOULD_PROCESS_TIME(subscriptionTtl); QVector > newBuckets(qMax(shouldProcessTime / REFRESH_INTERVAL, 1)); // rebalance (NOTE: this algorithm is not optimal) int nextBucketIndex = 0; for(int n = 0; n < subscriptionRefreshBuckets.count(); ++n) { foreach(Subscription *s, subscriptionRefreshBuckets[n]) { newBuckets[nextBucketIndex++] += s; if(nextBucketIndex >= newBuckets.count()) nextBucketIndex = 0; } } subscriptionRefreshBuckets = newBuckets; currentSubscriptionRefreshBucket = 0; } void setupReportTimer() { if(reportInterval > 0 && !reportTimer) { reportTimer = new QTimer(this); connect(reportTimer, &QTimer::timeout, this, &Private::report_timeout); reportTimer->start(reportInterval); } else if(reportInterval <= 0 && reportTimer) { reportTimer->disconnect(this); reportTimer->setParent(0); reportTimer->deleteLater(); reportTimer = 0; } } int smallestConnectionInfoRefreshBucket() { int best = -1; int bestSize = 0; for(int n = 0; n < connectionInfoRefreshBuckets.count(); ++n) { if(best == -1 || connectionInfoRefreshBuckets[n].count() < bestSize) { best = n; bestSize = connectionInfoRefreshBuckets[n].count(); } } return best; } int smallestSubscriptionRefreshBucket() { int best = -1; int bestSize = 0; for(int n = 0; n < subscriptionRefreshBuckets.count(); ++n) { if(best == -1 || subscriptionRefreshBuckets[n].count() < bestSize) { best = n; bestSize = subscriptionRefreshBuckets[n].count(); } } return best; } void wheelAdd(qint64 timeoutTime, TimerBase *obj) { if(timeoutTime < startTime) { timeoutTime = startTime; } if(obj->timerId >= 0) { wheel.remove(obj->timerId); } int id = wheel.add(durationToTicksRoundUp(timeoutTime - startTime), (size_t)obj); assert(id >= 0); obj->timerId = id; } void wheelRemove(TimerBase *obj) { if(obj->timerId >= 0) { wheel.remove(obj->timerId); } obj->timerId = -1; } void insertConnection(ConnectionInfo *c) { connectionInfoById[c->id] = c; QSet &cs = connectionInfoByRoute[c->routeId]; cs += c; assert(c->lastRefresh >= 0); wheelAdd(c->lastRefresh + SHOULD_PROCESS_TIME(connectionTtl), c); c->refreshBucket = smallestConnectionInfoRefreshBucket(); connectionInfoRefreshBuckets[c->refreshBucket] += c; } void removeConnection(ConnectionInfo *c) { connectionInfoById.remove(c->id); if(connectionInfoByRoute.contains(c->routeId)) { QSet &cs = connectionInfoByRoute[c->routeId]; cs.remove(c); if(cs.isEmpty()) connectionInfoByRoute.remove(c->routeId); } if(c->retrySeq >= 0) { RetryInfo &ri = retryInfoBySource[c->from]; ri.connectionInfoBySeq.remove(c->retrySeq); // FIXME: we keep the source entry even when there are no more // connections, to avoid resetting the seq value. if there is // a lot of proxy instance churn, retryInfoBySource could // fill up with unused entries that will never be cleaned up. } if(c->lastRefresh >= 0) { wheelRemove(c); connectionInfoRefreshBuckets[c->refreshBucket].remove(c); } } void insertExternalConnection(ConnectionInfo *c) { QHash &extConnectionInfoById = externalConnectionInfoByFrom[c->from]; extConnectionInfoById[c->id] = c; QSet &cs = externalConnectionInfoByRoute[c->routeId]; cs += c; assert(c->lastActive >= 0); wheelAdd(c->lastActive + connectionTtl, c); assert(c->lastRefresh == -1); assert(c->refreshBucket == -1); } void removeExternalConnection(ConnectionInfo *c) { assert(externalConnectionInfoByFrom.contains(c->from)); QHash &extConnectionInfoById = externalConnectionInfoByFrom[c->from]; extConnectionInfoById.remove(c->id); if(extConnectionInfoById.isEmpty()) externalConnectionInfoByFrom.remove(c->from); if(externalConnectionInfoByRoute.contains(c->routeId)) { QSet &cs = externalConnectionInfoByRoute[c->routeId]; cs.remove(c); if(cs.isEmpty()) externalConnectionInfoByRoute.remove(c->routeId); } wheelRemove(c); } void removeLingeringConnections(const QByteArray &source, quint64 retrySeq) { if(!retryInfoBySource.contains(source)) return; RetryInfo &ri = retryInfoBySource[source]; // invalid retry seq if(retrySeq >= ri.nextSeq) return; QList toRemove; QMap::iterator it = ri.connectionInfoBySeq.find(retrySeq); while(it != ri.connectionInfoBySeq.end()) { toRemove += it.value(); if(it == ri.connectionInfoBySeq.begin()) break; --it; } foreach(ConnectionInfo *c, toRemove) { removeConnection(c); delete c; } } void insertSubscription(Subscription *s) { SubscriptionKey subKey(s->mode, s->channel); subscriptionsByKey[subKey] = s; assert(s->lastRefresh >= 0); wheelAdd(s->lastRefresh + SHOULD_PROCESS_TIME(subscriptionTtl), s); s->refreshBucket = smallestSubscriptionRefreshBucket(); subscriptionRefreshBuckets[s->refreshBucket] += s; } void removeSubscription(Subscription *s) { SubscriptionKey subKey(s->mode, s->channel); subscriptionsByKey.remove(subKey); if(s->lastRefresh >= 0) { wheelRemove(s); subscriptionRefreshBuckets[s->refreshBucket].remove(s); } } Report *getOrCreateReport(const QByteArray &routeId) { Report *report = reports.value(routeId); if(!report) { report = new Report; report->routeId = routeId; report->startTime = QDateTime::currentMSecsSinceEpoch(); reports[routeId] = report; } return report; } void removeReport(Report *report) { // subtract the current total from the combined report combinedReport.connectionsMax -= report->connectionsMax; reports.remove(report->routeId); } ConnectionsMax & getOrCreateConnectionsMax(const QByteArray &routeId) { if(!connectionsMaxes.maxes.contains(routeId)) connectionsMaxes.maxes.insert(routeId, ConnectionsMax()); return connectionsMaxes.maxes[routeId]; } void write(const StatsPacket &packet) { assert(sock); QByteArray prefix; if(packet.type == StatsPacket::Activity) prefix = "activity"; else if(packet.type == StatsPacket::Message) prefix = "message"; else if(packet.type == StatsPacket::Connected || packet.type == StatsPacket::Disconnected) prefix = "conn"; else if(packet.type == StatsPacket::Subscribed || packet.type == StatsPacket::Unsubscribed) prefix = "sub"; else if(packet.type == StatsPacket::Report) prefix = "report"; else if(packet.type == StatsPacket::Counts) prefix = "counts"; else // ConnectionsMax prefix = "conn-max"; QVariant vpacket = packet.toVariant(); QByteArray buf; if(outputFormat == TnetStringFormat) { buf = prefix + " T" + TnetString::fromVariant(vpacket); } else if(outputFormat == JsonFormat) { QJsonObject obj = QJsonObject::fromVariantHash(vpacket.toHash()); buf = prefix + " J" + QJsonDocument(obj).toJson(QJsonDocument::Compact); } if(!buf.isEmpty()) { if(log_outputLevel() >= LOG_LEVEL_DEBUG) log_debug("stats: OUT %s %s", prefix.data(), qPrintable(TnetString::variantToString(vpacket, -1))); sock->write(QList() << buf); } } void sendActivity(const QByteArray &routeId, quint32 count) { if(!sock) return; StatsPacket p; p.type = StatsPacket::Activity; p.from = instanceId; p.route = routeId; p.count = count; write(p); } void sendMessage(const QString &channel, const QString &itemId, const QString &transport, quint32 count, int blocks) { if(!sock) return; StatsPacket p; p.type = StatsPacket::Message; p.from = instanceId; p.channel = channel.toUtf8(); p.itemId = itemId.toUtf8(); p.count = count; p.blocks = blocks; p.transport = transport.toUtf8(); write(p); } void sendConnected(ConnectionInfo *c) { if(!sock || !connectionSend) return; StatsPacket p; p.type = StatsPacket::Connected; p.from = instanceId; p.route = c->routeId; p.connectionId = c->id; if(c->type == WebSocket) p.connectionType = StatsPacket::WebSocket; else p.connectionType = StatsPacket::Http; p.peerAddress = c->peerAddress; p.ssl = c->ssl; p.ttl = connectionTtl / 1000; write(p); } void sendDisconnected(ConnectionInfo *c) { if(!sock || !connectionSend) return; StatsPacket p; p.type = StatsPacket::Disconnected; p.from = instanceId; p.route = c->routeId; p.connectionId = c->id; write(p); } void sendSubscribed(Subscription *s) { if(!sock) return; StatsPacket p; p.type = StatsPacket::Subscribed; p.from = instanceId; p.mode = s->mode.toUtf8(); p.channel = s->channel.toUtf8(); p.ttl = subscriptionTtl / 1000; p.subscribers = s->subscriberCount; write(p); } void sendUnsubscribed(Subscription *s) { if(!sock) return; StatsPacket p; p.type = StatsPacket::Unsubscribed; p.from = instanceId; p.mode = s->mode.toUtf8(); p.channel = s->channel.toUtf8(); write(p); } void sendCounts(const Counts &counts) { if(!sock) return; StatsPacket p; p.type = StatsPacket::Counts; p.from = instanceId; p.requestsReceived = counts.requestsReceived; write(p); } void sendConnectionsMax(const QByteArray &routeId, ConnectionsMax *cm, qint64 now) { q->connMax(getConnMaxPacket(routeId, cm, now)); } void updateConnectionsMax(const QByteArray &routeId, qint64 now) { quint32 localConns = connectionInfoByRoute.value(routeId).count(); quint32 extConns = externalConnectionInfoByRoute.value(routeId).count(); quint32 extConnsMax = 0; if(externalConnectionsMaxes.contains(routeId)) extConnsMax = externalConnectionsMaxes[routeId].total(); quint32 conns = localConns + extConns + extConnsMax; if(connectionsMaxSend) { ConnectionsMax &cm = getOrCreateConnectionsMax(routeId); cm.currentValue = conns; cm.maxValue = qMax(cm.maxValue, conns); if(cm.valueToSend() != cm.lastSentValue) { connectionsMaxes.needSend.insert(routeId); if(!activityTimer->isActive()) activityTimer->start(ACTIVITY_TIMEOUT); } else { connectionsMaxes.needSend.remove(routeId); } } if(reportInterval > 0) { Report *report = getOrCreateReport(routeId); // subtract the current total from the combined report combinedReport.connectionsMax -= report->connectionsMax; // update the individual report if(report->connectionsMaxStale) { report->connectionsMax = conns; report->connectionsMaxStale = false; } else report->connectionsMax = qMax(report->connectionsMax, conns); report->lastUpdate = now; // add the new total to the combined report combinedReport.connectionsMax += report->connectionsMax; combinedReport.lastUpdate = now; } } void updateConnectionsMinutes(ConnectionInfo *c, qint64 now) { // ignore lingering connections if(c->linger) return; Report *report = getOrCreateReport(c->routeId); int mins = (now - c->lastReport) / 60000; if(mins > 0) { // only advance as much as we've read c->lastReport += mins * 60000; report->addConnectionsMinutes(mins, now); combinedReport.addConnectionsMinutes(mins, now); } } void handleExpirations(qint64 now) { QList refreshedConnIds; QSet routesUpdated; for(int i = 0; i < EXPIRE_MAX; ++i) { TimerWheel::Expired expired = wheel.takeExpired(); if(expired.key < 0) { break; } TimerBase *obj = (TimerBase *)expired.userData; obj->timerId = -1; switch(obj->timerType) { case TimerBase::Type::Connection: { ConnectionInfo *c = static_cast(obj); if(c->linger) { // in linger mode, next refresh is set to the time we should // delete the connection rather than refresh connectionInfoRefreshBuckets[c->refreshBucket].remove(c); c->lastRefresh = -1; // note: we don't send a disconnect message when the // linger expires. the assumption is that the handler // owns the connection now removeConnection(c); delete c; } else { c->lastRefresh = now; wheelAdd(c->lastRefresh + SHOULD_PROCESS_TIME(connectionTtl), c); refreshedConnIds += c->id; updateConnectionsMinutes(c, now); sendConnected(c); } break; } case TimerBase::Type::ExternalConnection: { ConnectionInfo *c = static_cast(obj); routesUpdated += c->routeId; updateConnectionsMinutes(c, now); removeExternalConnection(c); delete c; break; } case TimerBase::Type::Subscription: { Subscription *s = static_cast(obj); if(s->linger) { // in linger mode, next refresh is set to the time we should // delete the subscription rather than refresh subscriptionRefreshBuckets[s->refreshBucket].remove(s); s->lastRefresh = -1; QString mode = s->mode; QString channel = s->channel; sendUnsubscribed(s); removeSubscription(s); delete s; q->unsubscribed(mode, channel); } else { s->lastRefresh = now; wheelAdd(s->lastRefresh + SHOULD_PROCESS_TIME(subscriptionTtl), s); sendSubscribed(s); } break; } } } if(!refreshedConnIds.isEmpty()) q->connectionsRefreshed(refreshedConnIds); foreach(const QByteArray &routeId, routesUpdated) updateConnectionsMax(routeId, now); } void refreshConnections(qint64 now) { QList refreshedIds; // process the current bucket const QSet &bucket = connectionInfoRefreshBuckets[currentConnectionInfoRefreshBucket]; foreach(ConnectionInfo *c, bucket) { // don't bucket-process lingered connections if(c->linger) continue; c->lastRefresh = now; wheelAdd(c->lastRefresh + SHOULD_PROCESS_TIME(connectionTtl), c); refreshedIds += c->id; updateConnectionsMinutes(c, now); sendConnected(c); } if(!refreshedIds.isEmpty()) q->connectionsRefreshed(refreshedIds); ++currentConnectionInfoRefreshBucket; if(currentConnectionInfoRefreshBucket >= connectionInfoRefreshBuckets.count()) currentConnectionInfoRefreshBucket = 0; } void refreshSubscriptions(qint64 now) { // process the current bucket const QSet &bucket = subscriptionRefreshBuckets[currentSubscriptionRefreshBucket]; foreach(Subscription *s, bucket) { // don't bucket-process lingered subscriptions if(s->linger) continue; s->lastRefresh = now; wheelAdd(s->lastRefresh + SHOULD_PROCESS_TIME(subscriptionTtl), s); sendSubscribed(s); } ++currentSubscriptionRefreshBucket; if(currentSubscriptionRefreshBucket >= subscriptionRefreshBuckets.count()) currentSubscriptionRefreshBucket = 0; } void refreshConnectionsMaxes(qint64 now) { if(now < connectionsMaxes.lastRefresh + (connectionsMaxTtl * 3 / 4)) return; connectionsMaxes.lastRefresh = now; QList toRemove; QMutableHashIterator it(connectionsMaxes.maxes); while(it.hasNext()) { it.next(); const QByteArray &routeId = it.key(); ConnectionsMax &cm = it.value(); if(cm.valueToSend() == 0) { if(cm.lastSentTime >= 0 && now >= cm.lastSentTime + connectionsMaxTtl) toRemove += routeId; continue; } sendConnectionsMax(routeId, &cm, now); } foreach(const QByteArray &routeId, toRemove) connectionsMaxes.maxes.remove(routeId); } void expireExternalConnectionsMaxes(qint64 now) { QMutableHashIterator it(externalConnectionsMaxes); while(it.hasNext()) { it.next(); QHash &maxesForRoute = it.value().maxes; QMutableHashIterator rit(maxesForRoute); while(rit.hasNext()) { rit.next(); ExternalConnectionsMax &cm = rit.value(); if(now >= cm.expires) rit.remove(); } if(maxesForRoute.isEmpty()) it.remove(); } } void mergeExternalConnectionsMax(const StatsPacket &packet, qint64 now) { if(packet.retrySeq >= 0) removeLingeringConnections(packet.from, (quint64)packet.retrySeq); QHash &maxes = externalConnectionsMaxes[packet.route].maxes; if(!maxes.contains(packet.from)) maxes.insert(packet.from, ExternalConnectionsMax()); ExternalConnectionsMax &cm = maxes[packet.from]; cm.value = (quint32)qMax(packet.connectionsMax, 0); cm.expires = now + (qMax(packet.ttl, 0) * 1000); updateConnectionsMax(packet.route, now); } void mergeExternalReport(const StatsPacket &packet, bool includeConnections) { if(reportInterval <= 0) return; Report *report = getOrCreateReport(packet.route); Stats::Counters counters; counters.inc(Stats::ClientHeaderBytesReceived, qMax(packet.clientHeaderBytesReceived, 0)); counters.inc(Stats::ClientHeaderBytesSent, qMax(packet.clientHeaderBytesSent, 0)); counters.inc(Stats::ClientContentBytesReceived, qMax(packet.clientContentBytesReceived, 0)); counters.inc(Stats::ClientContentBytesSent, qMax(packet.clientContentBytesSent, 0)); counters.inc(Stats::ClientMessagesReceived, qMax(packet.clientMessagesReceived, 0)); counters.inc(Stats::ClientMessagesSent, qMax(packet.clientMessagesSent, 0)); counters.inc(Stats::ServerHeaderBytesReceived, qMax(packet.serverHeaderBytesReceived, 0)); counters.inc(Stats::ServerHeaderBytesSent, qMax(packet.serverHeaderBytesSent, 0)); counters.inc(Stats::ServerContentBytesReceived, qMax(packet.serverContentBytesReceived, 0)); counters.inc(Stats::ServerContentBytesSent, qMax(packet.serverContentBytesSent, 0)); counters.inc(Stats::ServerMessagesReceived, qMax(packet.serverMessagesReceived, 0)); counters.inc(Stats::ServerMessagesSent, qMax(packet.serverMessagesSent, 0)); qint64 now = QDateTime::currentMSecsSinceEpoch(); report->addCounters(counters, now); combinedReport.addCounters(counters, now); if(includeConnections) { if(!report->externalReports.contains(packet.from)) report->externalReports[packet.from] = Report(); Report &r = report->externalReports[packet.from]; r.connectionsMinutes += qMax(packet.connectionsMinutes, 0); } } StatsPacket reportToPacket(Report *report, const QByteArray &routeId, qint64 now) { if(report->connectionsMaxStale) updateConnectionsMax(routeId, now); StatsPacket p; p.type = StatsPacket::Report; p.from = instanceId; p.route = routeId; p.connectionsMax = report->connectionsMax; p.connectionsMinutes = report->connectionsMinutes + report->externalConnectionsMinutes(); p.messagesReceived = report->messagesReceived; p.messagesSent = report->messagesSent; p.httpResponseMessagesSent = report->httpResponseMessagesSent; p.blocksReceived = report->blocksReceived; p.blocksSent = report->blocksSent; p.duration = now - report->startTime; p.clientHeaderBytesReceived = report->counters.get(Stats::ClientHeaderBytesReceived); p.clientHeaderBytesSent = report->counters.get(Stats::ClientHeaderBytesSent); p.clientContentBytesReceived = report->counters.get(Stats::ClientContentBytesReceived); p.clientContentBytesSent = report->counters.get(Stats::ClientContentBytesSent); p.clientMessagesReceived = report->counters.get(Stats::ClientMessagesReceived); p.clientMessagesSent = report->counters.get(Stats::ClientMessagesSent); p.serverHeaderBytesReceived = report->counters.get(Stats::ServerHeaderBytesReceived); p.serverHeaderBytesSent = report->counters.get(Stats::ServerHeaderBytesSent); p.serverContentBytesReceived = report->counters.get(Stats::ServerContentBytesReceived); p.serverContentBytesSent = report->counters.get(Stats::ServerContentBytesSent); p.serverMessagesReceived = report->counters.get(Stats::ServerMessagesReceived); p.serverMessagesSent = report->counters.get(Stats::ServerMessagesSent); report->startTime = now; report->connectionsMaxStale = true; report->connectionsMinutes = 0; report->messagesReceived = 0; report->messagesSent = 0; report->httpResponseMessagesSent = 0; report->blocksReceived = -1; report->blocksSent = -1; report->counters.reset(); report->externalReports.clear(); return p; } void flushReport(const QByteArray &routeId) { Report *report = getOrCreateReport(routeId); qint64 now = QDateTime::currentMSecsSinceEpoch(); StatsPacket p = reportToPacket(report, routeId, now); if(report->isEmpty()) { removeReport(report); delete report; } if(sock) write(p); q->reported(QList() << p); } StatsPacket getConnMaxPacket(const QByteArray &routeId, ConnectionsMax *cm, qint64 now) { quint32 value = cm->valueToSend(); StatsPacket p; p.type = StatsPacket::ConnectionsMax; p.from = instanceId; p.route = routeId; p.connectionsMax = value; p.ttl = connectionsMaxTtl / 1000; p.retrySeq = cm->retrySeq; cm->lastSentValue = value; cm->lastSentTime = now; cm->maxValue = cm->currentValue; return p; } private slots: void activity_timeout() { QHashIterator it(routeActivity); while(it.hasNext()) { it.next(); sendActivity(it.key(), it.value()); } routeActivity.clear(); if(!combinedCounts.isEmpty()) { sendCounts(combinedCounts); combinedCounts = Counts(); } qint64 now = QDateTime::currentMSecsSinceEpoch(); QSet needSendNext; foreach(const QByteArray &routeId, connectionsMaxes.needSend) { ConnectionsMax &cm = connectionsMaxes.maxes[routeId]; if(cm.valueToSend() == cm.lastSentValue) continue; sendConnectionsMax(routeId, &cm, now); if(cm.valueToSend() != cm.lastSentValue) needSendNext.insert(routeId); } connectionsMaxes.needSend = needSendNext; if(!connectionsMaxes.needSend.isEmpty() && !activityTimer->isActive()) activityTimer->start(ACTIVITY_TIMEOUT); } void report_timeout() { qint64 now = QDateTime::currentMSecsSinceEpoch(); QList reportPackets; QList toDelete; // note: here we iterate over all reports, which will be one per // route ID. this could become a problem if there are thousands // of route IDs (at which point we can consider bucketing) QHashIterator it(reports); while(it.hasNext()) { it.next(); const QByteArray &routeId = it.key(); Report *report = it.value(); StatsPacket p = reportToPacket(report, routeId, now); if(report->isEmpty()) { // if report is empty, we can throw it out after sending toDelete += report; } if(sock) write(p); reportPackets += p; } foreach(Report *report, toDelete) { removeReport(report); delete report; } if(!reportPackets.isEmpty()) q->reported(reportPackets); } void refresh_timeout() { qint64 currentTime = QDateTime::currentMSecsSinceEpoch(); // time must go forward if(currentTime > startTime) { quint64 currentTicks = (quint64)durationToTicksRoundDown(currentTime - startTime); wheel.update(currentTicks); } handleExpirations(currentTime); refreshConnections(currentTime); refreshSubscriptions(currentTime); refreshConnectionsMaxes(currentTime); } void externalConnectionsMax_timeout() { qint64 currentTime = QDateTime::currentMSecsSinceEpoch(); expireExternalConnectionsMaxes(currentTime); } private: void prometheus_requestReady() { SimpleHttpRequest *req = prometheusServer->takeNext(); QString data; foreach(const PrometheusMetric &m, prometheusMetrics) { QVariant value; switch(m.mtype) { case PrometheusMetric::RequestReceived: value = QVariant(combinedReport.requestsReceived); break; case PrometheusMetric::ConnectionConnected: value = QVariant(combinedReport.connectionsMax); break; case PrometheusMetric::ConnectionMinute: value = QVariant(combinedReport.connectionsMinutes); break; case PrometheusMetric::MessageReceived: value = QVariant(combinedReport.messagesReceived); break; case PrometheusMetric::MessageSent: value = QVariant(combinedReport.messagesSent); break; } if(value.isNull()) continue; data += QString( "# HELP %1%2 %3\n" "# TYPE %4%5 %6\n" "%7%8 %9\n" ).arg(prometheusPrefix, m.name, m.help, prometheusPrefix, m.name, m.type, prometheusPrefix, m.name, value.toString()); } req->finished.connect(boost::bind(&SimpleHttpRequest::deleteLater, req)); HttpHeaders headers; headers += HttpHeader("Content-Type", "text/plain"); req->respond(200, "OK", headers, data.toUtf8()); } }; StatsManager::StatsManager(int connectionsMax, int subscriptionsMax, QObject *parent) : QObject(parent) { d = new Private(this, connectionsMax, subscriptionsMax); } StatsManager::~StatsManager() { delete d; } void StatsManager::setInstanceId(const QByteArray &instanceId) { d->instanceId = instanceId; } void StatsManager::setIpcFileMode(int mode) { d->ipcFileMode = mode; } bool StatsManager::setSpec(const QString &spec) { d->spec = spec; return d->setupSock(); } bool StatsManager::connectionSendEnabled() const { return d->connectionSend; } void StatsManager::setConnectionSendEnabled(bool enabled) { d->connectionSend = enabled; } void StatsManager::setConnectionsMaxSendEnabled(bool enabled) { d->connectionsMaxSend = enabled; } void StatsManager::setConnectionTtl(int secs) { d->connectionTtl = secs * 1000; d->setupConnectionBuckets(); } void StatsManager::setConnectionsMaxTtl(int secs) { d->connectionsMaxTtl = secs * 1000; } void StatsManager::setSubscriptionTtl(int secs) { d->subscriptionTtl = secs * 1000; d->setupSubscriptionBuckets(); } void StatsManager::setSubscriptionLinger(int secs) { d->subscriptionLinger = secs * 1000; } void StatsManager::setReportInterval(int secs) { d->reportInterval = secs * 1000; d->setupReportTimer(); } void StatsManager::setOutputFormat(Format format) { d->outputFormat = format; } bool StatsManager::setPrometheusPort(const QString &port) { return d->setPrometheusPort(port); } void StatsManager::setPrometheusPrefix(const QString &prefix) { d->prometheusPrefix = prefix; } void StatsManager::addActivity(const QByteArray &routeId, quint32 count) { if(d->routeActivity.contains(routeId)) d->routeActivity[routeId] += count; else d->routeActivity[routeId] = count; if(!d->activityTimer->isActive()) d->activityTimer->start(ACTIVITY_TIMEOUT); } void StatsManager::addMessage(const QString &channel, const QString &itemId, const QString &transport, quint32 count, int blocks) { d->sendMessage(channel, itemId, transport, count, blocks); } void StatsManager::addConnection(const QByteArray &id, const QByteArray &routeId, ConnectionType type, const QHostAddress &peerAddress, bool ssl, bool quiet, int reportOffset) { qint64 now = QDateTime::currentMSecsSinceEpoch(); bool replacing = false; qint64 lastReport = now; if(reportOffset >= 0) lastReport -= reportOffset; if(d->reportInterval > 0) { // check if this connection should replace a lingering external one // note: this iterates over all the known external sources, which at // at the time of this writing is almost certainly just 1 (a single // pushpin-proxy source). QHashIterator > it(d->externalConnectionInfoByFrom); while(it.hasNext()) { it.next(); const QHash &extConnectionInfoById = it.value(); Private::ConnectionInfo *other = extConnectionInfoById.value(id); if(other) { replacing = true; lastReport = other->lastReport; d->removeExternalConnection(other); delete other; break; } } } // if we already had an entry, silently overwrite it. this can // happen if we sent an accepted connection off to the handler, // kept it lingering in our table, and then the handler passed // it back to us for retrying Private::ConnectionInfo *c = d->connectionInfoById.value(id); if(c) { replacing = true; lastReport = c->lastReport; d->removeConnection(c); delete c; } c = new Private::ConnectionInfo; c->timerType = Private::TimerBase::Type::Connection; c->id = id; c->routeId = routeId; c->type = type; c->peerAddress = peerAddress; c->ssl = ssl; c->lastRefresh = now; c->lastReport = lastReport; d->insertConnection(c); d->updateConnectionsMax(c->routeId, now); if(d->reportInterval > 0) { // only immediately count a minute if an offset wasn't set and we weren't replacing if(reportOffset < 0 && !replacing) { Private::Report *report = d->getOrCreateReport(c->routeId); // minutes are rounded up so count one immediately report->addConnectionsMinutes(1, now); d->combinedReport.addConnectionsMinutes(1, now); } } if(!quiet) d->sendConnected(c); } int StatsManager::removeConnection(const QByteArray &id, bool linger, const QByteArray &source) { Private::ConnectionInfo *c = d->connectionInfoById.value(id); if(!c) return 0; qint64 now = QDateTime::currentMSecsSinceEpoch(); QByteArray routeId = c->routeId; int unreportedTime = 0; if(d->reportInterval > 0) d->updateConnectionsMinutes(c, now); if(linger) { if(!c->linger) { c->linger = true; if(!d->retryInfoBySource.contains(source)) d->retryInfoBySource.insert(source, Private::RetryInfo()); Private::RetryInfo &ri = d->retryInfoBySource[source]; c->from = source; c->retrySeq = (qint64)ri.nextSeq++; ri.connectionInfoBySeq.insert((quint64)c->retrySeq, c); // hack to ensure full linger time honored by refresh processing qint64 lingerStartTime = now + (d->connectionLinger - SHOULD_PROCESS_TIME(d->connectionTtl)); c->lastRefresh = lingerStartTime; d->wheelAdd(c->lastRefresh + SHOULD_PROCESS_TIME(d->connectionTtl), c); } } else { if(now >= c->lastReport) unreportedTime = now - c->lastReport; d->sendDisconnected(c); d->removeConnection(c); delete c; } d->updateConnectionsMax(routeId, now); return unreportedTime; } void StatsManager::refreshConnection(const QByteArray &id) { Private::ConnectionInfo *c = d->connectionInfoById.value(id); if(!c) return; d->sendConnected(c); } void StatsManager::addSubscription(const QString &mode, const QString &channel, quint32 subscriberCount) { Private::SubscriptionKey subKey(mode, channel); Private::Subscription *s = d->subscriptionsByKey.value(subKey); if(!s) { qint64 now = QDateTime::currentMSecsSinceEpoch(); // add the subscription if we didn't have it s = new Private::Subscription; s->timerType = Private::TimerBase::Type::Subscription; s->mode = mode; s->channel = channel; s->subscriberCount = subscriberCount; s->lastRefresh = now; d->insertSubscription(s); d->sendSubscribed(s); } else { quint32 oldSubscriberCount = s->subscriberCount; s->subscriberCount = subscriberCount; if(s->linger) { qint64 now = QDateTime::currentMSecsSinceEpoch(); // if this was a lingering subscription, return it to normal s->linger = false; s->lastRefresh = now; d->wheelAdd(s->lastRefresh + SHOULD_PROCESS_TIME(d->subscriptionTtl), s); d->sendSubscribed(s); } else if(s->subscriberCount != oldSubscriberCount) { qint64 now = QDateTime::currentMSecsSinceEpoch(); // process soon s->lastRefresh = now - SHOULD_PROCESS_TIME(d->subscriptionTtl); d->wheelAdd(s->lastRefresh + SHOULD_PROCESS_TIME(d->subscriptionTtl), s); } } } void StatsManager::removeSubscription(const QString &mode, const QString &channel, bool linger) { Private::SubscriptionKey subKey(mode, channel); Private::Subscription *s = d->subscriptionsByKey.value(subKey); if(!s) return; if(linger) { if(!s->linger) { qint64 now = QDateTime::currentMSecsSinceEpoch(); s->linger = true; // hack to ensure full linger time honored by refresh processing qint64 lingerStartTime = now + (d->subscriptionLinger - SHOULD_PROCESS_TIME(d->subscriptionTtl)); s->lastRefresh = lingerStartTime; d->wheelAdd(s->lastRefresh + SHOULD_PROCESS_TIME(d->subscriptionTtl), s); } } else { d->sendUnsubscribed(s); d->removeSubscription(s); delete s; unsubscribed(mode, channel); } } void StatsManager::addMessageReceived(const QByteArray &routeId, int blocks) { if(d->reportInterval <= 0) return; Private::Report *report = d->getOrCreateReport(routeId); qint64 now = QDateTime::currentMSecsSinceEpoch(); report->addMessageReceived(blocks, now); d->combinedReport.addMessageReceived(blocks, now); } void StatsManager::addMessageSent(const QByteArray &routeId, const QString &transport, int blocks) { if(d->reportInterval <= 0) return; Private::Report *report = d->getOrCreateReport(routeId); qint64 now = QDateTime::currentMSecsSinceEpoch(); report->addMessageSent(transport, blocks, now); d->combinedReport.addMessageSent(transport, blocks, now); } void StatsManager::incCounter(const QByteArray &routeId, Stats::Counter c, quint32 count) { if(d->reportInterval <= 0) return; Private::Report *report = d->getOrCreateReport(routeId); qint64 now = QDateTime::currentMSecsSinceEpoch(); report->incCounter(c, count, now); d->combinedReport.incCounter(c, count, now); } void StatsManager::addRequestsReceived(quint32 count) { qint64 now = QDateTime::currentMSecsSinceEpoch(); d->combinedCounts.requestsReceived += count; d->combinedReport.addRequestsReceived(count, now); if(!d->activityTimer->isActive()) d->activityTimer->start(ACTIVITY_TIMEOUT); } bool StatsManager::checkConnection(const QByteArray &id) const { return d->connectionInfoById.contains(id); } bool StatsManager::processExternalPacket(const StatsPacket &packet, bool mergeConnectionReport) { if(d->reportInterval <= 0) return false; if(packet.type == StatsPacket::Connected || packet.type == StatsPacket::Disconnected) { qint64 now = QDateTime::currentMSecsSinceEpoch(); bool replacing = false; qint64 lastReport = now; if(packet.type == StatsPacket::Connected) { // is there a local connection with the same ID? Private::ConnectionInfo *c = d->connectionInfoById.value(packet.connectionId); if(c) { // if there is a non-lingering local connection, ignore the packet if(!c->linger) { return false; } // otherwise, remove local connection and it will be replaced with external replacing = true; lastReport = c->lastReport; d->removeConnection(c); delete c; } } // if the connection exists under a different from address, remove it. // note: this iterates over all the known external sources, which at // at the time of this writing is almost certainly just 1 (a single // pushpin-proxy source). QList toDelete; QHashIterator > it(d->externalConnectionInfoByFrom); while(it.hasNext()) { it.next(); const QByteArray &from = it.key(); if(from == packet.from) continue; const QHash &extConnectionInfoById = it.value(); Private::ConnectionInfo *c = extConnectionInfoById.value(packet.connectionId); if(c) toDelete += c; } foreach(Private::ConnectionInfo *c, toDelete) { d->removeExternalConnection(c); delete c; } QHash &extConnectionInfoById = d->externalConnectionInfoByFrom[packet.from]; if(packet.type == StatsPacket::Connected) { // add/update Private::ConnectionInfo *c = extConnectionInfoById.value(packet.connectionId); if(!c) { c = new Private::ConnectionInfo; c->timerType = Private::TimerBase::Type::ExternalConnection; c->id = packet.connectionId; c->routeId = packet.route; c->type = packet.connectionType == StatsPacket::Http ? Http : WebSocket; c->peerAddress = packet.peerAddress; c->ssl = packet.ssl; c->lastReport = lastReport; c->from = packet.from; c->lastActive = now; d->insertExternalConnection(c); d->updateConnectionsMax(c->routeId, now); // only count a minute if we weren't replacing if(!replacing) { Private::Report *report = d->getOrCreateReport(c->routeId); // minutes are rounded up so count one immediately report->addConnectionsMinutes(1, now); d->combinedReport.addConnectionsMinutes(1, now); } } else { c->ttl = packet.ttl; c->lastActive = now; d->wheelAdd(c->lastActive + d->connectionTtl, c); } d->updateConnectionsMinutes(c, now); } else // Disconnected { Private::ConnectionInfo *c = extConnectionInfoById.value(packet.connectionId); if(c) { QByteArray routeId = c->routeId; d->updateConnectionsMinutes(c, now); d->removeExternalConnection(c); delete c; d->updateConnectionsMax(routeId, now); } } return replacing; } else if(packet.type == StatsPacket::ConnectionsMax) { qint64 now = QDateTime::currentMSecsSinceEpoch(); d->mergeExternalConnectionsMax(packet, now); return true; } else if(packet.type == StatsPacket::Report) { d->mergeExternalReport(packet, mergeConnectionReport); return true; } return false; } void StatsManager::sendPacket(const StatsPacket &packet) { if(!d->sock) return; StatsPacket p = packet; p.from = d->instanceId; d->write(p); } void StatsManager::flushReport(const QByteArray &routeId) { d->flushReport(routeId); } qint64 StatsManager::lastRetrySeq(const QByteArray &source) const { if(!d->retryInfoBySource.contains(source)) return -1; Private::RetryInfo &ri = d->retryInfoBySource[source]; return ((qint64)ri.nextSeq) - 1; } StatsPacket StatsManager::getConnMaxPacket(const QByteArray &routeId) { Private::ConnectionsMax &cm = d->getOrCreateConnectionsMax(routeId); qint64 now = QDateTime::currentMSecsSinceEpoch(); return d->getConnMaxPacket(routeId, &cm, now); } void StatsManager::setRetrySeq(const QByteArray &routeId, int value) { Private::ConnectionsMax &cm = d->getOrCreateConnectionsMax(routeId); cm.retrySeq = value; } #include "statsmanager.moc" pushpin-1.39.1/src/cpp/statsmanager.h000066400000000000000000000076231457610542000175220ustar00rootroot00000000000000/* * Copyright (C) 2014-2023 Fanout, Inc. * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef STATSMANAGER_H #define STATSMANAGER_H #include #include "packet/statspacket.h" #include "stats.h" #include class QHostAddress; class StatsManager : public QObject { Q_OBJECT public: enum ConnectionType { Http, WebSocket }; enum Format { TnetStringFormat, JsonFormat }; StatsManager(int connectionsMax, int subscriptionsMax, QObject *parent = 0); ~StatsManager(); bool connectionSendEnabled() const; void setInstanceId(const QByteArray &instanceId); void setIpcFileMode(int mode); bool setSpec(const QString &spec); void setConnectionSendEnabled(bool enabled); void setConnectionsMaxSendEnabled(bool enabled); void setConnectionTtl(int secs); void setConnectionsMaxTtl(int secs); void setSubscriptionTtl(int secs); void setSubscriptionLinger(int secs); void setReportInterval(int secs); void setOutputFormat(Format format); bool setPrometheusPort(const QString &port); void setPrometheusPrefix(const QString &prefix); // routeId may be empty for non-identified route void addActivity(const QByteArray &routeId, quint32 count = 1); void addMessage(const QString &channel, const QString &itemId, const QString &transport, quint32 count = 1, int blocks = -1); void addConnection(const QByteArray &id, const QByteArray &routeId, ConnectionType type, const QHostAddress &peerAddress, bool ssl, bool quiet, int reportOffset = -1); int removeConnection(const QByteArray &id, bool linger, const QByteArray &source = QByteArray()); // return unreported time // manager automatically refreshes, but it may be useful to force a // send before removing with linger void refreshConnection(const QByteArray &id); void addSubscription(const QString &mode, const QString &channel, quint32 subscriberCount); // NOTE: may emit unsubscribed immediately (not DOR-DS) void removeSubscription(const QString &mode, const QString &channel, bool linger); // for reporting and combined void addMessageReceived(const QByteArray &routeId, int blocks = -1); void addMessageSent(const QByteArray &routeId, const QString &transport, int blocks = -1); void incCounter(const QByteArray &routeId, Stats::Counter c, quint32 count = 1); // for combined only void addRequestsReceived(quint32 count); bool checkConnection(const QByteArray &id) const; // conn, conn-max, and report packets received from the proxy should be // passed into this method. returns true if the packet should not also be // forwarded on bool processExternalPacket(const StatsPacket &packet, bool mergeConnectionReport); // directly send, for proxy->handler passthrough void sendPacket(const StatsPacket &packet); void flushReport(const QByteArray &routeId); qint64 lastRetrySeq(const QByteArray &source) const; StatsPacket getConnMaxPacket(const QByteArray &routeId); void setRetrySeq(const QByteArray &routeId, int value); boost::signals2::signal&)> connectionsRefreshed; boost::signals2::signal unsubscribed; boost::signals2::signal&)> reported; boost::signals2::signal connMax; private: class Private; friend class Private; Private *d; }; #endif pushpin-1.39.1/src/cpp/statusreasons.cpp000066400000000000000000000063051457610542000202760ustar00rootroot00000000000000/* * Copyright (C) 2015 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "statusreasons.h" #include namespace StatusReasons { // IANA assignments // http://www.iana.org/assignments/http-status-codes/http-status-codes.xml const char *getReasonRaw(int code) { switch(code) { case 100: return "Continue"; case 101: return "Switching Protocols"; case 102: return "Processing"; case 200: return "OK"; case 201: return "Created"; case 202: return "Accepted"; case 203: return "Non-Authoritative Information"; case 204: return "No Content"; case 205: return "Reset Content"; case 206: return "Partial Content"; case 207: return "Multi-Status"; case 208: return "Already Reported"; case 226: return "IM Used"; case 300: return "Multiple Choices"; case 301: return "Moved Permanently"; case 302: return "Found"; case 303: return "See Other"; case 304: return "Not Modified"; case 305: return "Use Proxy"; case 306: return "Reserved"; case 307: return "Temporary Redirect"; case 308: return "Permanent Redirect"; case 400: return "Bad Request"; case 401: return "Unauthorized"; case 402: return "Payment Required"; case 403: return "Forbidden"; case 404: return "Not Found"; case 405: return "Method Not Allowed"; case 406: return "Not Acceptable"; case 407: return "Proxy Authentication Required"; case 408: return "Request Timeout"; case 409: return "Conflict"; case 410: return "Gone"; case 411: return "Length Required"; case 412: return "Precondition Failed"; case 413: return "Request Entity Too Large"; case 414: return "Request-URI Too Long"; case 415: return "Unsupported Media Type"; case 416: return "Requested Range Not Satisfiable"; case 417: return "Expectation Failed"; case 422: return "Unprocessable Entity"; case 423: return "Locked"; case 424: return "Failed Dependency"; case 426: return "Upgrade Required"; case 428: return "Precondition Required"; case 429: return "Too Many Requests"; case 431: return "Request Header Fields Too Large"; case 500: return "Internal Server Error"; case 501: return "Not Implemented"; case 502: return "Bad Gateway"; case 503: return "Service Unavailable"; case 504: return "Gateway Timeout"; case 505: return "HTTP Version Not Supported"; case 506: return "Variant Also Negotiates"; case 507: return "Insufficient Storage"; case 508: return "Loop Detected"; case 510: return "Not Extended"; case 511: return "Network Authentication Required"; default: return "Undefined Reason"; } } QByteArray getReason(int code) { return getReasonRaw(code); } } pushpin-1.39.1/src/cpp/statusreasons.h000066400000000000000000000015071457610542000177420ustar00rootroot00000000000000/* * Copyright (C) 2015 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef STATUSREASONS_H #define STATUSREASONS_H class QByteArray; namespace StatusReasons { QByteArray getReason(int code); } #endif pushpin-1.39.1/src/cpp/tests/000077500000000000000000000000001457610542000160125ustar00rootroot00000000000000pushpin-1.39.1/src/cpp/tests/handlerenginetest.cpp000066400000000000000000000544421457610542000222320ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include #include #include "qzmqsocket.h" #include "qzmqvalve.h" #include "qzmqreqmessage.h" #include "qtcompat.h" #include "log.h" #include "tnetstring.h" #include "zhttprequestpacket.h" #include "zhttpresponsepacket.h" #include "packet/httpresponsedata.h" #include "rtimer.h" #include "handlerengine.h" namespace { class Wrapper : public QObject { Q_OBJECT public: QZmq::Socket *zhttpClientOutStreamSock; QZmq::Socket *zhttpClientInSock; QZmq::Valve *zhttpClientInValve; QZmq::Socket *zhttpServerInSock; QZmq::Valve *zhttpServerInValve; QZmq::Socket *zhttpServerInStreamSock; QZmq::Valve *zhttpServerInStreamValve; QZmq::Socket *zhttpServerOutSock; QZmq::Socket *proxyAcceptSock; QZmq::Valve *proxyAcceptValve; QZmq::Socket *publishPushSock; QDir workDir; bool acceptSuccess; QVariantHash acceptValue; bool finished; QHash responses; int serverReqs; bool serverFailed; int serverOutSeq; QByteArray requestBody; Connection zhttpClientInValveConnection; Connection zhttpServerInValveConnection; Connection zhttpServerInStreamValveConnection; Connection proxyAcceptValveConnection; Wrapper(QObject *parent, QDir _workDir) : QObject(parent), workDir(_workDir), acceptSuccess(false), finished(false), serverReqs(0), serverFailed(false), serverOutSeq(0) { // http sockets zhttpClientOutStreamSock = new QZmq::Socket(QZmq::Socket::Router, this); zhttpClientInSock = new QZmq::Socket(QZmq::Socket::Sub, this); zhttpClientInValve = new QZmq::Valve(zhttpClientInSock, this); zhttpClientInValveConnection = zhttpClientInValve->readyRead.connect(boost::bind(&Wrapper::zhttpClientIn_readyRead, this, boost::placeholders::_1)); zhttpServerInSock = new QZmq::Socket(QZmq::Socket::Pull, this); zhttpServerInValve = new QZmq::Valve(zhttpServerInSock, this); zhttpServerInValveConnection = zhttpServerInValve->readyRead.connect(boost::bind(&Wrapper::zhttpServerIn_readyRead, this, boost::placeholders::_1)); zhttpServerInStreamSock = new QZmq::Socket(QZmq::Socket::Router, this); zhttpServerInStreamSock->setIdentity("test-server"); zhttpServerInStreamValve = new QZmq::Valve(zhttpServerInStreamSock, this); zhttpServerInStreamValveConnection = zhttpServerInStreamValve->readyRead.connect(boost::bind(&Wrapper::zhttpServerInStream_readyRead, this, boost::placeholders::_1)); zhttpServerOutSock = new QZmq::Socket(QZmq::Socket::Pub, this); // proxy sockets proxyAcceptSock = new QZmq::Socket(QZmq::Socket::Dealer, this); proxyAcceptValve = new QZmq::Valve(proxyAcceptSock, this); proxyAcceptValveConnection = proxyAcceptValve->readyRead.connect(boost::bind(&Wrapper::proxyAccept_readyRead, this, boost::placeholders::_1)); // publish sockets publishPushSock = new QZmq::Socket(QZmq::Socket::Push, this); } void startHttp() { zhttpClientOutStreamSock->bind("ipc://" + workDir.filePath("client-out-stream")); zhttpClientInSock->bind("ipc://" + workDir.filePath("client-in")); zhttpServerInSock->bind("ipc://" + workDir.filePath("server-in")); zhttpServerInStreamSock->bind("ipc://" + workDir.filePath("server-in-stream")); zhttpServerOutSock->bind("ipc://" + workDir.filePath("server-out")); zhttpClientInSock->subscribe("test-client "); zhttpClientInValve->open(); zhttpServerInValve->open(); zhttpServerInStreamValve->open(); } void startProxy() { proxyAcceptSock->bind("ipc://" + workDir.filePath("accept")); proxyAcceptValve->open(); } void startPublish() { publishPushSock->connectToAddress("ipc://" + workDir.filePath("publish-pull")); } void reset() { acceptSuccess = false; acceptValue.clear(); finished = false; responses.clear(); serverReqs = 0; serverFailed = false; serverOutSeq = 0; requestBody.clear(); } private slots: void zhttpClientIn_readyRead(const QList &message) { log_debug("client in"); int at = message[0].indexOf(' '); QVariant v = TnetString::toVariant(message[0].mid(at + 2)); ZhttpResponsePacket zresp; zresp.fromVariant(v); if(zresp.type == ZhttpResponsePacket::Data) { if(!responses.contains(zresp.ids.first().id)) { HttpResponseData rd; rd.code = zresp.code; rd.reason = zresp.reason; rd.headers = zresp.headers; responses[zresp.ids.first().id] = rd; } responses[zresp.ids.first().id].body += zresp.body; if(!zresp.more) finished = true; } } void zhttpServerIn_readyRead(const QList &message) { ++serverReqs; log_debug("server in"); QVariant v = TnetString::toVariant(message[0].mid(1)); ZhttpRequestPacket zreq; zreq.fromVariant(v); handleServerIn(zreq); } void zhttpServerInStream_readyRead(const QList &message) { log_debug("server stream in"); QVariant v = TnetString::toVariant(message[2].mid(1)); ZhttpRequestPacket zreq; zreq.fromVariant(v); handleServerIn(zreq); } void handleServerIn(const ZhttpRequestPacket &zreq) { if(zreq.type == ZhttpRequestPacket::Cancel) { serverFailed = true; return; } if(zreq.type == ZhttpRequestPacket::Data) requestBody += zreq.body; if(zreq.more) { // ack if(serverOutSeq == 0) { ZhttpResponsePacket zresp; zresp.from = "test-server"; zresp.ids += ZhttpResponsePacket::Id(zreq.ids.first().id, serverOutSeq++); zresp.type = ZhttpResponsePacket::Credit; zresp.credits = 200000; QByteArray buf = zreq.from + " T" + TnetString::fromVariant(zresp.toVariant()); zhttpServerOutSock->write(QList() << buf); } return; } ZhttpResponsePacket zresp; zresp.from = "test-server"; zresp.ids += ZhttpResponsePacket::Id(zreq.ids.first().id, serverOutSeq++); zresp.type = ZhttpResponsePacket::Data; zresp.code = 200; zresp.reason = "OK"; QByteArray encPath = zreq.uri.path(QUrl::FullyEncoded).toUtf8(); zresp.headers += HttpHeader("Content-Type", "text/plain"); zresp.body = "this is what's next\n"; zresp.headers += HttpHeader("Content-Length", QByteArray::number(zresp.body.size())); QByteArray buf = zreq.from + " T" + TnetString::fromVariant(zresp.toVariant()); zhttpServerOutSock->write(QList() << buf); // zero out so we can accept another request serverOutSeq = 0; } void proxyAccept_readyRead(const QList &_message) { QZmq::ReqMessage message(_message); QVariant v = TnetString::toVariant(message.content()[0]); QVERIFY(typeId(v) == QMetaType::QVariantHash); QVariantHash vresp = v.toHash(); QVERIFY(vresp.value("success").toBool()); acceptSuccess = true; v = vresp.value("value"); QVERIFY(typeId(v) == QMetaType::QVariantHash); acceptValue = v.toHash(); } }; } class HandlerEngineTest : public QObject { Q_OBJECT private: HandlerEngine *engine; Wrapper *wrapper; private slots: void initTestCase() { log_setOutputLevel(LOG_LEVEL_WARNING); //log_setOutputLevel(LOG_LEVEL_DEBUG); QDir outDir(qgetenv("OUT_DIR")); QDir workDir(QDir::current().relativeFilePath(outDir.filePath("test-work"))); wrapper = new Wrapper(this, workDir); wrapper->startHttp(); wrapper->startProxy(); engine = new HandlerEngine(this); HandlerEngine::Configuration config; config.instanceId = "handler"; config.serverInStreamSpecs = QStringList() << ("ipc://" + workDir.filePath("client-out-stream")); config.serverOutSpecs = QStringList() << ("ipc://" + workDir.filePath("client-in")); config.clientOutSpecs = QStringList() << ("ipc://" + workDir.filePath("server-in")); config.clientOutStreamSpecs = QStringList() << ("ipc://" + workDir.filePath("server-in-stream")); config.clientInSpecs = QStringList() << ("ipc://" + workDir.filePath("server-out")); config.acceptSpecs = QStringList() << ("ipc://" + workDir.filePath("accept")); config.pushInSpec = ("ipc://" + workDir.filePath("publish-pull")); config.connectionSubscriptionMax = 20; config.connectionsMax = 20; QVERIFY(engine->start(config)); wrapper->startPublish(); QTest::qWait(500); } void cleanupTestCase() { delete engine; delete wrapper; RTimer::deinit(); } void acceptNoHold() { wrapper->reset(); QByteArray id = "1"; QVariantHash rid; rid["sender"] = QByteArray("test-client"); rid["id"] = id; QVariantHash reqState; reqState["rid"] = rid; reqState["in-seq"] = 1; reqState["out-seq"] = 1; reqState["out-credits"] = 1000; QVariantHash req; req["method"] = QByteArray("GET"); req["uri"] = QByteArray("http://example.com/path"); QVariantList reqHeaders; req["headers"] = reqHeaders; req["body"] = QByteArray(); QVariantHash resp; resp["code"] = 200; resp["reason"] = QByteArray("OK"); QVariantList respHeaders; respHeaders += QVariant(QVariantList() << QByteArray("Content-Type") << QByteArray("text/plain")); resp["headers"] = respHeaders; resp["body"] = QByteArray("hello world\n"); QVariantHash args; args["requests"] = QVariantList() << reqState; args["request-data"] = req; args["orig-request-data"] = req; args["response"] = resp; QVariantHash data; data["id"] = id; data["method"] = QByteArray("accept"); data["args"] = args; QByteArray buf = TnetString::fromVariant(data); wrapper->proxyAcceptSock->write(QList() << QByteArray() << buf); while(!wrapper->acceptSuccess) QTest::qWait(10); QVERIFY(!wrapper->acceptValue.value("accepted").toBool()); QCOMPARE(wrapper->acceptValue["response"].toHash()["body"].toByteArray(), QByteArray("hello world\n")); } void acceptNoHoldResponseSent() { wrapper->reset(); QByteArray id = "2"; QVariantHash rid; rid["sender"] = QByteArray("test-client"); rid["id"] = id; QVariantHash reqState; reqState["rid"] = rid; reqState["in-seq"] = 1; reqState["out-seq"] = 1; reqState["out-credits"] = 1000; QVariantHash req; req["method"] = QByteArray("GET"); req["uri"] = QByteArray("http://example.com/path"); QVariantList reqHeaders; req["headers"] = reqHeaders; req["body"] = QByteArray(); QVariantHash resp; resp["code"] = 200; resp["reason"] = QByteArray("OK"); QVariantList respHeaders; respHeaders += QVariant(QVariantList() << QByteArray("Content-Type") << QByteArray("text/plain")); resp["headers"] = respHeaders; resp["body"] = QByteArray("hello world\n"); QVariantHash args; args["requests"] = QVariantList() << reqState; args["request-data"] = req; args["orig-request-data"] = req; args["response"] = resp; args["response-sent"] = true; QVariantHash data; data["id"] = id; data["method"] = QByteArray("accept"); data["args"] = args; QByteArray buf = TnetString::fromVariant(data); wrapper->proxyAcceptSock->write(QList() << QByteArray() << buf); while(!wrapper->acceptSuccess) QTest::qWait(10); QVERIFY(!wrapper->acceptValue.value("accepted").toBool()); QVERIFY(!wrapper->acceptValue.contains("response")); } void acceptNoHoldNext() { wrapper->reset(); QByteArray id = "3"; QVariantHash rid; rid["sender"] = QByteArray("test-client"); rid["id"] = id; QVariantHash reqState; reqState["rid"] = rid; reqState["in-seq"] = 1; reqState["out-seq"] = 1; reqState["out-credits"] = 1000; QVariantHash req; req["method"] = QByteArray("GET"); req["uri"] = QByteArray("http://example.com/path"); QVariantList reqHeaders; req["headers"] = reqHeaders; req["body"] = QByteArray(); QVariantHash resp; resp["code"] = 200; resp["reason"] = QByteArray("OK"); QVariantList respHeaders; respHeaders += QVariant(QVariantList() << QByteArray("Content-Type") << QByteArray("text/plain")); respHeaders += QVariant(QVariantList() << QByteArray("Grip-Link") << QByteArray("; rel=next")); resp["headers"] = respHeaders; resp["body"] = QByteArray("hello world\n"); QVariantHash args; args["requests"] = QVariantList() << reqState; args["request-data"] = req; args["orig-request-data"] = req; args["response"] = resp; QVariantHash data; data["id"] = id; data["method"] = QByteArray("accept"); data["args"] = args; QByteArray buf = TnetString::fromVariant(data); wrapper->proxyAcceptSock->write(QList() << QByteArray() << buf); while(!wrapper->acceptSuccess) QTest::qWait(10); QVERIFY(wrapper->acceptValue.value("accepted").toBool()); while(!wrapper->finished) QTest::qWait(10); QVERIFY(wrapper->responses.contains(id)); QCOMPARE(wrapper->responses.value(id).body, QByteArray("hello world\nthis is what's next\n")); } void acceptNoHoldNextResponseSent() { wrapper->reset(); QByteArray id = "4"; QVariantHash rid; rid["sender"] = QByteArray("test-client"); rid["id"] = id; QVariantHash reqState; reqState["rid"] = rid; reqState["in-seq"] = 1; reqState["out-seq"] = 1; reqState["out-credits"] = 1000; reqState["response-code"] = 200; QVariantHash req; req["method"] = QByteArray("GET"); req["uri"] = QByteArray("http://example.com/path"); QVariantList reqHeaders; req["headers"] = reqHeaders; req["body"] = QByteArray(); QVariantHash resp; resp["code"] = 200; resp["reason"] = QByteArray("OK"); QVariantList respHeaders; respHeaders += QVariant(QVariantList() << QByteArray("Content-Type") << QByteArray("text/plain")); respHeaders += QVariant(QVariantList() << QByteArray("Grip-Link") << QByteArray("; rel=next")); resp["headers"] = respHeaders; resp["body"] = QByteArray("hello world\n"); QVariantHash args; args["requests"] = QVariantList() << reqState; args["request-data"] = req; args["orig-request-data"] = req; args["response"] = resp; args["response-sent"] = true; QVariantHash data; data["id"] = id; data["method"] = QByteArray("accept"); data["args"] = args; QByteArray buf = TnetString::fromVariant(data); wrapper->proxyAcceptSock->write(QList() << QByteArray() << buf); while(!wrapper->acceptSuccess) QTest::qWait(10); QVERIFY(wrapper->acceptValue.value("accepted").toBool()); while(!wrapper->finished) QTest::qWait(10); QVERIFY(wrapper->responses.contains(id)); QCOMPARE(wrapper->responses.value(id).body, QByteArray("this is what's next\n")); } void publishResponse() { wrapper->reset(); QByteArray id = "5"; QVariantHash rid; rid["sender"] = QByteArray("test-client"); rid["id"] = id; QVariantHash reqState; reqState["rid"] = rid; reqState["in-seq"] = 1; reqState["out-seq"] = 1; reqState["out-credits"] = 1000; QVariantHash req; req["method"] = QByteArray("GET"); req["uri"] = QByteArray("http://example.com/path"); QVariantList reqHeaders; req["headers"] = reqHeaders; req["body"] = QByteArray(); QVariantHash resp; resp["code"] = 200; resp["reason"] = QByteArray("OK"); QVariantList respHeaders; respHeaders += QVariant(QVariantList() << QByteArray("Content-Type") << QByteArray("text/plain")); respHeaders += QVariant(QVariantList() << QByteArray("Grip-Hold") << QByteArray("response")); respHeaders += QVariant(QVariantList() << QByteArray("Grip-Channel") << QByteArray("apple")); resp["headers"] = respHeaders; resp["body"] = QByteArray("timeout\n"); QVariantHash args; args["requests"] = QVariantList() << reqState; args["request-data"] = req; args["orig-request-data"] = req; args["response"] = resp; QVariantHash data; data["id"] = id; data["method"] = QByteArray("accept"); data["args"] = args; QByteArray buf = TnetString::fromVariant(data); wrapper->proxyAcceptSock->write(QList() << QByteArray() << buf); while(!wrapper->acceptSuccess) QTest::qWait(10); data.clear(); QVariantHash hr; hr["body"] = QByteArray("hello world\n"); QVariantHash formats; formats["http-response"] = hr; data["channel"] = QByteArray("apple"); data["formats"] = formats; buf = TnetString::fromVariant(data); wrapper->publishPushSock->write(QList() << buf); while(!wrapper->finished) QTest::qWait(10); QVERIFY(wrapper->responses.contains(id)); QCOMPARE(wrapper->responses.value(id).body, QByteArray("hello world\n")); } void publishStream() { wrapper->reset(); QByteArray id = "6"; QVariantHash rid; rid["sender"] = QByteArray("test-client"); rid["id"] = id; QVariantHash reqState; reqState["rid"] = rid; reqState["in-seq"] = 1; reqState["out-seq"] = 1; reqState["out-credits"] = 1000; QVariantHash req; req["method"] = QByteArray("GET"); req["uri"] = QByteArray("http://example.com/path"); QVariantList reqHeaders; req["headers"] = reqHeaders; req["body"] = QByteArray(); QVariantHash resp; resp["code"] = 200; resp["reason"] = QByteArray("OK"); QVariantList respHeaders; respHeaders += QVariant(QVariantList() << QByteArray("Content-Type") << QByteArray("text/plain")); respHeaders += QVariant(QVariantList() << QByteArray("Grip-Hold") << QByteArray("stream")); respHeaders += QVariant(QVariantList() << QByteArray("Grip-Channel") << QByteArray("apple")); resp["headers"] = respHeaders; resp["body"] = QByteArray("stream open\n"); QVariantHash args; args["requests"] = QVariantList() << reqState; args["request-data"] = req; args["orig-request-data"] = req; args["response"] = resp; QVariantHash data; data["id"] = id; data["method"] = QByteArray("accept"); data["args"] = args; QByteArray buf = TnetString::fromVariant(data); wrapper->proxyAcceptSock->write(QList() << QByteArray() << buf); while(!wrapper->acceptSuccess) QTest::qWait(10); data.clear(); { QVariantHash hs; hs["content"] = QByteArray("hello world\n"); QVariantHash formats; formats["http-stream"] = hs; data["channel"] = QByteArray("apple"); data["formats"] = formats; } buf = TnetString::fromVariant(data); wrapper->publishPushSock->write(QList() << buf); data.clear(); { QVariantHash hs; hs["action"] = QByteArray("close"); QVariantHash formats; formats["http-stream"] = hs; data["channel"] = QByteArray("apple"); data["formats"] = formats; } buf = TnetString::fromVariant(data); wrapper->publishPushSock->write(QList() << buf); while(!wrapper->finished) QTest::qWait(10); QVERIFY(wrapper->responses.contains(id)); QCOMPARE(wrapper->responses.value(id).body, QByteArray("stream open\nhello world\n")); } void publishStreamReorder() { wrapper->reset(); QByteArray id = "7"; QVariantHash rid; rid["sender"] = QByteArray("test-client"); rid["id"] = id; QVariantHash reqState; reqState["rid"] = rid; reqState["in-seq"] = 1; reqState["out-seq"] = 1; reqState["out-credits"] = 1000; QVariantHash req; req["method"] = QByteArray("GET"); req["uri"] = QByteArray("http://example.com/path"); QVariantList reqHeaders; req["headers"] = reqHeaders; req["body"] = QByteArray(); QVariantHash resp; resp["code"] = 200; resp["reason"] = QByteArray("OK"); QVariantList respHeaders; respHeaders += QVariant(QVariantList() << QByteArray("Content-Type") << QByteArray("text/plain")); respHeaders += QVariant(QVariantList() << QByteArray("Grip-Hold") << QByteArray("stream")); respHeaders += QVariant(QVariantList() << QByteArray("Grip-Channel") << QByteArray("apple")); resp["headers"] = respHeaders; resp["body"] = QByteArray("stream open\n"); QVariantHash args; args["requests"] = QVariantList() << reqState; args["request-data"] = req; args["orig-request-data"] = req; args["response"] = resp; QVariantHash data; data["id"] = id; data["method"] = QByteArray("accept"); data["args"] = args; QByteArray buf = TnetString::fromVariant(data); wrapper->proxyAcceptSock->write(QList() << QByteArray() << buf); while(!wrapper->acceptSuccess) QTest::qWait(10); data.clear(); { QVariantHash hs; hs["content"] = QByteArray("one\n"); QVariantHash formats; formats["http-stream"] = hs; data["channel"] = QByteArray("apple"); data["id"] = QByteArray("a"); data["formats"] = formats; } buf = TnetString::fromVariant(data); wrapper->publishPushSock->write(QList() << buf); data.clear(); { QVariantHash hs; hs["action"] = QByteArray("close"); QVariantHash formats; formats["http-stream"] = hs; data["channel"] = QByteArray("apple"); data["id"] = QByteArray("e"); data["prev-id"] = QByteArray("d"); data["formats"] = formats; } buf = TnetString::fromVariant(data); wrapper->publishPushSock->write(QList() << buf); data.clear(); { QVariantHash hs; hs["content"] = QByteArray("four\n"); QVariantHash formats; formats["http-stream"] = hs; data["channel"] = QByteArray("apple"); data["id"] = QByteArray("d"); data["prev-id"] = QByteArray("c"); data["formats"] = formats; } buf = TnetString::fromVariant(data); wrapper->publishPushSock->write(QList() << buf); data.clear(); { QVariantHash hs; hs["content"] = QByteArray("three\n"); QVariantHash formats; formats["http-stream"] = hs; data["channel"] = QByteArray("apple"); data["id"] = QByteArray("c"); data["prev-id"] = QByteArray("b"); data["formats"] = formats; } buf = TnetString::fromVariant(data); wrapper->publishPushSock->write(QList() << buf); data.clear(); { QVariantHash hs; hs["content"] = QByteArray("two\n"); QVariantHash formats; formats["http-stream"] = hs; data["channel"] = QByteArray("apple"); data["id"] = QByteArray("b"); data["prev-id"] = QByteArray("a"); data["formats"] = formats; } buf = TnetString::fromVariant(data); wrapper->publishPushSock->write(QList() << buf); while(!wrapper->finished) QTest::qWait(10); QVERIFY(wrapper->responses.contains(id)); QCOMPARE(wrapper->responses.value(id).body, QByteArray("stream open\none\ntwo\nthree\nfour\n")); } }; namespace { namespace Main { QTEST_MAIN(HandlerEngineTest) } } extern "C" { int handlerengine_test(int argc, char **argv) { return Main::main(argc, argv); } } #include "handlerenginetest.moc" pushpin-1.39.1/src/cpp/tests/httpheaderstest.cpp000066400000000000000000000054531457610542000217400ustar00rootroot00000000000000/* * Copyright (C) 2017 Fanout, Inc. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ * */ #include #include "httpheaders.h" class HttpHeadersTest : public QObject { Q_OBJECT private slots: void parseParameters() { HttpHeaders h; h += HttpHeader("Fruit", "apple"); h += HttpHeader("Fruit", "banana"); h += HttpHeader("Fruit", "cherry"); QList params = h.getAllAsParameters("Fruit"); QCOMPARE(params.count(), 3); QCOMPARE(params[0][0].first, QByteArray("apple")); QCOMPARE(params[1][0].first, QByteArray("banana")); QCOMPARE(params[2][0].first, QByteArray("cherry")); h.clear(); h += HttpHeader("Fruit", "apple, banana, cherry"); params = h.getAllAsParameters("Fruit"); QCOMPARE(params.count(), 3); QCOMPARE(params[0][0].first, QByteArray("apple")); QCOMPARE(params[1][0].first, QByteArray("banana")); QCOMPARE(params[2][0].first, QByteArray("cherry")); h.clear(); h += HttpHeader("Fruit", "apple; type=\"granny, smith\", banana; type=\"\\\"yellow\\\"\""); params = h.getAllAsParameters("Fruit"); QCOMPARE(params.count(), 2); QCOMPARE(params[0][0].first, QByteArray("apple")); QCOMPARE(params[0][1].first, QByteArray("type")); QCOMPARE(params[0][1].second, QByteArray("granny, smith")); QCOMPARE(params[1][0].first, QByteArray("banana")); QCOMPARE(params[1][1].first, QByteArray("type")); QCOMPARE(params[1][1].second, QByteArray("\"yellow\"")); h.clear(); h += HttpHeader("Fruit", "\"apple"); QList l = h.getAll("Fruit"); QCOMPARE(l.count(), 1); QCOMPARE(l[0], QByteArray("\"apple")); h.clear(); h += HttpHeader("Fruit", "\"apple\\"); l = h.getAll("Fruit"); QCOMPARE(l.count(), 1); QCOMPARE(l[0], QByteArray("\"apple\\")); h.clear(); h += HttpHeader("Fruit", "apple; type=gala, banana; type=\"yellow, cherry"); params = h.getAllAsParameters("Fruit"); QCOMPARE(params.count(), 1); QCOMPARE(params[0][0].first, QByteArray("apple")); QCOMPARE(params[0][1].first, QByteArray("type")); QCOMPARE(params[0][1].second, QByteArray("gala")); } }; namespace { namespace Main { QTEST_MAIN(HttpHeadersTest) } } extern "C" { int httpheaders_test(int argc, char **argv) { return Main::main(argc, argv); } } #include "httpheaderstest.moc" pushpin-1.39.1/src/cpp/tests/idformattest.cpp000066400000000000000000000053731457610542000212330ustar00rootroot00000000000000/* * Copyright (C) 2017 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include #include "idformat.h" class IdFormatTest : public QObject { Q_OBJECT private slots: void renderId() { QHash vars; QByteArray sformat = "This template has no directives."; QByteArray ret = IdFormat::renderId(sformat, vars); QCOMPARE(ret, QByteArray("This template has no directives.")); vars["name"] = "Alice"; vars["food\\fruit(type)"] = "apples"; sformat = "My name is %(name)s and I eat %(food\\\\fruit(type\\))s 10%% of the time."; ret = IdFormat::renderId(sformat, vars); QCOMPARE(ret, QByteArray("My name is Alice and I eat apples 10% of the time.")); } void renderContent() { QByteArray id = "C3PO"; QByteArray content = "This content has no directives."; QByteArray ret = IdFormat::ContentRenderer(id, false).process(content); QCOMPARE(ret, QByteArray("This content has no directives.")); content = "The ID is %I."; ret = IdFormat::ContentRenderer(id, false).process(content); QCOMPARE(ret, QByteArray("The ID is C3PO.")); ret = IdFormat::ContentRenderer(id, true).process(content); QCOMPARE(ret, QByteArray("The ID is 4333504f.")); content = "The ID is %(R2D2)I."; ret = IdFormat::ContentRenderer(id, true).process(content); QCOMPARE(ret, QByteArray("The ID is 52324432.")); } void renderContentIncremental() { IdFormat::ContentRenderer cr(QByteArray(), true); QByteArray ret = cr.update("The ID is %"); QCOMPARE(ret, QByteArray("The ID is ")); ret += cr.update("("); QCOMPARE(ret, QByteArray("The ID is ")); ret += cr.update("R2D"); QCOMPARE(ret, QByteArray("The ID is ")); ret += cr.update("2"); QCOMPARE(ret, QByteArray("The ID is ")); ret += cr.update(")"); QCOMPARE(ret, QByteArray("The ID is ")); ret += cr.update("I."); QCOMPARE(ret, QByteArray("The ID is 52324432.")); ret += cr.finalize(); QVERIFY(!ret.isNull()); QCOMPARE(ret, QByteArray("The ID is 52324432.")); } }; namespace { namespace Main { QTEST_MAIN(IdFormatTest) } } extern "C" { int idformat_test(int argc, char **argv) { return Main::main(argc, argv); } } #include "idformattest.moc" pushpin-1.39.1/src/cpp/tests/instructtest.cpp000066400000000000000000000267051457610542000213030ustar00rootroot00000000000000/* * Copyright (C) 2016-2019 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include #include "httpheaders.h" #include "packet/httpresponsedata.h" #include "instruct.h" class InstructTest : public QObject { Q_OBJECT private slots: void noHold() { HttpResponseData data; data.code = 200; data.reason = "OK"; data.headers += HttpHeader("Content-Type", "text/plain"); data.headers += HttpHeader("Grip-Channel", "test"); data.body = "hello world"; Instruct i; bool ok; i = Instruct::fromResponse(data, &ok); QVERIFY(ok); QCOMPARE(i.holdMode, Instruct::NoHold); QCOMPARE(i.response.code, 200); QCOMPARE(i.response.reason, QByteArray("OK")); QCOMPARE(i.response.headers.get("Content-Type"), QByteArray("text/plain")); QVERIFY(!i.response.headers.contains("Grip-Channel")); QCOMPARE(i.response.body, QByteArray("hello world")); data.headers += HttpHeader("Grip-Status", "404"); i = Instruct::fromResponse(data, &ok); QVERIFY(ok); QCOMPARE(i.holdMode, Instruct::NoHold); QCOMPARE(i.response.code, 404); QCOMPARE(i.response.reason, QByteArray("Not Found")); data.headers.removeAll("Grip-Status"); data.headers += HttpHeader("Grip-Status", "404 Nothing To See Here"); i = Instruct::fromResponse(data, &ok); QVERIFY(ok); QCOMPARE(i.holdMode, Instruct::NoHold); QCOMPARE(i.response.code, 404); QCOMPARE(i.response.reason, QByteArray("Nothing To See Here")); data.headers.clear(); data.headers += HttpHeader("Content-Type", "application/grip-instruct"); data.body = "{\"response\":{\"code\": 200,\"headers\":{\"Content-Type\":\"text/plain\"},\"body\":\"hello world\"}}"; i = Instruct::fromResponse(data, &ok); QVERIFY(ok); QCOMPARE(i.holdMode, Instruct::NoHold); QCOMPARE(i.response.code, 200); QCOMPARE(i.response.reason, QByteArray("OK")); QCOMPARE(i.response.headers.get("Content-Type"), QByteArray("text/plain")); QCOMPARE(i.response.body, QByteArray("hello world")); } void responseHold() { HttpResponseData data; data.code = 200; data.reason = "OK"; data.headers += HttpHeader("Content-Type", "text/plain"); data.headers += HttpHeader("Grip-Hold", "response"); data.headers += HttpHeader("Grip-Channel", "apple"); data.headers += HttpHeader("Grip-Channel", "banana, cherry"); data.headers += HttpHeader("Grip-Timeout", "120"); data.headers += HttpHeader("Grip-Set-Meta", "foo=bar, bar=baz"); data.body = "hello world"; Instruct i; bool ok; i = Instruct::fromResponse(data, &ok); QVERIFY(ok); QCOMPARE(i.holdMode, Instruct::ResponseHold); QCOMPARE(i.channels.count(), 3); QCOMPARE(i.channels[0].name, QString("apple")); QCOMPARE(i.channels[1].name, QString("banana")); QCOMPARE(i.channels[2].name, QString("cherry")); QCOMPARE(i.timeout, 120); QCOMPARE(i.meta.count(), 2); QCOMPARE(i.meta.value("foo"), QString("bar")); QCOMPARE(i.meta.value("bar"), QString("baz")); QCOMPARE(i.response.headers.get("Content-Type"), QByteArray("text/plain")); QVERIFY(!i.response.headers.contains("Grip-Channel")); QCOMPARE(i.response.body, QByteArray("hello world")); data.headers.clear(); data.headers += HttpHeader("Content-Type", "application/grip-instruct"); data.body = "{\"hold\":{\"mode\":\"response\",\"channels\":[{\"name\":\"test\"}],\"timeout\":120,\"meta\":{\"foo\":\"bar\",\"bar\":\"baz\"}},\"response\":{\"code\": 200,\"headers\":{\"Content-Type\":\"text/plain\"},\"body\":\"hello world\"}}"; i = Instruct::fromResponse(data, &ok); QVERIFY(ok); QCOMPARE(i.holdMode, Instruct::ResponseHold); QCOMPARE(i.channels.count(), 1); QCOMPARE(i.channels[0].name, QString("test")); QCOMPARE(i.timeout, 120); QCOMPARE(i.meta.count(), 2); QCOMPARE(i.meta.value("foo"), QString("bar")); QCOMPARE(i.meta.value("bar"), QString("baz")); QCOMPARE(i.response.code, 200); QCOMPARE(i.response.reason, QByteArray("OK")); QCOMPARE(i.response.headers.get("Content-Type"), QByteArray("text/plain")); QCOMPARE(i.response.body, QByteArray("hello world")); } void responseHoldChannelParams() { HttpResponseData data; data.code = 200; data.reason = "OK"; data.headers += HttpHeader("Content-Type", "text/plain"); data.headers += HttpHeader("Grip-Hold", "response"); data.headers += HttpHeader("Grip-Channel", "apple; prev-id=item1; filter=f1"); data.headers += HttpHeader("Grip-Channel", "banana; filter=f2, cherry; filter=f1; filter=f2"); data.body = "hello world"; Instruct i; bool ok; i = Instruct::fromResponse(data, &ok); QVERIFY(ok); QCOMPARE(i.holdMode, Instruct::ResponseHold); QCOMPARE(i.channels.count(), 3); QCOMPARE(i.channels[0].name, QString("apple")); QCOMPARE(i.channels[0].prevId, QString("item1")); QCOMPARE(i.channels[0].filters.count(), 1); QCOMPARE(i.channels[0].filters[0], QString("f1")); QCOMPARE(i.channels[1].name, QString("banana")); QVERIFY(i.channels[1].prevId.isNull()); QCOMPARE(i.channels[1].filters.count(), 1); QCOMPARE(i.channels[1].filters[0], QString("f2")); QCOMPARE(i.channels[2].name, QString("cherry")); QVERIFY(i.channels[2].prevId.isNull()); QCOMPARE(i.channels[2].filters.count(), 2); QCOMPARE(i.channels[2].filters[0], QString("f1")); QCOMPARE(i.channels[2].filters[1], QString("f2")); QCOMPARE(i.response.headers.get("Content-Type"), QByteArray("text/plain")); QVERIFY(!i.response.headers.contains("Grip-Channel")); QCOMPARE(i.response.body, QByteArray("hello world")); data.headers.clear(); data.headers += HttpHeader("Content-Type", "application/grip-instruct"); data.body = "{\"hold\":{\"mode\":\"response\",\"channels\":[{\"name\":\"apple\",\"prev-id\":\"item1\",\"filters\":[\"f1\"]},{\"name\":\"banana\",\"filters\":[\"f2\"]},{\"name\":\"cherry\",\"filters\":[\"f1\",\"f2\"]}]},\"response\":{\"code\": 200,\"headers\":{\"Content-Type\":\"text/plain\"},\"body\":\"hello world\"}}"; i = Instruct::fromResponse(data, &ok); QVERIFY(ok); QCOMPARE(i.holdMode, Instruct::ResponseHold); QCOMPARE(i.channels.count(), 3); QCOMPARE(i.channels[0].name, QString("apple")); QCOMPARE(i.channels[0].prevId, QString("item1")); QCOMPARE(i.channels[0].filters.count(), 1); QCOMPARE(i.channels[0].filters[0], QString("f1")); QCOMPARE(i.channels[1].name, QString("banana")); QVERIFY(i.channels[1].prevId.isNull()); QCOMPARE(i.channels[1].filters.count(), 1); QCOMPARE(i.channels[1].filters[0], QString("f2")); QCOMPARE(i.channels[2].name, QString("cherry")); QVERIFY(i.channels[2].prevId.isNull()); QCOMPARE(i.channels[2].filters.count(), 2); QCOMPARE(i.channels[2].filters[0], QString("f1")); QCOMPARE(i.channels[2].filters[1], QString("f2")); QCOMPARE(i.response.code, 200); QCOMPARE(i.response.reason, QByteArray("OK")); QCOMPARE(i.response.headers.get("Content-Type"), QByteArray("text/plain")); QCOMPARE(i.response.body, QByteArray("hello world")); } void streamHold() { HttpResponseData data; data.code = 200; data.reason = "OK"; data.headers += HttpHeader("Content-Type", "text/plain"); data.headers += HttpHeader("Grip-Hold", "stream"); data.headers += HttpHeader("Grip-Channel", "apple"); data.headers += HttpHeader("Grip-Channel", "banana, cherry"); data.body = "hello world"; Instruct i; bool ok; i = Instruct::fromResponse(data, &ok); QVERIFY(ok); QCOMPARE(i.holdMode, Instruct::StreamHold); QCOMPARE(i.channels.count(), 3); QCOMPARE(i.channels[0].name, QString("apple")); QCOMPARE(i.channels[1].name, QString("banana")); QCOMPARE(i.channels[2].name, QString("cherry")); QCOMPARE(i.response.headers.get("Content-Type"), QByteArray("text/plain")); QVERIFY(!i.response.headers.contains("Grip-Channel")); QCOMPARE(i.response.body, QByteArray("hello world")); data.headers.clear(); data.headers += HttpHeader("Content-Type", "application/grip-instruct"); data.body = "{\"hold\":{\"mode\":\"stream\",\"channels\":[{\"name\":\"test\"}]},\"response\":{\"code\": 200,\"headers\":{\"Content-Type\":\"text/plain\"},\"body\":\"hello world\"}}"; i = Instruct::fromResponse(data, &ok); QVERIFY(ok); QCOMPARE(i.holdMode, Instruct::StreamHold); QCOMPARE(i.channels.count(), 1); QCOMPARE(i.channels[0].name, QString("test")); QCOMPARE(i.response.code, 200); QCOMPARE(i.response.reason, QByteArray("OK")); QCOMPARE(i.response.headers.get("Content-Type"), QByteArray("text/plain")); QCOMPARE(i.response.body, QByteArray("hello world")); } void streamHoldKeepAlive() { HttpResponseData data; data.code = 200; data.reason = "OK"; data.body = "hello world"; data.headers += HttpHeader("Content-Type", "text/plain"); data.headers += HttpHeader("Grip-Hold", "stream"); data.headers += HttpHeader("Grip-Channel", "test"); Instruct i; bool ok; i = Instruct::fromResponse(data, &ok); QVERIFY(ok); QCOMPARE(i.holdMode, Instruct::StreamHold); QCOMPARE(i.keepAliveMode, Instruct::NoKeepAlive); data.headers.clear(); data.headers += HttpHeader("Content-Type", "text/plain"); data.headers += HttpHeader("Grip-Hold", "stream"); data.headers += HttpHeader("Grip-Channel", "test"); data.headers += HttpHeader("Grip-Keep-Alive", "ping1\\n; timeout=10"); i = Instruct::fromResponse(data, &ok); QVERIFY(ok); QCOMPARE(i.holdMode, Instruct::StreamHold); QCOMPARE(i.keepAliveMode, Instruct::Idle); QCOMPARE(i.keepAliveData, QByteArray("ping1\\n")); QCOMPARE(i.keepAliveTimeout, 10); data.headers.clear(); data.headers += HttpHeader("Content-Type", "text/plain"); data.headers += HttpHeader("Grip-Hold", "stream"); data.headers += HttpHeader("Grip-Channel", "test"); data.headers += HttpHeader("Grip-Keep-Alive", "ping2\\n; format=cstring"); i = Instruct::fromResponse(data, &ok); QVERIFY(ok); QCOMPARE(i.holdMode, Instruct::StreamHold); QCOMPARE(i.keepAliveMode, Instruct::Idle); QCOMPARE(i.keepAliveData, QByteArray("ping2\n")); QVERIFY(i.keepAliveTimeout > 0); data.headers.clear(); data.headers += HttpHeader("Content-Type", "text/plain"); data.headers += HttpHeader("Grip-Hold", "stream"); data.headers += HttpHeader("Grip-Channel", "test"); data.headers += HttpHeader("Grip-Keep-Alive", "cGluZzMK; format=base64"); i = Instruct::fromResponse(data, &ok); QVERIFY(ok); QCOMPARE(i.holdMode, Instruct::StreamHold); QCOMPARE(i.keepAliveMode, Instruct::Idle); QCOMPARE(i.keepAliveData, QByteArray("ping3\n")); QVERIFY(i.keepAliveTimeout > 0); data.headers.clear(); data.headers += HttpHeader("Content-Type", "text/plain"); data.headers += HttpHeader("Grip-Hold", "stream"); data.headers += HttpHeader("Grip-Channel", "test"); data.headers += HttpHeader("Grip-Keep-Alive", "ping4\\n; mode=interval"); i = Instruct::fromResponse(data, &ok); QVERIFY(ok); QCOMPARE(i.holdMode, Instruct::StreamHold); QCOMPARE(i.keepAliveMode, Instruct::Interval); QCOMPARE(i.keepAliveData, QByteArray("ping4\\n")); QVERIFY(i.keepAliveTimeout > 0); } }; namespace { namespace Main { QTEST_MAIN(InstructTest) } } extern "C" { int instruct_test(int argc, char **argv) { return Main::main(argc, argv); } } #include "instructtest.moc" pushpin-1.39.1/src/cpp/tests/jsonpatchtest.cpp000066400000000000000000000057471457610542000214240ustar00rootroot00000000000000/* * Copyright (C) 2015 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include #include "qtcompat.h" #include "jsonpatch.h" class JsonPatchTest : public QObject { Q_OBJECT private slots: void patch() { QVariantMap data; data["foo"] = "bar"; QVariantMap op; op["op"] = "test"; op["path"] = "/foo"; op["value"] = "bar"; QString msg; QVariant ret = JsonPatch::patch(data, QVariantList() << op, &msg); QVERIFY(ret.isValid()); data = ret.toMap(); op.clear(); op["op"] = "add"; op["path"] = "/fruit"; op["value"] = QVariantList() << "apple"; ret = JsonPatch::patch(data, QVariantList() << op); QVERIFY(ret.isValid()); data = ret.toMap(); QCOMPARE(typeId(data["fruit"]), QMetaType::QVariantList); QCOMPARE(data["fruit"].toList()[0].toString(), QString("apple")); op.clear(); op["op"] = "copy"; op["from"] = "/foo"; op["path"] = "/fruit/-"; ret = JsonPatch::patch(data, QVariantList() << op); QVERIFY(ret.isValid()); data = ret.toMap(); QCOMPARE(data["fruit"].toList()[1].toString(), QString("bar")); op.clear(); op["op"] = "replace"; op["path"] = "/fruit/1"; QVariantMap bowl; bowl["cherries"] = true; bowl["grapes"] = 5; op["value"] = bowl; ret = JsonPatch::patch(data, QVariantList() << op); QVERIFY(ret.isValid()); data = ret.toMap(); QCOMPARE(typeId(data["fruit"].toList()[1]), QMetaType::QVariantMap); QCOMPARE(data["fruit"].toList()[1].toMap().value("cherries").toBool(), true); QCOMPARE(data["fruit"].toList()[1].toMap().value("grapes").toInt(), 5); op.clear(); op["op"] = "remove"; op["path"] = "/fruit/1/cherries"; ret = JsonPatch::patch(data, QVariantList() << op); QVERIFY(ret.isValid()); data = ret.toMap(); QVERIFY(!data["fruit"].toList()[1].toMap().contains("cherries")); QCOMPARE(data["fruit"].toList()[1].toMap().value("grapes").toInt(), 5); op.clear(); op["op"] = "move"; op["from"] = "/fruit/0"; op["path"] = "/foo"; ret = JsonPatch::patch(data, QVariantList() << op); QVERIFY(ret.isValid()); data = ret.toMap(); QCOMPARE(data["foo"].toString(), QString("apple")); QCOMPARE(data["fruit"].toList()[0].toMap().value("grapes").toInt(), 5); } }; namespace { namespace Main { QTEST_MAIN(JsonPatchTest) } } extern "C" { int jsonpatch_test(int argc, char **argv) { return Main::main(argc, argv); } } #include "jsonpatchtest.moc" pushpin-1.39.1/src/cpp/tests/jwttest.cpp000066400000000000000000000160371457610542000202310ustar00rootroot00000000000000/* * Copyright (C) 2013-2022 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include #include #include #include "qtcompat.h" #include "jwt.h" static const char *test_ec_private_key_pem = "-----BEGIN PRIVATE KEY-----\n" "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgFcZQVV16cpGC4QUQ\n" "8O8H85totFiAB54WBTxKQQElI7KhRANCAAQA3D4/QkBACQuC99MFqZllTOaamPAJ\n" "3+Z3JkPsrd/z651PYmlywcdEGVWRiD2PNhvdzM7Nckxx1ZofDLlkvoxH\n" "-----END PRIVATE KEY-----\n"; static const char *test_ec_public_key_pem = "-----BEGIN PUBLIC KEY-----\n" "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEANw+P0JAQAkLgvfTBamZZUzmmpjw\n" "Cd/mdyZD7K3f8+udT2JpcsHHRBlVkYg9jzYb3czOzXJMcdWaHwy5ZL6MRw==\n" "-----END PUBLIC KEY-----\n"; static const char *test_rsa_private_key_pem = "-----BEGIN PRIVATE KEY-----\n" "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDOgE+exziD5kFF\n" "4x3F76G64XAccp1KqWMgrTYM3c4C/7hxwuu7kMdGXlXL+xQOHe6vX6EM/H9tWaIf\n" "CyQ+KfdyBBDO05MXZcxEl3496ShN/UN1TghJk12gg3yPm3+V2mfh+NQi7jEFt1uv\n" "beco5T1ve5yhtu58PrCC87TuWINW+iFrUg41MEHcXWL/7COBR/azFOZqedZPCdnL\n" "5SoY1H5WAazZUftD6W7PvCYmQN+uCSr1SjbGg5g+9OQ6i7viHXRg0U9mIZVII56V\n" "g2sD0w6ClO4Tq+mQs94frKakD960drvg2QNCvW0cUiRBLkadOSqZkIp0It4r5ivi\n" "S2gJQO8XAgMBAAECggEBAJs4W6DwAw0yULIlq8WTALCmsEzR4mWyuW5ghJZbS3V5\n" "nrz0VZmhlAjS9A7l5gdOfJGagkZuraIWlARdrZqElRlA8Rlmc9RMkqSkcyI6Vi95\n" "RfGw/A3CFciHzWNs8RRFHX0AOwUeof63+tT8+ZsF5Y4dDnmINe9yd9+XLNNT+TWw\n" "aCFJ+RQ8j7xGtZb2N/AOI0prTCka/SNRYxNommdS1x9qCaTVKd1fXM/ZhRjIlsEo\n" "OzmcoG0Kdfq6pu2OgJ8DzSigXyWbCEy/amSWgPX80kubG1Xjc8MSFlQcg493Gve1\n" "JagUZEbKIQFNCxN42cAzuuf3hKV9vIT+L8yApuacwQECgYEA+Lgp8UtANVFOBSuE\n" "5HHP+dB3Ot8HdbK2FIQEQ+xwVUHgLnnWpQHhw8COpZgAoMGMPl37KGrTuPW/C/4o\n" "yGj/hK+df1ksLR8ViXFVpB5GbzfdsvMgPo1GCYVFVGJlVHO/oFxV6YQtydhiAMp+\n" "dcgQO3paKrzEoFSJdomNtoqMdUECgYEA1IvGiaiwk5yPafs2mbsoMM7K6NpzlO3x\n" "pPlTqgGgVgIM+Lg6FWEm3kWN6A/hELyfCIosHP5pdkPKxgkzs6OqVFxKa2anHSRT\n" "1lLUhU0kOrkYyfq1oMXumPb5Kc4zzbOnxScF7lCIzMo9y82OJSjHDbjAgmzNyJbm\n" "CEhOgf2RllcCgYAfyqKJ1j2R0x+u534oGSglXXEwFDwG3l4Jx0ooSHufWjlGl4pJ\n" "MzFhbSaOohxKcBL2Eds9slH3zWmrJcSewVUP58aw9XwBFH0TQWpZ/QixxKlQ62TO\n" "ug4ev2s6Ow2KuvTekY7lt2CG8WKtiTSa54SzpZMK7XAQsl2TykdT8ue7QQKBgGrG\n" "KR/gkYwmG1m3bK9/+OnECOU/UM8hVcJ1ylTeakiq0Q9lpTA2VQtWT7qjt4Hr78yf\n" "dRe/qwVRex1PZBy7fIbSskQQFqWqKT/C7qZkoW2qrMxS2UmCBaHseDFLOHT+6qo9\n" "N1qINKEEfFTU17LNMGoxROyAckRxoe/JOz9MPgYTAoGBAJKreX73d6s1s9oVB3u/\n" "DS1YXRmek+OkXQhFxekKXB3KxG8obx2uveeg18PtNf0RoYq9LF0hKcTqSCusfF9m\n" "lM+s5xc1mQfXI55AEOjT+8AssmhebHbFkpjr1/DSUUsCssO+1znkeZwAOApm/4kR\n" "pGokHI67k9CxNFZW3Z0U9EeW\n" "-----END PRIVATE KEY-----\n"; static const char *test_rsa_public_key_pem = "-----BEGIN PUBLIC KEY-----\n" "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzoBPnsc4g+ZBReMdxe+h\n" "uuFwHHKdSqljIK02DN3OAv+4ccLru5DHRl5Vy/sUDh3ur1+hDPx/bVmiHwskPin3\n" "cgQQztOTF2XMRJd+PekoTf1DdU4ISZNdoIN8j5t/ldpn4fjUIu4xBbdbr23nKOU9\n" "b3ucobbufD6wgvO07liDVvoha1IONTBB3F1i/+wjgUf2sxTmannWTwnZy+UqGNR+\n" "VgGs2VH7Q+luz7wmJkDfrgkq9Uo2xoOYPvTkOou74h10YNFPZiGVSCOelYNrA9MO\n" "gpTuE6vpkLPeH6ympA/etHa74NkDQr1tHFIkQS5GnTkqmZCKdCLeK+Yr4ktoCUDv\n" "FwIDAQAB\n" "-----END PUBLIC KEY-----\n"; class JwtTest : public QObject { Q_OBJECT private slots: void validToken() { QVariant vclaim = Jwt::decode("eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJmb28iOiAiYmFyIn0.oBia0Fph39FwQWv0TS7Disg4qa0aFa8qpMaYDrIXZqs", Jwt::DecodingKey::fromSecret("secret")); QVERIFY(typeId(vclaim) == QMetaType::QVariantMap); QVariantMap claim = vclaim.toMap(); QVERIFY(claim.value("foo") == "bar"); } void validTokenBinaryKey() { QByteArray key; key += 0x01; key += 0x61; key += 0x80; key += 0xfe; QVariant vclaim = Jwt::decode("eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJmb28iOiAiYmFyIn0.-eLxyGEITnd6IP4WvGJx9CmIOt--Qcs3LB6wblJ7KXI", Jwt::DecodingKey::fromSecret(key)); QVERIFY(typeId(vclaim) == QMetaType::QVariantMap); QVariantMap claim = vclaim.toMap(); QVERIFY(claim.value("foo") == "bar"); } void invalidKey() { QVariant vclaim = Jwt::decode("eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJmb28iOiAiYmFyIn0.oBia0Fph39FwQWv0TS7Disg4qa0aFa8qpMaYDrIXZqs", Jwt::DecodingKey::fromSecret("wrong")); QVERIFY(vclaim.isNull()); } void es256EncodeDecode() { Jwt::EncodingKey privateKey = Jwt::EncodingKey::fromPem(QByteArray(test_ec_private_key_pem)); QVERIFY(!privateKey.isNull()); QCOMPARE(privateKey.type(), Jwt::KeyType::Ec); Jwt::DecodingKey publicKey = Jwt::DecodingKey::fromPem(QByteArray(test_ec_public_key_pem)); QVERIFY(!publicKey.isNull()); QCOMPARE(publicKey.type(), Jwt::KeyType::Ec); QVariantMap claim; claim["iss"] = "nobody"; QByteArray claimJson = QJsonDocument(QJsonObject::fromVariantMap(claim)).toJson(QJsonDocument::Compact); QVERIFY(!claimJson.isNull()); QByteArray token = Jwt::encodeWithAlgorithm(Jwt::ES256, claimJson, privateKey); QVERIFY(!token.isNull()); QByteArray resultJson = Jwt::decodeWithAlgorithm(Jwt::ES256, token, publicKey); QVERIFY(!resultJson.isNull()); QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(resultJson, &error); QVERIFY(error.error == QJsonParseError::NoError); QVERIFY(doc.isObject()); QVariantMap result = doc.object().toVariantMap(); QCOMPARE(result["iss"], "nobody"); } void rs256EncodeDecode() { Jwt::EncodingKey privateKey = Jwt::EncodingKey::fromPem(QByteArray(test_rsa_private_key_pem)); QVERIFY(!privateKey.isNull()); QCOMPARE(privateKey.type(), Jwt::KeyType::Rsa); Jwt::DecodingKey publicKey = Jwt::DecodingKey::fromPem(QByteArray(test_rsa_public_key_pem)); QVERIFY(!publicKey.isNull()); QCOMPARE(publicKey.type(), Jwt::KeyType::Rsa); QVariantMap claim; claim["iss"] = "nobody"; QByteArray claimJson = QJsonDocument(QJsonObject::fromVariantMap(claim)).toJson(QJsonDocument::Compact); QVERIFY(!claimJson.isNull()); QByteArray token = Jwt::encodeWithAlgorithm(Jwt::RS256, claimJson, privateKey); QVERIFY(!token.isNull()); QByteArray resultJson = Jwt::decodeWithAlgorithm(Jwt::RS256, token, publicKey); QVERIFY(!resultJson.isNull()); QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(resultJson, &error); QVERIFY(error.error == QJsonParseError::NoError); QVERIFY(doc.isObject()); QVariantMap result = doc.object().toVariantMap(); QCOMPARE(result["iss"], "nobody"); } }; namespace { namespace Main { QTEST_MAIN(JwtTest) } } extern "C" { int jwt_test(int argc, char **argv) { return Main::main(argc, argv); } } #include "jwttest.moc" pushpin-1.39.1/src/cpp/tests/main.h000066400000000000000000000010261457610542000171060ustar00rootroot00000000000000#ifndef PUSHPIN_TEST_H #define PUSHPIN_TEST_H int httpheaders_test(int argc, char **argv); int jwt_test(int argc, char **argv); int routesfile_test(int argc, char **argv); int proxyengine_test(int argc, char **argv); int jsonpatch_test(int argc, char **argv); int instruct_test(int argc, char **argv); int idformat_test(int argc, char **argv); int publishformat_test(int argc, char **argv); int publishitem_test(int argc, char **argv); int handlerengine_test(int argc, char **argv); int template_test(int argc, char **argv); #endif pushpin-1.39.1/src/cpp/tests/proxyenginetest.cpp000066400000000000000000001351701457610542000217740ustar00rootroot00000000000000/* * Copyright (C) 2013-2022 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include #include #include #include #include #include #include "qzmqsocket.h" #include "qzmqvalve.h" #include "qzmqreqmessage.h" #include "log.h" #include "tnetstring.h" #include "zhttprequestpacket.h" #include "zhttpresponsepacket.h" #include "packet/httprequestdata.h" #include "packet/httpresponsedata.h" #include "packet/statspacket.h" #include "rtimer.h" #include "zhttpmanager.h" #include "statsmanager.h" #include "domainmap.h" #include "engine.h" Q_DECLARE_METATYPE(QList); // NOTE: based on proxysession hardcoded max #define PROXY_MAX_ACCEPT_RESPONSE_BODY 100000 namespace { class Wrapper : public QObject { Q_OBJECT public: QZmq::Socket *zhttpClientOutSock; QZmq::Socket *zhttpClientOutStreamSock; QZmq::Socket *zhttpClientInSock; QZmq::Valve *zhttpClientInValve; QZmq::Socket *zhttpServerInSock; QZmq::Valve *zhttpServerInValve; QZmq::Socket *zhttpServerInStreamSock; QZmq::Valve *zhttpServerInStreamValve; QZmq::Socket *zhttpServerOutSock; QZmq::Socket *handlerInspectSock; QZmq::Valve *handlerInspectValve; QZmq::Socket *handlerAcceptSock; QZmq::Valve *handlerAcceptValve; QZmq::Socket *handlerRetryOutSock; QDir workDir; QHash serverReqs; bool isWs; bool serverFailed; bool inspectEnabled; bool inspected; QByteArray sharingKey; QByteArray in; HttpHeaders acceptHeaders; QByteArray acceptIn; bool retried; bool finished; int serverOutSeq; int clientReqsFinished; QByteArray requestBody; QHash responses; Connection zhttpClientInValveConnection; Connection zhttpServerInValveConnection; Connection zhttpServerInStreamValveConnection; Connection handlerAcceptValveConnection; Connection handlerInspectValveConnection; Wrapper(QObject *parent, QDir _workDir) : QObject(parent), workDir(_workDir), isWs(false), serverFailed(false), inspectEnabled(true), inspected(false), retried(false), finished(false), serverOutSeq(0), clientReqsFinished(0) { // http sockets zhttpClientOutSock = new QZmq::Socket(QZmq::Socket::Push, this); zhttpClientOutStreamSock = new QZmq::Socket(QZmq::Socket::Router, this); zhttpClientInSock = new QZmq::Socket(QZmq::Socket::Sub, this); zhttpClientInValve = new QZmq::Valve(zhttpClientInSock, this); zhttpClientInValveConnection = zhttpClientInValve->readyRead.connect(boost::bind(&Wrapper::zhttpClientIn_readyRead, this, boost::placeholders::_1)); zhttpServerInSock = new QZmq::Socket(QZmq::Socket::Pull, this); zhttpServerInValve = new QZmq::Valve(zhttpServerInSock, this); zhttpServerInValveConnection = zhttpServerInValve->readyRead.connect(boost::bind(&Wrapper::zhttpServerIn_readyRead, this, boost::placeholders::_1)); zhttpServerInStreamSock = new QZmq::Socket(QZmq::Socket::Router, this); zhttpServerInStreamSock->setIdentity("test-server"); zhttpServerInStreamValve = new QZmq::Valve(zhttpServerInStreamSock, this); zhttpServerInStreamValveConnection = zhttpServerInStreamValve->readyRead.connect(boost::bind(&Wrapper::zhttpServerInStream_readyRead, this, boost::placeholders::_1)); zhttpServerOutSock = new QZmq::Socket(QZmq::Socket::Pub, this); // handler sockets handlerInspectSock = new QZmq::Socket(QZmq::Socket::Router, this); handlerAcceptSock = new QZmq::Socket(QZmq::Socket::Router, this); handlerAcceptValve = new QZmq::Valve(handlerAcceptSock, this); handlerAcceptValveConnection = handlerAcceptValve->readyRead.connect(boost::bind(&Wrapper::handlerAccept_readyRead, this, boost::placeholders::_1)); handlerInspectValve = new QZmq::Valve(handlerInspectSock, this); handlerInspectValveConnection = handlerInspectValve->readyRead.connect(boost::bind(&Wrapper::handlerInspect_readyRead, this, boost::placeholders::_1)); handlerRetryOutSock = new QZmq::Socket(QZmq::Socket::Router, this); } void startHttp() { zhttpClientOutSock->bind("ipc://" + workDir.filePath("client-out")); zhttpClientOutStreamSock->bind("ipc://" + workDir.filePath("client-out-stream")); zhttpClientInSock->bind("ipc://" + workDir.filePath("client-in")); zhttpServerInSock->bind("ipc://" + workDir.filePath("server-in")); zhttpServerInStreamSock->bind("ipc://" + workDir.filePath("server-in-stream")); zhttpServerOutSock->bind("ipc://" + workDir.filePath("server-out")); zhttpClientInSock->subscribe("test-client "); zhttpClientInValve->open(); zhttpServerInValve->open(); zhttpServerInStreamValve->open(); } void startHandler() { handlerInspectSock->connectToAddress("ipc://" + workDir.filePath("inspect")); handlerAcceptSock->connectToAddress("ipc://" + workDir.filePath("accept")); handlerRetryOutSock->connectToAddress("ipc://" + workDir.filePath("retry-out")); handlerInspectValve->open(); handlerAcceptValve->open(); } void reset() { serverReqs.clear(); isWs = false; serverFailed = false; inspectEnabled = true; inspected = false; sharingKey.clear(); in.clear(); acceptHeaders.clear(); acceptIn.clear(); retried = false; finished = false; serverOutSeq = 0; clientReqsFinished = 0; requestBody.clear(); responses.clear(); } private: void zhttpClientIn_readyRead(const QList &message) { log_debug("client in"); int at = message[0].indexOf(' '); QVariant v = TnetString::toVariant(message[0].mid(at + 2)); ZhttpResponsePacket zresp; zresp.fromVariant(v); if(zresp.type == ZhttpResponsePacket::Data) { if(!responses.contains(zresp.ids.first().id)) { HttpResponseData rd; rd.code = zresp.code; rd.reason = zresp.reason; rd.headers = zresp.headers; responses[zresp.ids.first().id] = rd; } responses[zresp.ids.first().id].body += zresp.body; in += zresp.body; if(!isWs && !zresp.more) { finished = true; ++clientReqsFinished; } } else if(zresp.type == ZhttpResponsePacket::HandoffStart) { ZhttpRequestPacket zreq; zreq.from = "test-client"; zreq.ids += ZhttpRequestPacket::Id(zresp.ids.first().id, 1); zreq.type = ZhttpRequestPacket::HandoffProceed; QByteArray buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); QList msg; msg.append("proxy"); msg.append(QByteArray()); msg.append(buf); zhttpClientOutStreamSock->write(msg); } else if(zresp.type == ZhttpResponsePacket::Close) { finished = true; ++clientReqsFinished; } } void zhttpServerIn_readyRead(const QList &message) { log_debug("server in"); QVariant v = TnetString::toVariant(message[0].mid(1)); ZhttpRequestPacket zreq; zreq.fromVariant(v); HttpRequestData rd; rd.method = zreq.method; rd.uri = zreq.uri; rd.headers = zreq.headers; serverReqs[zreq.ids[0].id] = rd; handleServerIn(zreq); } void zhttpServerInStream_readyRead(const QList &message) { log_debug("server stream in"); QVariant v = TnetString::toVariant(message[2].mid(1)); ZhttpRequestPacket zreq; zreq.fromVariant(v); handleServerIn(zreq); } void handleServerIn(const ZhttpRequestPacket &zreq) { if(zreq.type == ZhttpRequestPacket::Close || zreq.type == ZhttpRequestPacket::Credit) { return; } if(zreq.type == ZhttpRequestPacket::Cancel) { serverFailed = true; return; } serverReqs[zreq.ids[0].id].body += zreq.body; if(zreq.type == ZhttpRequestPacket::Data) requestBody += zreq.body; if(zreq.more) { // ack if(serverOutSeq == 0) { ZhttpResponsePacket zresp; zresp.from = "test-server"; zresp.ids += ZhttpResponsePacket::Id(zreq.ids.first().id, serverOutSeq++); zresp.type = ZhttpResponsePacket::Credit; zresp.credits = 200000; QByteArray buf = zreq.from + " T" + TnetString::fromVariant(zresp.toVariant()); zhttpServerOutSock->write(QList() << buf); } return; } ZhttpResponsePacket zresp; zresp.from = "test-server"; zresp.ids += ZhttpResponsePacket::Id(zreq.ids.first().id, serverOutSeq++); zresp.type = ZhttpResponsePacket::Data; if(!isWs && zreq.uri.scheme() == "ws") { isWs = true; // accept websocket zresp.code = 101; zresp.reason = "Switching Protocols"; zresp.credits = 200000; QByteArray buf = zreq.from + " T" + TnetString::fromVariant(zresp.toVariant()); zhttpServerOutSock->write(QList() << buf); // send message zresp.ids[0].seq = serverOutSeq++; zresp.credits = -1; zresp.code = -1; zresp.reason.clear(); zresp.body = "hello world"; buf = zreq.from + " T" + TnetString::fromVariant(zresp.toVariant()); zhttpServerOutSock->write(QList() << buf); return; } if(isWs) { // close zresp.type = ZhttpResponsePacket::Close; QByteArray buf = zreq.from + " T" + TnetString::fromVariant(zresp.toVariant()); zhttpServerOutSock->write(QList() << buf); return; } zresp.code = 200; zresp.reason = "OK"; QByteArray encPath = zreq.uri.path(QUrl::FullyEncoded).toUtf8(); QUrlQuery query(zreq.uri.query()); QString hold = query.queryItemValue("hold"); bool bodyInstruct = (query.queryItemValue("body-instruct") == "true"); bool large = (query.queryItemValue("large") == "true"); if(!retried && (hold == "response" || hold == "stream")) { if(encPath == "/path2") { if(bodyInstruct) { zresp.headers += HttpHeader("Content-Type", "application/grip-instruct"); zresp.body = "{ \"hold\": { \"mode\": \"response\", \"channels\": [ { \"name\": \"test-channel\", \"prev-id\": \"1\" } ] } }"; } else { zresp.headers += HttpHeader("Grip-Hold", "response"); zresp.headers += HttpHeader("Grip-Channel", "test-channel; prev-id=1"); } } else { if(bodyInstruct) { zresp.headers += HttpHeader("Content-Type", "application/grip-instruct"); zresp.body = "{ \"hold\": { \"mode\": \"response\", \"channels\": [ { \"name\": \"test-channel\" } ] } }"; } else { if(hold == "stream") { zresp.headers += HttpHeader("Grip-Hold", "stream"); zresp.headers += HttpHeader("Grip-Channel", "test-channel"); if(large) zresp.body = QByteArray(PROXY_MAX_ACCEPT_RESPONSE_BODY + 10000, 'a') + '\n'; else zresp.body = "stream open\n"; } else { zresp.headers += HttpHeader("Grip-Hold", "response"); zresp.headers += HttpHeader("Grip-Channel", "test-channel"); } } } } else { if(encPath.startsWith("/jsonp")) { zresp.headers += HttpHeader("Content-Type", "application/json"); zresp.body = "{\"hello\": \"world\"}"; } else if(encPath == "/path3") { zresp.headers += HttpHeader("Content-Type", "text/plain"); zresp.body = "next page"; } else { if(hold == "none") { if(bodyInstruct) { zresp.headers += HttpHeader("Content-Type", "application/grip-instruct"); zresp.body = "{ \"response\": { \"body\": \"hello world\" } }"; } else { zresp.headers += HttpHeader("Content-Type", "text/plain"); if(large) { // Grip-Link required to trigger accept after // sending large response. note that the link // won't be followed in this test since that's // not a proxy issue zresp.headers += HttpHeader("Grip-Link", "; rel=next"); zresp.body = QByteArray(PROXY_MAX_ACCEPT_RESPONSE_BODY + 10000, 'a') + '\n'; } else { zresp.headers += HttpHeader("Grip-Foo", "bar"); // something to trigger accept zresp.body = "hello world"; } } } else { zresp.headers += HttpHeader("Content-Type", "text/plain"); zresp.body = "hello world"; } } } zresp.headers += HttpHeader("Content-Length", QByteArray::number(zresp.body.size())); QByteArray buf = zreq.from + " T" + TnetString::fromVariant(zresp.toVariant()); zhttpServerOutSock->write(QList() << buf); // zero out so we can accept another request serverOutSeq = 0; } void handlerInspect_readyRead(const QList &_message) { QZmq::ReqMessage message(_message); QVariant v = TnetString::toVariant(message.content()[0]); log_debug("inspect: %s", qPrintable(TnetString::variantToString(v, -1))); inspected = true; if(inspectEnabled) { QVariantHash vreq = v.toHash(); QVariantHash args = vreq["args"].toHash(); QVariantHash respValue; respValue["no-proxy"] = false; if(!sharingKey.isEmpty()) respValue["sharing-key"] = sharingKey; QVariantHash vresp; vresp["id"] = vreq["id"]; vresp["success"] = true; vresp["value"] = respValue; log_debug("inspect response: %s", qPrintable(TnetString::variantToString(vresp, -1))); handlerInspectSock->write(message.createReply(QList() << TnetString::fromVariant(vresp)).toRawMessage()); } } void handlerAccept_readyRead(const QList &_message) { QZmq::ReqMessage message(_message); QVariant v = TnetString::toVariant(message.content()[0]); log_debug("accept: %s", qPrintable(TnetString::variantToString(v, -1))); QVariantHash vreq = v.toHash(); if(vreq["method"].toString() != "accept") return; QVariantHash vaccept = vreq["args"].toHash(); QVariantHash vresponse = vaccept["response"].toHash(); acceptHeaders.clear(); foreach(const QVariant &vheader, vresponse["headers"].toList()) { QVariantList h = vheader.toList(); acceptHeaders += HttpHeader(h[0].toByteArray(), h[1].toByteArray()); } acceptIn = vresponse["body"].toByteArray(); QVariantMap jsonInstruct; QByteArray hold; if(acceptHeaders.get("Content-Type") == "application/grip-instruct") { QJsonParseError e; QJsonDocument doc = QJsonDocument::fromJson(acceptIn, &e); QVERIFY(e.error == QJsonParseError::NoError); QVERIFY(doc.isObject()); jsonInstruct = doc.object().toVariantMap(); if(jsonInstruct.contains("hold")) hold = jsonInstruct["hold"].toMap().value("mode").toString().toUtf8(); } else { hold = acceptHeaders.get("Grip-Hold"); } QVariantList vheaders = vresponse["headers"].toList(); for(int n = 0; n < vheaders.count(); ++n) { QVariantList h = vheaders[n].toList(); if(h[0].toByteArray().startsWith("Grip-")) { vheaders.removeAt(n); --n; // adjust position } } vresponse["headers"] = vheaders; QVariantHash vresp; vresp["id"] = vreq["id"]; vresp["success"] = true; QVariantHash respValue; if(!hold.isEmpty()) respValue["accepted"] = true; else if(!vaccept.value("response-sent").toBool()) respValue["response"] = vresponse; vresp["value"] = respValue; handlerAcceptSock->write(message.createReply(QList() << TnetString::fromVariant(vresp)).toRawMessage()); log_debug("instruct: [%s]", acceptIn.data()); if(acceptHeaders.get("Content-Type") == "application/grip-instruct") { if(jsonInstruct.contains("hold") && jsonInstruct["hold"].toMap()["channels"].toList()[0].toMap().contains("prev-id")) { retried = true; QVariantHash vretry; vretry["requests"] = vaccept["requests"]; vretry["request-data"] = vaccept["request-data"]; QByteArray buf = TnetString::fromVariant(vretry); log_debug("retrying: %s", qPrintable(TnetString::variantToString(vretry, -1))); QList msg; msg.append("proxy"); msg.append(QByteArray()); msg.append(buf); handlerRetryOutSock->write(msg); return; } } if(!hold.isEmpty()) finished = true; } }; } class ProxyEngineTest : public QObject { Q_OBJECT private: DomainMap *domainMap; Engine *engine; Wrapper *wrapper; QList trackedPackets; private: void reset() { wrapper->reset(); engine->statsManager()->flushReport(QByteArray()); trackedPackets.clear(); } void appendTrackedPackets(const QList& packets) { trackedPackets.append(packets); } private slots: void initTestCase() { qRegisterMetaType>(); log_setOutputLevel(LOG_LEVEL_WARNING); //log_setOutputLevel(LOG_LEVEL_DEBUG); QDir rootDir(qgetenv("CARGO_MANIFEST_DIR")); QDir configDir(rootDir.filePath("src/cpp/tests")); QDir outDir(qgetenv("OUT_DIR")); QDir workDir(QDir::current().relativeFilePath(outDir.filePath("test-work"))); wrapper = new Wrapper(this, workDir); wrapper->startHttp(); domainMap = new DomainMap(configDir.filePath("routes"), this); engine = new Engine(domainMap, this); Engine::Configuration config; config.clientId = "proxy"; config.serverInSpecs = QStringList() << ("ipc://" + workDir.filePath("client-out")); config.serverInStreamSpecs = QStringList() << ("ipc://" + workDir.filePath("client-out-stream")); config.serverOutSpecs = QStringList() << ("ipc://" + workDir.filePath("client-in")); config.clientOutSpecs = QStringList() << ("ipc://" + workDir.filePath("server-in")); config.clientOutStreamSpecs = QStringList() << ("ipc://" + workDir.filePath("server-in-stream")); config.clientInSpecs = QStringList() << ("ipc://" + workDir.filePath("server-out")); config.inspectSpec = ("ipc://" + workDir.filePath("inspect")); config.acceptSpec = ("ipc://" + workDir.filePath("accept")); config.retryInSpec = ("ipc://" + workDir.filePath("retry-out")); config.statsSpec = ("ipc://" + workDir.filePath("stats")); config.sessionsMax = 20; config.inspectTimeout = 500; config.inspectPrefetch = 5; config.sigIss = "pushpin"; config.sigKey = Jwt::EncodingKey::fromSecret("changeme"); config.statsConnectionTtl = 120; config.statsReportInterval = 1000; // set a large interval so there's only one working report QVERIFY(engine->start(config)); wrapper->startHandler(); QTest::qWait(500); } void cleanupTestCase() { delete engine; delete domainMap; delete wrapper; QCoreApplication::instance()->sendPostedEvents(); RTimer::deinit(); } void passthrough() { reset(); boost::signals2::scoped_connection reportConnection = engine->statsManager()->reported.connect( boost::bind(&ProxyEngineTest::appendTrackedPackets, this, boost::placeholders::_1) ); ZhttpRequestPacket zreq; zreq.from = "test-client"; zreq.ids += ZhttpRequestPacket::Id("1", 0); zreq.type = ZhttpRequestPacket::Data; zreq.uri = "http://example/path?a=b"; zreq.method = "GET"; zreq.headers += HttpHeader("Host", "example"); zreq.stream = true; zreq.credits = 200000; QByteArray buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); while(!wrapper->finished) QTest::qWait(10); engine->statsManager()->flushReport(QByteArray()); QCOMPARE(wrapper->serverReqs.count(), 1); QCOMPARE(wrapper->in, QByteArray("hello world")); HttpRequestData reqData = QHashIterator(wrapper->serverReqs).next().value(); QCOMPARE(trackedPackets.size(), 1); StatsPacket p = trackedPackets.takeFirst(); QCOMPARE(p.clientHeaderBytesReceived, 23); // "GET" + "/path?a=b" + "Host" + "example" QCOMPARE(p.clientContentBytesReceived, 0); QCOMPARE(p.clientHeaderBytesSent, 43); // "200" + "OK" + "Content-Type" + "text/plain" + "Content-Length" + "11" QCOMPARE(p.clientContentBytesSent, 11); // "hello world" QCOMPARE(p.serverHeaderBytesSent, ZhttpManager::estimateRequestHeaderBytes(reqData.method, reqData.uri, reqData.headers)); QCOMPARE(p.serverContentBytesSent, 0); QCOMPARE(p.serverHeaderBytesReceived, 43); // "200" + "OK" + "Content-Type" + "text/plain" + "Content-Length" + "11" QCOMPARE(p.serverContentBytesReceived, 11); // "hello world" } void passthroughWithoutInspect() { reset(); wrapper->inspectEnabled = false; ZhttpRequestPacket zreq; zreq.from = "test-client"; zreq.ids += ZhttpRequestPacket::Id("2", 0); zreq.type = ZhttpRequestPacket::Data; zreq.uri = "http://example/path"; zreq.method = "GET"; zreq.stream = true; zreq.credits = 200000; QByteArray buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); while(!wrapper->finished) QTest::qWait(10); QCOMPARE(wrapper->in, QByteArray("hello world")); } void passthroughJsonp() { reset(); ZhttpRequestPacket zreq; zreq.from = "test-client"; zreq.ids += ZhttpRequestPacket::Id("3", 0); zreq.type = ZhttpRequestPacket::Data; zreq.uri = "http://example/jsonp?callback=jpcb"; zreq.method = "GET"; zreq.stream = true; zreq.credits = 200000; QByteArray buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); while(!wrapper->finished) QTest::qWait(10); QVERIFY(wrapper->in.startsWith("/**/jpcb({")); QVERIFY(wrapper->in.endsWith("});\n")); QByteArray dataRaw = wrapper->in.mid(9, wrapper->in.size() - 9 - 3); QJsonParseError e; QJsonDocument doc = QJsonDocument::fromJson(dataRaw, &e); QVERIFY(e.error == QJsonParseError::NoError); QVERIFY(doc.isObject()); QVariantMap data = doc.object().toVariantMap(); QCOMPARE(data["code"].toInt(), 200); QCOMPARE(data["body"].toByteArray(), QByteArray("{\"hello\": \"world\"}")); } void passthroughJsonpBasic() { reset(); ZhttpRequestPacket zreq; zreq.from = "test-client"; zreq.ids += ZhttpRequestPacket::Id("4", 0); zreq.type = ZhttpRequestPacket::Data; zreq.uri = "http://example/jsonp-basic?bparam={}"; zreq.method = "GET"; zreq.stream = true; zreq.credits = 200000; QByteArray buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); while(!wrapper->finished) QTest::qWait(10); QCOMPARE(wrapper->in, QByteArray("/**/jpcb({\"hello\": \"world\"});\n")); } void passthroughPostStream() { reset(); boost::signals2::scoped_connection reportConnection = engine->statsManager()->reported.connect( boost::bind(&ProxyEngineTest::appendTrackedPackets, this, boost::placeholders::_1) ); ZhttpRequestPacket zreq; zreq.from = "test-client"; zreq.ids += ZhttpRequestPacket::Id("5", 0); zreq.type = ZhttpRequestPacket::Data; zreq.uri = "http://example/path"; zreq.method = "POST"; zreq.stream = true; zreq.body = "hello"; // enough to hit the prefetch amount zreq.more = true; zreq.credits = 200000; QByteArray buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); // ensure the server gets hit without finishing the request while(wrapper->serverReqs.count() < 1) QTest::qWait(10); // now finish the request zreq = ZhttpRequestPacket(); zreq.from = "test-client"; zreq.ids += ZhttpRequestPacket::Id("5", 1); zreq.type = ZhttpRequestPacket::Data; zreq.body = " world"; buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); QList msg; msg.append("proxy"); msg.append(QByteArray()); msg.append(buf); wrapper->zhttpClientOutStreamSock->write(msg); while(!wrapper->finished) QTest::qWait(10); engine->statsManager()->flushReport(QByteArray()); QCOMPARE(wrapper->requestBody, QByteArray("hello world")); QCOMPARE(wrapper->responses["5"].body, QByteArray("hello world")); HttpRequestData reqData = QHashIterator(wrapper->serverReqs).next().value(); QCOMPARE(trackedPackets.size(), 1); StatsPacket p = trackedPackets.takeFirst(); QCOMPARE(p.clientHeaderBytesReceived, 9); // "POST" + "/path" QCOMPARE(p.clientContentBytesReceived, 11); // "hello world" QCOMPARE(p.clientHeaderBytesSent, 43); // "200" + "OK" + "Content-Type" + "text/plain" + "Content-Length" + "11" QCOMPARE(p.clientContentBytesSent, 11); // "hello world" QCOMPARE(p.serverHeaderBytesSent, ZhttpManager::estimateRequestHeaderBytes(reqData.method, reqData.uri, reqData.headers)); QCOMPARE(p.serverContentBytesSent, 11); QCOMPARE(p.serverHeaderBytesReceived, 43); // "200" + "OK" + "Content-Type" + "text/plain" + "Content-Length" + "11" QCOMPARE(p.serverContentBytesReceived, 11); // "hello world" } void passthroughPostStreamFail() { reset(); boost::signals2::scoped_connection reportConnection = engine->statsManager()->reported.connect( boost::bind(&ProxyEngineTest::appendTrackedPackets, this, boost::placeholders::_1) ); ZhttpRequestPacket zreq; zreq.from = "test-client"; zreq.ids += ZhttpRequestPacket::Id("6", 0); zreq.type = ZhttpRequestPacket::Data; zreq.uri = "http://example/path"; zreq.method = "POST"; zreq.stream = true; zreq.body = "hello"; // enough to hit the prefetch amount zreq.more = true; zreq.credits = 200000; QByteArray buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); // ensure the server gets hit without finishing the request while(wrapper->serverReqs.count() < 1) QTest::qWait(10); // now cancel the request zreq = ZhttpRequestPacket(); zreq.from = "test-client"; zreq.ids += ZhttpRequestPacket::Id("6", 1); zreq.type = ZhttpRequestPacket::Cancel; buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); QList msg; msg.append("proxy"); msg.append(QByteArray()); msg.append(buf); wrapper->zhttpClientOutStreamSock->write(msg); // wait for server side to receive error while(!wrapper->serverFailed) QTest::qWait(10); engine->statsManager()->flushReport(QByteArray()); HttpRequestData reqData = QHashIterator(wrapper->serverReqs).next().value(); QCOMPARE(trackedPackets.size(), 1); StatsPacket p = trackedPackets.takeFirst(); QCOMPARE(p.clientHeaderBytesReceived, 9); // "POST" + "/path" QCOMPARE(p.clientContentBytesReceived, 5); // "hello" QCOMPARE(p.clientHeaderBytesSent, 0); QCOMPARE(p.clientContentBytesSent, 0); QCOMPARE(p.serverHeaderBytesSent, ZhttpManager::estimateRequestHeaderBytes(reqData.method, reqData.uri, reqData.headers)); QCOMPARE(p.serverContentBytesSent, 5); // "hello" QCOMPARE(p.serverHeaderBytesReceived, 0); QCOMPARE(p.serverContentBytesReceived, 0); } void acceptResponse() { reset(); boost::signals2::scoped_connection reportConnection = engine->statsManager()->reported.connect( boost::bind(&ProxyEngineTest::appendTrackedPackets, this, boost::placeholders::_1) ); ZhttpRequestPacket zreq; zreq.from = "test-client"; zreq.ids += ZhttpRequestPacket::Id("7", 0); zreq.type = ZhttpRequestPacket::Data; zreq.uri = "http://example/path?hold=response"; zreq.method = "GET"; zreq.stream = true; zreq.credits = 200000; QByteArray buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); while(!wrapper->finished) QTest::qWait(10); engine->statsManager()->flushReport(QByteArray()); QCOMPARE(wrapper->acceptHeaders.get("Grip-Hold"), QByteArray("response")); QCOMPARE(wrapper->acceptHeaders.get("Grip-Channel"), QByteArray("test-channel")); QVERIFY(wrapper->acceptIn.isEmpty()); HttpRequestData reqData = QHashIterator(wrapper->serverReqs).next().value(); QCOMPARE(trackedPackets.size(), 1); StatsPacket p = trackedPackets.takeFirst(); QCOMPARE(p.clientHeaderBytesReceived, 22); // "GET" + "/path?hold=response" QCOMPARE(p.clientContentBytesReceived, 0); QCOMPARE(p.clientHeaderBytesSent, 0); QCOMPARE(p.clientContentBytesSent, 0); QCOMPARE(p.serverHeaderBytesSent, ZhttpManager::estimateRequestHeaderBytes(reqData.method, reqData.uri, reqData.headers)); QCOMPARE(p.serverContentBytesSent, 0); QCOMPARE(p.serverHeaderBytesReceived, 61); // "200" + "OK" + "Grip-Hold" + "response" + "Grip-Channel" + "test-channel" + "Content-Length" + "0" QCOMPARE(p.serverContentBytesReceived, 0); } void acceptStream() { reset(); boost::signals2::scoped_connection reportConnection = engine->statsManager()->reported.connect( boost::bind(&ProxyEngineTest::appendTrackedPackets, this, boost::placeholders::_1) ); ZhttpRequestPacket zreq; zreq.from = "test-client"; zreq.ids += ZhttpRequestPacket::Id("8", 0); zreq.type = ZhttpRequestPacket::Data; zreq.uri = "http://example/path?hold=stream"; zreq.method = "GET"; zreq.stream = true; zreq.credits = 200000; QByteArray buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); while(!wrapper->finished) QTest::qWait(10); engine->statsManager()->flushReport(QByteArray()); QCOMPARE(wrapper->acceptHeaders.get("Grip-Hold"), QByteArray("stream")); QCOMPARE(wrapper->acceptHeaders.get("Grip-Channel"), QByteArray("test-channel")); QCOMPARE(wrapper->acceptIn, QByteArray("stream open\n")); QVERIFY(wrapper->in.isEmpty()); HttpRequestData reqData = QHashIterator(wrapper->serverReqs).next().value(); QCOMPARE(trackedPackets.size(), 1); StatsPacket p = trackedPackets.takeFirst(); QCOMPARE(p.clientHeaderBytesReceived, 20); // "GET" + "/path?hold=stream" QCOMPARE(p.clientContentBytesReceived, 0); QCOMPARE(p.clientHeaderBytesSent, 0); QCOMPARE(p.clientContentBytesSent, 0); QCOMPARE(p.serverHeaderBytesSent, ZhttpManager::estimateRequestHeaderBytes(reqData.method, reqData.uri, reqData.headers)); QCOMPARE(p.serverContentBytesSent, 0); QCOMPARE(p.serverHeaderBytesReceived, 60); // "200" + "OK" + "Grip-Hold" + "stream" + "Grip-Channel" + "test-channel" + "Content-Length" + "12" QCOMPARE(p.serverContentBytesReceived, 12); // "stream open\n" } void acceptResponseBodyInstruct() { reset(); ZhttpRequestPacket zreq; zreq.from = "test-client"; zreq.ids += ZhttpRequestPacket::Id("9", 0); zreq.type = ZhttpRequestPacket::Data; zreq.uri = "http://example/path?hold=response&body-instruct=true"; zreq.method = "GET"; zreq.stream = true; zreq.credits = 200000; QByteArray buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); while(!wrapper->finished) QTest::qWait(10); QCOMPARE(wrapper->acceptIn, QByteArray("{ \"hold\": { \"mode\": \"response\", \"channels\": [ { \"name\": \"test-channel\" } ] } }")); } void acceptNoHold() { reset(); boost::signals2::scoped_connection reportConnection = engine->statsManager()->reported.connect( boost::bind(&ProxyEngineTest::appendTrackedPackets, this, boost::placeholders::_1) ); ZhttpRequestPacket zreq; zreq.from = "test-client"; zreq.ids += ZhttpRequestPacket::Id("10", 0); zreq.type = ZhttpRequestPacket::Data; zreq.uri = "http://example/path?hold=none"; zreq.method = "GET"; zreq.stream = true; zreq.credits = 200000; QByteArray buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); while(!wrapper->finished) QTest::qWait(10); engine->statsManager()->flushReport(QByteArray()); QCOMPARE(wrapper->in, QByteArray("hello world")); HttpRequestData reqData = QHashIterator(wrapper->serverReqs).next().value(); QCOMPARE(trackedPackets.size(), 1); StatsPacket p = trackedPackets.takeFirst(); QCOMPARE(p.clientHeaderBytesReceived, 18); // "GET" + "/path?hold=none" QCOMPARE(p.clientContentBytesReceived, 0); QCOMPARE(p.clientHeaderBytesSent, 43); // "200" + "OK" + "Content-Type" + "text/plain" + "Content-Length" + "11" QCOMPARE(p.clientContentBytesSent, 11); // "hello world" QCOMPARE(p.serverHeaderBytesSent, ZhttpManager::estimateRequestHeaderBytes(reqData.method, reqData.uri, reqData.headers)); QCOMPARE(p.serverContentBytesSent, 0); QCOMPARE(p.serverHeaderBytesReceived, 54); // "200" + "OK" + "Content-Type" + "text/plain" + "Grip-Foo" + "bar" + "Content-Length" + "11" QCOMPARE(p.serverContentBytesReceived, 11); // "hello world" } void acceptNoHoldBodyInstruct() { reset(); ZhttpRequestPacket zreq; zreq.from = "test-client"; zreq.ids += ZhttpRequestPacket::Id("11", 0); zreq.type = ZhttpRequestPacket::Data; zreq.uri = "http://example/path?hold=none&body-instruct=true"; zreq.method = "GET"; zreq.stream = true; zreq.credits = 200000; QByteArray buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); while(!wrapper->finished) QTest::qWait(10); QCOMPARE(wrapper->acceptIn, QByteArray("{ \"response\": { \"body\": \"hello world\" } }")); } void passthroughThenAcceptStream() { reset(); boost::signals2::scoped_connection reportConnection = engine->statsManager()->reported.connect( boost::bind(&ProxyEngineTest::appendTrackedPackets, this, boost::placeholders::_1) ); ZhttpRequestPacket zreq; zreq.from = "test-client"; zreq.ids += ZhttpRequestPacket::Id("12", 0); zreq.type = ZhttpRequestPacket::Data; zreq.uri = "http://example/path?hold=stream&large=true"; zreq.method = "GET"; zreq.stream = true; zreq.credits = 200000; QByteArray buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); while(!wrapper->finished) QTest::qWait(10); engine->statsManager()->flushReport(QByteArray()); QCOMPARE(wrapper->in.size(), 110001); QCOMPARE(wrapper->in.mid(wrapper->in.size() - 2), QByteArray("a\n")); QCOMPARE(wrapper->acceptHeaders.get("Grip-Hold"), QByteArray("stream")); QCOMPARE(wrapper->acceptHeaders.get("Grip-Channel"), QByteArray("test-channel")); QVERIFY(wrapper->acceptIn.isEmpty()); HttpRequestData reqData = QHashIterator(wrapper->serverReqs).next().value(); QCOMPARE(trackedPackets.size(), 1); StatsPacket p = trackedPackets.takeFirst(); QCOMPARE(p.clientHeaderBytesReceived, 31); // "GET" + "/path?hold=stream&large=true" QCOMPARE(p.clientContentBytesReceived, 0); QCOMPARE(p.clientHeaderBytesSent, 5); // "200" + "OK" QCOMPARE(p.clientContentBytesSent, 110001); QCOMPARE(p.serverHeaderBytesSent, ZhttpManager::estimateRequestHeaderBytes(reqData.method, reqData.uri, reqData.headers)); QCOMPARE(p.serverContentBytesSent, 0); QCOMPARE(p.serverHeaderBytesReceived, 64); // "200" + "OK" + "Grip-Hold" + "stream" + "Grip-Channel" + "test-channel" + "Content-Length" + "110001" QCOMPARE(p.serverContentBytesReceived, 110001); } void passthroughThenAcceptNext() { reset(); boost::signals2::scoped_connection reportConnection = engine->statsManager()->reported.connect( boost::bind(&ProxyEngineTest::appendTrackedPackets, this, boost::placeholders::_1) ); ZhttpRequestPacket zreq; zreq.from = "test-client"; zreq.ids += ZhttpRequestPacket::Id("13", 0); zreq.type = ZhttpRequestPacket::Data; zreq.uri = "http://example/path?hold=none&large=true"; zreq.method = "GET"; zreq.stream = true; zreq.credits = 200000; QByteArray buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); while(!wrapper->finished) QTest::qWait(10); engine->statsManager()->flushReport(QByteArray()); QCOMPARE(wrapper->in.size(), 110001); QCOMPARE(wrapper->in.mid(wrapper->in.size() - 2), QByteArray("a\n")); QVERIFY(wrapper->acceptIn.isEmpty()); QCOMPARE(wrapper->acceptHeaders.get("Grip-Link"), QByteArray("; rel=next")); HttpRequestData reqData = QHashIterator(wrapper->serverReqs).next().value(); QCOMPARE(trackedPackets.size(), 1); StatsPacket p = trackedPackets.takeFirst(); QCOMPARE(p.clientHeaderBytesReceived, 29); // "GET" + "/path?hold=none&large=true" QCOMPARE(p.clientContentBytesReceived, 0); QCOMPARE(p.clientHeaderBytesSent, 27); // "200" + "OK" + "Content-Type" + "text/plain" QCOMPARE(p.clientContentBytesSent, 110001); QCOMPARE(p.serverHeaderBytesSent, ZhttpManager::estimateRequestHeaderBytes(reqData.method, reqData.uri, reqData.headers)); QCOMPARE(p.serverContentBytesSent, 0); QCOMPARE(p.serverHeaderBytesReceived, 74); // "200" + "OK" + "Content-Type" + "text/plain" + "Grip-Link" + "; rel=next" + "Content-Length" + "110001" QCOMPARE(p.serverContentBytesReceived, 110001); } void acceptWithRetry() { reset(); boost::signals2::scoped_connection reportConnection = engine->statsManager()->reported.connect( boost::bind(&ProxyEngineTest::appendTrackedPackets, this, boost::placeholders::_1) ); ZhttpRequestPacket zreq; zreq.from = "test-client"; zreq.ids += ZhttpRequestPacket::Id("14", 0); zreq.type = ZhttpRequestPacket::Data; zreq.uri = "http://example/path2?hold=response&body-instruct=true"; zreq.method = "GET"; zreq.stream = true; zreq.credits = 200000; QByteArray buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); while(!wrapper->finished) QTest::qWait(10); engine->statsManager()->flushReport(QByteArray()); QCOMPARE(wrapper->in, QByteArray("hello world")); QCOMPARE(wrapper->serverReqs.count(), 2); QHashIterator it(wrapper->serverReqs); HttpRequestData req1Data = it.next().value(); HttpRequestData req2Data = it.next().value(); int headerBytes = 0; int contentBytes = 0; headerBytes += ZhttpManager::estimateRequestHeaderBytes(req1Data.method, req1Data.uri, req1Data.headers); contentBytes += req1Data.body.size(); headerBytes += ZhttpManager::estimateRequestHeaderBytes(req2Data.method, req2Data.uri, req2Data.headers); contentBytes += req2Data.body.size(); QCOMPARE(trackedPackets.size(), 1); StatsPacket p = trackedPackets.takeFirst(); QCOMPARE(p.clientHeaderBytesReceived, 42); // "GET" + "/path2?hold=response&body-instruct=true" QCOMPARE(p.clientContentBytesReceived, 0); QCOMPARE(p.clientHeaderBytesSent, 43); // "200" + "OK" + "Content-Type" + "text/plain" + "Content-Length" + "11" QCOMPARE(p.clientContentBytesSent, 11); // "hello world" QCOMPARE(p.serverHeaderBytesSent, headerBytes); QCOMPARE(p.serverContentBytesSent, contentBytes); QCOMPARE(p.serverHeaderBytesReceived, 101); // "200" + "OK + "Content-Type" + "application/grip-instruct" + "Content-Length": "94" + "200" + "OK" + "Content-Type" + "text/plain" + "Content-Length" + "11" QCOMPARE(p.serverContentBytesReceived, 105); // "{ \"hold\": { \"mode\": \"response\", \"channels\": [ { \"name\": \"test-channel\", \"prev-id\": \"1\" } ] } }" + "hello world" } void passthroughShared() { reset(); boost::signals2::scoped_connection reportConnection = engine->statsManager()->reported.connect( boost::bind(&ProxyEngineTest::appendTrackedPackets, this, boost::placeholders::_1) ); wrapper->sharingKey = "test"; ZhttpRequestPacket zreq; zreq.from = "test-client"; zreq.type = ZhttpRequestPacket::Data; zreq.uri = "http://example/path"; zreq.method = "GET"; zreq.stream = true; zreq.credits = 200000; QByteArray buf; // send two requests QByteArray id1 = "15"; QByteArray id2 = "16"; zreq.ids = QList() << ZhttpRequestPacket::Id(id1, 0); buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); zreq.ids = QList() << ZhttpRequestPacket::Id(id2, 0); buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); while(wrapper->clientReqsFinished < 2) QTest::qWait(10); engine->statsManager()->flushReport(QByteArray()); // there should have only been 1 request to the server QCOMPARE(wrapper->serverReqs.count(), 1); QCOMPARE(wrapper->responses[id1].body, QByteArray("hello world")); QCOMPARE(wrapper->responses[id2].body, QByteArray("hello world")); HttpRequestData reqData = QHashIterator(wrapper->serverReqs).next().value(); QCOMPARE(trackedPackets.size(), 1); StatsPacket p = trackedPackets.takeFirst(); QCOMPARE(p.clientHeaderBytesReceived, 16); // "GET" + "/path" + "GET" + "/path" QCOMPARE(p.clientContentBytesReceived, 0); QCOMPARE(p.clientHeaderBytesSent, 86); // "200" + "OK" + "Content-Type" + "text/plain" + "Content-Length" + "11" + "200" + "OK" + "Content-Type" + "text/plain" + "Content-Length" + "11" QCOMPARE(p.clientContentBytesSent, 22); // "hello world" + "hello world" QCOMPARE(p.serverHeaderBytesSent, ZhttpManager::estimateRequestHeaderBytes(reqData.method, reqData.uri, reqData.headers)); QCOMPARE(p.serverContentBytesSent, 0); QCOMPARE(p.serverHeaderBytesReceived, 43); // "200" + "OK" + "Content-Type" + "text/plain" + "Content-Length" + "11" QCOMPARE(p.serverContentBytesReceived, 11); // "hello world" } void passthroughSharedPost() { reset(); boost::signals2::scoped_connection reportConnection = engine->statsManager()->reported.connect( boost::bind(&ProxyEngineTest::appendTrackedPackets, this, boost::placeholders::_1) ); wrapper->sharingKey = "test"; ZhttpRequestPacket zreq; zreq.from = "test-client"; zreq.type = ZhttpRequestPacket::Data; zreq.uri = "http://example/path"; zreq.method = "POST"; zreq.stream = true; zreq.body = "hello"; // enough to hit the prefetch amount zreq.more = true; zreq.credits = 200000; QByteArray buf; // send two requests QByteArray id1 = "17"; QByteArray id2 = "18"; zreq.ids = QList() << ZhttpRequestPacket::Id(id1, 0); buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); zreq.ids = QList() << ZhttpRequestPacket::Id(id2, 0); buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); // we've hit prefetch, wait for inspect while(!wrapper->inspected) QTest::qWait(10); // finish the requests zreq = ZhttpRequestPacket(); zreq.from = "test-client"; zreq.type = ZhttpRequestPacket::Data; zreq.body = " world"; zreq.ids = QList() << ZhttpRequestPacket::Id(id1, 1); buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); QList msg; msg.append("proxy"); msg.append(QByteArray()); msg.append(buf); wrapper->zhttpClientOutStreamSock->write(msg); zreq.ids = QList() << ZhttpRequestPacket::Id(id2, 1); buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); msg.clear(); msg.append("proxy"); msg.append(QByteArray()); msg.append(buf); wrapper->zhttpClientOutStreamSock->write(msg); while(wrapper->clientReqsFinished < 2) QTest::qWait(10); engine->statsManager()->flushReport(QByteArray()); // there should have only been 1 request to the server QCOMPARE(wrapper->serverReqs.count(), 1); QCOMPARE(wrapper->responses[id1].body, QByteArray("hello world")); QCOMPARE(wrapper->responses[id2].body, QByteArray("hello world")); HttpRequestData reqData = QHashIterator(wrapper->serverReqs).next().value(); QCOMPARE(trackedPackets.size(), 1); StatsPacket p = trackedPackets.takeFirst(); QCOMPARE(p.clientHeaderBytesReceived, 18); // "POST" + "/path" + "POST" + "/path" QCOMPARE(p.clientContentBytesReceived, 22); // "hello world" + "hello world" QCOMPARE(p.clientHeaderBytesSent, 86); // "200" + "OK" + "Content-Type" + "text/plain" + "Content-Length" + "11" + "200" + "OK" + "Content-Type" + "text/plain" + "Content-Length" + "11" QCOMPARE(p.clientContentBytesSent, 22); // "hello world" + "hello world" QCOMPARE(p.serverHeaderBytesSent, ZhttpManager::estimateRequestHeaderBytes(reqData.method, reqData.uri, reqData.headers)); QCOMPARE(p.serverContentBytesSent, 11); // "hello world" QCOMPARE(p.serverHeaderBytesReceived, 43); // "200" + "OK" + "Content-Type" + "text/plain" + "Content-Length" + "11" QCOMPARE(p.serverContentBytesReceived, 11); // "hello world" } void passthroughWs() { reset(); boost::signals2::scoped_connection reportConnection = engine->statsManager()->reported.connect( boost::bind(&ProxyEngineTest::appendTrackedPackets, this, boost::placeholders::_1) ); ZhttpRequestPacket zreq; zreq.from = "test-client"; zreq.ids += ZhttpRequestPacket::Id("19", 0); zreq.type = ZhttpRequestPacket::Data; zreq.uri = "ws://example/path"; zreq.stream = true; zreq.credits = 200000; QByteArray buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); wrapper->zhttpClientOutSock->write(QList() << buf); while(!wrapper->isWs) QTest::qWait(10); zreq = ZhttpRequestPacket(); zreq.from = "test-client"; zreq.type = ZhttpRequestPacket::Data; zreq.body = "hello"; zreq.ids = QList() << ZhttpRequestPacket::Id("19", 1); buf = 'T' + TnetString::fromVariant(zreq.toVariant()); log_debug("writing: %s", buf.data()); QList msg; msg.append("proxy"); msg.append(QByteArray()); msg.append(buf); wrapper->zhttpClientOutStreamSock->write(msg); while(!wrapper->finished) QTest::qWait(10); engine->statsManager()->flushReport(QByteArray()); QCOMPARE(wrapper->serverReqs.count(), 1); QCOMPARE(wrapper->in, QByteArray("hello world")); HttpRequestData reqData = QHashIterator(wrapper->serverReqs).next().value(); QCOMPARE(trackedPackets.size(), 1); StatsPacket p = trackedPackets.takeFirst(); QCOMPARE(p.clientHeaderBytesReceived, 8); // "GET" + "/path" QCOMPARE(p.clientContentBytesReceived, 5); QCOMPARE(p.clientHeaderBytesSent, 22); // "101" + "Switching Protocols" QCOMPARE(p.clientContentBytesSent, 11); // "hello world" QCOMPARE(p.serverHeaderBytesSent, ZhttpManager::estimateRequestHeaderBytes("GET", reqData.uri, reqData.headers)); QCOMPARE(p.serverContentBytesSent, 5); QCOMPARE(p.serverHeaderBytesReceived, 22); // "101" + "Switching Protocols" QCOMPARE(p.serverContentBytesReceived, 11); // "hello world" } }; namespace { namespace Main { QTEST_MAIN(ProxyEngineTest) } } extern "C" { int proxyengine_test(int argc, char **argv) { return Main::main(argc, argv); } } #include "proxyenginetest.moc" pushpin-1.39.1/src/cpp/tests/publishformattest.cpp000066400000000000000000000075771457610542000223150ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include #include #include "publishformat.h" class PublishFormatTest : public QObject { Q_OBJECT private slots: void responseFormat() { QVariantHash data; data["code"] = 200; data["reason"] = QByteArray("OK"); data["headers"] = QVariantList() << QVariant(QVariantList() << QByteArray("Content-Type") << QByteArray("text/plain")); data["body"] = QByteArray("hello world"); bool ok; PublishFormat f = PublishFormat::fromVariant(PublishFormat::HttpResponse, data, &ok); QVERIFY(ok); QCOMPARE(f.code, 200); QCOMPARE(f.reason, QByteArray("OK")); QCOMPARE(f.headers.count(), 1); QCOMPARE(f.headers[0].first, QByteArray("Content-Type")); QCOMPARE(f.headers[0].second, QByteArray("text/plain")); QCOMPARE(f.body, QByteArray("hello world")); data.clear(); data["body"] = QByteArray("other fields implied"); f = PublishFormat::fromVariant(PublishFormat::HttpResponse, data, &ok); QVERIFY(ok); QCOMPARE(f.code, 200); QCOMPARE(f.reason, QByteArray("OK")); QCOMPARE(f.headers.count(), 0); QCOMPARE(f.body, QByteArray("other fields implied")); } void streamFormat() { QVariantHash data; data["content"] = QByteArray("hello world"); bool ok; PublishFormat f = PublishFormat::fromVariant(PublishFormat::HttpStream, data, &ok); QVERIFY(ok); QVERIFY(f.action == PublishFormat::Send); QCOMPARE(f.body, QByteArray("hello world")); data.clear(); data["action"] = QByteArray("close"); f = PublishFormat::fromVariant(PublishFormat::HttpStream, data, &ok); QVERIFY(ok); QVERIFY(f.action == PublishFormat::Close); QVERIFY(f.body.isEmpty()); } void webSocketMessageFormat() { QVariantHash data; data["content"] = QByteArray("hello world"); bool ok; PublishFormat f = PublishFormat::fromVariant(PublishFormat::WebSocketMessage, data, &ok); QVERIFY(ok); QVERIFY(f.action == PublishFormat::Send); QCOMPARE(f.messageType, PublishFormat::Text); QCOMPARE(f.body, QByteArray("hello world")); data.clear(); data["type"] = "binary"; data["content"] = QByteArray("hello world"); f = PublishFormat::fromVariant(PublishFormat::WebSocketMessage, data, &ok); QVERIFY(ok); QVERIFY(f.action == PublishFormat::Send); QCOMPARE(f.messageType, PublishFormat::Binary); QCOMPARE(f.body, QByteArray("hello world")); data.clear(); data["content-bin"] = QByteArray("hello world"); f = PublishFormat::fromVariant(PublishFormat::WebSocketMessage, data, &ok); QVERIFY(ok); QVERIFY(f.action == PublishFormat::Send); QCOMPARE(f.messageType, PublishFormat::Binary); QCOMPARE(f.body, QByteArray("hello world")); data.clear(); data["action"] = "close"; f = PublishFormat::fromVariant(PublishFormat::WebSocketMessage, data, &ok); QVERIFY(ok); QVERIFY(f.action == PublishFormat::Close); QCOMPARE(f.code, -1); data.clear(); data["action"] = "close"; data["code"] = 1001; f = PublishFormat::fromVariant(PublishFormat::WebSocketMessage, data, &ok); QVERIFY(ok); QVERIFY(f.action == PublishFormat::Close); QCOMPARE(f.code, 1001); } }; namespace { namespace Main { QTEST_MAIN(PublishFormatTest) } } extern "C" { int publishformat_test(int argc, char **argv) { return Main::main(argc, argv); } } #include "publishformattest.moc" pushpin-1.39.1/src/cpp/tests/publishitemtest.cpp000066400000000000000000000054471457610542000217550ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include #include "publishformat.h" #include "publishitem.h" class PublishItemTest : public QObject { Q_OBJECT private slots: void parseItem() { QVariantHash meta; meta["foo"] = QByteArray("bar"); meta["bar"] = QByteArray("baz"); QVariantHash hs; hs["content"] = QByteArray("hello world"); QVariantHash formats; formats["http-stream"] = hs; QVariantHash data; data["channel"] = QByteArray("apple"); data["id"] = QByteArray("item1"); data["prev-id"] = QByteArray("item0"); data["meta"] = meta; data["formats"] = formats; bool ok; PublishItem i = PublishItem::fromVariant(data, QString(), &ok); QVERIFY(ok); QCOMPARE(i.channel, QString("apple")); QCOMPARE(i.id, QString("item1")); QCOMPARE(i.prevId, QString("item0")); QCOMPARE(i.meta.count(), 2); QCOMPARE(i.meta.value("foo"), QString("bar")); QCOMPARE(i.meta.value("bar"), QString("baz")); QVERIFY(i.formats.contains(PublishFormat::HttpStream)); QCOMPARE(i.formats.value(PublishFormat::HttpStream).body, QByteArray("hello world")); } void parseItemJsonStyle() { QVariantMap meta; meta["foo"] = QString("bar"); meta["bar"] = QString("baz"); QVariantMap hs; hs["content"] = QString("hello world"); QVariantMap formats; formats["http-stream"] = hs; QVariantMap data; data["channel"] = QString("apple"); data["id"] = QString("item1"); data["prev-id"] = QString("item0"); data["meta"] = meta; data["formats"] = formats; bool ok; PublishItem i = PublishItem::fromVariant(data, QString(), &ok); QVERIFY(ok); QCOMPARE(i.channel, QString("apple")); QCOMPARE(i.id, QString("item1")); QCOMPARE(i.prevId, QString("item0")); QCOMPARE(i.meta.count(), 2); QCOMPARE(i.meta.value("foo"), QString("bar")); QCOMPARE(i.meta.value("bar"), QString("baz")); QVERIFY(i.formats.contains(PublishFormat::HttpStream)); QCOMPARE(i.formats.value(PublishFormat::HttpStream).body, QByteArray("hello world")); } }; namespace { namespace Main { QTEST_MAIN(PublishItemTest) } } extern "C" { int publishitem_test(int argc, char **argv) { return Main::main(argc, argv); } } #include "publishitemtest.moc" pushpin-1.39.1/src/cpp/tests/routes000066400000000000000000000002301457610542000172510ustar00rootroot00000000000000*,debug origin:80 *,path_beg=/jsonp,aco,debug origin:80 *,path_beg=/jsonp-basic,aco,jsonp_mode=basic,jsonp_body=bparam,jsonp_defcb=jpcb,debug origin:80 pushpin-1.39.1/src/cpp/tests/routesfiletest.cpp000066400000000000000000000067371457610542000216140ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include #include "log.h" #include "routesfile.h" class RoutesFileTest : public QObject { Q_OBJECT private slots: void initTestCase() { //log_setOutputLevel(LOG_LEVEL_WARNING); } void cleanupTestCase() { } void lineTests() { QList r; bool ok; r = RoutesFile::parseLine("apple", &ok); QVERIFY(ok); QCOMPARE(r.count(), 1); QCOMPARE(r[0].value, QString("apple")); QVERIFY(r[0].props.isEmpty()); r = RoutesFile::parseLine("apple banana", &ok); QVERIFY(ok); QCOMPARE(r.count(), 2); QCOMPARE(r[0].value, QString("apple")); QVERIFY(r[0].props.isEmpty()); QCOMPARE(r[1].value, QString("banana")); QVERIFY(r[1].props.isEmpty()); r = RoutesFile::parseLine(" apple banana # comment", &ok); QVERIFY(ok); QCOMPARE(r.count(), 2); QCOMPARE(r[0].value, QString("apple")); QVERIFY(r[0].props.isEmpty()); QCOMPARE(r[1].value, QString("banana")); QVERIFY(r[1].props.isEmpty()); r = RoutesFile::parseLine("apple,organic,type=gala,from=\"washington, \\\"usa\\\"\"", &ok); QVERIFY(ok); QCOMPARE(r.count(), 1); QCOMPARE(r[0].value, QString("apple")); QCOMPARE(r[0].props.count(), 3); QVERIFY(r[0].props.contains("organic")); QVERIFY(r[0].props.value("organic").isEmpty()); QCOMPARE(r[0].props.value("type"), QString("gala")); QCOMPARE(r[0].props.value("from"), QString("washington, \"usa\"")); r = RoutesFile::parseLine("apple,organic banana cherry,type=bing", &ok); QVERIFY(ok); QCOMPARE(r.count(), 3); QCOMPARE(r[0].value, QString("apple")); QCOMPARE(r[0].props.count(), 1); QVERIFY(r[0].props.contains("organic")); QVERIFY(r[0].props.value("organic").isEmpty()); QCOMPARE(r[1].value, QString("banana")); QVERIFY(r[1].props.isEmpty()); QCOMPARE(r[2].value, QString("cherry")); QCOMPARE(r[2].props.value("type"), QString("bing")); r = RoutesFile::parseLine(",organic", &ok); QVERIFY(ok); QCOMPARE(r.count(), 1); QCOMPARE(r[0].value, QString("")); QCOMPARE(r[0].props.count(), 1); QVERIFY(r[0].props.contains("organic")); QVERIFY(r[0].props.value("organic").isEmpty()); r = RoutesFile::parseLine("type=gala", &ok); QVERIFY(ok); QCOMPARE(r.count(), 1); QCOMPARE(r[0].value, QString("")); QCOMPARE(r[0].props.count(), 1); QVERIFY(r[0].props.contains("type")); QCOMPARE(r[0].props.value("type"), QString("gala")); // unterminated quote r = RoutesFile::parseLine("apple,organic,type=\"gala", &ok); QVERIFY(!ok); // empty prop name r = RoutesFile::parseLine("apple,organic,", &ok); QVERIFY(!ok); // empty prop name r = RoutesFile::parseLine("apple,organic,=gala", &ok); QVERIFY(!ok); } }; namespace { namespace Main { QTEST_MAIN(RoutesFileTest) } } extern "C" { int routesfile_test(int argc, char **argv) { return Main::main(argc, argv); } } #include "routesfiletest.moc" pushpin-1.39.1/src/cpp/tests/templatetest.cpp000066400000000000000000000037161457610542000212400ustar00rootroot00000000000000/* * Copyright (C) 2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include #include "template.h" class TemplateTest : public QObject { Q_OBJECT private slots: void render() { QVariantMap context; context["place"] = "world"; QVariantMap user; user["first"] = "john"; user["last"] = "smith"; context["user"] = user; QVariantList fruits; fruits.append("apple"); fruits.append("banana"); context["fruits"] = fruits; QString content("hello {{ place }}!"); QString output = Template::render(content, context); QCOMPARE(output, QString("hello world!")); content = QString("hello {% if formal %}{{ user.last }}{% endif %}{% if not formal %}{{ user.first }}{% endif %}!"); output = Template::render(content, context); QCOMPARE(output, QString("hello john!")); context["formal"] = true; output = Template::render(content, context); QCOMPARE(output, QString("hello smith!")); content = QString("please eat {% for f in fruits %}{% if not loop.first %} and {% endif %}fresh {{ f }}s{% endfor %}."); output = Template::render(content, context); QCOMPARE(output, QString("please eat fresh apples and fresh bananas.")); } }; namespace { namespace Main { QTEST_MAIN(TemplateTest) } } extern "C" { int template_test(int argc, char **argv) { return Main::main(argc, argv); } } #include "templatetest.moc" pushpin-1.39.1/src/cpp/tests/tests.pro000066400000000000000000000015421457610542000177000ustar00rootroot00000000000000TEMPLATE = lib CONFIG -= app_bundle CONFIG += staticlib c++14 QT -= gui QT *= network testlib TARGET = pushpin-cpptest cpp_build_dir = $$OUT_PWD MOC_DIR = $$cpp_build_dir/test-moc OBJECTS_DIR = $$cpp_build_dir/test-obj SRC_DIR = $$PWD/.. QZMQ_DIR = $$SRC_DIR/qzmq RUST_DIR = $$SRC_DIR/.. include($$cpp_build_dir/conf.pri) INCLUDEPATH += $$SRC_DIR INCLUDEPATH += $$SRC_DIR/proxy INCLUDEPATH += $$SRC_DIR/handler INCLUDEPATH += $$SRC_DIR/runner INCLUDEPATH += $$QZMQ_DIR/src DEFINES += NO_IRISNET INCLUDEPATH += $$RUST_DIR INCLUDES += \ main.h SOURCES += \ $$PWD/httpheaderstest.cpp \ $$PWD/jwttest.cpp \ $$PWD/routesfiletest.cpp \ $$PWD/proxyenginetest.cpp \ $$PWD/jsonpatchtest.cpp \ $$PWD/instructtest.cpp \ $$PWD/idformattest.cpp \ $$PWD/publishformattest.cpp \ $$PWD/publishitemtest.cpp \ $$PWD/handlerenginetest.cpp \ $$PWD/templatetest.cpp pushpin-1.39.1/src/cpp/timerwheel.cpp000066400000000000000000000026041457610542000175230ustar00rootroot00000000000000/* * Copyright (C) 2021 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "timerwheel.h" #include "rust/timer.h" TimerWheel::TimerWheel(int capacity) { raw_ = timer_wheel_create(capacity); } TimerWheel::~TimerWheel() { timer_wheel_destroy(raw_); } int TimerWheel::add(quint64 expires, size_t userData) { return timer_add(raw_, expires, userData); } void TimerWheel::remove(int key) { timer_remove(raw_, key); } qint64 TimerWheel::timeout() const { return timer_wheel_timeout(raw_); } void TimerWheel::update(quint64 curtime) { timer_wheel_update(raw_, curtime); } TimerWheel::Expired TimerWheel::takeExpired() { ExpiredTimer ret = timer_wheel_take_expired(raw_); Expired expired; expired.key = ret.key; expired.userData = ret.user_data; return expired; } pushpin-1.39.1/src/cpp/timerwheel.h000066400000000000000000000022031457610542000171630ustar00rootroot00000000000000/* * Copyright (C) 2021 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef TIMERWHEEL_H #define TIMERWHEEL_H #include class TimerWheel { public: class Expired { public: int key; // <0 if invalid size_t userData; }; TimerWheel(int capacity); ~TimerWheel(); // returns <0 if no capacity int add(quint64 expires, size_t userData); void remove(int key); // returns <0 if no timers qint64 timeout() const; void update(quint64 curtime); Expired takeExpired(); private: void *raw_; }; #endif pushpin-1.39.1/src/cpp/tnetstring.cpp000066400000000000000000000216511457610542000175620ustar00rootroot00000000000000/* * Copyright (C) 2012-2022 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "tnetstring.h" #include #include "qtcompat.h" namespace TnetString { QByteArray fromByteArray(const QByteArray &in) { return QByteArray::number(in.size()) + ':' + in + ','; } QByteArray fromInt(qint64 in) { QByteArray val = QByteArray::number(in); return QByteArray::number(val.size()) + ':' + val + '#'; } QByteArray fromDouble(double in) { QByteArray val = QByteArray::number(in); return QByteArray::number(val.size()) + ':' + val + '^'; } QByteArray fromBool(bool in) { QByteArray val = in ? "true" : "false"; return QByteArray::number(val.size()) + ':' + val + '!'; } QByteArray fromNull() { return QByteArray("0:~"); } QByteArray fromVariant(const QVariant &in) { switch(typeId(in)) { case QMetaType::QByteArray: return fromByteArray(in.toByteArray()); case QMetaType::Double: return fromDouble(in.toDouble()); case QMetaType::Bool: return fromBool(in.toBool()); case QMetaType::UnknownType: return fromNull(); case QMetaType::QVariantHash: return fromHash(in.toHash()); case QMetaType::QVariantList: return fromList(in.toList()); default: if(canConvert(in, QMetaType::LongLong)) return fromInt(in.toLongLong()); // unsupported type assert(0); return QByteArray(); } } QByteArray fromHash(const QVariantHash &in) { QByteArray val; QHashIterator it(in); while(it.hasNext()) { it.next(); val += fromByteArray(it.key().toUtf8()); val += fromVariant(it.value()); } return QByteArray::number(val.size()) + ':' + val + '}'; } QByteArray fromList(const QVariantList &in) { QByteArray val; foreach(const QVariant &v, in) val += fromVariant(v); return QByteArray::number(val.size()) + ':' + val + ']'; } bool check(const QByteArray &in, int offset, Type *type, int *dataOffset, int *dataSize) { int at = in.indexOf(':', offset); if(at == -1) return false; bool ok; int size = in.mid(offset, at - offset).toInt(&ok); if(!ok || size < 0) return false; char typeChar = in[at + 1 + size]; Type type_; switch(typeChar) { case ',': type_ = ByteArray; break; case '#': type_ = Int; break; case '^': type_ = Double; break; case '!': type_ = Bool; break; case '~': type_ = Null; break; case '}': type_ = Hash; break; case ']': type_ = List; break; default: return false; } *type = type_; *dataOffset = at + 1; *dataSize = size; return true; } QByteArray toByteArray(const QByteArray &in, int offset, int dataOffset, int dataSize, bool *ok) { Q_UNUSED(offset); if(ok) *ok = true; return in.mid(dataOffset, dataSize); } qint64 toInt(const QByteArray &in, int offset, int dataOffset, int dataSize, bool *ok) { Q_UNUSED(offset); QByteArray val = in.mid(dataOffset, dataSize); bool ok_; qint64 x = val.toLongLong(&ok_); if(!ok_) x = 0; if(ok) *ok = ok_; return x; } double toDouble(const QByteArray &in, int offset, int dataOffset, int dataSize, bool *ok) { Q_UNUSED(offset); QByteArray val = in.mid(dataOffset, dataSize); bool ok_; double x = val.toDouble(&ok_); if(!ok_) x = 0; if(ok) *ok = ok_; return x; } bool toBool(const QByteArray &in, int offset, int dataOffset, int dataSize, bool *ok) { Q_UNUSED(offset); QByteArray val = in.mid(dataOffset, dataSize); if(val == "true") { if(ok) *ok = true; return true; } else if(val == "false") { if(ok) *ok = true; return false; } if(ok) *ok = false; return false; } void toNull(const QByteArray &in, int offset, int dataOffset, int dataSize, bool *ok) { Q_UNUSED(in); Q_UNUSED(offset); Q_UNUSED(dataOffset); Q_UNUSED(dataSize); *ok = true; } QVariant toVariant(const QByteArray &in, int offset, Type type, int dataOffset, int dataSize, bool *ok) { QVariant val; bool ok_ = false; switch(type) { case ByteArray: val = toByteArray(in, offset, dataOffset, dataSize, &ok_); break; case Int: val = toInt(in, offset, dataOffset, dataSize, &ok_); break; case Double: val = toDouble(in, offset, dataOffset, dataSize, &ok_); break; case Bool: val = toBool(in, offset, dataOffset, dataSize, &ok_); break; case Null: toNull(in, offset, dataOffset, dataSize, &ok_); break; case Hash: val = toHash(in, offset, dataOffset, dataSize, &ok_); break; case List: val = toList(in, offset, dataOffset, dataSize, &ok_); break; } if(!ok_) { if(ok) *ok = false; return QVariant(); } if(ok) *ok = true; return val; } QVariant toVariant(const QByteArray &in, int offset, bool *ok) { Type type; int dataOffset; int dataSize; if(!check(in, offset, &type, &dataOffset, &dataSize)) { if(ok) *ok = false; return QVariant(); } return toVariant(in, offset, type, dataOffset, dataSize, ok); } QVariantHash toHash(const QByteArray &in, int offset, int dataOffset, int dataSize, bool *ok) { Q_UNUSED(offset); QVariantHash out; int at = dataOffset; while(at < dataSize + dataOffset) { Type itype; int ioffset; int isize; if(!check(in, at, &itype, &ioffset, &isize)) { if(ok) *ok = false; return QVariantHash(); } if(itype != ByteArray) { if(ok) *ok = false; return QVariantHash(); } bool ok_; QByteArray key = toByteArray(in, at, ioffset, isize, &ok_); if(!ok_) { if(ok) *ok = false; return QVariantHash(); } at = ioffset + isize + 1; // position to value if(!check(in, at, &itype, &ioffset, &isize)) { if(ok) *ok = false; return QVariantHash(); } QVariant val = toVariant(in, at, itype, ioffset, isize, &ok_); if(!ok_) { if(ok) *ok = false; return QVariantHash(); } out[QString::fromUtf8(key)] = val; at = ioffset + isize + 1; // position to next item } if(ok) *ok = true; return out; } QVariantList toList(const QByteArray &in, int offset, int dataOffset, int dataSize, bool *ok) { Q_UNUSED(offset); QVariantList out; int at = dataOffset; while(at < dataOffset + dataSize) { Type itype; int ioffset; int isize; if(!check(in, at, &itype, &ioffset, &isize)) { if(ok) *ok = false; return QVariantList(); } bool ok_; QVariant val = toVariant(in, at, itype, ioffset, isize, &ok_); if(!ok_) { if(ok) *ok = false; return QVariantList(); } out += val; at = ioffset + isize + 1; // position to next item } if(ok) *ok = true; return out; } QString byteArrayToEscapedString(const QByteArray &in) { QString out; for(int n = 0; n < in.size(); ++n) { char c = in[n]; if(c == '\\') out += "\\\\"; else if(c == '\"') out += "\\\""; else if(c == '\n') out += "\\n"; else if(c >= 0x20 && c < 0x7f) out += QChar::fromLatin1(c); else out += QString::asprintf("\\x%02x", (unsigned char)c); } return out; } QString variantToString(const QVariant &in, int indent) { QString out; QMetaType::Type type = typeId(in); if(type == QMetaType::QVariantHash) { QVariantHash hash = in.toHash(); out += '{'; if(indent >= 0) out += '\n'; else out += ' '; QHashIterator it(hash); while(it.hasNext()) { it.next(); if(indent >= 0) out += QString(indent + 2, ' '); out += '\"' + byteArrayToEscapedString(it.key().toUtf8()) + "\": " + variantToString(it.value(), indent >= 0 ? indent + 2 : -1); if(it.hasNext()) out += ','; if(indent >= 0) out += '\n'; else out += ' '; } if(indent >= 0) out += QString(indent, ' '); out += '}'; } else if(type == QMetaType::QVariantList) { QVariantList list = in.toList(); out += '['; if(indent >= 0) out += '\n'; else out += ' '; for(int n = 0; n < list.count(); ++n) { if(indent >= 0) out += QString(indent + 2, ' '); out += variantToString(list[n], indent >= 0 ? indent + 2 : -1); if(n + 1 < list.count()) out += ','; if(indent >= 0) out += '\n'; else out += ' '; } if(indent >= 0) out += QString(indent, ' '); out += ']'; } else if(type == QMetaType::QByteArray) { QByteArray val = in.toByteArray(); out += '\"' + byteArrayToEscapedString(val) + '\"'; } else if(type == QMetaType::Double) out += QString::number(in.toDouble()); else if(type == QMetaType::Bool) out += in.toBool() ? "true" : "false"; else if(type == QMetaType::UnknownType) out += "null"; else if(canConvert(in, QMetaType::LongLong)) out += QString::number(in.toLongLong()); else out += QString("").arg((int)type); return out; } } pushpin-1.39.1/src/cpp/tnetstring.h000066400000000000000000000043051457610542000172240ustar00rootroot00000000000000/* * Copyright (C) 2012-2013 Fanout, Inc. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef TNETSTRING_H #define TNETSTRING_H #include namespace TnetString { enum Type { ByteArray, Int, Double, Bool, Null, Hash, List }; QByteArray fromByteArray(const QByteArray &in); QByteArray fromInt(qint64 in); QByteArray fromDouble(double in); QByteArray fromBool(bool in); QByteArray fromNull(); QByteArray fromHash(const QVariantHash &in); QByteArray fromList(const QVariantList &in); QByteArray fromVariant(const QVariant &in); bool check(const QByteArray &in, int offset, Type *type, int *dataOffset, int *dataSize); QByteArray toByteArray(const QByteArray &in, int offset, int dataOffset, int dataSize, bool *ok = 0); qint64 toInt(const QByteArray &in, int offset, int dataOffset, int dataSize, bool *ok = 0); double toDouble(const QByteArray &in, int offset, int dataOffset, int dataSize, bool *ok = 0); bool toBool(const QByteArray &in, int offset, int dataOffset, int dataSize, bool *ok = 0); void toNull(const QByteArray &in, int offset, int dataOffset, int dataSize, bool *ok = 0); QVariantHash toHash(const QByteArray &in, int offset, int dataOffset, int dataSize, bool *ok = 0); QVariantList toList(const QByteArray &in, int offset, int dataOffset, int dataSize, bool *ok = 0); QVariant toVariant(const QByteArray &in, int offset, Type type, int dataOffset, int dataSize, bool *ok = 0); QVariant toVariant(const QByteArray &in, int offset = 0, bool *ok = 0); QString byteArrayToEscapedString(const QByteArray &in); // pass >= 0 for pretty print, -1 for compact QString variantToString(const QVariant &in, int indent = 0); } #endif pushpin-1.39.1/src/cpp/uuidutil.cpp000066400000000000000000000016761457610542000172320ustar00rootroot00000000000000/* * Copyright (C) 2014 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "uuidutil.h" #include namespace UuidUtil { QByteArray createUuid() { QByteArray out = QUuid::createUuid().toString().toLatin1(); if(out[0] == '{' && out[out.length() - 1] == '}') out = out.mid(1, out.length() - 2); return out; } } pushpin-1.39.1/src/cpp/uuidutil.h000066400000000000000000000014611457610542000166670ustar00rootroot00000000000000/* * Copyright (C) 2014 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef UUIDUTIL_H #define UUIDUTIL_H class QByteArray; namespace UuidUtil { QByteArray createUuid(); } #endif pushpin-1.39.1/src/cpp/websocket.h000066400000000000000000000060331457610542000170110ustar00rootroot00000000000000/* * Copyright (C) 2014 Fanout, Inc. * Copyright (C) 2023 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef WEBSOCKET_H #define WEBSOCKET_H #include #include #include #include "httpheaders.h" #include using Signal = boost::signals2::signal; class WebSocket : public QObject { Q_OBJECT public: enum State { Idle, Connecting, Connected, Closing }; enum ErrorCondition { ErrorGeneric, ErrorPolicy, ErrorConnect, ErrorConnectTimeout, ErrorTls, ErrorRejected, ErrorTimeout, ErrorUnavailable }; class Frame { public: enum Type { Continuation, Text, Binary, Ping, Pong }; Type type; QByteArray data; bool more; Frame(Type _type, const QByteArray &_data, bool _more) : type(_type), data(_data), more(_more) { } }; WebSocket(QObject *parent = 0) : QObject(parent) {} virtual QHostAddress peerAddress() const = 0; virtual void setConnectHost(const QString &host) = 0; virtual void setConnectPort(int port) = 0; virtual void setIgnorePolicies(bool on) = 0; virtual void setTrustConnectHost(bool on) = 0; virtual void setIgnoreTlsErrors(bool on) = 0; virtual void start(const QUrl &uri, const HttpHeaders &headers) = 0; virtual void respondSuccess(const QByteArray &reason, const HttpHeaders &headers) = 0; virtual void respondError(int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body) = 0; virtual State state() const = 0; virtual QUrl requestUri() const = 0; virtual HttpHeaders requestHeaders() const = 0; virtual int responseCode() const = 0; virtual QByteArray responseReason() const = 0; virtual HttpHeaders responseHeaders() const = 0; virtual QByteArray responseBody() const = 0; virtual int framesAvailable() const = 0; virtual int writeBytesAvailable() const = 0; virtual int peerCloseCode() const = 0; virtual QString peerCloseReason() const = 0; virtual ErrorCondition errorCondition() const = 0; virtual void writeFrame(const Frame &frame) = 0; virtual Frame readFrame() = 0; virtual void close(int code = -1, const QString &reason = QString()) = 0; Signal connected; Signal readyRead; boost::signals2::signal framesWritten; Signal writeBytesChanged; Signal peerClosed; // emitted only if peer closes before we do Signal closed; // emitted after peer acks our close, or immediately if we were acking Signal error; }; #endif pushpin-1.39.1/src/cpp/wscontrol.h000066400000000000000000000014771457610542000170640ustar00rootroot00000000000000/* * Copyright (C) 2019 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef WSCONTROL_H #define WSCONTROL_H namespace WsControl { enum KeepAliveMode { NoKeepAlive, Idle, Interval }; } #endif pushpin-1.39.1/src/cpp/zhttpmanager.cpp000066400000000000000000000746001457610542000200670ustar00rootroot00000000000000/* * Copyright (C) 2012-2021 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "zhttpmanager.h" #include #include #include #include #include #include "qzmqsocket.h" #include "qzmqvalve.h" #include "tnetstring.h" #include "zhttprequestpacket.h" #include "zhttpresponsepacket.h" #include "log.h" #include "zutil.h" #include "logutil.h" #define OUT_HWM 100 #define IN_HWM 100 #define DEFAULT_HWM 101000 #define CLIENT_WAIT_TIME 0 #define CLIENT_STREAM_WAIT_TIME 500 #define SERVER_WAIT_TIME 500 #define PENDING_MAX 100 #define REFRESH_INTERVAL 1000 #define ZHTTP_EXPIRE 60000 #define ZHTTP_SHOULD_PROCESS (ZHTTP_EXPIRE * 3 / 4) #define ZHTTP_REFRESH_BUCKETS (ZHTTP_SHOULD_PROCESS / REFRESH_INTERVAL) // needs to match the peer #define ZHTTP_IDS_MAX 128 class ZhttpManager::Private : public QObject { Q_OBJECT public: enum SessionType { UnknownSession, HttpSession, WebSocketSession }; class KeepAliveRegistration { public: SessionType type; union { ZhttpRequest *req; ZWebSocket *sock; } p; int refreshBucket; }; ZhttpManager *q; QStringList client_out_specs; QStringList client_out_stream_specs; QStringList client_in_specs; QStringList client_req_specs; QStringList server_in_specs; QStringList server_in_stream_specs; QStringList server_out_specs; QZmq::Socket *client_out_sock; QZmq::Socket *client_out_stream_sock; QZmq::Socket *client_in_sock; QZmq::Socket *client_req_sock; QZmq::Socket *server_in_sock; QZmq::Socket *server_in_stream_sock; QZmq::Socket *server_out_sock; QZmq::Valve *client_in_valve; QZmq::Valve *server_in_valve; QZmq::Valve *server_in_stream_valve; QByteArray instanceId; int ipcFileMode; bool doBind; QHash clientReqsByRid; QHash serverReqsByRid; QList serverPendingReqs; QHash clientSocksByRid; QHash serverSocksByRid; QList serverPendingSocks; QTimer *refreshTimer; QHash keepAliveRegistrations; QSet sessionRefreshBuckets[ZHTTP_REFRESH_BUCKETS]; int currentSessionRefreshBucket; Connection cosConnection; Connection cossConnection; Connection sosConnection; Connection rrConnection; Connection clientConnection; Connection serverConnection; Connection serverStreamConnection; Private(ZhttpManager *_q) : QObject(_q), q(_q), client_out_sock(0), client_out_stream_sock(0), client_in_sock(0), client_req_sock(0), server_in_sock(0), server_in_stream_sock(0), server_out_sock(0), client_in_valve(0), server_in_valve(0), server_in_stream_valve(0), ipcFileMode(-1), doBind(false), currentSessionRefreshBucket(0) { refreshTimer = new QTimer(this); connect(refreshTimer, &QTimer::timeout, this, &Private::refresh_timeout); } ~Private() { while(!serverPendingReqs.isEmpty()) { ZhttpRequest *req = serverPendingReqs.takeFirst(); serverReqsByRid.remove(req->rid()); delete req; } while(!serverPendingSocks.isEmpty()) { ZWebSocket *sock = serverPendingSocks.takeFirst(); serverSocksByRid.remove(sock->rid()); delete sock; } assert(clientReqsByRid.isEmpty()); assert(serverReqsByRid.isEmpty()); assert(clientSocksByRid.isEmpty()); assert(serverSocksByRid.isEmpty()); assert(keepAliveRegistrations.isEmpty()); refreshTimer->disconnect(this); refreshTimer->setParent(0); refreshTimer->deleteLater(); } bool setupClientOut() { cosConnection.disconnect(); rrConnection.disconnect(); delete client_req_sock; delete client_out_sock; client_out_sock = new QZmq::Socket(QZmq::Socket::Push, this); cosConnection = client_out_sock->messagesWritten.connect(boost::bind(&Private::client_out_messagesWritten, this, boost::placeholders::_1)); client_out_sock->setHwm(OUT_HWM); client_out_sock->setShutdownWaitTime(CLIENT_WAIT_TIME); QString errorMessage; if(!ZUtil::setupSocket(client_out_sock, client_out_specs, doBind, ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } return true; } bool setupClientOutStream() { rrConnection.disconnect(); cossConnection.disconnect(); delete client_req_sock; delete client_out_stream_sock; client_out_stream_sock = new QZmq::Socket(QZmq::Socket::Router, this); cossConnection = client_out_stream_sock->messagesWritten.connect(boost::bind(&Private::client_out_stream_messagesWritten, this, boost::placeholders::_1)); client_out_stream_sock->setWriteQueueEnabled(false); client_out_stream_sock->setHwm(DEFAULT_HWM); client_out_stream_sock->setShutdownWaitTime(CLIENT_STREAM_WAIT_TIME); client_out_stream_sock->setImmediateEnabled(true); QString errorMessage; if(!ZUtil::setupSocket(client_out_stream_sock, client_out_stream_specs, doBind, ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } return true; } bool setupClientIn() { rrConnection.disconnect(); delete client_req_sock; delete client_in_sock; client_in_sock = new QZmq::Socket(QZmq::Socket::Sub, this); client_in_sock->setHwm(DEFAULT_HWM); client_in_sock->setShutdownWaitTime(0); client_in_sock->subscribe(instanceId + ' '); QString errorMessage; if(!ZUtil::setupSocket(client_in_sock, client_in_specs, doBind, ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } client_in_valve = new QZmq::Valve(client_in_sock, this); clientConnection = client_in_valve->readyRead.connect(boost::bind(&Private::client_in_readyRead, this, boost::placeholders::_1)); client_in_valve->open(); return true; } bool setupClientReq() { cosConnection.disconnect(); cossConnection.disconnect(); delete client_out_sock; delete client_out_stream_sock; delete client_in_sock; client_req_sock = new QZmq::Socket(QZmq::Socket::Dealer, this); rrConnection = client_req_sock->readyRead.connect(boost::bind(&Private::client_req_readyRead, this)); client_req_sock->setHwm(OUT_HWM); client_req_sock->setShutdownWaitTime(CLIENT_WAIT_TIME); QString errorMessage; if(!ZUtil::setupSocket(client_req_sock, client_req_specs, doBind, ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } return true; } bool setupServerIn() { delete server_in_sock; server_in_sock = new QZmq::Socket(QZmq::Socket::Pull, this); server_in_sock->setHwm(IN_HWM); QString errorMessage; if(!ZUtil::setupSocket(server_in_sock, server_in_specs, doBind, ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } server_in_valve = new QZmq::Valve(server_in_sock, this); serverConnection = server_in_valve->readyRead.connect(boost::bind(&Private::server_in_readyRead, this, boost::placeholders::_1)); server_in_valve->open(); return true; } bool setupServerInStream() { serverStreamConnection.disconnect(); delete server_in_stream_sock; server_in_stream_sock = new QZmq::Socket(QZmq::Socket::Router, this); server_in_stream_sock->setIdentity(instanceId); server_in_stream_sock->setHwm(DEFAULT_HWM); QString errorMessage; if(!ZUtil::setupSocket(server_in_stream_sock, server_in_stream_specs, doBind, ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } server_in_stream_valve = new QZmq::Valve(server_in_stream_sock, this); serverStreamConnection = server_in_stream_valve->readyRead.connect(boost::bind(&Private::server_in_stream_readyRead, this, boost::placeholders::_1)); server_in_stream_valve->open(); return true; } bool setupServerOut() { sosConnection.disconnect(); delete server_out_sock; server_out_sock = new QZmq::Socket(QZmq::Socket::Pub, this); sosConnection = server_out_sock->messagesWritten.connect(boost::bind(&Private::server_out_messagesWritten, this, boost::placeholders::_1)); server_out_sock->setWriteQueueEnabled(false); server_out_sock->setHwm(DEFAULT_HWM); server_out_sock->setShutdownWaitTime(SERVER_WAIT_TIME); QString errorMessage; if(!ZUtil::setupSocket(server_out_sock, server_out_specs, doBind, ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } return true; } int smallestSessionRefreshBucket() { int best = -1; int bestSize = 0; for(int n = 0; n < ZHTTP_REFRESH_BUCKETS; ++n) { if(best == -1 || sessionRefreshBuckets[n].count() < bestSize) { best = n; bestSize = sessionRefreshBuckets[n].count(); } } return best; } void tryRespondCancel(SessionType type, const QByteArray &id, const ZhttpRequestPacket &packet) { assert(!packet.from.isEmpty()); // if this was not an error packet, send cancel if(packet.type != ZhttpRequestPacket::Error && packet.type != ZhttpRequestPacket::Cancel) { ZhttpResponsePacket out; out.from = instanceId; out.ids += ZhttpResponsePacket::Id(id); out.type = ZhttpResponsePacket::Cancel; write(type, out, packet.from); } } void write(SessionType type, const ZhttpRequestPacket &packet) { assert(client_out_sock || client_req_sock); const char *logprefix = logPrefixForType(type); QVariant vpacket = packet.toVariant(); QByteArray buf = QByteArray("T") + TnetString::fromVariant(vpacket); if(client_out_sock) { if(log_outputLevel() >= LOG_LEVEL_DEBUG) LogUtil::logVariantWithContent(LOG_LEVEL_DEBUG, vpacket, "body", "%s client: OUT", logprefix); client_out_sock->write(QList() << buf); } else { if(log_outputLevel() >= LOG_LEVEL_DEBUG) LogUtil::logVariantWithContent(LOG_LEVEL_DEBUG, vpacket, "body", "%s client req: OUT", logprefix); client_req_sock->write(QList() << QByteArray() << buf); } } void write(SessionType type, const ZhttpRequestPacket &packet, const QByteArray &instanceAddress) { assert(client_out_stream_sock); const char *logprefix = logPrefixForType(type); QVariant vpacket = packet.toVariant(); QByteArray buf = QByteArray("T") + TnetString::fromVariant(vpacket); if(log_outputLevel() >= LOG_LEVEL_DEBUG) LogUtil::logVariantWithContent(LOG_LEVEL_DEBUG, vpacket, "body", "%s client: OUT %s", logprefix, instanceAddress.data()); QList msg; msg += instanceAddress; msg += QByteArray(); msg += buf; client_out_stream_sock->write(msg); } void write(SessionType type, const ZhttpResponsePacket &packet, const QByteArray &instanceAddress) { assert(server_out_sock); const char *logprefix = logPrefixForType(type); QVariant vpacket = packet.toVariant(); QByteArray buf = instanceAddress + " T" + TnetString::fromVariant(vpacket); if(log_outputLevel() >= LOG_LEVEL_DEBUG) LogUtil::logVariantWithContent(LOG_LEVEL_DEBUG, vpacket, "body", "%s server: OUT %s", logprefix, instanceAddress.data()); server_out_sock->write(QList() << buf); } static const char *logPrefixForType(SessionType type) { switch(type) { case HttpSession: return "zhttp"; case WebSocketSession: return "zws"; default: return "zhttp/zws"; } } void registerKeepAlive(void *p, SessionType type) { if(keepAliveRegistrations.contains(p)) return; KeepAliveRegistration *r = new KeepAliveRegistration; r->type = type; if(type == HttpSession) r->p.req = (ZhttpRequest *)p; else // WebSocketSession r->p.sock = (ZWebSocket *)p; keepAliveRegistrations.insert(p, r); r->refreshBucket = smallestSessionRefreshBucket(); sessionRefreshBuckets[r->refreshBucket] += r; setupKeepAlive(); } void unregisterKeepAlive(void *p) { KeepAliveRegistration *r = keepAliveRegistrations.value(p); if(!r) return; sessionRefreshBuckets[r->refreshBucket].remove(r); keepAliveRegistrations.remove(p); delete r; setupKeepAlive(); } void setupKeepAlive() { if(!keepAliveRegistrations.isEmpty()) { if(!refreshTimer->isActive()) refreshTimer->start(REFRESH_INTERVAL); } else refreshTimer->stop(); } void writeKeepAlive(SessionType type, const QList &ids, const QByteArray &zhttpAddress) { ZhttpRequestPacket zreq; zreq.from = instanceId; zreq.ids = ids; zreq.type = ZhttpRequestPacket::KeepAlive; write(type, zreq, zhttpAddress); } void writeKeepAlive(SessionType type, const QList &ids, const QByteArray &zhttpAddress) { ZhttpResponsePacket zresp; zresp.from = instanceId; zresp.ids = ids; zresp.type = ZhttpResponsePacket::KeepAlive; write(type, zresp, zhttpAddress); } void client_out_messagesWritten(int count) { Q_UNUSED(count); } void client_out_stream_messagesWritten(int count) { Q_UNUSED(count); } void server_out_messagesWritten(int count) { Q_UNUSED(count); } void client_req_readyRead() { QPointer self = this; while(client_req_sock->canRead()) { QList msg = client_req_sock->read(); if(msg.count() != 2) { log_warning("zhttp/zws client req: received message with parts != 2, skipping"); continue; } QByteArray dataRaw = msg[1]; if(dataRaw.length() < 1 || dataRaw[0] != 'T') { log_warning("zhttp/zws client req: received message with invalid format (missing type), skipping"); continue; } QVariant data = TnetString::toVariant(dataRaw.mid(1)); if(data.isNull()) { log_warning("zhttp/zws client req: received message with invalid format (tnetstring parse failed), skipping"); continue; } if(log_outputLevel() >= LOG_LEVEL_DEBUG) LogUtil::logVariantWithContent(LOG_LEVEL_DEBUG, data, "body", "zhttp/zws client req: IN"); ZhttpResponsePacket p; if(!p.fromVariant(data)) { log_warning("zhttp/zws client req: received message with invalid format (parse failed), skipping"); continue; } if(p.ids.count() != 1) { log_warning("zhttp/zws client req: received message with multiple ids, skipping"); return; } const ZhttpResponsePacket::Id &id = p.ids.first(); ZhttpRequest *req = clientReqsByRid.value(ZhttpRequest::Rid(instanceId, id.id)); if(req) { req->handle(id.id, id.seq, p); if(!self) return; continue; } log_debug("zhttp/zws client req: received message for unknown request id"); // NOTE: we don't respond with a cancel message in req mode } } void client_in_readyRead(const QList &msg) { if(msg.count() != 1) { log_warning("zhttp/zws client: received message with parts != 1, skipping"); return; } int at = msg[0].indexOf(' '); if(at == -1) { log_warning("zhttp/zws client: received message with invalid format, skipping"); return; } QByteArray receiver = msg[0].mid(0, at); QByteArray dataRaw = msg[0].mid(at + 1); if(dataRaw.length() < 1 || dataRaw[0] != 'T') { log_warning("zhttp/zws client: received message with invalid format (missing type), skipping"); return; } QVariant data = TnetString::toVariant(dataRaw.mid(1)); if(data.isNull()) { log_warning("zhttp/zws client: received message with invalid format (tnetstring parse failed), skipping"); return; } if(log_outputLevel() >= LOG_LEVEL_DEBUG) LogUtil::logVariantWithContent(LOG_LEVEL_DEBUG, data, "body", "zhttp/zws client: IN %s", receiver.data()); ZhttpResponsePacket p; if(!p.fromVariant(data)) { log_warning("zhttp/zws client: received message with invalid format (parse failed), skipping"); return; } QPointer self = this; foreach(const ZhttpResponsePacket::Id &id, p.ids) { // is this for a websocket? ZWebSocket *sock = clientSocksByRid.value(ZWebSocket::Rid(instanceId, id.id)); if(sock) { sock->handle(id.id, id.seq, p); if(!self) return; continue; } // is this for an http request? ZhttpRequest *req = clientReqsByRid.value(ZhttpRequest::Rid(instanceId, id.id)); if(req) { req->handle(id.id, id.seq, p); if(!self) return; continue; } log_debug("zhttp/zws client: received message for unknown request id, skipping"); } } void server_in_readyRead(const QList &msg) { if(msg.count() != 1) { log_warning("zhttp/zws server: received message with parts != 1, skipping"); return; } if(msg[0].length() < 1 || msg[0][0] != 'T') { log_warning("zhttp/zws server: received message with invalid format (missing type), skipping"); return; } QVariant data = TnetString::toVariant(msg[0].mid(1)); if(data.isNull()) { log_warning("zhttp/zws server: received message with invalid format (tnetstring parse failed), skipping"); return; } if(log_outputLevel() >= LOG_LEVEL_DEBUG) LogUtil::logVariantWithContent(LOG_LEVEL_DEBUG, data, "body", "zhttp/zws server: IN"); ZhttpRequestPacket p; if(!p.fromVariant(data)) { log_warning("zhttp/zws server: received message with invalid format (parse failed), skipping"); return; } if(p.from.isEmpty()) { log_warning("zhttp/zws server: received message without from address, skipping"); return; } if(p.ids.count() != 1) { log_warning("zhttp/zws server: received initial message with multiple ids, skipping"); return; } const ZhttpRequestPacket::Id &id = p.ids.first(); if(p.uri.scheme() == "wss" || p.uri.scheme() == "ws") { ZWebSocket::Rid rid(p.from, id.id); ZWebSocket *sock = serverSocksByRid.value(rid); if(sock) { log_warning("zws server: received message for existing request id, canceling"); tryRespondCancel(WebSocketSession, id.id, p); return; } sock = new ZWebSocket; if(!sock->setupServer(q, id.id, id.seq, p)) { delete sock; return; } serverSocksByRid.insert(rid, sock); serverPendingSocks += sock; if(serverPendingReqs.count() + serverPendingSocks.count() >= PENDING_MAX) server_in_valve->close(); q->socketReady(); } else if(p.uri.scheme() == "https" || p.uri.scheme() == "http") { ZhttpRequest::Rid rid(p.from, id.id); ZhttpRequest *req = serverReqsByRid.value(rid); if(req) { log_warning("zhttp server: received message for existing request id, canceling"); tryRespondCancel(HttpSession, id.id, p); return; } req = new ZhttpRequest; if(!req->setupServer(q, id.id, id.seq, p)) { delete req; return; } serverReqsByRid.insert(rid, req); serverPendingReqs += req; if(serverPendingReqs.count() + serverPendingSocks.count() >= PENDING_MAX) server_in_valve->close(); q->requestReady(); } else { log_debug("zhttp/zws server: rejecting unsupported scheme: %s", qPrintable(p.uri.scheme())); tryRespondCancel(UnknownSession, id.id, p); return; } } void server_in_stream_readyRead(const QList &msg) { if(msg.count() != 3) { log_warning("zhttp/zws server: received message with parts != 3, skipping"); return; } if(msg[2].length() < 1 || msg[2][0] != 'T') { log_warning("zhttp/zws server: received message with invalid format (missing type), skipping"); return; } QVariant data = TnetString::toVariant(msg[2].mid(1)); if(data.isNull()) { log_warning("zhttp/zws server: received message with invalid format (tnetstring parse failed), skipping"); return; } if(log_outputLevel() >= LOG_LEVEL_DEBUG) LogUtil::logVariantWithContent(LOG_LEVEL_DEBUG, data, "body", "zhttp/zws server: IN stream"); ZhttpRequestPacket p; if(!p.fromVariant(data)) { log_warning("zhttp/zws server: received message with invalid format (parse failed), skipping"); return; } QPointer self = this; foreach(const ZhttpRequestPacket::Id &id, p.ids) { // is this for a websocket? ZWebSocket *sock = serverSocksByRid.value(ZWebSocket::Rid(p.from, id.id)); if(sock) { sock->handle(id.id, id.seq, p); if(!self) return; continue; } // is this for an http request? ZhttpRequest *req = serverReqsByRid.value(ZhttpRequest::Rid(p.from, id.id)); if(req) { req->handle(id.id, id.seq, p); if(!self) return; continue; } log_debug("zhttp/zws server: received message for unknown request id, skipping"); } } public slots: void refresh_timeout() { QHash > clientSessionsBySender[2]; // index corresponds to type QHash > serverSessionsBySender[2]; // index corresponds to type // process the current bucket const QSet &bucket = sessionRefreshBuckets[currentSessionRefreshBucket]; foreach(KeepAliveRegistration *r, bucket) { QPair rid; bool isServer; if(r->type == HttpSession) { rid = r->p.req->rid(); isServer = r->p.req->isServer(); } else // WebSocketSession { rid = r->p.sock->rid(); isServer = r->p.sock->isServer(); } QByteArray sender; if(isServer) { sender = rid.first; } else { if(r->type == HttpSession) sender = r->p.req->toAddress(); else // WebSocketSession sender = r->p.sock->toAddress(); } assert(!sender.isEmpty()); QHash > &sessionsBySender = (isServer ? serverSessionsBySender[r->type - 1] : clientSessionsBySender[r->type - 1]); if(!sessionsBySender.contains(sender)) sessionsBySender.insert(sender, QList()); QList &sessions = sessionsBySender[sender]; sessions += r; // if we're at max, send out now if(sessions.count() >= ZHTTP_IDS_MAX) { if(isServer) { QList ids; foreach(KeepAliveRegistration *i, sessions) { assert(i->type == r->type); if(r->type == HttpSession) ids += ZhttpResponsePacket::Id(i->p.req->rid().second, i->p.req->outSeqInc()); else // WebSocketSession ids += ZhttpResponsePacket::Id(i->p.sock->rid().second, i->p.sock->outSeqInc()); } writeKeepAlive(r->type, ids, sender); } else { QList ids; foreach(KeepAliveRegistration *i, sessions) { assert(i->type == r->type); if(r->type == HttpSession) ids += ZhttpRequestPacket::Id(i->p.req->rid().second, i->p.req->outSeqInc()); else // WebSocketSession ids += ZhttpRequestPacket::Id(i->p.sock->rid().second, i->p.sock->outSeqInc()); } writeKeepAlive(r->type, ids, sender); } sessions.clear(); sessionsBySender.remove(sender); } } // send last packets for(int n = 0; n < 2; ++n) { SessionType type = (SessionType)(n + 1); { QHashIterator > sit(clientSessionsBySender[n]); while(sit.hasNext()) { sit.next(); const QByteArray &sender = sit.key(); const QList &sessions = sit.value(); if(!sessions.isEmpty()) { QList ids; foreach(KeepAliveRegistration *i, sessions) { assert(i->type == type); if(type == HttpSession) ids += ZhttpRequestPacket::Id(i->p.req->rid().second, i->p.req->outSeqInc()); else // WebSocketSession ids += ZhttpRequestPacket::Id(i->p.sock->rid().second, i->p.sock->outSeqInc()); } writeKeepAlive(type, ids, sender); } } } { QHashIterator > sit(serverSessionsBySender[n]); while(sit.hasNext()) { sit.next(); const QByteArray &sender = sit.key(); const QList &sessions = sit.value(); if(!sessions.isEmpty()) { QList ids; foreach(KeepAliveRegistration *i, sessions) { assert(i->type == type); if(type == HttpSession) ids += ZhttpResponsePacket::Id(i->p.req->rid().second, i->p.req->outSeqInc()); else // WebSocketSession ids += ZhttpResponsePacket::Id(i->p.sock->rid().second, i->p.sock->outSeqInc()); } writeKeepAlive(type, ids, sender); } } } } ++currentSessionRefreshBucket; if(currentSessionRefreshBucket >= ZHTTP_REFRESH_BUCKETS) currentSessionRefreshBucket = 0; } }; ZhttpManager::ZhttpManager(QObject *parent) : QObject(parent) { d = new Private(this); } ZhttpManager::~ZhttpManager() { delete d; } int ZhttpManager::connectionCount() const { int total = 0; total += d->clientReqsByRid.count(); total += d->serverReqsByRid.count(); total += d->clientSocksByRid.count(); total += d->serverSocksByRid.count(); return total; } bool ZhttpManager::clientUsesReq() const { return (!d->client_out_sock && d->client_req_sock); } ZhttpRequest *ZhttpManager::serverRequestByRid(const ZhttpRequest::Rid &rid) const { return d->serverReqsByRid.value(rid); } QByteArray ZhttpManager::instanceId() const { return d->instanceId; } void ZhttpManager::setInstanceId(const QByteArray &id) { d->instanceId = id; } void ZhttpManager::setIpcFileMode(int mode) { d->ipcFileMode = mode; } void ZhttpManager::setBind(bool enable) { d->doBind = enable; } bool ZhttpManager::setClientOutSpecs(const QStringList &specs) { d->client_out_specs = specs; return d->setupClientOut(); } bool ZhttpManager::setClientOutStreamSpecs(const QStringList &specs) { d->client_out_stream_specs = specs; return d->setupClientOutStream(); } bool ZhttpManager::setClientInSpecs(const QStringList &specs) { d->client_in_specs = specs; return d->setupClientIn(); } bool ZhttpManager::setClientReqSpecs(const QStringList &specs) { d->client_req_specs = specs; return d->setupClientReq(); } bool ZhttpManager::setServerInSpecs(const QStringList &specs) { d->server_in_specs = specs; return d->setupServerIn(); } bool ZhttpManager::setServerInStreamSpecs(const QStringList &specs) { d->server_in_stream_specs = specs; return d->setupServerInStream(); } bool ZhttpManager::setServerOutSpecs(const QStringList &specs) { d->server_out_specs = specs; return d->setupServerOut(); } ZhttpRequest *ZhttpManager::createRequest() { ZhttpRequest *req = new ZhttpRequest; req->setupClient(this, d->client_req_sock ? true : false); return req; } ZhttpRequest *ZhttpManager::takeNextRequest() { ZhttpRequest *req = 0; while(!req) { if(d->serverPendingReqs.isEmpty()) return 0; req = d->serverPendingReqs.takeFirst(); if(!d->serverReqsByRid.contains(req->rid())) { // this means the object was a zombie. clean up and take next delete req; req = 0; continue; } d->server_in_valve->open(); } req->startServer(); return req; } ZWebSocket *ZhttpManager::createSocket() { // websockets not allowed in req mode assert(!d->client_req_sock); ZWebSocket *sock = new ZWebSocket; sock->setupClient(this); return sock; } ZWebSocket *ZhttpManager::takeNextSocket() { ZWebSocket *sock = 0; while(!sock) { if(d->serverPendingSocks.isEmpty()) return 0; sock = d->serverPendingSocks.takeFirst(); if(!d->serverSocksByRid.contains(sock->rid())) { // this means the object was a zombie. clean up and take next delete sock; sock = 0; continue; } d->server_in_valve->open(); } sock->startServer(); return sock; } ZhttpRequest *ZhttpManager::createRequestFromState(const ZhttpRequest::ServerState &state) { ZhttpRequest *req = new ZhttpRequest; req->setupServer(this, state); return req; } void ZhttpManager::link(ZhttpRequest *req) { if(req->isServer()) d->serverReqsByRid.insert(req->rid(), req); else d->clientReqsByRid.insert(req->rid(), req); } void ZhttpManager::unlink(ZhttpRequest *req) { if(req->isServer()) d->serverReqsByRid.remove(req->rid()); else d->clientReqsByRid.remove(req->rid()); } void ZhttpManager::link(ZWebSocket *sock) { if(sock->isServer()) d->serverSocksByRid.insert(sock->rid(), sock); else d->clientSocksByRid.insert(sock->rid(), sock); } void ZhttpManager::unlink(ZWebSocket *sock) { if(sock->isServer()) d->serverSocksByRid.remove(sock->rid()); else d->clientSocksByRid.remove(sock->rid()); } bool ZhttpManager::canWriteImmediately() const { assert(d->client_out_sock || d->client_req_sock); if(d->client_out_sock) return d->client_out_sock->canWriteImmediately(); else return d->client_req_sock->canWriteImmediately(); } void ZhttpManager::writeHttp(const ZhttpRequestPacket &packet) { d->write(Private::HttpSession, packet); } void ZhttpManager::writeHttp(const ZhttpRequestPacket &packet, const QByteArray &instanceAddress) { d->write(Private::HttpSession, packet, instanceAddress); } void ZhttpManager::writeHttp(const ZhttpResponsePacket &packet, const QByteArray &instanceAddress) { d->write(Private::HttpSession, packet, instanceAddress); } void ZhttpManager::writeWs(const ZhttpRequestPacket &packet) { d->write(Private::WebSocketSession, packet); } void ZhttpManager::writeWs(const ZhttpRequestPacket &packet, const QByteArray &instanceAddress) { d->write(Private::WebSocketSession, packet, instanceAddress); } void ZhttpManager::writeWs(const ZhttpResponsePacket &packet, const QByteArray &instanceAddress) { d->write(Private::WebSocketSession, packet, instanceAddress); } void ZhttpManager::registerKeepAlive(ZhttpRequest *req) { d->registerKeepAlive(req, Private::HttpSession); } void ZhttpManager::unregisterKeepAlive(ZhttpRequest *req) { d->unregisterKeepAlive(req); } void ZhttpManager::registerKeepAlive(ZWebSocket *sock) { d->registerKeepAlive(sock, Private::WebSocketSession); } void ZhttpManager::unregisterKeepAlive(ZWebSocket *sock) { d->unregisterKeepAlive(sock); } int ZhttpManager::estimateRequestHeaderBytes(const QString &method, const QUrl &uri, const HttpHeaders &headers) { int total = method.toUtf8().length(); total += uri.path(QUrl::FullyEncoded).length(); if(uri.hasQuery()) total += uri.query(QUrl::FullyEncoded).length() + 1; // +1 for question mark foreach(const HttpHeader &h, headers) { total += h.first.length(); total += h.second.length(); } return total; } int ZhttpManager::estimateResponseHeaderBytes(int code, const QByteArray &reason, const HttpHeaders &headers) { int total = QString::number(code).length(); total += reason.length(); foreach(const HttpHeader &h, headers) { total += h.first.length(); total += h.second.length(); } return total; } #include "zhttpmanager.moc" pushpin-1.39.1/src/cpp/zhttpmanager.h000066400000000000000000000060771457610542000175370ustar00rootroot00000000000000/* * Copyright (C) 2012-2013 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef ZHTTPMANAGER_H #define ZHTTPMANAGER_H #include #include "zhttprequest.h" #include "zwebsocket.h" #include using Signal = boost::signals2::signal; class ZhttpRequestPacket; class ZhttpResponsePacket; class ZhttpManager : public QObject { Q_OBJECT public: ZhttpManager(QObject *parent = 0); ~ZhttpManager(); int connectionCount() const; bool clientUsesReq() const; ZhttpRequest *serverRequestByRid(const ZhttpRequest::Rid &rid) const; QByteArray instanceId() const; void setInstanceId(const QByteArray &id); void setIpcFileMode(int mode); void setBind(bool enable); bool setClientOutSpecs(const QStringList &specs); bool setClientOutStreamSpecs(const QStringList &specs); bool setClientInSpecs(const QStringList &specs); bool setClientReqSpecs(const QStringList &specs); bool setServerInSpecs(const QStringList &specs); bool setServerInStreamSpecs(const QStringList &specs); bool setServerOutSpecs(const QStringList &specs); ZhttpRequest *createRequest(); ZhttpRequest *takeNextRequest(); ZWebSocket *createSocket(); ZWebSocket *takeNextSocket(); // for server mode, jump directly to responding state ZhttpRequest *createRequestFromState(const ZhttpRequest::ServerState &state); static int estimateRequestHeaderBytes(const QString &method, const QUrl &uri, const HttpHeaders &headers); static int estimateResponseHeaderBytes(int code, const QByteArray &reason, const HttpHeaders &headers); Signal requestReady; Signal socketReady; private: class Private; friend class Private; Private *d; friend class ZhttpRequest; friend class ZWebSocket; void link(ZhttpRequest *req); void unlink(ZhttpRequest *req); void link(ZWebSocket *sock); void unlink(ZWebSocket *sock); bool canWriteImmediately() const; void writeHttp(const ZhttpRequestPacket &packet); void writeHttp(const ZhttpRequestPacket &packet, const QByteArray &instanceAddress); void writeHttp(const ZhttpResponsePacket &packet, const QByteArray &instanceAddress); void writeWs(const ZhttpRequestPacket &packet); void writeWs(const ZhttpRequestPacket &packet, const QByteArray &instanceAddress); void writeWs(const ZhttpResponsePacket &packet, const QByteArray &instanceAddress); void registerKeepAlive(ZhttpRequest *req); void unregisterKeepAlive(ZhttpRequest *req); void registerKeepAlive(ZWebSocket *sock); void unregisterKeepAlive(ZWebSocket *sock); }; #endif pushpin-1.39.1/src/cpp/zhttprequest.cpp000066400000000000000000000714301457610542000201430ustar00rootroot00000000000000/* * Copyright (C) 2012-2021 Fanout, Inc. * Copyright (C) 2023 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "zhttprequest.h" #include #include #include "zhttprequestpacket.h" #include "zhttpresponsepacket.h" #include "bufferlist.h" #include "log.h" #include "rtimer.h" #include "zhttpmanager.h" #include "uuidutil.h" #define IDEAL_CREDITS 200000 #define SESSION_EXPIRE 60000 #define KEEPALIVE_INTERVAL 45000 #define REQ_BUF_MAX 1000000 class ZhttpRequest::Private : public QObject { Q_OBJECT public: enum State { Stopped, // response finished, error, or not even started ClientStarting, // prepared to send the first packet ClientRequestStartWait, // sent the first packet of streamed input, waiting for ack ClientRequesting, // sending the rest of streamed input ClientRequestFinishWait, // completed sending the request, waiting for ack ClientReceiving, // completed sending the request, waiting on response ServerStarting, // prepared to process the first packet ServerReceiving, // receiving the rest of streamed input ServerResponseWait, // waiting for the response to start ServerResponseStarting, // about to send the first packet ServerResponding // sending the response }; ZhttpRequest *q; ZhttpManager *manager; bool server; State state; ZhttpRequest::Rid rid; bool doReq; QByteArray toAddress; QHostAddress peerAddress; QString connectHost; int connectPort; bool ignorePolicies; bool trustConnectHost; bool ignoreTlsErrors; bool sendBodyAfterAck; QVariant passthrough; QString requestMethod; QUrl requestUri; HttpHeaders requestHeaders; BufferList requestBodyBuf; int inSeq; int outSeq; int outCredits; bool bodyFinished; // user has finished providing input int pendingInCredits; bool haveRequestBody; bool haveResponseValues; int responseCode; QByteArray responseReason; HttpHeaders responseHeaders; BufferList responseBodyBuf; QVariant userData; bool pausing; bool paused; bool pendingUpdate; bool needPause; bool readableChanged; bool writableChanged; bool errored; ErrorCondition errorCondition; RTimer *expireTimer; RTimer *keepAliveTimer; bool multi; bool quiet; Connection expTimerConnection; Connection keepAliveTimerConnection; Private(ZhttpRequest *_q) : QObject(_q), q(_q), manager(0), server(false), state(Stopped), doReq(false), connectPort(-1), ignorePolicies(false), trustConnectHost(false), ignoreTlsErrors(false), sendBodyAfterAck(false), inSeq(0), outSeq(0), outCredits(0), bodyFinished(false), pendingInCredits(0), haveRequestBody(false), haveResponseValues(false), pausing(false), paused(false), pendingUpdate(false), needPause(false), readableChanged(false), writableChanged(false), errored(false), expireTimer(0), keepAliveTimer(0), multi(false), quiet(false) { expireTimer = new RTimer; expTimerConnection = expireTimer->timeout.connect(boost::bind(&Private::expire_timeout, this)); expireTimer->setSingleShot(true); keepAliveTimer = new RTimer; keepAliveTimerConnection = keepAliveTimer->timeout.connect(boost::bind(&Private::keepAlive_timeout, this)); } ~Private() { if(manager && !paused && state != Stopped) tryCancel(); cleanup(); } void cleanup() { needPause = false; readableChanged = false; writableChanged = false; if(expireTimer) { expTimerConnection.disconnect(); expireTimer->setParent(0); expireTimer->deleteLater(); expireTimer = 0; } if(keepAliveTimer) { keepAliveTimerConnection.disconnect(); keepAliveTimer->setParent(0); keepAliveTimer->deleteLater(); keepAliveTimer = 0; } if(manager) { manager->unregisterKeepAlive(q); manager->unlink(q); manager = 0; } } bool setupServer(int seq, const ZhttpRequestPacket &packet) { if(packet.type != ZhttpRequestPacket::Data) { log_warning("zhttp server: received request with invalid type, canceling"); tryRespondCancel(packet); return false; } if(seq != -1 && seq != 0) { log_warning("zhttp server: error, received request with non-zero seq field"); writeError("bad-request"); state = Stopped; return false; } if(!packet.stream) { log_warning("zhttp server: error, received request for non-stream response"); writeError("bad-request"); state = Stopped; return false; } if(seq == -1 && packet.more) { log_warning("zhttp server: error, received stream request with no seq field"); writeError("bad-request"); state = Stopped; return false; } inSeq = 1; // next expected seq if(packet.credits != -1) outCredits = packet.credits; requestMethod = packet.method; requestUri = packet.uri; requestHeaders = packet.headers; requestBodyBuf += packet.body; passthrough = packet.passthrough; userData = packet.userData; peerAddress = packet.peerAddress; if(packet.multi) multi = true; if(!packet.more) haveRequestBody = true; return true; } void setupServer(const ZhttpRequest::ServerState &ss) { peerAddress = ss.peerAddress; requestMethod = ss.requestMethod; requestUri = ss.requestUri; requestHeaders = ss.requestHeaders; requestBodyBuf += ss.requestBody; if(ss.inSeq >= 0) inSeq = ss.inSeq; if(ss.outSeq >= 0) outSeq = ss.outSeq; if(ss.outCredits >= 0) outCredits = ss.outCredits; userData = ss.userData; if(ss.responseCode != -1) { responseCode = ss.responseCode; state = ServerResponding; } else { state = ServerResponseWait; } refreshTimeout(); startKeepAlive(); // send a keep-alive right away to accept after handoff ZhttpResponsePacket p; p.type = ZhttpResponsePacket::KeepAlive; p.multi = true; // request multi support writePacket(p); } void startClient() { state = ClientStarting; refreshTimeout(); update(); } void startServer() { state = ServerStarting; startKeepAlive(); refreshTimeout(); update(); } void pause() { assert(!pausing && !paused); assert(!doReq); stopKeepAlive(); pausing = true; needPause = true; update(); } void resume() { assert(paused); paused = false; startKeepAlive(); ZhttpResponsePacket p; p.type = ZhttpResponsePacket::KeepAlive; writePacket(p); } void beginResponse() { assert(!pausing && !paused); state = ServerResponseStarting; update(); } void startKeepAlive() { if(multi) { if(keepAliveTimer->isActive()) { // need to flush the current keepalive, since the // manager registration may extend the timeout keepAlive_timeout(); keepAliveTimer->stop(); } manager->registerKeepAlive(q); } else { manager->unregisterKeepAlive(q); if(!keepAliveTimer->isActive()) keepAliveTimer->start(KEEPALIVE_INTERVAL); } } void stopKeepAlive() { if(keepAliveTimer->isActive()) keepAliveTimer->stop(); manager->unregisterKeepAlive(q); } void refreshTimeout() { expireTimer->start(SESSION_EXPIRE); } void update() { if(!pendingUpdate) { pendingUpdate = true; QMetaObject::invokeMethod(this, "doUpdate", Qt::QueuedConnection); } } QByteArray readBody(int size) { if(server) { QByteArray out = requestBodyBuf.take(size); if(out.isEmpty()) return out; pendingInCredits += out.size(); if(!pausing && !paused) { ZhttpResponsePacket p; p.type = ZhttpResponsePacket::Credit; p.credits = pendingInCredits; pendingInCredits = 0; writePacket(p); } return out; } else { QByteArray out = responseBodyBuf.take(size); if(out.isEmpty()) return out; pendingInCredits += out.size(); if(state == ClientReceiving) tryWrite(); // this should not emit signals in current state return out; } } void tryWrite() { QPointer self = this; if(state == ClientRequesting) { // if all we have to send is EOF, we don't need credits for that if(requestBodyBuf.isEmpty() && bodyFinished) { state = ClientReceiving; ZhttpRequestPacket p; p.type = ZhttpRequestPacket::Data; writePacket(p); q->bytesWritten(0); } else if(!requestBodyBuf.isEmpty() && outCredits > 0) { // if we have data to send, and the credits to do so, then send data. // also send credits if we need to. QByteArray buf = requestBodyBuf.take(outCredits); outCredits -= buf.size(); writableChanged = true; ZhttpRequestPacket p; p.type = ZhttpRequestPacket::Data; p.body = buf; if(!requestBodyBuf.isEmpty() || !bodyFinished) p.more = true; if(pendingInCredits > 0) { p.credits = pendingInCredits; pendingInCredits = 0; } if(!p.more) state = ClientReceiving; writePacket(p); q->bytesWritten(buf.size()); } } else if(state == ClientReceiving) { if(pendingInCredits > 0) { // if we have no data to send but we need to send credits, do at least that ZhttpRequestPacket p; p.type = ZhttpRequestPacket::Credit; p.credits = pendingInCredits; pendingInCredits = 0; writePacket(p); } } else if(state == ServerResponding) { if((!responseBodyBuf.isEmpty() && outCredits > 0) || (responseBodyBuf.isEmpty() && bodyFinished)) { ZhttpResponsePacket packet; packet.type = ZhttpResponsePacket::Data; packet.body = responseBodyBuf.take(outCredits); outCredits -= packet.body.size(); if(!packet.body.isEmpty()) writableChanged = true; packet.more = (!responseBodyBuf.isEmpty() || !bodyFinished); writePacket(packet); if(!packet.more) { state = Stopped; cleanup(); } q->bytesWritten(packet.body.size()); } } if(!self) return; trySendPause(); } void trySendPause() { if(needPause && (state == ServerResponseWait || state == ServerResponding) && responseBodyBuf.isEmpty()) { needPause = false; ZhttpResponsePacket p; p.type = ZhttpResponsePacket::HandoffStart; writePacket(p); } } void handle(const QByteArray &id, int seq, const ZhttpRequestPacket &packet) { if(paused) return; if(packet.type == ZhttpRequestPacket::Error) { errored = true; errorCondition = convertError(packet.condition); log_debug("zhttp server: error id=%s cond=%s", id.data(), packet.condition.data()); state = Stopped; cleanup(); q->error(); return; } else if(packet.type == ZhttpRequestPacket::Cancel) { log_debug("zhttp server: received cancel id=%s", id.data()); errored = true; errorCondition = ErrorGeneric; state = Stopped; cleanup(); q->error(); return; } if(seq != -1) { if(seq != inSeq) { log_warning("zhttp server: error id=%s received message out of sequence (expected %d, got %d), canceling", id.data(), inSeq, seq); // if this was not an error packet, send cancel if(packet.type != ZhttpRequestPacket::Error && packet.type != ZhttpRequestPacket::Cancel) { ZhttpResponsePacket p; p.type = ZhttpResponsePacket::Cancel; writePacket(p); } state = Stopped; errored = true; errorCondition = ErrorGeneric; cleanup(); q->error(); return; } ++inSeq; } if(!multi && packet.multi) { // switch on multi support multi = true; if(!pausing) { // re-setup keep alive startKeepAlive(); } } refreshTimeout(); if(packet.type == ZhttpRequestPacket::Data) { requestBodyBuf += packet.body; bool done = haveRequestBody; if(!packet.more) { haveRequestBody = true; state = ServerResponseWait; } if(packet.credits > 0) { outCredits += packet.credits; writableChanged = true; } if(!packet.body.isEmpty() || (!done && haveRequestBody)) readableChanged = true; if(readableChanged || writableChanged) update(); } else if(packet.type == ZhttpRequestPacket::Credit) { if(packet.credits > 0) { outCredits += packet.credits; writableChanged = true; update(); } } else if(packet.type == ZhttpRequestPacket::KeepAlive) { // nothing to do } else if(packet.type == ZhttpRequestPacket::HandoffProceed) { if(pausing) { pausing = false; paused = true; q->paused(); } } else { log_debug("zhttp server: unsupported packet type id=%s type=%d", id.data(), (int)packet.type); } } void handle(const QByteArray &id, int seq, const ZhttpResponsePacket &packet) { if(state == ClientRequestStartWait) { if(packet.from.isEmpty()) { state = Stopped; errored = true; errorCondition = ErrorGeneric; cleanup(); log_warning("zhttp client: error id=%s initial ack for streamed input request did not contain from field", id.data()); q->error(); return; } toAddress = packet.from; state = ClientRequesting; startKeepAlive(); } else if(state == ClientRequestFinishWait) { toAddress = packet.from; state = ClientReceiving; if(!doReq) startKeepAlive(); } if(packet.type == ZhttpResponsePacket::Error) { errored = true; errorCondition = convertError(packet.condition); log_debug("zhttp client: error id=%s cond=%s", id.data(), packet.condition.data()); state = Stopped; cleanup(); q->error(); return; } else if(packet.type == ZhttpResponsePacket::Cancel) { log_debug("zhttp client: received cancel id=%s", id.data()); errored = true; errorCondition = ErrorGeneric; state = Stopped; cleanup(); q->error(); return; } // if non-req mode, check sequencing if(!doReq && seq != inSeq) { log_warning("zhttp client: error id=%s received message out of sequence (expected %d, got %d), canceling", id.data(), inSeq, seq); // if this was not an error packet, send cancel if(packet.type != ZhttpResponsePacket::Error && packet.type != ZhttpResponsePacket::Cancel) { ZhttpRequestPacket p; p.type = ZhttpRequestPacket::Cancel; writePacket(p); } state = Stopped; errored = true; errorCondition = ErrorGeneric; cleanup(); q->error(); return; } ++inSeq; if(!multi && packet.multi) { // switch on multi support multi = true; startKeepAlive(); // re-setup keep alive } refreshTimeout(); if(doReq && (packet.type != ZhttpResponsePacket::Data || packet.more)) { log_warning("zhttp/zws client req: received invalid req response"); state = Stopped; errored = true; errorCondition = ErrorGeneric; cleanup(); q->error(); return; } if(packet.type == ZhttpResponsePacket::Data) { bool needToSendHeaders = false; if(!haveResponseValues) { haveResponseValues = true; responseCode = packet.code; responseReason = packet.reason; responseHeaders = packet.headers; needToSendHeaders = true; } if(doReq) { if(responseBodyBuf.size() + packet.body.size() > REQ_BUF_MAX) log_warning("zhttp client req: id=%s server response too large", id.data()); } else { if(responseBodyBuf.size() + packet.body.size() > IDEAL_CREDITS) log_warning("zhttp client: id=%s server is sending too fast", id.data()); } responseBodyBuf += packet.body; if(packet.more) { if(!doReq && packet.credits > 0) { outCredits += packet.credits; writableChanged = true; } if(needToSendHeaders || !packet.body.isEmpty()) readableChanged = true; if(readableChanged || writableChanged) update(); } else { // always emit readyRead here even if body is empty, for EOF state = Stopped; cleanup(); q->readyRead(); } } else if(packet.type == ZhttpResponsePacket::Credit) { if(packet.credits > 0) { outCredits += packet.credits; writableChanged = true; update(); } } else if(packet.type == ZhttpResponsePacket::KeepAlive) { // nothing to do } else { log_debug("zhttp client: unsupported packet type id=%s type=%d", id.data(), (int)packet.type); } } void writeBody(const QByteArray &body) { assert(!bodyFinished); assert(!pausing && !paused); if(server) responseBodyBuf += body; else requestBodyBuf += body; update(); } void endBody() { assert(!bodyFinished); assert(!pausing && !paused); bodyFinished = true; update(); } void writePacket(const ZhttpRequestPacket &packet) { assert(manager); ZhttpRequestPacket out = packet; out.from = rid.first; if(doReq) { out.ids += ZhttpRequestPacket::Id(rid.second); manager->writeHttp(out); } else { bool first = (outSeq == 0); out.ids += ZhttpRequestPacket::Id(rid.second, outSeq++); if(first) { manager->writeHttp(out); } else { assert(!toAddress.isEmpty()); manager->writeHttp(out, toAddress); } } } void writePacket(const ZhttpResponsePacket &packet) { assert(manager); ZhttpResponsePacket out = packet; out.from = manager->instanceId(); out.ids += ZhttpResponsePacket::Id(rid.second, outSeq++); out.userData = userData; manager->writeHttp(out, rid.first); } void writeCancel() { ZhttpResponsePacket out; out.type = ZhttpResponsePacket::Cancel; writePacket(out); } void writeError(const QByteArray &condition) { ZhttpResponsePacket out; out.type = ZhttpResponsePacket::Error; out.condition = condition; writePacket(out); } void tryCancel() { if(state == ClientRequesting || state == ClientReceiving) { state = Stopped; if(!doReq) { ZhttpRequestPacket p; p.type = ZhttpRequestPacket::Cancel; writePacket(p); } } else if(server) { state = Stopped; ZhttpResponsePacket p; p.type = ZhttpResponsePacket::Cancel; writePacket(p); } } void tryRespondCancel(const ZhttpRequestPacket &packet) { // if this was not an error packet, send cancel if(packet.type != ZhttpRequestPacket::Error && packet.type != ZhttpRequestPacket::Cancel) writeCancel(); } static ErrorCondition convertError(const QByteArray &cond) { // zhttp conditions: // remote-connection-failed // connection-timeout // tls-error // bad-request // policy-violation // max-size-exceeded // session-timeout if(cond == "policy-violation") return ErrorPolicy; else if(cond == "remote-connection-failed") return ErrorConnect; else if(cond == "tls-error") return ErrorTls; else if(cond == "length-required") return ErrorLengthRequired; else if(cond == "connection-timeout") return ErrorConnectTimeout; else if(cond == "disconnected") return ErrorDisconnected; else // lump the rest as generic return ErrorGeneric; } public slots: void doUpdate() { pendingUpdate = false; if(state == ClientStarting) { if(doReq) { if(requestBodyBuf.size() > REQ_BUF_MAX) { state = Stopped; errored = true; errorCondition = ErrorRequestTooLarge; cleanup(); q->error(); return; } // for req mode, wait until request is fully supplied then send in one packet if(bodyFinished) { ZhttpRequestPacket p; p.type = ZhttpRequestPacket::Data; p.method = requestMethod; p.uri = requestUri; p.headers = requestHeaders; p.body = requestBodyBuf.take(); p.maxSize = REQ_BUF_MAX; p.connectHost = connectHost; p.connectPort = connectPort; if(ignorePolicies) p.ignorePolicies = true; if(trustConnectHost) p.trustConnectHost = true; if(ignoreTlsErrors) p.ignoreTlsErrors = true; if(passthrough.isValid()) p.passthrough = passthrough; if(quiet) p.quiet = true; writePacket(p); state = ClientRequestFinishWait; q->bytesWritten(p.body.size()); } } else { // NOTE: not quite sure why we do this. maybe to avoid a // zhttp PUSH/SUB race? if(!manager->canWriteImmediately()) { state = Stopped; errored = true; errorCondition = ErrorUnavailable; cleanup(); q->error(); return; } ZhttpRequestPacket p; p.type = ZhttpRequestPacket::Data; p.method = requestMethod; p.uri = requestUri; p.headers = requestHeaders; if(!sendBodyAfterAck) { // even though we don't have credits yet, we can act // like we do on the first packet. we'll still cap // our potential size though. p.body = requestBodyBuf.take(IDEAL_CREDITS); } if(!requestBodyBuf.isEmpty() || !bodyFinished) p.more = true; p.stream = true; p.connectHost = connectHost; p.connectPort = connectPort; if(ignorePolicies) p.ignorePolicies = true; if(trustConnectHost) p.trustConnectHost = true; if(ignoreTlsErrors) p.ignoreTlsErrors = true; if(passthrough.isValid()) p.passthrough = passthrough; if(quiet) p.quiet = true; p.credits = IDEAL_CREDITS; p.multi = true; writePacket(p); if(p.more) state = ClientRequestStartWait; else state = ClientRequestFinishWait; if(!p.body.isEmpty()) q->bytesWritten(p.body.size()); else if(!p.more) q->bytesWritten(0); } } else if(state == ClientRequesting) { QPointer self = this; tryWrite(); if(!self) return; if(writableChanged) { writableChanged = false; q->writeBytesChanged(); } } else if(state == ClientReceiving) { if(readableChanged) { readableChanged = false; q->readyRead(); } } else if(state == ServerStarting) { if(haveRequestBody) { state = ServerResponseWait; // send ack ZhttpResponsePacket p; p.type = ZhttpResponsePacket::KeepAlive; if(multi) p.multi = true; writePacket(p); } else { state = ServerReceiving; // send credits ack ZhttpResponsePacket p; p.type = ZhttpResponsePacket::Credit; p.credits = IDEAL_CREDITS - responseBodyBuf.size(); if(multi) p.multi = true; writePacket(p); } q->readyRead(); } else if(state == ServerReceiving) { if(readableChanged) { readableChanged = false; q->readyRead(); } } else if(state == ServerResponseWait) { trySendPause(); if(readableChanged) { readableChanged = false; q->readyRead(); } } else if(state == ServerResponseStarting) { state = ServerResponding; ZhttpResponsePacket packet; packet.type = ZhttpResponsePacket::Data; packet.code = responseCode; packet.reason = responseReason; packet.headers = responseHeaders; packet.body = responseBodyBuf.take(outCredits); outCredits -= packet.body.size(); packet.more = (!responseBodyBuf.isEmpty() || !bodyFinished); writePacket(packet); if(!packet.more) { state = Stopped; cleanup(); } QPointer self = this; if(!packet.body.isEmpty()) q->bytesWritten(packet.body.size()); else if(!packet.more) q->bytesWritten(0); if(!self) return; trySendPause(); } else if(state == ServerResponding) { QPointer self = this; tryWrite(); if(!self) return; if(writableChanged) { writableChanged = false; q->writeBytesChanged(); } } } public: void expire_timeout() { state = Stopped; errored = true; errorCondition = ErrorTimeout; cleanup(); q->error(); } void keepAlive_timeout() { if(server) { ZhttpResponsePacket p; p.type = ZhttpResponsePacket::KeepAlive; writePacket(p); } else { ZhttpRequestPacket p; p.type = ZhttpRequestPacket::KeepAlive; writePacket(p); } } }; ZhttpRequest::ZhttpRequest(QObject *parent) : HttpRequest(parent) { d = new Private(this); } ZhttpRequest::~ZhttpRequest() { delete d; } ZhttpRequest::Rid ZhttpRequest::rid() const { return d->rid; } QVariant ZhttpRequest::passthroughData() const { return d->passthrough; } QHostAddress ZhttpRequest::peerAddress() const { return d->peerAddress; } void ZhttpRequest::setConnectHost(const QString &host) { d->connectHost = host; } void ZhttpRequest::setConnectPort(int port) { d->connectPort = port; } void ZhttpRequest::setIgnorePolicies(bool on) { d->ignorePolicies = on; } void ZhttpRequest::setTrustConnectHost(bool on) { d->trustConnectHost = on; } void ZhttpRequest::setIgnoreTlsErrors(bool on) { d->ignoreTlsErrors = on; } void ZhttpRequest::setIsTls(bool on) { d->requestUri.setScheme(on ? "https" : "http"); } void ZhttpRequest::setSendBodyAfterAcknowledgement(bool on) { d->sendBodyAfterAck = on; } void ZhttpRequest::setPassthroughData(const QVariant &data) { d->passthrough = data; } void ZhttpRequest::setQuiet(bool on) { d->quiet = on; } void ZhttpRequest::start(const QString &method, const QUrl &uri, const HttpHeaders &headers) { assert(!d->server); d->requestMethod = method; d->requestUri = uri; d->requestHeaders = headers; d->startClient(); } void ZhttpRequest::beginResponse(int code, const QByteArray &reason, const HttpHeaders &headers) { assert(d->server); assert(d->state == Private::ServerReceiving || d->state == Private::ServerResponseWait); d->responseCode = code; d->responseReason = reason; d->responseHeaders = headers; d->beginResponse(); } void ZhttpRequest::writeBody(const QByteArray &body) { d->writeBody(body); } void ZhttpRequest::endBody() { d->endBody(); } void ZhttpRequest::pause() { assert(d->server); d->pause(); } void ZhttpRequest::resume() { assert(d->server); d->resume(); } ZhttpRequest::ServerState ZhttpRequest::serverState() const { ServerState ss; ss.peerAddress = d->peerAddress; ss.requestMethod = d->requestMethod; ss.requestUri = d->requestUri; ss.requestHeaders = d->requestHeaders; if(d->state == Private::ServerResponding) ss.responseCode = d->responseCode; ss.inSeq = d->inSeq; ss.outSeq = d->outSeq; ss.outCredits = d->outCredits; ss.userData = d->userData; return ss; } int ZhttpRequest::bytesAvailable() const { if(d->server) return d->requestBodyBuf.size(); else return d->responseBodyBuf.size(); } int ZhttpRequest::writeBytesAvailable() const { if(d->server && d->responseBodyBuf.size() < d->outCredits) return d->outCredits - d->responseBodyBuf.size(); else if(!d->server && d->requestBodyBuf.size() < d->outCredits) return d->outCredits - d->requestBodyBuf.size(); else return 0; } bool ZhttpRequest::isFinished() const { return d->state == Private::Stopped; } bool ZhttpRequest::isInputFinished() const { if(d->server) return (d->state == Private::Stopped || d->state == Private::ServerResponseWait || d->state == Private::ServerResponseStarting || d->state == Private::ServerResponding); else return (d->state == Private::Stopped); } bool ZhttpRequest::isOutputFinished() const { if(d->server) return (d->state == Private::Stopped); else return (d->state == Private::Stopped || d->state == Private::ClientRequestFinishWait || d->state == Private::ClientReceiving); } bool ZhttpRequest::isErrored() const { return d->errored; } HttpRequest::ErrorCondition ZhttpRequest::errorCondition() const { return d->errorCondition; } QString ZhttpRequest::requestMethod() const { return d->requestMethod; } QUrl ZhttpRequest::requestUri() const { return d->requestUri; } HttpHeaders ZhttpRequest::requestHeaders() const { return d->requestHeaders; } int ZhttpRequest::responseCode() const { return d->responseCode; } QByteArray ZhttpRequest::responseReason() const { return d->responseReason; } HttpHeaders ZhttpRequest::responseHeaders() const { return d->responseHeaders; } QByteArray ZhttpRequest::readBody(int size) { return d->readBody(size); } void ZhttpRequest::setupClient(ZhttpManager *manager, bool req) { d->manager = manager; d->rid = Rid(manager->instanceId(), UuidUtil::createUuid()); d->doReq = req; d->manager->link(this); } bool ZhttpRequest::setupServer(ZhttpManager *manager, const QByteArray &id, int seq, const ZhttpRequestPacket &packet) { d->manager = manager; d->server = true; d->rid = Rid(packet.from, id); return d->setupServer(seq, packet); } void ZhttpRequest::setupServer(ZhttpManager *manager, const ZhttpRequest::ServerState &state) { d->manager = manager; d->server = true; d->rid = state.rid; d->manager->link(this); d->setupServer(state); } void ZhttpRequest::startServer() { d->startServer(); } bool ZhttpRequest::isServer() const { return d->server; } QByteArray ZhttpRequest::toAddress() const { return d->toAddress; } int ZhttpRequest::outSeqInc() { return d->outSeq++; } void ZhttpRequest::handle(const QByteArray &id, int seq, const ZhttpRequestPacket &packet) { assert(d->manager); d->handle(id, seq, packet); } void ZhttpRequest::handle(const QByteArray &id, int seq, const ZhttpResponsePacket &packet) { assert(d->manager); d->handle(id, seq, packet); } #include "zhttprequest.moc" pushpin-1.39.1/src/cpp/zhttprequest.h000066400000000000000000000067261457610542000176160ustar00rootroot00000000000000/* * Copyright (C) 2012-2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef ZHTTPREQUEST_H #define ZHTTPREQUEST_H #include #include "httprequest.h" #include using Connection = boost::signals2::scoped_connection; class ZhttpRequestPacket; class ZhttpResponsePacket; class ZhttpManager; class ZhttpRequest : public HttpRequest { Q_OBJECT public: // pair of sender + request id typedef QPair Rid; class ServerState { public: Rid rid; QHostAddress peerAddress; QString requestMethod; QUrl requestUri; HttpHeaders requestHeaders; QByteArray requestBody; int responseCode; int inSeq; int outSeq; int outCredits; QVariant userData; ServerState() : responseCode(-1), inSeq(-1), outSeq(-1), outCredits(-1) { } }; ~ZhttpRequest(); Rid rid() const; QVariant passthroughData() const; void setIsTls(bool on); // updates scheme void setSendBodyAfterAcknowledgement(bool on); // only works in push/sub mode void setPassthroughData(const QVariant &data); void setQuiet(bool on); // for server requests only void pause(); void resume(); ServerState serverState() const; // reimplemented virtual QHostAddress peerAddress() const; virtual void setConnectHost(const QString &host); virtual void setConnectPort(int port); virtual void setIgnorePolicies(bool on); virtual void setTrustConnectHost(bool on); virtual void setIgnoreTlsErrors(bool on); virtual void start(const QString &method, const QUrl &uri, const HttpHeaders &headers); virtual void beginResponse(int code, const QByteArray &reason, const HttpHeaders &headers); virtual void writeBody(const QByteArray &body); virtual void endBody(); virtual int bytesAvailable() const; virtual int writeBytesAvailable() const; virtual bool isFinished() const; virtual bool isInputFinished() const; virtual bool isOutputFinished() const; virtual bool isErrored() const; virtual ErrorCondition errorCondition() const; virtual QString requestMethod() const; virtual QUrl requestUri() const; virtual HttpHeaders requestHeaders() const; virtual int responseCode() const; virtual QByteArray responseReason() const; virtual HttpHeaders responseHeaders() const; virtual QByteArray readBody(int size = -1); private: class Private; friend class Private; Private *d; friend class ZhttpManager; ZhttpRequest(QObject *parent = 0); void setupClient(ZhttpManager *manager, bool req); bool setupServer(ZhttpManager *manager, const QByteArray &id, int seq, const ZhttpRequestPacket &packet); void setupServer(ZhttpManager *manager, const ServerState &state); void startServer(); bool isServer() const; QByteArray toAddress() const; int outSeqInc(); void handle(const QByteArray &id, int seq, const ZhttpRequestPacket &packet); void handle(const QByteArray &id, int seq, const ZhttpResponsePacket &packet); }; #endif pushpin-1.39.1/src/cpp/zhttprequestpacket.cpp000066400000000000000000000227651457610542000213420ustar00rootroot00000000000000/* * Copyright (C) 2012-2016 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "zhttprequestpacket.h" #include #include "qtcompat.h" #include "tnetstring.h" QVariant ZhttpRequestPacket::toVariant() const { QVariantHash obj; if(!from.isEmpty()) obj["from"] = from; if(!ids.isEmpty()) { if(ids.count() == 1) { const Id &id = ids.first(); if(!id.id.isEmpty()) obj["id"] = id.id; if(id.seq != -1) obj["seq"] = id.seq; } else { QVariantList vl; foreach(const Id &id, ids) { QVariantHash vh; if(!id.id.isEmpty()) vh["id"] = id.id; if(id.seq != -1) vh["seq"] = id.seq; vl += vh; } obj["id"] = vl; } } QByteArray typeStr; switch(type) { case Error: typeStr = "error"; break; case Credit: typeStr = "credit"; break; case KeepAlive: typeStr = "keep-alive"; break; case Cancel: typeStr = "cancel"; break; case HandoffStart: typeStr = "handoff-start"; break; case HandoffProceed: typeStr = "handoff-proceed"; break; case Close: typeStr = "close"; break; case Ping: typeStr = "ping"; break; case Pong: typeStr = "pong"; break; default: break; } if(!typeStr.isEmpty()) obj["type"] = typeStr; if(type == Error && !condition.isEmpty()) obj["condition"] = condition; if(credits != -1) obj["credits"] = credits; if(more) obj["more"] = true; if(stream) obj["stream"] = true; if(maxSize != -1) obj["max-size"] = maxSize; if(timeout != -1) obj["timeout"] = timeout; if(!method.isEmpty()) obj["method"] = method.toLatin1(); if(!uri.isEmpty()) obj["uri"] = uri.toEncoded(); if(!headers.isEmpty()) { QVariantList vheaders; foreach(const HttpHeader &h, headers) { QVariantList vheader; vheader += h.first; vheader += h.second; vheaders += QVariant(vheader); } obj["headers"] = vheaders; } if(!body.isNull()) obj["body"] = body; if(!contentType.isEmpty()) obj["content-type"] = contentType; if(code != -1) obj["code"] = code; if(userData.isValid()) obj["user-data"] = userData; if(!peerAddress.isNull()) obj["peer-address"] = peerAddress.toString().toUtf8(); if(peerPort != -1) obj["peer-port"] = QByteArray::number(peerPort); if(!connectHost.isEmpty()) obj["connect-host"] = connectHost.toUtf8(); if(connectPort != -1) obj["connect-port"] = connectPort; if(ignorePolicies) obj["ignore-policies"] = true; if(trustConnectHost) obj["trust-connect-host"] = true; if(ignoreTlsErrors) obj["ignore-tls-errors"] = true; if(followRedirects) obj["follow-redirects"] = true; if(passthrough.isValid()) obj["passthrough"] = passthrough; if(multi || quiet) { QVariantHash ext; if(multi) ext["multi"] = true; if(quiet) ext["quiet"] = true; obj["ext"] = ext; } return obj; } bool ZhttpRequestPacket::fromVariant(const QVariant &in) { if(typeId(in) != QMetaType::QVariantHash) return false; QVariantHash obj = in.toHash(); from.clear(); if(obj.contains("from")) { if(typeId(obj["from"]) != QMetaType::QByteArray) return false; from = obj["from"].toByteArray(); } ids.clear(); if(obj.contains("id")) { if(typeId(obj["id"]) == QMetaType::QByteArray) { Id id; id.id = obj["id"].toByteArray(); ids += id; } else if(typeId(obj["id"]) == QMetaType::QVariantList) { QVariantList vl = obj["id"].toList(); foreach(const QVariant &v, vl) { if(typeId(v) != QMetaType::QVariantHash) return false; Id id; QVariantHash vh = v.toHash(); if(vh.contains("id")) { if(typeId(vh["id"]) != QMetaType::QByteArray) return false; id.id = vh["id"].toByteArray(); } if(vh.contains("seq")) { if(!canConvert(vh["seq"], QMetaType::Int)) return false; id.seq = vh["seq"].toInt(); } ids += id; } } else return false; } if(obj.contains("seq")) { if(!canConvert(obj["seq"], QMetaType::Int)) return false; if(ids.isEmpty()) ids += Id(); ids.first().seq = obj["seq"].toInt(); } type = Data; if(obj.contains("type")) { if(typeId(obj["type"]) != QMetaType::QByteArray) return false; QByteArray typeStr = obj["type"].toByteArray(); if(typeStr == "error") type = Error; else if(typeStr == "credit") type = Credit; else if(typeStr == "keep-alive") type = KeepAlive; else if(typeStr == "cancel") type = Cancel; else if(typeStr == "handoff-start") type = HandoffStart; else if(typeStr == "handoff-proceed") type = HandoffProceed; else if(typeStr == "close") type = Close; else if(typeStr == "ping") type = Ping; else if(typeStr == "pong") type = Pong; else return false; } if(type == Error) { condition.clear(); if(obj.contains("condition")) { if(typeId(obj["condition"]) != QMetaType::QByteArray) return false; condition = obj["condition"].toByteArray(); } } credits = -1; if(obj.contains("credits")) { if(!canConvert(obj["credits"], QMetaType::Int)) return false; credits = obj["credits"].toInt(); } more = false; if(obj.contains("more")) { if(typeId(obj["more"]) != QMetaType::Bool) return false; more = obj["more"].toBool(); } stream = false; if(obj.contains("stream")) { if(typeId(obj["stream"]) != QMetaType::Bool) return false; stream = obj["stream"].toBool(); } maxSize = -1; if(obj.contains("max-size")) { if(!canConvert(obj["max-size"], QMetaType::Int)) return false; maxSize = obj["max-size"].toInt(); } timeout = -1; if(obj.contains("timeout")) { if(!canConvert(obj["timeout"], QMetaType::Int)) return false; timeout = obj["timeout"].toInt(); } method.clear(); if(obj.contains("method")) { if(typeId(obj["method"]) != QMetaType::QByteArray) return false; method = QString::fromLatin1(obj["method"].toByteArray()); } uri.clear(); if(obj.contains("uri")) { if(typeId(obj["uri"]) != QMetaType::QByteArray) return false; uri = QUrl::fromEncoded(obj["uri"].toByteArray(), QUrl::StrictMode); } headers.clear(); if(obj.contains("headers")) { if(typeId(obj["headers"]) != QMetaType::QVariantList) return false; foreach(const QVariant &i, obj["headers"].toList()) { QVariantList list = i.toList(); if(list.count() != 2) return false; if(typeId(list[0]) != QMetaType::QByteArray || typeId(list[1]) != QMetaType::QByteArray) return false; headers += HttpHeader(list[0].toByteArray(), list[1].toByteArray()); } } body.clear(); if(obj.contains("body")) { if(typeId(obj["body"]) != QMetaType::QByteArray) return false; body = obj["body"].toByteArray(); } contentType.clear(); if(obj.contains("content-type")) { if(typeId(obj["content-type"]) != QMetaType::QByteArray) return false; contentType = obj["content-type"].toByteArray(); } code = -1; if(obj.contains("code")) { if(!canConvert(obj["code"], QMetaType::Int)) return false; code = obj["code"].toInt(); } userData = obj.value("user-data"); peerAddress = QHostAddress(); if(obj.contains("peer-address")) { if(typeId(obj["peer-address"]) != QMetaType::QByteArray) return false; peerAddress = QHostAddress(QString::fromUtf8(obj["peer-address"].toByteArray())); } peerPort = -1; if(obj.contains("peer-port")) { if(!canConvert(obj["peer-port"], QMetaType::Int)) return false; peerPort = obj["peer-port"].toInt(); } connectHost.clear(); if(obj.contains("connect-host")) { if(typeId(obj["connect-host"]) != QMetaType::QByteArray) return false; connectHost = QString::fromUtf8(obj["connect-host"].toByteArray()); } connectPort = -1; if(obj.contains("connect-port")) { if(!canConvert(obj["connect-port"], QMetaType::Int)) return false; connectPort = obj["connect-port"].toInt(); } ignorePolicies = false; if(obj.contains("ignore-policies")) { if(typeId(obj["ignore-policies"]) != QMetaType::Bool) return false; ignorePolicies = obj["ignore-policies"].toBool(); } trustConnectHost = false; if(obj.contains("trust-connect-host")) { if(typeId(obj["trust-connect-host"]) != QMetaType::Bool) return false; trustConnectHost = obj["trust-connect-host"].toBool(); } ignoreTlsErrors = false; if(obj.contains("ignore-tls-errors")) { if(typeId(obj["ignore-tls-errors"]) != QMetaType::Bool) return false; ignoreTlsErrors = obj["ignore-tls-errors"].toBool(); } followRedirects = false; if(obj.contains("follow-redirects")) { if(typeId(obj["follow-redirects"]) != QMetaType::Bool) return false; followRedirects = obj["follow-redirects"].toBool(); } passthrough = obj.value("passthrough"); multi = false; if(obj.contains("ext")) { if(typeId(obj["ext"]) != QMetaType::QVariantHash) return false; QVariantHash ext = obj["ext"].toHash(); if(ext.contains("multi") && typeId(ext["multi"]) == QMetaType::Bool) { multi = ext["multi"].toBool(); } if(ext.contains("quiet") && typeId(ext["quiet"]) == QMetaType::Bool) { quiet = ext["quiet"].toBool(); } } return true; } pushpin-1.39.1/src/cpp/zhttprequestpacket.h000066400000000000000000000041371457610542000210000ustar00rootroot00000000000000/* * Copyright (C) 2012-2016 Fanout, Inc. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef ZHTTPREQUESTPACKET_H #define ZHTTPREQUESTPACKET_H #include #include #include #include "httpheaders.h" class ZhttpRequestPacket { public: class Id { public: QByteArray id; int seq; Id() : seq(-1) { } Id(const QByteArray &_id, int _seq = -1) : id(_id), seq(_seq) { } }; enum Type { Data, Error, Credit, KeepAlive, Cancel, HandoffStart, HandoffProceed, Close, // WebSocket Ping, // WebSocket Pong // WebSocket }; QByteArray from; QList ids; Type type; QByteArray condition; int credits; bool more; bool stream; int maxSize; int timeout; QString method; QUrl uri; HttpHeaders headers; QByteArray body; QByteArray contentType; // WebSocket int code; // WebSocket QVariant userData; QHostAddress peerAddress; int peerPort; QString connectHost; int connectPort; bool ignorePolicies; bool trustConnectHost; bool ignoreTlsErrors; bool followRedirects; QVariant passthrough; // if valid, may contain pushpin-specific passthrough info bool multi; bool quiet; ZhttpRequestPacket() : type((Type)-1), credits(-1), more(false), stream(false), maxSize(-1), timeout(-1), code(-1), peerPort(-1), connectPort(-1), ignorePolicies(false), trustConnectHost(false), ignoreTlsErrors(false), followRedirects(false), multi(false), quiet(false) { } QVariant toVariant() const; bool fromVariant(const QVariant &in); }; #endif pushpin-1.39.1/src/cpp/zhttpresponsepacket.cpp000066400000000000000000000144561457610542000215060ustar00rootroot00000000000000/* * Copyright (C) 2012-2013 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "zhttpresponsepacket.h" #include "qtcompat.h" QVariant ZhttpResponsePacket::toVariant() const { QVariantHash obj; if(!from.isEmpty()) obj["from"] = from; if(!ids.isEmpty()) { if(ids.count() == 1) { const Id &id = ids.first(); if(!id.id.isEmpty()) obj["id"] = id.id; if(id.seq != -1) obj["seq"] = id.seq; } else { QVariantList vl; foreach(const Id &id, ids) { QVariantHash vh; if(!id.id.isEmpty()) vh["id"] = id.id; if(id.seq != -1) vh["seq"] = id.seq; vl += vh; } obj["id"] = vl; } } QByteArray typeStr; switch(type) { case Error: typeStr = "error"; break; case Credit: typeStr = "credit"; break; case KeepAlive: typeStr = "keep-alive"; break; case Cancel: typeStr = "cancel"; break; case HandoffStart: typeStr = "handoff-start"; break; case HandoffProceed: typeStr = "handoff-proceed"; break; case Close: typeStr = "close"; break; case Ping: typeStr = "ping"; break; case Pong: typeStr = "pong"; break; default: break; } if(!typeStr.isEmpty()) obj["type"] = typeStr; if(type == Error && !condition.isEmpty()) obj["condition"] = condition; if(credits != -1) obj["credits"] = credits; if(more) obj["more"] = true; if(code != -1) { obj["code"] = code; if(type == Data || (type == Error && condition == "rejected")) { obj["reason"] = reason; QVariantList vheaders; foreach(const HttpHeader &h, headers) { QVariantList vheader; vheader += h.first; vheader += h.second; vheaders += QVariant(vheader); } obj["headers"] = vheaders; } } if(!body.isNull()) obj["body"] = body; if(!contentType.isEmpty()) obj["content-type"] = contentType; if(userData.isValid()) obj["user-data"] = userData; if(multi) { QVariantHash ext; ext["multi"] = true; obj["ext"] = ext; } return obj; } bool ZhttpResponsePacket::fromVariant(const QVariant &in) { if(typeId(in) != QMetaType::QVariantHash) return false; QVariantHash obj = in.toHash(); from.clear(); if(obj.contains("from")) { if(typeId(obj["from"]) != QMetaType::QByteArray) return false; from = obj["from"].toByteArray(); } ids.clear(); if(obj.contains("id")) { if(typeId(obj["id"]) == QMetaType::QByteArray) { Id id; id.id = obj["id"].toByteArray(); ids += id; } else if(typeId(obj["id"]) == QMetaType::QVariantList) { QVariantList vl = obj["id"].toList(); foreach(const QVariant &v, vl) { if(typeId(v) != QMetaType::QVariantHash) return false; Id id; QVariantHash vh = v.toHash(); if(vh.contains("id")) { if(typeId(vh["id"]) != QMetaType::QByteArray) return false; id.id = vh["id"].toByteArray(); } if(vh.contains("seq")) { if(!canConvert(vh["seq"], QMetaType::Int)) return false; id.seq = vh["seq"].toInt(); } ids += id; } } else return false; } if(obj.contains("seq")) { if(!canConvert(obj["seq"], QMetaType::Int)) return false; if(ids.isEmpty()) ids += Id(); ids.first().seq = obj["seq"].toInt(); } type = Data; if(obj.contains("type")) { if(typeId(obj["type"]) != QMetaType::QByteArray) return false; QByteArray typeStr = obj["type"].toByteArray(); if(typeStr == "error") type = Error; else if(typeStr == "credit") type = Credit; else if(typeStr == "keep-alive") type = KeepAlive; else if(typeStr == "cancel") type = Cancel; else if(typeStr == "handoff-start") type = HandoffStart; else if(typeStr == "handoff-proceed") type = HandoffProceed; else if(typeStr == "close") type = Close; else if(typeStr == "ping") type = Ping; else if(typeStr == "pong") type = Pong; else return false; } if(type == Error) { condition.clear(); if(obj.contains("condition")) { if(typeId(obj["condition"]) != QMetaType::QByteArray) return false; condition = obj["condition"].toByteArray(); } } credits = -1; if(obj.contains("credits")) { if(!canConvert(obj["credits"], QMetaType::Int)) return false; credits = obj["credits"].toInt(); } more = false; if(obj.contains("more")) { if(typeId(obj["more"]) != QMetaType::Bool) return false; more = obj["more"].toBool(); } code = -1; if(obj.contains("code")) { if(!canConvert(obj["code"], QMetaType::Int)) return false; code = obj["code"].toInt(); } reason.clear(); if(obj.contains("reason")) { if(typeId(obj["reason"]) != QMetaType::QByteArray) return false; reason = obj["reason"].toByteArray(); } headers.clear(); if(obj.contains("headers")) { if(typeId(obj["headers"]) != QMetaType::QVariantList) return false; foreach(const QVariant &i, obj["headers"].toList()) { QVariantList list = i.toList(); if(list.count() != 2) return false; if(typeId(list[0]) != QMetaType::QByteArray || typeId(list[1]) != QMetaType::QByteArray) return false; headers += HttpHeader(list[0].toByteArray(), list[1].toByteArray()); } } body.clear(); if(obj.contains("body")) { if(typeId(obj["body"]) != QMetaType::QByteArray) return false; body = obj["body"].toByteArray(); } contentType.clear(); if(obj.contains("content-type")) { if(typeId(obj["content-type"]) != QMetaType::QByteArray) return false; contentType = obj["content-type"].toByteArray(); } userData = obj["user-data"]; multi = false; if(obj.contains("ext")) { if(typeId(obj["ext"]) != QMetaType::QVariantHash) return false; QVariantHash ext = obj["ext"].toHash(); if(ext.contains("multi") && typeId(ext["multi"]) == QMetaType::Bool) { multi = ext["multi"].toBool(); } } return true; } pushpin-1.39.1/src/cpp/zhttpresponsepacket.h000066400000000000000000000030441457610542000211420ustar00rootroot00000000000000/* * Copyright (C) 2012-2016 Fanout, Inc. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef ZHTTPRESPONSEPACKET_H #define ZHTTPRESPONSEPACKET_H #include #include "httpheaders.h" class ZhttpResponsePacket { public: class Id { public: QByteArray id; int seq; Id() : seq(-1) { } Id(const QByteArray &_id, int _seq = -1) : id(_id), seq(_seq) { } }; enum Type { Data, Error, Credit, KeepAlive, Cancel, HandoffStart, HandoffProceed, Close, // WebSocket Ping, // WebSocket Pong // WebSocket }; QByteArray from; QList ids; Type type; QByteArray condition; int credits; bool more; int code; QByteArray reason; HttpHeaders headers; QByteArray body; QByteArray contentType; // WebSocket QVariant userData; bool multi; ZhttpResponsePacket() : type((Type)-1), credits(-1), more(false), code(-1), multi(false) { } QVariant toVariant() const; bool fromVariant(const QVariant &in); }; #endif pushpin-1.39.1/src/cpp/zrpcmanager.cpp000066400000000000000000000166271457610542000177010ustar00rootroot00000000000000/* * Copyright (C) 2014-2016 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "zrpcmanager.h" #include #include #include #include "qzmqsocket.h" #include "qzmqvalve.h" #include "qzmqreqmessage.h" #include "log.h" #include "tnetstring.h" #include "packet/zrpcrequestpacket.h" #include "packet/zrpcresponsepacket.h" #include "zrpcrequest.h" #include "zutil.h" #define OUT_HWM 100 #define IN_HWM 100 #define REQ_WAIT_TIME 0 #define REP_WAIT_TIME 500 #define PENDING_MAX 100 class ZrpcManager::Private : public QObject { Q_OBJECT public: class PendingItem { public: QList headers; ZrpcRequestPacket packet; }; ZrpcManager *q; QByteArray instanceId; int ipcFileMode; bool doBind; int timeout; QStringList clientSpecs; QStringList serverSpecs; QZmq::Socket *clientSock; QZmq::Socket *serverSock; QZmq::Valve *clientValve; QZmq::Valve *serverValve; QHash clientReqsById; QList pending; Connection clientValveConnection; Connection serverValveConnection; Private(ZrpcManager *_q) : QObject(_q), q(_q), ipcFileMode(-1), doBind(false), timeout(-1), clientSock(0), serverSock(0), clientValve(0), serverValve(0) { } ~Private() { assert(clientReqsById.isEmpty()); } bool setupClient() { clientValveConnection.disconnect(); delete clientValve; delete clientSock; clientSock = new QZmq::Socket(QZmq::Socket::Dealer, this); clientSock->setSendHwm(OUT_HWM); clientSock->setShutdownWaitTime(REQ_WAIT_TIME); QString errorMessage; if(!ZUtil::setupSocket(clientSock, clientSpecs, doBind, ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } clientValve = new QZmq::Valve(clientSock, this); clientValveConnection = clientValve->readyRead.connect(boost::bind(&Private::client_readyRead, this, boost::placeholders::_1)); clientValve->open(); return true; } bool setupServer() { serverValveConnection.disconnect(); delete serverValve; delete serverSock; serverSock = new QZmq::Socket(QZmq::Socket::Router, this); serverSock->setReceiveHwm(IN_HWM); serverSock->setShutdownWaitTime(REP_WAIT_TIME); QString errorMessage; if(!ZUtil::setupSocket(serverSock, serverSpecs, doBind, ipcFileMode, &errorMessage)) { log_error("%s", qPrintable(errorMessage)); return false; } serverValve = new QZmq::Valve(serverSock, this); serverValveConnection = serverValve->readyRead.connect(boost::bind(&Private::server_readyRead, this, boost::placeholders::_1)); serverValve->open(); return true; } void write(const ZrpcRequestPacket &packet) { assert(clientSock); ZrpcRequestPacket p = packet; p.from = instanceId; QVariant vpacket = p.toVariant(); QByteArray buf = TnetString::fromVariant(vpacket); if(log_outputLevel() >= LOG_LEVEL_DEBUG) log_debug("zrpc client: OUT %s", qPrintable(TnetString::variantToString(vpacket, -1))); clientSock->write(QList() << QByteArray() << buf); } void write(const QList &headers, const ZrpcResponsePacket &packet) { assert(serverSock); QVariant vpacket = packet.toVariant(); QByteArray buf = TnetString::fromVariant(vpacket); if(log_outputLevel() >= LOG_LEVEL_DEBUG) log_debug("zrpc server: OUT %s", qPrintable(TnetString::variantToString(vpacket, -1))); QList message; message += headers; message += QByteArray(); message += buf; serverSock->write(message); } void client_readyRead(const QList &message) { if(message.count() != 2) { log_warning("zrpc client: received message with parts != 2, skipping"); return; } if(!message[0].isEmpty()) { log_warning("zrpc client: received message with first part non-empty, skipping"); return; } QVariant data = TnetString::toVariant(message[1]); if(data.isNull()) { log_warning("zrpc client: received message with invalid format (tnetstring parse failed), skipping"); return; } if(log_outputLevel() >= LOG_LEVEL_DEBUG) log_debug("zrpc client: IN %s", qPrintable(TnetString::variantToString(data, -1))); ZrpcResponsePacket p; if(!p.fromVariant(data)) { log_warning("zrpc client: received message with invalid format (parse failed), skipping"); return; } ZrpcRequest *req = clientReqsById.value(p.id); if(!req) { log_debug("zrpc client: received message for unknown request id, skipping"); return; } req->handle(p); } void server_readyRead(const QList &message) { QZmq::ReqMessage req(message); if(req.content().count() != 1) { log_warning("zrpc server: received message with parts != 1, skipping"); return; } QVariant data = TnetString::toVariant(req.content()[0]); if(data.isNull()) { log_warning("zrpc server: received message with invalid format (tnetstring parse failed), skipping"); return; } if(log_outputLevel() >= LOG_LEVEL_DEBUG) log_debug("zrpc server: IN %s", qPrintable(TnetString::variantToString(data, -1))); ZrpcRequestPacket p; if(!p.fromVariant(data)) { log_warning("zrpc server: received message with invalid format (parse failed), skipping"); return; } PendingItem i; i.headers = req.headers(); i.packet = p; pending += i; if(pending.count() >= PENDING_MAX) serverValve->close(); q->requestReady(); } }; ZrpcManager::ZrpcManager(QObject *parent) : QObject(parent) { d = new Private(this); } ZrpcManager::~ZrpcManager() { delete d; } int ZrpcManager::timeout() const { return d->timeout; } void ZrpcManager::setInstanceId(const QByteArray &instanceId) { d->instanceId = instanceId; } void ZrpcManager::setIpcFileMode(int mode) { d->ipcFileMode = mode; } void ZrpcManager::setBind(bool enable) { d->doBind = enable; } void ZrpcManager::setTimeout(int ms) { d->timeout = ms; } bool ZrpcManager::setClientSpecs(const QStringList &specs) { d->clientSpecs = specs; return d->setupClient(); } bool ZrpcManager::setServerSpecs(const QStringList &specs) { d->serverSpecs = specs; return d->setupServer(); } ZrpcRequest *ZrpcManager::takeNext() { if(d->pending.isEmpty()) return 0; Private::PendingItem i = d->pending.takeFirst(); ZrpcRequest *req = new ZrpcRequest; req->setupServer(this); req->handle(i.headers, i.packet); d->serverValve->open(); return req; } bool ZrpcManager::canWriteImmediately() const { assert(d->clientSock); return d->clientSock->canWriteImmediately(); } void ZrpcManager::link(ZrpcRequest *req) { d->clientReqsById.insert(req->id(), req); } void ZrpcManager::unlink(ZrpcRequest *req) { d->clientReqsById.remove(req->id()); } void ZrpcManager::write(const ZrpcRequestPacket &packet) { d->write(packet); } void ZrpcManager::write(const QList &headers, const ZrpcResponsePacket &packet) { d->write(headers, packet); } #include "zrpcmanager.moc" pushpin-1.39.1/src/cpp/zrpcmanager.h000066400000000000000000000032671457610542000173420ustar00rootroot00000000000000/* * Copyright (C) 2014 Fanout, Inc. * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef ZRPCMANAGER_H #define ZRPCMANAGER_H #include #include using Signal = boost::signals2::signal; class ZrpcRequestPacket; class ZrpcResponsePacket; class ZrpcRequest; class ZrpcManager : public QObject { Q_OBJECT public: ZrpcManager(QObject *parent = 0); ~ZrpcManager(); int timeout() const; void setInstanceId(const QByteArray &instanceId); void setIpcFileMode(int mode); void setBind(bool enable); void setTimeout(int ms); void setUnavailableOnTimeout(bool enable); bool setClientSpecs(const QStringList &specs); bool setServerSpecs(const QStringList &specs); ZrpcRequest *takeNext(); bool canWriteImmediately() const; void write(const ZrpcRequestPacket &packet); Signal requestReady; private: class Private; Private *d; friend class ZrpcRequest; void link(ZrpcRequest *req); void unlink(ZrpcRequest *req); void write(const QList &headers, const ZrpcResponsePacket &packet); }; #endif pushpin-1.39.1/src/cpp/zrpcrequest.cpp000066400000000000000000000121251457610542000177440ustar00rootroot00000000000000/* * Copyright (C) 2014-2015 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "zrpcrequest.h" #include #include #include "packet/zrpcrequestpacket.h" #include "packet/zrpcresponsepacket.h" #include "zrpcmanager.h" #include "uuidutil.h" #include "log.h" class ZrpcRequest::Private : public QObject { Q_OBJECT public: ZrpcRequest *q; ZrpcManager *manager; QList reqHeaders; QByteArray from; QByteArray id; QString method; QVariantHash args; bool success; QVariant result; ErrorCondition condition; QByteArray conditionString; QTimer *timer; Private(ZrpcRequest *_q) : QObject(_q), q(_q), manager(0), success(false), condition(ErrorGeneric), timer(0) { } ~Private() { cleanup(); } void cleanup() { if(timer) { timer->disconnect(this); timer->setParent(0); timer->deleteLater(); timer = 0; } if(manager) { manager->unlink(q); manager = 0; } } void respond(const QVariant &value) { ZrpcResponsePacket p; p.id = id; p.success = true; p.value = value; manager->write(reqHeaders, p); } void respondError(const QByteArray &condition, const QVariant &value) { ZrpcResponsePacket p; p.id = id; p.success = false; p.condition = condition; p.value = value; manager->write(reqHeaders, p); } void handle(const QList &headers, const ZrpcRequestPacket &packet) { reqHeaders = headers; from = packet.from; id = packet.id; method = packet.method; args = packet.args; } void handle(const ZrpcResponsePacket &packet) { cleanup(); success = packet.success; if(success) { result = packet.value; q->onSuccess(); } else { if(packet.condition == "bad-format") condition = ErrorFormat; else condition = ErrorGeneric; conditionString = packet.condition; result = packet.value; q->onError(); } q->finished(); } private slots: void doStart() { if(!manager->canWriteImmediately()) { success = false; condition = ErrorUnavailable; conditionString = "service-unavailable"; cleanup(); q->finished(); return; } ZrpcRequestPacket p; p.id = id; p.method = method; p.args = args; if(manager->timeout() >= 0) { timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &Private::timer_timeout); timer->setSingleShot(true); timer->start(manager->timeout()); } manager->write(p); } void timer_timeout() { success = false; condition = ErrorTimeout; conditionString = "timeout"; cleanup(); q->finished(); } }; ZrpcRequest::ZrpcRequest(QObject *parent) : QObject(parent) { d = new Private(this); } ZrpcRequest::ZrpcRequest(ZrpcManager *manager, QObject *parent) : QObject(parent) { d = new Private(this); setupClient(manager); } ZrpcRequest::~ZrpcRequest() { destroyed(); delete d; } QByteArray ZrpcRequest::from() const { return d->from; } QByteArray ZrpcRequest::id() const { return d->id; } QString ZrpcRequest::method() const { return d->method; } QVariantHash ZrpcRequest::args() const { return d->args; } bool ZrpcRequest::success() const { return d->success; } QVariant ZrpcRequest::result() const { return d->result; } ZrpcRequest::ErrorCondition ZrpcRequest::errorCondition() const { return d->condition; } QByteArray ZrpcRequest::errorConditionString() const { return d->conditionString; } void ZrpcRequest::start(const QString &method, const QVariantHash &args) { d->method = method; d->args = args; QMetaObject::invokeMethod(d, "doStart", Qt::QueuedConnection); } void ZrpcRequest::respond(const QVariant &result) { d->respond(result); } void ZrpcRequest::respondError(const QByteArray &condition, const QVariant &result) { d->respondError(condition, result); } void ZrpcRequest::setError(ErrorCondition condition, const QVariant &result) { d->success = false; d->condition = condition; d->result = result; } void ZrpcRequest::onSuccess() { // by default, do nothing } void ZrpcRequest::onError() { // by default, do nothing } void ZrpcRequest::setupClient(ZrpcManager *manager) { d->id = UuidUtil::createUuid(); d->manager = manager; d->manager->link(this); } void ZrpcRequest::setupServer(ZrpcManager *manager) { d->manager = manager; } void ZrpcRequest::handle(const QList &headers, const ZrpcRequestPacket &packet) { assert(d->manager); d->handle(headers, packet); } void ZrpcRequest::handle(const ZrpcResponsePacket &packet) { assert(d->manager); d->handle(packet); } #include "zrpcrequest.moc" pushpin-1.39.1/src/cpp/zrpcrequest.h000066400000000000000000000041601457610542000174110ustar00rootroot00000000000000/* * Copyright (C) 2014-2015 Fanout, Inc. * Copyright (C) 2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef ZRPCREQUEST_H #define ZRPCREQUEST_H #include #include #include using Signal = boost::signals2::signal; class ZrpcRequestPacket; class ZrpcResponsePacket; class ZrpcManager; class ZrpcRequest : public QObject { Q_OBJECT public: enum ErrorCondition { ErrorGeneric, ErrorFormat, ErrorUnavailable, ErrorTimeout }; ZrpcRequest(ZrpcManager *manager, QObject *parent = 0); ~ZrpcRequest(); QByteArray from() const; QByteArray id() const; QString method() const; QVariantHash args() const; bool success() const; QVariant result() const; ErrorCondition errorCondition() const; QByteArray errorConditionString() const; void start(const QString &method, const QVariantHash &args = QVariantHash()); void respond(const QVariant &result = QVariant()); void respondError(const QByteArray &condition, const QVariant &result = QVariant()); void setError(ErrorCondition condition, const QVariant &result = QVariant()); Signal finished; Signal destroyed; protected: virtual void onSuccess(); virtual void onError(); private: class Private; Private *d; friend class ZrpcManager; ZrpcRequest(QObject *parent = 0); void setupClient(ZrpcManager *manager); void setupServer(ZrpcManager *manager); void handle(const QList &headers, const ZrpcRequestPacket &packet); void handle(const ZrpcResponsePacket &packet); }; #endif pushpin-1.39.1/src/cpp/zutil.cpp000066400000000000000000000045061457610542000165300ustar00rootroot00000000000000/* * Copyright (C) 2015 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "zutil.h" #include #include "qzmqsocket.h" namespace ZUtil { bool bindSpec(QZmq::Socket *sock, const QString &spec, int ipcFileMode, QString *errorMessage) { if(!sock->bind(spec)) { if(errorMessage) *errorMessage = QString("unable to bind to %1").arg(spec); return false; } if(spec.startsWith("ipc://") && ipcFileMode != -1) { QFile::Permissions perms; if(ipcFileMode & 0400) perms |= QFile::ReadUser; if(ipcFileMode & 0200) perms |= QFile::WriteUser; if(ipcFileMode & 0100) perms |= QFile::ExeUser; if(ipcFileMode & 0040) perms |= QFile::ReadGroup; if(ipcFileMode & 0020) perms |= QFile::WriteGroup; if(ipcFileMode & 0010) perms |= QFile::ExeGroup; if(ipcFileMode & 0004) perms |= QFile::ReadOther; if(ipcFileMode & 0002) perms |= QFile::WriteOther; if(ipcFileMode & 0001) perms |= QFile::ExeOther; if(!QFile::setPermissions(spec.mid(6), perms)) { if(errorMessage) *errorMessage = QString("unable to set permissions on %1").arg(spec); return false; } } if(errorMessage) *errorMessage = QString(); return true; } bool setupSocket(QZmq::Socket *sock, const QStringList &specs, bool bind, int ipcFileMode, QString *errorMessage) { if(bind) { if(!bindSpec(sock, specs[0], ipcFileMode, errorMessage)) return false; } else { foreach(const QString &spec, specs) sock->connectToAddress(spec); } if(errorMessage) *errorMessage = QString(); return true; } bool setupSocket(QZmq::Socket *sock, const QString &spec, bool bind, int ipcFileMode, QString *errorMessage) { return setupSocket(sock, QStringList() << spec, bind, ipcFileMode, errorMessage); } } pushpin-1.39.1/src/cpp/zutil.h000066400000000000000000000022321457610542000161670ustar00rootroot00000000000000/* * Copyright (C) 2015 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef ZUTIL_H #define ZUTIL_H #include #include namespace QZmq { class Socket; } namespace ZUtil { bool bindSpec(QZmq::Socket *sock, const QString &spec, int ipcFileMode, QString *errorMessage = 0); bool setupSocket(QZmq::Socket *sock, const QStringList &specs, bool bind, int ipcFileMode, QString *errorMessage = 0); bool setupSocket(QZmq::Socket *sock, const QString &spec, bool bind, int ipcFileMode, QString *errorMessage = 0); } #endif pushpin-1.39.1/src/cpp/zwebsocket.cpp000066400000000000000000000632171457610542000175450ustar00rootroot00000000000000/* * Copyright (C) 2014-2023 Fanout, Inc. * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #include "zwebsocket.h" #include #include #include "zhttprequestpacket.h" #include "zhttpresponsepacket.h" #include "log.h" #include "rtimer.h" #include "zhttpmanager.h" #include "uuidutil.h" #define IDEAL_CREDITS 200000 #define SESSION_EXPIRE 60000 #define KEEPALIVE_INTERVAL 45000 class ZWebSocket::Private : public QObject { Q_OBJECT public: enum InternalState { Idle, AboutToConnect, Connecting, Connected, ConnectedPeerClosed, ClosedPeerConnected }; ZWebSocket *q; ZhttpManager *manager; bool server; InternalState state; ZWebSocket::Rid rid; QByteArray toAddress; QHostAddress peerAddress; QString connectHost; int connectPort; bool ignorePolicies; bool trustConnectHost; bool ignoreTlsErrors; QUrl requestUri; HttpHeaders requestHeaders; int inSeq; int outSeq; int outCredits; int pendingInCredits; int responseCode; QByteArray responseReason; HttpHeaders responseHeaders; QByteArray responseBody; // for rejections only bool inClosed; bool outClosed; int closeCode; QString closeReason; int peerCloseCode; QString peerCloseReason; QVariant userData; bool pendingUpdate; bool readableChanged; bool writableChanged; ErrorCondition errorCondition; RTimer *expireTimer; RTimer *keepAliveTimer; QList inFrames; QList outFrames; int inSize; int outSize; int inContentType; int outContentType; bool multi; Connection expireTimerConnection; Connection keppAliveTimerConnection; Private(ZWebSocket *_q) : QObject(_q), q(_q), manager(0), server(false), state(Idle), connectPort(-1), ignorePolicies(false), trustConnectHost(false), ignoreTlsErrors(false), inSeq(0), outSeq(0), outCredits(0), pendingInCredits(0), responseCode(-1), inClosed(false), outClosed(false), closeCode(-1), peerCloseCode(-1), pendingUpdate(false), readableChanged(false), writableChanged(false), expireTimer(0), keepAliveTimer(0), inSize(0), outSize(0), inContentType(-1), outContentType((int)Frame::Text), multi(false) { expireTimer = new RTimer; expireTimerConnection = expireTimer->timeout.connect(boost::bind(&Private::expire_timeout, this)); expireTimer->setSingleShot(true); keepAliveTimer = new RTimer; keppAliveTimerConnection = keepAliveTimer->timeout.connect(boost::bind(&Private::keepAlive_timeout, this)); } ~Private() { if(manager && state != Idle) tryCancel(); cleanup(); } void cleanup() { readableChanged = false; writableChanged = false; if(expireTimer) { expireTimerConnection.disconnect(); expireTimer->setParent(0); expireTimer->deleteLater(); expireTimer = 0; } if(keepAliveTimer) { keppAliveTimerConnection.disconnect(); keepAliveTimer->setParent(0); keepAliveTimer->deleteLater(); keepAliveTimer = 0; } if(manager) { manager->unregisterKeepAlive(q); manager->unlink(q); manager = 0; } } bool setupServer(int seq, const ZhttpRequestPacket &packet) { if(packet.type != ZhttpRequestPacket::Data) { log_warning("zws server: received request with invalid type, canceling"); tryRespondCancel(packet); return false; } if(seq != 0) { log_warning("zws server: error, received request with non-zero seq field"); writeError("bad-request"); state = Idle; return false; } inSeq = 1; // next expected seq if(packet.credits != -1) outCredits = packet.credits; requestUri = packet.uri; requestHeaders = packet.headers; userData = packet.userData; peerAddress = packet.peerAddress; if(packet.multi) multi = true; return true; } void startClient() { state = AboutToConnect; refreshTimeout(); update(); } void startServer() { state = Connecting; startKeepAlive(); refreshTimeout(); update(); } void startKeepAlive() { if(multi) { if(keepAliveTimer->isActive()) { // need to flush the current keepalive, since the // manager registration may extend the timeout keepAlive_timeout(); keepAliveTimer->stop(); } manager->registerKeepAlive(q); } else { manager->unregisterKeepAlive(q); if(!keepAliveTimer->isActive()) keepAliveTimer->start(KEEPALIVE_INTERVAL); } } void stopKeepAlive() { if(keepAliveTimer->isActive()) keepAliveTimer->stop(); manager->unregisterKeepAlive(q); } void refreshTimeout() { expireTimer->start(SESSION_EXPIRE); } void update() { if(!pendingUpdate) { pendingUpdate = true; QMetaObject::invokeMethod(this, "doUpdate", Qt::QueuedConnection); } } void respond() { state = Connected; ZhttpResponsePacket out; out.type = ZhttpResponsePacket::Data; out.code = responseCode; out.reason = responseReason; out.headers = responseHeaders; out.credits = IDEAL_CREDITS; if(multi) out.multi = true; writePacket(out); } void reject() { ZhttpResponsePacket out; out.type = ZhttpResponsePacket::Error; out.condition = "rejected"; out.code = responseCode; out.reason = responseReason; out.headers = responseHeaders; out.body = responseBody; writePacket(out); state = Idle; cleanup(); QMetaObject::invokeMethod(this, "doClosed", Qt::QueuedConnection); } Frame readFrame() { Frame f = inFrames.takeFirst(); inSize -= f.data.size(); pendingInCredits += f.data.size(); update(); return f; } void writeFrame(const Frame &frame) { if(state != Connected && state != ConnectedPeerClosed) return; outFrames += frame; outSize += frame.data.size(); update(); } void close(int code, const QString &reason) { if((state != Connected && state != ConnectedPeerClosed) || outClosed) return; outClosed = true; closeCode = code; closeReason = reason; if(outFrames.isEmpty()) { writeClose(code, reason); if(state == ConnectedPeerClosed) { // if peer was already closed, then we're done! state = Idle; cleanup(); QMetaObject::invokeMethod(this, "doClosed", Qt::QueuedConnection); } else { // if peer was not closed, then we wait around state = ClosedPeerConnected; } } } void tryWrite() { QPointer self = this; if(state == Connected || state == ConnectedPeerClosed) { int written = 0; int contentBytesWritten = 0; while(!outFrames.isEmpty()) { Frame &nextFrame = outFrames.first(); int contentSize = 0; if((nextFrame.type == Frame::Ping || nextFrame.type == Frame::Pong) && outCredits >= nextFrame.data.size()) { contentSize = nextFrame.data.size(); } else if((nextFrame.type == Frame::Text || nextFrame.type == Frame::Binary || nextFrame.type == Frame::Continuation) && (nextFrame.data.isEmpty() || outCredits > 0)) { contentSize = qMin(nextFrame.data.size(), outCredits); } else { break; } // if we have data to send, and the credits to do so, then send data. // also send credits if we need to. Frame f = nextFrame; bool outFrameDone = false; if(contentSize >= nextFrame.data.size()) { outFrames.removeFirst(); outFrameDone = true; } else { f.data = f.data.mid(0, contentSize); f.more = true; nextFrame.type = Frame::Continuation; nextFrame.data = nextFrame.data.mid(contentSize); } outSize -= f.data.size(); outCredits -= f.data.size(); int credits = -1; if(state != ConnectedPeerClosed && pendingInCredits > 0) { credits = pendingInCredits; pendingInCredits = 0; } writeFrameInternal(f, credits); if(outFrameDone) ++written; contentBytesWritten += f.data.size(); } if(written > 0 || contentBytesWritten > 0) { q->framesWritten(written, contentBytesWritten); if(!self) return; } if(outFrames.isEmpty() && outClosed) { writeClose(closeCode, closeReason); if(state == ConnectedPeerClosed) { // if peer was already closed, then we're done! state = Idle; cleanup(); q->closed(); return; } else { // if peer was not closed, then we wait around state = ClosedPeerConnected; } } } // if we didn't send credits in a data packet, then do them now if(state != ConnectedPeerClosed && pendingInCredits > 0) { int credits = pendingInCredits; pendingInCredits = 0; writeCredits(credits); } } void handleIncomingDataPacket(const QByteArray &contentType, const QByteArray &data, bool more) { Frame::Type ftype; if(inContentType != -1) { ftype = Frame::Continuation; } else { if(contentType == "binary") ftype = Frame::Binary; else ftype = Frame::Text; inContentType = (int)ftype; } inFrames += Frame(ftype, !data.isNull() ? data : QByteArray(""), more); inSize += data.size(); if(!more) inContentType = -1; } void handle(const QByteArray &id, int seq, const ZhttpRequestPacket &packet) { if(packet.type == ZhttpRequestPacket::Error) { errorCondition = convertError(packet.condition); log_debug("zws server: error id=%s cond=%s", id.data(), packet.condition.data()); state = Idle; cleanup(); q->error(); return; } else if(packet.type == ZhttpRequestPacket::Cancel) { log_debug("zws server: received cancel id=%s", id.data()); errorCondition = ErrorGeneric; state = Idle; cleanup(); q->error(); return; } if(seq != inSeq) { log_warning("zws server: error id=%s received message out of sequence, canceling", id.data()); tryRespondCancel(packet); state = Idle; errorCondition = ErrorGeneric; cleanup(); q->error(); return; } ++inSeq; if(!multi && packet.multi) { // switch on multi support multi = true; startKeepAlive(); // re-setup keep alive } refreshTimeout(); if(packet.type == ZhttpRequestPacket::Data || packet.type == ZhttpRequestPacket::Ping || packet.type == ZhttpRequestPacket::Pong) { if(inSize + packet.body.size() > IDEAL_CREDITS) log_warning("zws client: id=%s server is sending too fast", id.data()); if(packet.type == ZhttpRequestPacket::Data) { handleIncomingDataPacket(packet.contentType, packet.body, packet.more); } else if(packet.type == ZhttpRequestPacket::Ping) { inFrames += Frame(Frame::Ping, packet.body, false); inSize += packet.body.size(); } else if(packet.type == ZhttpRequestPacket::Pong) { inFrames += Frame(Frame::Pong, packet.body, false); inSize += packet.body.size(); } if(packet.credits > 0) { outCredits += packet.credits; writableChanged = true; } readableChanged = true; update(); } else if(packet.type == ZhttpRequestPacket::Close) { handlePeerClose(packet.code, QString::fromUtf8(packet.body)); } else if(packet.type == ZhttpRequestPacket::Credit) { if(packet.credits > 0) { outCredits += packet.credits; writableChanged = true; update(); } } else if(packet.type == ZhttpRequestPacket::KeepAlive) { // nothing to do } else { log_debug("zws server: unsupported packet type id=%s type=%d", id.data(), (int)packet.type); } } void handle(const QByteArray &id, int seq, const ZhttpResponsePacket &packet) { if(packet.type == ZhttpResponsePacket::Error) { errorCondition = convertError(packet.condition); log_debug("zws client: error id=%s cond=%s", id.data(), packet.condition.data()); responseCode = packet.code; responseReason = packet.reason; responseHeaders = packet.headers; responseBody = packet.body; state = Idle; cleanup(); q->error(); return; } else if(packet.type == ZhttpResponsePacket::Cancel) { log_debug("zws client: received cancel id=%s", id.data()); errorCondition = ErrorGeneric; state = Idle; cleanup(); q->error(); return; } if(!packet.from.isEmpty()) toAddress = packet.from; if(seq != inSeq) { log_warning("zws client: error id=%s received message out of sequence, canceling", id.data()); tryRespondCancel(packet); state = Idle; errorCondition = ErrorGeneric; cleanup(); q->error(); return; } if(!toAddress.isEmpty()) startKeepAlive(); // only starts if wasn't started already ++inSeq; if(!multi && packet.multi) { // switch on multi support multi = true; startKeepAlive(); // re-setup keep alive } refreshTimeout(); if(state == Connecting) { if(packet.type != ZhttpResponsePacket::Data && packet.type != ZhttpResponsePacket::Credit && packet.type != ZhttpResponsePacket::KeepAlive) { state = Idle; errorCondition = ErrorGeneric; cleanup(); log_warning("zws client: error id=%s initial response wrong type", id.data()); q->error(); return; } if(packet.from.isEmpty()) { state = Idle; errorCondition = ErrorGeneric; cleanup(); log_warning("zws client: error id=%s initial ack did not contain from field", id.data()); q->error(); return; } } if(packet.type == ZhttpResponsePacket::Data || packet.type == ZhttpResponsePacket::Ping || packet.type == ZhttpResponsePacket::Pong) { if(state == Connecting) { // this is assured earlier assert(packet.type == ZhttpResponsePacket::Data); responseCode = packet.code; responseReason = packet.reason; responseHeaders = packet.headers; if(packet.credits > 0) outCredits += packet.credits; state = Connected; update(); q->connected(); } else { if(inSize + packet.body.size() > IDEAL_CREDITS) log_warning("zws client: id=%s server is sending too fast", id.data()); if(packet.type == ZhttpResponsePacket::Data) { handleIncomingDataPacket(packet.contentType, packet.body, packet.more); } else if(packet.type == ZhttpResponsePacket::Ping) { inFrames += Frame(Frame::Ping, packet.body, false); inSize += packet.body.size(); } else if(packet.type == ZhttpResponsePacket::Pong) { inFrames += Frame(Frame::Pong, packet.body, false); inSize += packet.body.size(); } if(packet.credits > 0) { outCredits += packet.credits; writableChanged = true; } readableChanged = true; update(); } } else if(packet.type == ZhttpResponsePacket::Close) { handlePeerClose(packet.code, QString::fromUtf8(packet.body)); } else if(packet.type == ZhttpResponsePacket::Credit) { if(packet.credits > 0) { outCredits += packet.credits; writableChanged = true; update(); } } else if(packet.type == ZhttpResponsePacket::KeepAlive) { // nothing to do } else { log_debug("zws client: unsupported packet type id=%s type=%d", id.data(), (int)packet.type); } } void handlePeerClose(int code, const QString &reason) { if((state == Connected || state == ClosedPeerConnected) && !inClosed) { inClosed = true; peerCloseCode = code; peerCloseReason = reason; if(inFrames.isEmpty()) { if(state == ClosedPeerConnected) { state = Idle; cleanup(); q->closed(); } else { state = ConnectedPeerClosed; q->peerClosed(); } } } } void writePacket(const ZhttpRequestPacket &packet) { assert(manager); bool first = (outSeq == 0); ZhttpRequestPacket out = packet; out.from = rid.first; out.ids += ZhttpRequestPacket::Id(rid.second, outSeq++); if(first) { manager->writeWs(out); } else { assert(!toAddress.isEmpty()); manager->writeWs(out, toAddress); } } void writePacket(const ZhttpResponsePacket &packet) { assert(manager); ZhttpResponsePacket out = packet; out.from = manager->instanceId(); out.ids += ZhttpResponsePacket::Id(rid.second, outSeq++); out.userData = userData; manager->writeWs(out, rid.first); } void writeFrameInternal(const Frame &frame, int credits = -1) { // for content frames, set the type QByteArray contentType; if(frame.type == Frame::Binary || frame.type == Frame::Text || frame.type == Frame::Continuation) { Frame::Type ftype = (Frame::Type)-1; if(frame.type == Frame::Binary || frame.type == Frame::Text) { ftype = frame.type; outContentType = (int)frame.type; } else if(frame.type == Frame::Continuation) { ftype = (Frame::Type)outContentType; } if(ftype != (Frame::Type)-1) { if(ftype == Frame::Binary) contentType = "binary"; else // Text contentType = "text"; } } if(server) { ZhttpResponsePacket p; if(frame.type == Frame::Ping) { p.type = ZhttpResponsePacket::Ping; } else if(frame.type == Frame::Pong) { p.type = ZhttpResponsePacket::Pong; } else { p.type = ZhttpResponsePacket::Data; p.contentType = contentType; } p.body = frame.data; p.more = frame.more; p.credits = credits; writePacket(p); } else { ZhttpRequestPacket p; if(frame.type == Frame::Ping) { p.type = ZhttpRequestPacket::Ping; } else if(frame.type == Frame::Pong) { p.type = ZhttpRequestPacket::Pong; } else { p.type = ZhttpRequestPacket::Data; p.contentType = contentType; } p.body = frame.data; p.more = frame.more; p.credits = credits; writePacket(p); } } void writeCredits(int credits) { if(server) { ZhttpResponsePacket p; p.type = ZhttpResponsePacket::Credit; p.credits = credits; writePacket(p); } else { ZhttpRequestPacket p; p.type = ZhttpRequestPacket::Credit; p.credits = credits; writePacket(p); } } void writeClose(int code = -1, const QString &reason = QString()) { if(server) { ZhttpResponsePacket out; out.type = ZhttpResponsePacket::Close; out.code = code; if(!reason.isEmpty()) out.body = reason.toUtf8(); writePacket(out); } else { ZhttpRequestPacket out; out.type = ZhttpRequestPacket::Close; out.code = code; if(!reason.isEmpty()) out.body = reason.toUtf8(); writePacket(out); } } void writeCancel() { if(server) { ZhttpResponsePacket out; out.type = ZhttpResponsePacket::Cancel; writePacket(out); } else { ZhttpRequestPacket out; out.type = ZhttpRequestPacket::Cancel; writePacket(out); } } void writeError(const QByteArray &condition) { ZhttpResponsePacket out; out.type = ZhttpResponsePacket::Error; out.condition = condition; writePacket(out); } void tryCancel() { if(state != Idle) { state = Idle; // can't send cancel in client mode without address if(!server && toAddress.isEmpty()) return; writeCancel(); } } void tryRespondCancel(const ZhttpRequestPacket &packet) { // if this was not an error packet, send cancel if(packet.type != ZhttpRequestPacket::Error && packet.type != ZhttpRequestPacket::Cancel) writeCancel(); } void tryRespondCancel(const ZhttpResponsePacket &packet) { // if this was not an error packet, send cancel if(packet.type != ZhttpResponsePacket::Error && packet.type != ZhttpResponsePacket::Cancel && !toAddress.isEmpty()) writeCancel(); } static ErrorCondition convertError(const QByteArray &cond) { // zws conditions: // remote-connection-failed // connection-timeout // tls-error // bad-request // policy-violation // max-size-exceeded // session-timeout // rejected if(cond == "policy-violation") return ErrorPolicy; else if(cond == "remote-connection-failed") return ErrorConnect; else if(cond == "tls-error") return ErrorTls; else if(cond == "connection-timeout") return ErrorConnectTimeout; else if(cond == "rejected") return ErrorRejected; else // lump the rest as generic return ErrorGeneric; } public slots: void doClosed() { q->closed(); } void doUpdate() { pendingUpdate = false; if(state == Connected || state == ClosedPeerConnected) { if(inFrames.isEmpty() && inClosed) { if(state == ClosedPeerConnected) { state = Idle; cleanup(); q->closed(); return; } else { QPointer self = this; state = ConnectedPeerClosed; q->peerClosed(); if(!self) return; } } else { if(readableChanged) { readableChanged = false; QPointer self = this; q->readyRead(); if(!self) return; } } } if(state == AboutToConnect) { if(!manager->canWriteImmediately()) { state = Idle; errorCondition = ErrorUnavailable; q->error(); cleanup(); return; } state = Connecting; ZhttpRequestPacket p; p.type = ZhttpRequestPacket::Data; p.uri = requestUri; p.headers = requestHeaders; p.connectHost = connectHost; p.connectPort = connectPort; if(ignorePolicies) p.ignorePolicies = true; if(trustConnectHost) p.trustConnectHost = true; if(ignoreTlsErrors) p.ignoreTlsErrors = true; p.credits = IDEAL_CREDITS; p.multi = true; writePacket(p); } else if(state == Connected || state == ConnectedPeerClosed) { QPointer self = this; tryWrite(); if(!self) return; if(writableChanged) { writableChanged = false; q->writeBytesChanged(); } } } public: void expire_timeout() { state = Idle; errorCondition = ErrorTimeout; cleanup(); q->error(); } void keepAlive_timeout() { if(server) { ZhttpResponsePacket p; p.type = ZhttpResponsePacket::KeepAlive; writePacket(p); } else { ZhttpRequestPacket p; p.type = ZhttpRequestPacket::KeepAlive; writePacket(p); } } }; ZWebSocket::ZWebSocket(QObject *parent) : WebSocket(parent) { d = new Private(this); } ZWebSocket::~ZWebSocket() { delete d; } ZWebSocket::Rid ZWebSocket::rid() const { return d->rid; } QHostAddress ZWebSocket::peerAddress() const { return d->peerAddress; } void ZWebSocket::setConnectHost(const QString &host) { d->connectHost = host; } void ZWebSocket::setConnectPort(int port) { d->connectPort = port; } void ZWebSocket::setIgnorePolicies(bool on) { d->ignorePolicies = on; } void ZWebSocket::setTrustConnectHost(bool on) { d->trustConnectHost = on; } void ZWebSocket::setIgnoreTlsErrors(bool on) { d->ignoreTlsErrors = on; } void ZWebSocket::setIsTls(bool on) { d->requestUri.setScheme(on ? "wss" : "ws"); } void ZWebSocket::start(const QUrl &uri, const HttpHeaders &headers) { assert(!d->server); d->requestUri = uri; d->requestHeaders = headers; d->startClient(); } void ZWebSocket::respondSuccess(const QByteArray &reason, const HttpHeaders &headers) { assert(d->server); assert(d->state == Private::Connecting); d->responseCode = 101; d->responseReason = reason; d->responseHeaders = headers; d->respond(); } void ZWebSocket::respondError(int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body) { assert(d->server); assert(d->state == Private::Connecting); d->responseCode = code; d->responseReason = reason; d->responseHeaders = headers; d->responseBody = body; d->reject(); } WebSocket::State ZWebSocket::state() const { switch(d->state) { case Private::Idle: return Idle; case Private::AboutToConnect: case Private::Connecting: if(d->outClosed) return Closing; else return Connecting; case Private::Connected: case Private::ConnectedPeerClosed: case Private::ClosedPeerConnected: default: if(d->outClosed) return Closing; else return Connected; } } QUrl ZWebSocket::requestUri() const { return d->requestUri; } HttpHeaders ZWebSocket::requestHeaders() const { return d->requestHeaders; } int ZWebSocket::responseCode() const { return d->responseCode; } QByteArray ZWebSocket::responseReason() const { return d->responseReason; } HttpHeaders ZWebSocket::responseHeaders() const { return d->responseHeaders; } QByteArray ZWebSocket::responseBody() const { return d->responseBody; } int ZWebSocket::framesAvailable() const { return d->inFrames.count(); } int ZWebSocket::writeBytesAvailable() const { if(d->outSize < d->outCredits) return d->outCredits - d->outSize; else return 0; } int ZWebSocket::peerCloseCode() const { return d->peerCloseCode; } QString ZWebSocket::peerCloseReason() const { return d->peerCloseReason; } WebSocket::ErrorCondition ZWebSocket::errorCondition() const { return d->errorCondition; } void ZWebSocket::writeFrame(const Frame &frame) { d->writeFrame(frame); } WebSocket::Frame ZWebSocket::readFrame() { return d->readFrame(); } void ZWebSocket::close(int code, const QString &reason) { d->close(code, reason); } void ZWebSocket::setupClient(ZhttpManager *manager) { d->manager = manager; d->rid = Rid(manager->instanceId(), UuidUtil::createUuid()); d->manager->link(this); } bool ZWebSocket::setupServer(ZhttpManager *manager, const QByteArray &id, int seq, const ZhttpRequestPacket &packet) { d->manager = manager; d->server = true; d->rid = Rid(packet.from, id); return d->setupServer(seq, packet); } void ZWebSocket::startServer() { d->startServer(); } bool ZWebSocket::isServer() const { return d->server; } QByteArray ZWebSocket::toAddress() const { return d->toAddress; } int ZWebSocket::outSeqInc() { return d->outSeq++; } void ZWebSocket::handle(const QByteArray &id, int seq, const ZhttpRequestPacket &packet) { assert(d->manager); d->handle(id, seq, packet); } void ZWebSocket::handle(const QByteArray &id, int seq, const ZhttpResponsePacket &packet) { assert(d->manager); d->handle(id, seq, packet); } #include "zwebsocket.moc" pushpin-1.39.1/src/cpp/zwebsocket.h000066400000000000000000000054361457610542000172110ustar00rootroot00000000000000/* * Copyright (C) 2014-2016 Fanout, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ #ifndef ZWEBSOCKET_H #define ZWEBSOCKET_H #include "websocket.h" #include using Connection = boost::signals2::scoped_connection; class ZhttpRequestPacket; class ZhttpResponsePacket; class ZhttpManager; class ZWebSocket : public WebSocket { Q_OBJECT public: // pair of sender + request id typedef QPair Rid; ~ZWebSocket(); Rid rid() const; void setIsTls(bool on); // reimplemented virtual QHostAddress peerAddress() const; virtual void setConnectHost(const QString &host); virtual void setConnectPort(int port); virtual void setIgnorePolicies(bool on); virtual void setTrustConnectHost(bool on); virtual void setIgnoreTlsErrors(bool on); virtual void start(const QUrl &uri, const HttpHeaders &headers); virtual void respondSuccess(const QByteArray &reason, const HttpHeaders &headers); virtual void respondError(int code, const QByteArray &reason, const HttpHeaders &headers, const QByteArray &body); virtual State state() const; virtual QUrl requestUri() const; virtual HttpHeaders requestHeaders() const; virtual int responseCode() const; virtual QByteArray responseReason() const; virtual HttpHeaders responseHeaders() const; virtual QByteArray responseBody() const; virtual int framesAvailable() const; virtual int writeBytesAvailable() const; virtual int peerCloseCode() const; virtual QString peerCloseReason() const; virtual ErrorCondition errorCondition() const; virtual void writeFrame(const Frame &frame); virtual Frame readFrame(); virtual void close(int code = -1, const QString &reason = QString()); private: class Private; friend class Private; Private *d; friend class ZhttpManager; ZWebSocket(QObject *parent = 0); void setupClient(ZhttpManager *manager); bool setupServer(ZhttpManager *manager, const QByteArray &id, int seq, const ZhttpRequestPacket &packet); void startServer(); bool isServer() const; QByteArray toAddress() const; int outSeqInc(); void handle(const QByteArray &id, int seq, const ZhttpRequestPacket &packet); void handle(const QByteArray &id, int seq, const ZhttpResponsePacket &packet); }; #endif pushpin-1.39.1/src/event.rs000066400000000000000000000524621457610542000155660ustar00rootroot00000000000000/* * Copyright (C) 2021-2023 Fanout, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use crate::arena; use crate::list; use mio::event::Source; use mio::{Events, Interest, Poll, Token, Waker}; use slab::Slab; use std::cell::{Cell, RefCell}; use std::io; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::Duration; const EVENTS_MAX: usize = 1024; const LOCAL_BUDGET: u32 = 10; pub type Readiness = Option; pub trait ReadinessExt { fn contains_any(&self, readiness: Interest) -> bool; fn merge(&mut self, readiness: Interest); } impl ReadinessExt for Readiness { fn contains_any(&self, readiness: Interest) -> bool { match *self { Some(cur) => { (readiness.is_readable() && cur.is_readable()) || (readiness.is_writable() && cur.is_writable()) } None => false, } } fn merge(&mut self, readiness: Interest) { match *self { Some(cur) => *self = Some(cur.add(readiness)), None => *self = Some(readiness), } } } struct SourceItem { subtoken: Token, interests: Interest, readiness: Readiness, } struct RegisteredSources { nodes: Slab>, ready: list::List, } struct LocalSources { registered_sources: RefCell, } impl LocalSources { fn new(max_sources: usize) -> Self { Self { registered_sources: RefCell::new(RegisteredSources { nodes: Slab::with_capacity(max_sources), ready: list::List::default(), }), } } fn register(&self, subtoken: Token, interests: Interest) -> Result { let sources = &mut *self.registered_sources.borrow_mut(); if sources.nodes.len() == sources.nodes.capacity() { return Err(io::Error::from(io::ErrorKind::WriteZero)); } Ok(sources.nodes.insert(list::Node::new(SourceItem { subtoken, interests, readiness: None, }))) } fn deregister(&self, key: usize) -> Result<(), io::Error> { let sources = &mut *self.registered_sources.borrow_mut(); if sources.nodes.contains(key) { sources.ready.remove(&mut sources.nodes, key); sources.nodes.remove(key); } Ok(()) } fn set_readiness(&self, key: usize, readiness: Interest) -> Result<(), io::Error> { let sources = &mut *self.registered_sources.borrow_mut(); if !sources.nodes.contains(key) { return Err(io::Error::from(io::ErrorKind::NotFound)); } let item = &mut sources.nodes[key].value; if !((item.interests.is_readable() && readiness.is_readable()) || (item.interests.is_writable() && readiness.is_writable())) { // not of interest return Ok(()); } let orig = item.readiness; item.readiness.merge(readiness); if item.readiness != orig { sources.ready.remove(&mut sources.nodes, key); sources.ready.push_back(&mut sources.nodes, key); } Ok(()) } fn has_events(&self) -> bool { let sources = &*self.registered_sources.borrow(); !sources.ready.is_empty() } fn next_event(&self) -> Option<(Token, Interest)> { let sources = &mut *self.registered_sources.borrow_mut(); match sources.ready.pop_front(&mut sources.nodes) { Some(key) => { let item = &mut sources.nodes[key].value; let readiness = item.readiness.take().unwrap(); Some((item.subtoken, readiness)) } None => None, } } } struct SyncSources { registered_sources: Mutex, waker: Waker, } impl SyncSources { fn new(max_sources: usize, waker: Waker) -> Self { Self { registered_sources: Mutex::new(RegisteredSources { nodes: Slab::with_capacity(max_sources), ready: list::List::default(), }), waker, } } fn register(&self, subtoken: Token, interests: Interest) -> Result { let sources = &mut *self.registered_sources.lock().unwrap(); if sources.nodes.len() == sources.nodes.capacity() { return Err(io::Error::from(io::ErrorKind::WriteZero)); } Ok(sources.nodes.insert(list::Node::new(SourceItem { subtoken, interests, readiness: None, }))) } fn deregister(&self, key: usize) -> Result<(), io::Error> { let sources = &mut *self.registered_sources.lock().unwrap(); if sources.nodes.contains(key) { sources.ready.remove(&mut sources.nodes, key); sources.nodes.remove(key); } Ok(()) } fn set_readiness(&self, key: usize, readiness: Interest) -> Result<(), io::Error> { let sources = &mut *self.registered_sources.lock().unwrap(); if !sources.nodes.contains(key) { return Err(io::Error::from(io::ErrorKind::NotFound)); } let item = &mut sources.nodes[key].value; if !((item.interests.is_readable() && readiness.is_readable()) || (item.interests.is_writable() && readiness.is_writable())) { // not of interest return Ok(()); } let orig = item.readiness; item.readiness.merge(readiness); if item.readiness != orig { let need_wake = sources.ready.is_empty(); sources.ready.remove(&mut sources.nodes, key); sources.ready.push_back(&mut sources.nodes, key); if need_wake { self.waker.wake()?; } } Ok(()) } fn has_events(&self) -> bool { let sources = &*self.registered_sources.lock().unwrap(); !sources.ready.is_empty() } fn next_event(&self) -> Option<(Token, Interest)> { let sources = &mut *self.registered_sources.lock().unwrap(); match sources.ready.pop_front(&mut sources.nodes) { Some(key) => { let item = &mut sources.nodes[key].value; let readiness = item.readiness.take().unwrap(); Some((item.subtoken, readiness)) } None => None, } } } struct CustomSources { local: Rc, sync: Arc, next_local_only: Cell, } impl CustomSources { fn new(poll: &Poll, token: Token, max_sources: usize) -> Result { let waker = Waker::new(poll.registry(), token)?; Ok(Self { local: Rc::new(LocalSources::new(max_sources)), sync: Arc::new(SyncSources::new(max_sources, waker)), next_local_only: Cell::new(false), }) } fn set_next_local_only(&self, enabled: bool) { self.next_local_only.set(enabled); } fn register_local( &self, registration: &LocalRegistration, subtoken: Token, interests: Interest, ) -> Result<(), io::Error> { let mut reg = registration.entry.get().data.borrow_mut(); if reg.data.is_none() { let key = self.local.register(subtoken, interests)?; reg.data = Some((key, self.local.clone())); if let Some(readiness) = reg.readiness { self.local.set_readiness(key, readiness).unwrap(); reg.readiness = None; } } Ok(()) } fn deregister_local(&self, registration: &LocalRegistration) -> Result<(), io::Error> { let mut reg = registration.entry.get().data.borrow_mut(); if let Some((key, _)) = reg.data { self.local.deregister(key)?; reg.data = None; } Ok(()) } fn register( &self, registration: &Registration, subtoken: Token, interests: Interest, ) -> Result<(), io::Error> { let mut reg = registration.inner.lock().unwrap(); if reg.data.is_none() { let key = self.sync.register(subtoken, interests)?; reg.data = Some((key, self.sync.clone())); if let Some(readiness) = reg.readiness { self.sync.set_readiness(key, readiness).unwrap(); reg.readiness = None; } } Ok(()) } fn deregister(&self, registration: &Registration) -> Result<(), io::Error> { let mut reg = registration.inner.lock().unwrap(); if let Some((key, _)) = reg.data { self.sync.deregister(key)?; reg.data = None; } Ok(()) } fn has_local_events(&self) -> bool { self.local.has_events() } fn has_events(&self) -> bool { if self.local.has_events() { return true; } if self.next_local_only.get() { return false; } self.sync.has_events() } fn next_event(&self) -> Option<(Token, Interest)> { if let Some(e) = self.local.next_event() { return Some(e); } if self.next_local_only.get() { return None; } if let Some(e) = self.sync.next_event() { return Some(e); } None } } struct RegistrationInner { data: Option<(usize, Arc)>, readiness: Readiness, } pub struct Registration { inner: Arc>, } impl Registration { pub fn new() -> (Self, SetReadiness) { let reg = Arc::new(Mutex::new(RegistrationInner { data: None, readiness: None, })); let registration = Self { inner: reg.clone() }; let set_readiness = SetReadiness { inner: reg }; (registration, set_readiness) } } impl Drop for Registration { fn drop(&mut self) { let mut reg = self.inner.lock().unwrap(); if let Some((key, sources)) = ®.data { sources.deregister(*key).unwrap(); reg.data = None; } } } pub struct SetReadiness { inner: Arc>, } impl SetReadiness { pub fn set_readiness(&self, readiness: Interest) -> Result<(), io::Error> { let mut reg = self.inner.lock().unwrap(); match ®.data { Some((key, sources)) => sources.set_readiness(*key, readiness)?, None => reg.readiness.merge(readiness), } Ok(()) } } struct LocalRegistrationData { data: Option<(usize, Rc)>, readiness: Readiness, } pub struct LocalRegistrationEntry { data: RefCell, } pub struct LocalRegistration { entry: arena::Rc, } impl LocalRegistration { pub fn new(memory: &Rc>) -> (Self, LocalSetReadiness) { let reg = arena::Rc::new( LocalRegistrationEntry { data: RefCell::new(LocalRegistrationData { data: None, readiness: None, }), }, memory, ) .unwrap(); let registration = Self { entry: arena::Rc::clone(®), }; let set_readiness = LocalSetReadiness { entry: reg }; (registration, set_readiness) } } impl Drop for LocalRegistration { fn drop(&mut self) { let mut reg = self.entry.get().data.borrow_mut(); if let Some((key, sources)) = ®.data { sources.deregister(*key).unwrap(); reg.data = None; } } } pub struct LocalSetReadiness { entry: arena::Rc, } impl LocalSetReadiness { pub fn set_readiness(&self, readiness: Interest) -> Result<(), io::Error> { let mut reg = self.entry.get().data.borrow_mut(); match ®.data { Some((key, sources)) => sources.set_readiness(*key, readiness)?, None => reg.readiness.merge(readiness), } Ok(()) } } #[derive(Debug, PartialEq)] pub struct Event { token: Token, readiness: Interest, } impl Event { pub fn token(&self) -> Token { self.token } pub fn readiness(&self) -> Interest { self.readiness } pub fn is_readable(&self) -> bool { self.readiness.is_readable() } pub fn is_writable(&self) -> bool { self.readiness.is_writable() } } pub struct Poller { poll: Poll, events: Events, custom_sources: CustomSources, local_registration_memory: Rc>, local_budget: u32, } impl Poller { pub fn new(max_custom_sources: usize) -> Result { let poll = Poll::new()?; let events = Events::with_capacity(EVENTS_MAX); let custom_sources = CustomSources::new(&poll, Token(0), max_custom_sources)?; Ok(Self { poll, events, custom_sources, local_registration_memory: Rc::new(arena::RcMemory::new(max_custom_sources)), local_budget: LOCAL_BUDGET, }) } pub fn register( &self, source: &mut S, token: Token, interests: Interest, ) -> Result<(), io::Error> where S: Source + ?Sized, { if token == Token(0) { return Err(io::Error::from(io::ErrorKind::InvalidInput)); } self.poll.registry().register(source, token, interests) } pub fn deregister(&self, source: &mut S) -> Result<(), io::Error> where S: Source + ?Sized, { self.poll.registry().deregister(source) } pub fn register_custom( &self, registration: &Registration, token: Token, interests: Interest, ) -> Result<(), io::Error> { if token == Token(0) { return Err(io::Error::from(io::ErrorKind::InvalidInput)); } self.custom_sources.register(registration, token, interests) } pub fn deregister_custom(&self, registration: &Registration) -> Result<(), io::Error> { self.custom_sources.deregister(registration) } pub fn local_registration_memory(&self) -> &Rc> { &self.local_registration_memory } pub fn register_custom_local( &self, registration: &LocalRegistration, token: Token, interests: Interest, ) -> Result<(), io::Error> { if token == Token(0) { return Err(io::Error::from(io::ErrorKind::InvalidInput)); } self.custom_sources .register_local(registration, token, interests) } pub fn deregister_custom_local( &self, registration: &LocalRegistration, ) -> Result<(), io::Error> { self.custom_sources.deregister_local(registration) } pub fn poll(&mut self, timeout: Option) -> Result<(), io::Error> { if self.custom_sources.has_local_events() && self.local_budget > 0 { self.local_budget -= 1; self.custom_sources.set_next_local_only(true); self.events.clear(); // don't reread previous mio events return Ok(()); } self.local_budget = LOCAL_BUDGET; self.custom_sources.set_next_local_only(false); let timeout = if self.custom_sources.has_events() { Some(Duration::from_millis(0)) } else { timeout }; loop { match self.poll.poll(&mut self.events, timeout) { Err(e) if e.kind() == io::ErrorKind::Interrupted => {} ret => break ret, } } } pub fn iter_events(&self) -> EventsIterator<'_, '_> { EventsIterator { events: self.events.iter(), custom_sources: &self.custom_sources, custom_left: EVENTS_MAX, } } } pub struct EventsIterator<'a, 'b> { events: mio::event::Iter<'b>, custom_sources: &'a CustomSources, custom_left: usize, } impl Iterator for EventsIterator<'_, '_> { type Item = Event; fn next(&mut self) -> Option { for event in self.events.by_ref() { if event.token() == Token(0) { continue; } let mut readiness = None; if event.is_readable() { readiness.merge(Interest::READABLE); } if event.is_writable() { readiness.merge(Interest::WRITABLE); } if let Some(readiness) = readiness { return Some(Event { token: event.token(), readiness, }); } } if self.custom_left > 0 { self.custom_left -= 1; if let Some((token, readiness)) = self.custom_sources.next_event() { return Some(Event { token, readiness }); } } None } } #[cfg(test)] mod tests { use super::*; use std::time::Duration; #[test] fn test_readiness() { let token = Token(123); let subtoken = Token(456); let mut poll = Poll::new().unwrap(); let sources = CustomSources::new(&poll, token, 1).unwrap(); assert_eq!(sources.has_events(), false); assert_eq!(sources.next_event(), None); let (reg, sr) = Registration::new(); sources .register(®, subtoken, Interest::READABLE) .unwrap(); let mut events = Events::with_capacity(1024); poll.poll(&mut events, Some(Duration::from_millis(0))) .unwrap(); assert!(events.is_empty()); sr.set_readiness(Interest::READABLE).unwrap(); 'poll: loop { poll.poll(&mut events, None).unwrap(); for event in &events { if event.token() == token { break 'poll; } } } assert_eq!(sources.has_events(), true); assert_eq!(sources.next_event(), Some((subtoken, Interest::READABLE))); assert_eq!(sources.has_events(), false); assert_eq!(sources.next_event(), None); } #[test] fn test_readiness_early() { let token = Token(123); let subtoken = Token(456); let mut poll = Poll::new().unwrap(); let sources = CustomSources::new(&poll, token, 1).unwrap(); assert_eq!(sources.has_events(), false); assert_eq!(sources.next_event(), None); let (reg, sr) = Registration::new(); sr.set_readiness(Interest::READABLE).unwrap(); sources .register(®, subtoken, Interest::READABLE) .unwrap(); let mut events = Events::with_capacity(1024); poll.poll(&mut events, Some(Duration::from_millis(0))) .unwrap(); let event = events.iter().next(); assert!(event.is_some()); let event = event.unwrap(); assert_eq!(event.token(), token); assert_eq!(sources.has_events(), true); assert_eq!(sources.next_event(), Some((subtoken, Interest::READABLE))); assert_eq!(sources.has_events(), false); assert_eq!(sources.next_event(), None); } #[test] fn test_readiness_local() { let poller = Poller::new(1).unwrap(); let token = Token(123); let subtoken = Token(456); let mut poll = Poll::new().unwrap(); let sources = CustomSources::new(&poll, token, 1).unwrap(); assert_eq!(sources.has_events(), false); assert_eq!(sources.next_event(), None); let (reg, sr) = LocalRegistration::new(poller.local_registration_memory()); sources .register_local(®, subtoken, Interest::READABLE) .unwrap(); let mut events = Events::with_capacity(1024); poll.poll(&mut events, Some(Duration::from_millis(0))) .unwrap(); assert!(events.is_empty()); sr.set_readiness(Interest::READABLE).unwrap(); assert_eq!(sources.has_events(), true); assert_eq!(sources.next_event(), Some((subtoken, Interest::READABLE))); assert_eq!(sources.has_events(), false); assert_eq!(sources.next_event(), None); } #[test] fn test_poller() { let token = Token(123); let mut poller = Poller::new(1).unwrap(); assert_eq!(poller.iter_events().next(), None); let (reg, sr) = Registration::new(); poller .register_custom(®, token, Interest::READABLE) .unwrap(); poller.poll(Some(Duration::from_millis(0))).unwrap(); assert_eq!(poller.iter_events().next(), None); sr.set_readiness(Interest::READABLE).unwrap(); poller.poll(None).unwrap(); let mut it = poller.iter_events(); let event = it.next().unwrap(); assert_eq!(event.token(), token); assert_eq!(event.is_readable(), true); assert_eq!(it.next(), None); } } pushpin-1.39.1/src/executor.rs000066400000000000000000000371601457610542000163010ustar00rootroot00000000000000/* * Copyright (C) 2020-2023 Fanout, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use crate::list; use crate::waker; use log::debug; use slab::Slab; use std::cell::RefCell; use std::future::Future; use std::io; use std::mem; use std::pin::Pin; use std::rc::{Rc, Weak}; use std::task::{Context, Waker}; use std::time::Duration; thread_local! { static EXECUTOR: RefCell>> = RefCell::new(None); } type BoxFuture = Pin>>; struct TaskWaker { tasks: Weak, task_id: usize, } impl waker::RcWake for TaskWaker { fn wake(self: Rc) { if let Some(tasks) = self.tasks.upgrade() { tasks.wake(self.task_id); } } } fn poll_fut(fut: &mut BoxFuture, waker: Waker) -> bool { // convert from Pin to Pin<&mut> let fut: Pin<&mut dyn Future> = fut.as_mut(); let mut cx = Context::from_waker(&waker); fut.poll(&mut cx).is_ready() } struct Task { fut: Option>>>, wakeable: bool, } struct TasksData { nodes: Slab>, next: list::List, wakers: Vec>, } struct Tasks { data: RefCell, pre_poll: RefCell>>, } impl Tasks { fn new(max: usize) -> Rc { let data = TasksData { nodes: Slab::with_capacity(max), next: list::List::default(), wakers: Vec::with_capacity(max), }; let tasks = Rc::new(Self { data: RefCell::new(data), pre_poll: RefCell::new(None), }); { let data = &mut *tasks.data.borrow_mut(); for task_id in 0..data.nodes.capacity() { data.wakers.push(Rc::new(TaskWaker { tasks: Rc::downgrade(&tasks), task_id, })); } } tasks } fn is_empty(&self) -> bool { self.data.borrow().nodes.is_empty() } fn have_next(&self) -> bool { !self.data.borrow().next.is_empty() } fn add(&self, fut: F) -> Result<(), ()> where F: Future + 'static, { let data = &mut *self.data.borrow_mut(); if data.nodes.len() == data.nodes.capacity() { return Err(()); } let entry = data.nodes.vacant_entry(); let nkey = entry.key(); let task = Task { fut: Some(Box::pin(fut)), wakeable: false, }; entry.insert(list::Node::new(task)); data.next.push_back(&mut data.nodes, nkey); Ok(()) } fn remove(&self, task_id: usize) { let nkey = task_id; let data = &mut *self.data.borrow_mut(); let task = &mut data.nodes[nkey].value; // drop the future. this should cause it to drop any owned wakers task.fut = None; // at this point, we should be the only remaining owner assert_eq!(Rc::strong_count(&data.wakers[nkey]), 1); data.next.remove(&mut data.nodes, nkey); data.nodes.remove(nkey); } fn take_next_list(&self) -> list::List { let data = &mut *self.data.borrow_mut(); let mut l = list::List::default(); l.concat(&mut data.nodes, &mut data.next); l } fn append_to_next_list(&self, mut l: list::List) { let data = &mut *self.data.borrow_mut(); data.next.concat(&mut data.nodes, &mut l); } fn take_task(&self, l: &mut list::List) -> Option<(usize, BoxFuture, Waker)> { let nkey = match l.head { Some(nkey) => nkey, None => return None, }; let data = &mut *self.data.borrow_mut(); l.remove(&mut data.nodes, nkey); let task = &mut data.nodes[nkey].value; // both of these are cheap let fut = task.fut.take().unwrap(); let waker = waker::into_std(data.wakers[nkey].clone()); task.wakeable = true; Some((nkey, fut, waker)) } fn process_next(&self) { let mut l = self.take_next_list(); while let Some((task_id, mut fut, waker)) = self.take_task(&mut l) { self.pre_poll(); let done = poll_fut(&mut fut, waker); // take_task() took the future out of the task, so we // could poll it without having to maintain a borrow of // the tasks set. we'll put it back now self.set_fut(task_id, fut); if done { self.remove(task_id); } } } fn set_fut(&self, task_id: usize, fut: BoxFuture) { let nkey = task_id; let data = &mut *self.data.borrow_mut(); let task = &mut data.nodes[nkey].value; task.fut = Some(fut); } fn wake(&self, task_id: usize) { let nkey = task_id; let data = &mut *self.data.borrow_mut(); let node = &mut data.nodes[nkey]; if !node.value.wakeable { return; } node.value.wakeable = false; data.next.push_back(&mut data.nodes, nkey); } fn set_pre_poll(&self, pre_poll_fn: F) where F: FnMut() + 'static, { *self.pre_poll.borrow_mut() = Some(Box::new(pre_poll_fn)); } fn pre_poll(&self) { let pre_poll = &mut *self.pre_poll.borrow_mut(); if let Some(f) = pre_poll { f(); } } } pub struct Executor { tasks: Rc, } impl Executor { pub fn new(tasks_max: usize) -> Self { let tasks = Tasks::new(tasks_max); EXECUTOR.with(|ex| { if ex.borrow().is_some() { panic!("thread already has an Executor"); } ex.replace(Some(Rc::downgrade(&tasks))); }); Self { tasks } } #[allow(clippy::result_unit_err)] pub fn spawn(&self, fut: F) -> Result<(), ()> where F: Future + 'static, { debug!("spawning future with size {}", mem::size_of::()); self.tasks.add(fut) } pub fn set_pre_poll(&self, pre_poll_fn: F) where F: FnMut() + 'static, { self.tasks.set_pre_poll(pre_poll_fn); } pub fn have_tasks(&self) -> bool { !self.tasks.is_empty() } pub fn run_until_stalled(&self) { while self.tasks.have_next() { self.tasks.process_next() } } pub fn run(&self, mut park: F) -> Result<(), io::Error> where F: FnMut(Option) -> Result<(), io::Error>, { loop { self.tasks.process_next(); if !self.have_tasks() { break; } let (timeout, low_priority_tasks) = if self.tasks.have_next() { // some tasks trigger their own waker and return Pending in // order to achieve a yielding effect. in that case they will // already be queued up for processing again. move these // tasks aside so that they can be deprioritized, and use a // timeout of 0 when parking so we can quickly resume them let timeout = Duration::from_millis(0); let l = self.tasks.take_next_list(); (Some(timeout), Some(l)) } else { (None, None) }; park(timeout)?; // requeue any tasks that had yielded if let Some(l) = low_priority_tasks { self.tasks.append_to_next_list(l); } } Ok(()) } pub fn current() -> Option { EXECUTOR.with(|ex| { (*ex.borrow_mut()).as_mut().map(|tasks| Self { tasks: tasks.upgrade().unwrap(), }) }) } pub fn spawner(&self) -> Spawner { Spawner { tasks: Rc::downgrade(&self.tasks), } } } impl Drop for Executor { fn drop(&mut self) { EXECUTOR.with(|ex| { if Rc::strong_count(&self.tasks) == 1 { ex.replace(None); } }); } } pub struct Spawner { tasks: Weak, } impl Spawner { #[allow(clippy::result_unit_err)] pub fn spawn(&self, fut: F) -> Result<(), ()> where F: Future + 'static, { let tasks = match self.tasks.upgrade() { Some(tasks) => tasks, None => return Err(()), }; let ex = Executor { tasks }; ex.spawn(fut) } } #[cfg(test)] mod tests { use super::*; use std::cell::Cell; use std::mem; use std::task::Poll; struct TestFutureData { ready: bool, waker: Option, } struct TestFuture { data: Rc>, } impl TestFuture { fn new() -> Self { let data = TestFutureData { ready: false, waker: None, }; Self { data: Rc::new(RefCell::new(data)), } } fn handle(&self) -> TestHandle { TestHandle { data: Rc::clone(&self.data), } } } impl Future for TestFuture { type Output = (); fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { let mut data = self.data.borrow_mut(); match data.ready { true => Poll::Ready(()), false => { data.waker = Some(cx.waker().clone()); Poll::Pending } } } } struct TestHandle { data: Rc>, } impl TestHandle { fn set_ready(&self) { let data = &mut *self.data.borrow_mut(); data.ready = true; if let Some(waker) = data.waker.take() { waker.wake(); } } } struct EarlyWakeFuture { done: bool, } impl EarlyWakeFuture { fn new() -> Self { Self { done: false } } } impl Future for EarlyWakeFuture { type Output = (); fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { if !self.done { self.done = true; cx.waker().wake_by_ref(); return Poll::Pending; } Poll::Ready(()) } } #[test] fn test_executor_step() { let executor = Executor::new(1); let fut1 = TestFuture::new(); let fut2 = TestFuture::new(); let handle1 = fut1.handle(); let handle2 = fut2.handle(); let started = Rc::new(Cell::new(false)); let fut1_done = Rc::new(Cell::new(false)); let finishing = Rc::new(Cell::new(false)); { let started = Rc::clone(&started); let fut1_done = Rc::clone(&fut1_done); let finishing = Rc::clone(&finishing); executor .spawn(async move { started.set(true); fut1.await; fut1_done.set(true); fut2.await; finishing.set(true); }) .unwrap(); } // not started yet, no progress assert_eq!(executor.have_tasks(), true); assert_eq!(started.get(), false); executor.run_until_stalled(); // started, but fut1 not ready assert_eq!(executor.have_tasks(), true); assert_eq!(started.get(), true); assert_eq!(fut1_done.get(), false); handle1.set_ready(); executor.run_until_stalled(); // fut1 finished assert_eq!(executor.have_tasks(), true); assert_eq!(fut1_done.get(), true); assert_eq!(finishing.get(), false); handle2.set_ready(); executor.run_until_stalled(); // fut2 finished, and thus the task finished assert_eq!(finishing.get(), true); assert_eq!(executor.have_tasks(), false); } #[test] fn test_executor_run() { let executor = Executor::new(1); let fut = TestFuture::new(); let handle = fut.handle(); executor .spawn(async move { fut.await; }) .unwrap(); executor .run(|_| { handle.set_ready(); Ok(()) }) .unwrap(); assert_eq!(executor.have_tasks(), false); } #[test] fn test_executor_spawn_error() { let executor = Executor::new(1); assert!(executor.spawn(async {}).is_ok()); assert!(executor.spawn(async {}).is_err()); } #[test] fn test_executor_current() { assert!(Executor::current().is_none()); let executor = Executor::new(2); let flag = Rc::new(Cell::new(false)); { let flag = flag.clone(); executor .spawn(async move { Executor::current() .unwrap() .spawn(async move { flag.set(true); }) .unwrap(); }) .unwrap(); } assert_eq!(flag.get(), false); executor.run(|_| Ok(())).unwrap(); assert_eq!(flag.get(), true); let current = Executor::current().unwrap(); assert_eq!(executor.have_tasks(), false); assert!(current.spawn(async {}).is_ok()); assert_eq!(executor.have_tasks(), true); mem::drop(executor); assert!(Executor::current().is_some()); mem::drop(current); assert!(Executor::current().is_none()); } #[test] fn test_executor_spawner() { let executor = Executor::new(2); let flag = Rc::new(Cell::new(false)); { let flag = flag.clone(); let spawner = executor.spawner(); executor .spawn(async move { spawner .spawn(async move { flag.set(true); }) .unwrap(); }) .unwrap(); } assert_eq!(flag.get(), false); executor.run(|_| Ok(())).unwrap(); assert_eq!(flag.get(), true); } #[test] fn test_executor_early_wake() { let executor = Executor::new(1); let fut = EarlyWakeFuture::new(); executor .spawn(async move { fut.await; }) .unwrap(); let mut park_count = 0; executor .run(|_| { park_count += 1; Ok(()) }) .unwrap(); assert_eq!(park_count, 1); } #[test] fn test_executor_pre_poll() { let executor = Executor::new(1); let flag = Rc::new(Cell::new(false)); { let flag = flag.clone(); executor.set_pre_poll(move || { flag.set(true); }); } executor.spawn(async {}).unwrap(); assert_eq!(flag.get(), false); executor.run(|_| Ok(())).unwrap(); assert_eq!(flag.get(), true); } } pushpin-1.39.1/src/ffi.rs000066400000000000000000000715741457610542000152160ustar00rootroot00000000000000/* * Copyright (C) 2021-2022 Fanout, Inc. * Copyright (C) 2023-2024 Fastly, Inc. * * This file is part of Pushpin. * * $FANOUT_BEGIN_LICENSE:APACHE2$ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $FANOUT_END_LICENSE$ */ use crate::jwt; use crate::timer::TimerWheel; use crate::version; use libc; use std::collections::HashSet; use std::env; use std::ffi::{CStr, CString}; use std::os::raw::c_char; use std::ptr; use std::slice; #[cfg(test)] use crate::import_cpptest; #[repr(C)] pub struct ExpiredTimer { key: libc::c_int, user_data: libc::size_t, } #[no_mangle] pub extern "C" fn timer_wheel_create(capacity: libc::c_uint) -> *mut TimerWheel { let wheel = TimerWheel::new(capacity as usize); Box::into_raw(Box::new(wheel)) } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn timer_wheel_destroy(wheel: *mut TimerWheel) { if !wheel.is_null() { drop(Box::from_raw(wheel)); } } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn timer_add( wheel: *mut TimerWheel, expires: u64, user_data: libc::size_t, ) -> libc::c_int { match TimerWheel::add(&mut *wheel, expires, user_data) { Ok(key) => key as libc::c_int, Err(_) => -1, } } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn timer_remove(wheel: *mut TimerWheel, key: libc::c_int) { TimerWheel::remove(&mut *wheel, key as usize); } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn timer_wheel_timeout(wheel: *mut TimerWheel) -> i64 { match TimerWheel::timeout(&*wheel) { Some(timeout) => timeout as i64, None => -1, } } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn timer_wheel_update(wheel: *mut TimerWheel, curtime: u64) { TimerWheel::update(&mut *wheel, curtime); } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn timer_wheel_take_expired(wheel: *mut TimerWheel) -> ExpiredTimer { match TimerWheel::take_expired(&mut *wheel) { Some((key, user_data)) => ExpiredTimer { key: key as libc::c_int, user_data: user_data as libc::size_t, }, None => ExpiredTimer { key: -1, user_data: 0, }, } } const JWT_KEYTYPE_SECRET: libc::c_int = 0; const JWT_KEYTYPE_EC: libc::c_int = 1; const JWT_KEYTYPE_RSA: libc::c_int = 2; const JWT_ALGORITHM_HS256: libc::c_int = 0; const JWT_ALGORITHM_ES256: libc::c_int = 1; const JWT_ALGORITHM_RS256: libc::c_int = 2; #[repr(C)] pub struct JwtEncodingKey { r#type: libc::c_int, key: *mut jsonwebtoken::EncodingKey, } #[repr(C)] pub struct JwtDecodingKey { r#type: libc::c_int, key: *mut jsonwebtoken::DecodingKey, } type EncodingKeyFromPemFn = fn(&[u8]) -> Result; type DecodingKeyFromPemFn = fn(&[u8]) -> Result; fn load_encoding_key_pem( key: &[u8], ) -> Result<(libc::c_int, jsonwebtoken::EncodingKey), jsonwebtoken::errors::Error> { // pem data includes the key type, however the jsonwebtoken crate // requires specifying the expected type when decoding. we'll just try // the data against multiple possible types let decoders: [(libc::c_int, EncodingKeyFromPemFn); 2] = [ (JWT_KEYTYPE_EC, jsonwebtoken::EncodingKey::from_ec_pem), (JWT_KEYTYPE_RSA, jsonwebtoken::EncodingKey::from_rsa_pem), ]; let mut last_err = None; for (ktype, f) in decoders { match f(key) { Ok(key) => return Ok((ktype, key)), Err(e) => last_err = Some(e), } } Err(last_err.unwrap()) } fn load_decoding_key_pem( key: &[u8], ) -> Result<(libc::c_int, jsonwebtoken::DecodingKey), jsonwebtoken::errors::Error> { // pem data includes the key type, however the jsonwebtoken crate // requires specifying the expected type when decoding. we'll just try // the data against multiple possible types let decoders: [(libc::c_int, DecodingKeyFromPemFn); 2] = [ (JWT_KEYTYPE_EC, jsonwebtoken::DecodingKey::from_ec_pem), (JWT_KEYTYPE_RSA, jsonwebtoken::DecodingKey::from_rsa_pem), ]; let mut last_err = None; for (ktype, f) in decoders { match f(key) { Ok(key) => return Ok((ktype, key)), Err(e) => last_err = Some(e), } } Err(last_err.unwrap()) } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn jwt_encoding_key_from_secret( data: *const u8, len: libc::size_t, ) -> JwtEncodingKey { let key = jsonwebtoken::EncodingKey::from_secret(slice::from_raw_parts(data, len)); JwtEncodingKey { r#type: JWT_KEYTYPE_SECRET, key: Box::into_raw(Box::new(key)), } } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn jwt_encoding_key_from_pem( data: *const u8, len: libc::size_t, ) -> JwtEncodingKey { match load_encoding_key_pem(slice::from_raw_parts(data, len)) { Ok((ktype, key)) => JwtEncodingKey { r#type: ktype, key: Box::into_raw(Box::new(key)), }, Err(_) => JwtEncodingKey { r#type: -1, key: ptr::null_mut(), }, } } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn jwt_encoding_key_destroy(key: *mut jsonwebtoken::EncodingKey) { if !key.is_null() { drop(Box::from_raw(key)); } } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn jwt_decoding_key_from_secret( data: *const u8, len: libc::size_t, ) -> JwtDecodingKey { let key = jsonwebtoken::DecodingKey::from_secret(slice::from_raw_parts(data, len)); JwtDecodingKey { r#type: JWT_KEYTYPE_SECRET, key: Box::into_raw(Box::new(key)), } } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn jwt_decoding_key_from_pem( data: *const u8, len: libc::size_t, ) -> JwtDecodingKey { match load_decoding_key_pem(slice::from_raw_parts(data, len)) { Ok((ktype, key)) => JwtDecodingKey { r#type: ktype, key: Box::into_raw(Box::new(key)), }, Err(_) => JwtDecodingKey { r#type: -1, key: ptr::null_mut(), }, } } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn jwt_decoding_key_destroy(key: *mut jsonwebtoken::DecodingKey) { if !key.is_null() { drop(Box::from_raw(key)); } } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn jwt_str_destroy(s: *mut c_char) { if !s.is_null() { drop(CString::from_raw(s)); } } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn jwt_encode( alg: libc::c_int, claim: *const c_char, key: *const jsonwebtoken::EncodingKey, out_token: *mut *const c_char, ) -> libc::c_int { if claim.is_null() || out_token.is_null() { return 1; // null pointers } let key = match key.as_ref() { Some(r) => r, None => return 1, // null pointer }; let header = match alg { JWT_ALGORITHM_HS256 => jsonwebtoken::Header::new(jsonwebtoken::Algorithm::HS256), JWT_ALGORITHM_ES256 => jsonwebtoken::Header::new(jsonwebtoken::Algorithm::ES256), JWT_ALGORITHM_RS256 => jsonwebtoken::Header::new(jsonwebtoken::Algorithm::RS256), _ => return 1, // unsupported algorithm }; let claim = match CStr::from_ptr(claim).to_str() { Ok(s) => s, Err(_) => return 1, // claim is a JSON string which will be valid UTF-8 }; let token = match jwt::encode(&header, claim, key) { Ok(token) => token, Err(_) => return 1, // failed to sign }; let token = match CString::new(token) { Ok(s) => s, Err(_) => return 1, // unexpected token string format }; *out_token = token.into_raw(); 0 } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn jwt_decode( alg: libc::c_int, token: *const c_char, key: *const jsonwebtoken::DecodingKey, out_claim: *mut *const c_char, ) -> libc::c_int { if token.is_null() || out_claim.is_null() { return 1; // null pointers } let key = match key.as_ref() { Some(r) => r, None => return 1, // null pointer }; let mut validation = match alg { JWT_ALGORITHM_HS256 => jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256), JWT_ALGORITHM_ES256 => jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::ES256), JWT_ALGORITHM_RS256 => jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::RS256), _ => return 1, // unsupported algorithm }; // don't check exp or anything. that's left to the caller validation.required_spec_claims = HashSet::new(); let token = match CStr::from_ptr(token).to_str() { Ok(s) => s, Err(_) => return 1, // token string will be valid UTF-8 }; let claim = match jwt::decode(token, key, &validation) { Ok(claim) => claim, Err(_) => return 1, // failed to validate }; let claim = match CString::new(claim) { Ok(s) => s, Err(_) => return 1, // unexpected claim string format }; *out_claim = claim.into_raw(); 0 } // NOTE: must match values in wzmq.h const WZMQ_PAIR: libc::c_int = 0; const WZMQ_PUB: libc::c_int = 1; const WZMQ_SUB: libc::c_int = 2; const WZMQ_REQ: libc::c_int = 3; const WZMQ_REP: libc::c_int = 4; const WZMQ_DEALER: libc::c_int = 5; const WZMQ_ROUTER: libc::c_int = 6; const WZMQ_PULL: libc::c_int = 7; const WZMQ_PUSH: libc::c_int = 8; const WZMQ_XPUB: libc::c_int = 9; const WZMQ_XSUB: libc::c_int = 10; const WZMQ_STREAM: libc::c_int = 11; // NOTE: must match values in wzmq.h const WZMQ_FD: libc::c_int = 0; const WZMQ_SUBSCRIBE: libc::c_int = 1; const WZMQ_UNSUBSCRIBE: libc::c_int = 2; const WZMQ_LINGER: libc::c_int = 3; const WZMQ_IDENTITY: libc::c_int = 4; const WZMQ_IMMEDIATE: libc::c_int = 5; const WZMQ_RCVMORE: libc::c_int = 6; const WZMQ_EVENTS: libc::c_int = 7; const WZMQ_SNDHWM: libc::c_int = 8; const WZMQ_RCVHWM: libc::c_int = 9; const WZMQ_TCP_KEEPALIVE: libc::c_int = 10; const WZMQ_TCP_KEEPALIVE_IDLE: libc::c_int = 11; const WZMQ_TCP_KEEPALIVE_CNT: libc::c_int = 12; const WZMQ_TCP_KEEPALIVE_INTVL: libc::c_int = 13; const WZMQ_ROUTER_MANDATORY: libc::c_int = 14; // NOTE: must match values in wzmq.h const WZMQ_DONTWAIT: libc::c_int = 0x01; const WZMQ_SNDMORE: libc::c_int = 0x02; // NOTE: must match values in wzmq.h const WZMQ_POLLIN: libc::c_int = 0x01; const WZMQ_POLLOUT: libc::c_int = 0x02; #[repr(C)] pub struct WZmqMessage { data: *mut zmq::Message, } fn convert_io_flags(flags: libc::c_int) -> i32 { let mut out = 0; if flags & WZMQ_DONTWAIT != 0 { out |= zmq::DONTWAIT; } if flags & WZMQ_SNDMORE != 0 { out |= zmq::SNDMORE; } out } fn convert_events(events: zmq::PollEvents) -> libc::c_int { let mut out = 0; if events.contains(zmq::POLLIN) { out |= WZMQ_POLLIN; } if events.contains(zmq::POLLOUT) { out |= WZMQ_POLLOUT; } out } #[cfg(target_os = "macos")] fn set_errno(value: libc::c_int) { unsafe { *libc::__error() = value; } } #[cfg(not(target_os = "macos"))] fn set_errno(value: libc::c_int) { unsafe { *libc::__errno_location() = value; } } #[no_mangle] pub extern "C" fn wzmq_init(_io_threads: libc::c_int) -> *mut zmq::Context { let ctx = zmq::Context::new(); // NOTE: io_threads is ignored since zmq 0.9 doesn't provide a way to specify it Box::into_raw(Box::new(ctx)) } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn wzmq_term(context: *mut zmq::Context) -> libc::c_int { if !context.is_null() { drop(Box::from_raw(context)); } 0 } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn wzmq_socket( context: *mut zmq::Context, stype: libc::c_int, ) -> *mut zmq::Socket { let ctx = match context.as_ref() { Some(ctx) => ctx, None => return ptr::null_mut(), }; let stype = match stype { WZMQ_PAIR => zmq::PAIR, WZMQ_PUB => zmq::PUB, WZMQ_SUB => zmq::SUB, WZMQ_REQ => zmq::REQ, WZMQ_REP => zmq::REP, WZMQ_DEALER => zmq::DEALER, WZMQ_ROUTER => zmq::ROUTER, WZMQ_PULL => zmq::PULL, WZMQ_PUSH => zmq::PUSH, WZMQ_XPUB => zmq::XPUB, WZMQ_XSUB => zmq::XSUB, WZMQ_STREAM => zmq::STREAM, _ => return ptr::null_mut(), }; let sock = match ctx.socket(stype) { Ok(sock) => sock, Err(_) => return ptr::null_mut(), }; Box::into_raw(Box::new(sock)) } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn wzmq_close(socket: *mut zmq::Socket) -> libc::c_int { if socket.is_null() { return -1; } drop(Box::from_raw(socket)); 0 } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn wzmq_getsockopt( socket: *mut zmq::Socket, option_name: libc::c_int, option_value: *mut libc::c_void, option_len: *mut libc::size_t, ) -> libc::c_int { let sock = match socket.as_ref() { Some(sock) => sock, None => return -1, }; if option_value.is_null() { return -1; } let option_len = match option_len.as_mut() { Some(x) => x, None => return -1, }; match option_name { WZMQ_FD => { if *option_len as u32 != libc::c_int::BITS / 8 { return -1; } let x = match (option_value as *mut libc::c_int).as_mut() { Some(x) => x, None => return -1, }; let v = match sock.get_fd() { Ok(v) => v, Err(e) => { println!("get_fd failed: {:?}", e); set_errno(e.to_raw()); return -1; } }; *x = v; } WZMQ_IDENTITY => { let identity = match sock.get_identity() { Ok(v) => v, Err(_) => return -1, }; let s = slice::from_raw_parts_mut(option_value as *mut u8, *option_len); if s.len() < identity.len() { return -1; } s[..identity.len()].copy_from_slice(&identity); } WZMQ_RCVMORE => { if *option_len as u32 != libc::c_int::BITS / 8 { return -1; } let x = match (option_value as *mut libc::c_int).as_mut() { Some(x) => x, None => return -1, }; let v = match sock.get_rcvmore() { Ok(v) => v, Err(e) => { set_errno(e.to_raw()); return -1; } }; if v { *x = 1; } else { *x = 0; } } WZMQ_EVENTS => { if *option_len as u32 != libc::c_int::BITS / 8 { return -1; } let x = match (option_value as *mut libc::c_int).as_mut() { Some(x) => x, None => return -1, }; let v = match sock.get_events() { Ok(v) => v, Err(e) => { set_errno(e.to_raw()); return -1; } }; *x = convert_events(v); } WZMQ_SNDHWM => { if *option_len as u32 != libc::c_int::BITS / 8 { return -1; } let x = match (option_value as *mut libc::c_int).as_mut() { Some(x) => x, None => return -1, }; let v = match sock.get_sndhwm() { Ok(v) => v, Err(e) => { set_errno(e.to_raw()); return -1; } }; *x = v; } WZMQ_RCVHWM => { if *option_len as u32 != libc::c_int::BITS / 8 { return -1; } let x = match (option_value as *mut libc::c_int).as_mut() { Some(x) => x, None => return -1, }; let v = match sock.get_rcvhwm() { Ok(v) => v, Err(e) => { set_errno(e.to_raw()); return -1; } }; *x = v; } _ => return -1, } 0 } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn wzmq_setsockopt( socket: *mut zmq::Socket, option_name: libc::c_int, option_value: *mut libc::c_void, option_len: libc::size_t, ) -> libc::c_int { let sock = match socket.as_ref() { Some(sock) => sock, None => return -1, }; if option_value.is_null() { return -1; } match option_name { WZMQ_SUBSCRIBE => { let s = slice::from_raw_parts(option_value as *mut u8, option_len); if let Err(e) = sock.set_subscribe(s) { set_errno(e.to_raw()); return -1; } } WZMQ_UNSUBSCRIBE => { let s = slice::from_raw_parts(option_value as *mut u8, option_len); if let Err(e) = sock.set_unsubscribe(s) { set_errno(e.to_raw()); return -1; } } WZMQ_LINGER => { if option_len as u32 != libc::c_int::BITS / 8 { return -1; } let x = match (option_value as *mut libc::c_int).as_ref() { Some(x) => x, None => return -1, }; if let Err(e) = sock.set_linger(*x) { set_errno(e.to_raw()); return -1; } } WZMQ_IDENTITY => { let s = slice::from_raw_parts(option_value as *mut u8, option_len); if let Err(e) = sock.set_identity(s) { set_errno(e.to_raw()); return -1; } } WZMQ_IMMEDIATE => { if option_len as u32 != libc::c_int::BITS / 8 { return -1; } let x = match (option_value as *mut libc::c_int).as_ref() { Some(x) => x, None => return -1, }; if let Err(e) = sock.set_immediate(*x != 0) { set_errno(e.to_raw()); return -1; } } WZMQ_ROUTER_MANDATORY => { if option_len as u32 != libc::c_int::BITS / 8 { return -1; } let x = match (option_value as *mut libc::c_int).as_ref() { Some(x) => x, None => return -1, }; if let Err(e) = sock.set_router_mandatory(*x != 0) { set_errno(e.to_raw()); return -1; } } WZMQ_SNDHWM => { if option_len as u32 != libc::c_int::BITS / 8 { return -1; } let x = match (option_value as *mut libc::c_int).as_ref() { Some(x) => x, None => return -1, }; if let Err(e) = sock.set_sndhwm(*x) { set_errno(e.to_raw()); return -1; } } WZMQ_RCVHWM => { if option_len as u32 != libc::c_int::BITS / 8 { return -1; } let x = match (option_value as *mut libc::c_int).as_ref() { Some(x) => x, None => return -1, }; if let Err(e) = sock.set_rcvhwm(*x) { set_errno(e.to_raw()); return -1; } } WZMQ_TCP_KEEPALIVE => { if option_len as u32 != libc::c_int::BITS / 8 { return -1; } let x = match (option_value as *mut libc::c_int).as_ref() { Some(x) => x, None => return -1, }; if let Err(e) = sock.set_tcp_keepalive(*x) { set_errno(e.to_raw()); return -1; } } WZMQ_TCP_KEEPALIVE_IDLE => { if option_len as u32 != libc::c_int::BITS / 8 { return -1; } let x = match (option_value as *mut libc::c_int).as_ref() { Some(x) => x, None => return -1, }; if let Err(e) = sock.set_tcp_keepalive_idle(*x) { set_errno(e.to_raw()); return -1; } } WZMQ_TCP_KEEPALIVE_CNT => { if option_len as u32 != libc::c_int::BITS / 8 { return -1; } let x = match (option_value as *mut libc::c_int).as_ref() { Some(x) => x, None => return -1, }; if let Err(e) = sock.set_tcp_keepalive_cnt(*x) { set_errno(e.to_raw()); return -1; } } WZMQ_TCP_KEEPALIVE_INTVL => { if option_len as u32 != libc::c_int::BITS / 8 { return -1; } let x = match (option_value as *mut libc::c_int).as_ref() { Some(x) => x, None => return -1, }; if let Err(e) = sock.set_tcp_keepalive_intvl(*x) { set_errno(e.to_raw()); return -1; } } _ => return -1, } 0 } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn wzmq_connect( socket: *mut zmq::Socket, endpoint: *const libc::c_char, ) -> libc::c_int { let sock = match socket.as_ref() { Some(sock) => sock, None => return -1, }; if endpoint.is_null() { return -1; } let endpoint = match CStr::from_ptr(endpoint).to_str() { Ok(s) => s, Err(_) => return -1, }; if let Err(e) = sock.connect(endpoint) { set_errno(e.to_raw()); return -1; } 0 } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn wzmq_bind( socket: *mut zmq::Socket, endpoint: *const libc::c_char, ) -> libc::c_int { let sock = match socket.as_ref() { Some(sock) => sock, None => return -1, }; if endpoint.is_null() { return -1; } let endpoint = match CStr::from_ptr(endpoint).to_str() { Ok(s) => s, Err(_) => return -1, }; if let Err(e) = sock.bind(endpoint) { set_errno(e.to_raw()); return -1; } 0 } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn wzmq_send( socket: *mut zmq::Socket, buf: *const u8, len: libc::size_t, flags: libc::c_int, ) -> libc::c_int { let sock = match socket.as_ref() { Some(sock) => sock, None => return -1, }; if buf.is_null() { return -1; } let buf = slice::from_raw_parts(buf, len); if let Err(e) = sock.send(buf, convert_io_flags(flags)) { set_errno(e.to_raw()); return -1; } buf.len() as libc::c_int } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn wzmq_recv( socket: *mut zmq::Socket, buf: *mut u8, len: libc::size_t, flags: libc::c_int, ) -> libc::c_int { let sock = match socket.as_ref() { Some(sock) => sock, None => return -1, }; if buf.is_null() { return -1; } let buf = slice::from_raw_parts_mut(buf, len); let size = match sock.recv_into(buf, convert_io_flags(flags)) { Ok(size) => size, Err(e) => { set_errno(e.to_raw()); return -1; } }; size as libc::c_int } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn wzmq_msg_init(msg: *mut WZmqMessage) -> libc::c_int { let msg = msg.as_mut().unwrap(); msg.data = Box::into_raw(Box::new(zmq::Message::new())); 0 } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn wzmq_msg_init_size( msg: *mut WZmqMessage, size: libc::size_t, ) -> libc::c_int { let msg = msg.as_mut().unwrap(); msg.data = Box::into_raw(Box::new(zmq::Message::with_size(size))); 0 } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn wzmq_msg_data(msg: *mut WZmqMessage) -> *mut libc::c_void { let msg = msg.as_mut().unwrap(); let data = msg.data.as_mut().unwrap(); data.as_mut_ptr() as *mut libc::c_void } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn wzmq_msg_size(msg: *const WZmqMessage) -> libc::size_t { let msg = msg.as_ref().unwrap(); let data = msg.data.as_ref().unwrap(); data.len() } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn wzmq_msg_close(msg: *mut WZmqMessage) -> libc::c_int { let msg = match msg.as_mut() { Some(msg) => msg, None => return -1, }; if !msg.data.is_null() { drop(Box::from_raw(msg.data)); msg.data = ptr::null_mut(); } 0 } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn wzmq_msg_send( msg: *mut WZmqMessage, socket: *mut zmq::Socket, flags: libc::c_int, ) -> libc::c_int { let msg = match msg.as_mut() { Some(msg) => msg, None => return -1, }; if msg.data.is_null() { return -1; } let sock = match socket.as_ref() { Some(sock) => sock, None => return -1, }; let data = Box::from_raw(msg.data); msg.data = ptr::null_mut(); let size = data.len(); if let Err(e) = sock.send(*data, convert_io_flags(flags)) { set_errno(e.to_raw()); return -1; } size as libc::c_int } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn wzmq_msg_recv( msg: *mut WZmqMessage, socket: *mut zmq::Socket, flags: libc::c_int, ) -> libc::c_int { let msg = match msg.as_mut() { Some(msg) => msg, None => return -1, }; let sock = match socket.as_ref() { Some(sock) => sock, None => return -1, }; if !msg.data.is_null() { drop(Box::from_raw(msg.data)); msg.data = ptr::null_mut(); } let data = match sock.recv_msg(convert_io_flags(flags)) { Ok(msg) => msg, Err(e) => { set_errno(e.to_raw()); return -1; } }; let size = data.len(); msg.data = Box::into_raw(Box::new(data)); size as libc::c_int } #[repr(C)] pub struct BuildConfig { version: *mut libc::c_char, config_dir: *mut libc::c_char, lib_dir: *mut libc::c_char, } #[no_mangle] pub fn build_config_new() -> *mut BuildConfig { let lib_dir = env!("LIB_DIR"); let config_dir = env!("CONFIG_DIR"); let c = BuildConfig { version: CString::new(version()).unwrap().into_raw(), config_dir: CString::new(config_dir).unwrap().into_raw(), lib_dir: CString::new(lib_dir).unwrap().into_raw(), }; Box::into_raw(Box::new(c)) } #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe fn build_config_destroy(c: *mut BuildConfig) { let c = match c.as_mut() { Some(c) => Box::from_raw(c), None => return, }; drop(CString::from_raw(c.version)); drop(CString::from_raw(c.config_dir)); drop(CString::from_raw(c.lib_dir)); } #[cfg(test)] import_cpptest! { pub fn httpheaders_test(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; pub fn jwt_test(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; pub fn routesfile_test(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; pub fn proxyengine_test(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; pub fn jsonpatch_test(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; pub fn instruct_test(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; pub fn idformat_test(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; pub fn publishformat_test(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; pub fn publishitem_test(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; pub fn handlerengine_test(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; pub fn template_test(argc: libc::c_int, argv: *const *const libc::c_char) -> libc::c_int; } pushpin-1.39.1/src/future.rs000066400000000000000000003150631457610542000157560ustar00rootroot00000000000000/* * Copyright (C) 2020-2023 Fanout, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use crate::arena; use crate::channel; use crate::event::{self, ReadinessExt}; use crate::net::{NetListener, NetStream, SocketAddr}; use crate::reactor::{CustomEvented, FdEvented, IoEvented, Reactor, Registration, TimerEvented}; use crate::resolver; use crate::shuffle::shuffle; use crate::tls::{TlsStream, TlsStreamError, VerifyMode}; use crate::waker::{RefWake, RefWaker, RefWakerData}; use crate::zmq::{MultipartHeader, ZmqSocket}; use mio::net::{TcpListener, TcpStream, UnixListener, UnixStream}; use openssl::ssl; use paste::paste; use std::cell::{Cell, Ref, RefCell}; use std::future::Future; use std::io::{self, Read, Write}; use std::mem; use std::os::fd::{FromRawFd, IntoRawFd}; use std::path::Path; use std::pin::Pin; use std::rc::Rc; use std::sync::mpsc; use std::task::{Context, Poll, Waker}; use std::time::{Duration, Instant}; pub const REGISTRATIONS_PER_CHANNEL: usize = 1; // 1 for the zmq fd, and potentially 1 for the retry timer pub const REGISTRATIONS_PER_ZMQSOCKET: usize = 2; pub struct PollFuture { fut: F, } impl Future for PollFuture where F: Future + Unpin, { type Output = Poll; fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { let s = &mut *self; Poll::Ready(Pin::new(&mut s.fut).poll(cx)) } } pub fn poll_async(fut: F) -> PollFuture where F: Future + Unpin, { PollFuture { fut } } fn range_unordered(dest: &mut [usize]) -> &[usize] { for (index, v) in dest.iter_mut().enumerate() { *v = index; } shuffle(dest); dest } fn map_poll(cx: &mut Context, fut: &mut F, wrap_func: W) -> Poll where F: Future + Unpin, W: FnOnce(F::Output) -> V, { match Pin::new(fut).poll(cx) { Poll::Ready(v) => Poll::Ready(wrap_func(v)), Poll::Pending => Poll::Pending, } } macro_rules! declare_select { ($count: literal, ( $($num:literal),* )) => { paste! { pub enum []<$([], )*> { $( []: [], )* } impl<$([], )*> Future for []<$([]::Output, )*>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { let mut indexes = [0; $count]; for i in range_unordered(&mut indexes) { let s = &mut *self; let p = match i + 1 { $( $num => map_poll(cx, &mut s.[], |v| []<$([], )*> where $( []: Future + Unpin, )* { [