service-binding-3.0.0/.cargo_vcs_info.json0000644000000001360000000000100141040ustar { "git": { "sha1": "9d3a36da17bf458089b585db5d664387ed48ecfe" }, "path_in_vcs": "" }service-binding-3.0.0/.codespellrc000064400000000000000000000001171046102023000151730ustar 00000000000000[codespell] skip = .cargo,.git,target,Cargo.lock ignore-words-list = crate,ser service-binding-3.0.0/.gitattributes000064400000000000000000000000161046102023000155640ustar 00000000000000* text eol=lf service-binding-3.0.0/.gitignore000064400000000000000000000000101046102023000146530ustar 00000000000000/target service-binding-3.0.0/.justfile000075500000000000000000000042441046102023000145310ustar 00000000000000#!/usr/bin/env -S just --working-directory . --justfile # Since this is a first recipe it's being run by default. # Faster checks need to be executed first for better UX. For example # codespell is very fast. cargo fmt does not need to download crates etc. check: spelling formatting docs lints dependencies tests # Checks common spelling mistakes spelling: codespell # Checks source code formatting formatting: just --unstable --fmt --check # We're using nightly to properly group imports, see .rustfmt.toml cargo +nightly fmt --all -- --check # Lints the source code lints: cargo clippy --workspace --no-deps --all-targets -- -D warnings # Checks for issues with dependencies dependencies: cargo deny check # Runs all unit tests. By default ignored tests are not run. Run with `ignored=true` to run only ignored tests tests: cargo test --all # Build docs for this crate only docs: cargo doc --no-deps # Checks for commit messages check-commits REFS='main..': #!/usr/bin/env bash set -euo pipefail for commit in $(git rev-list "{{ REFS }}"); do MSG="$(git show -s --format=%B "$commit")" CODESPELL_RC="$(mktemp)" git show "$commit:.codespellrc" > "$CODESPELL_RC" if ! grep -q "Signed-off-by: " <<< "$MSG"; then printf "Commit %s lacks \"Signed-off-by\" line.\n" "$commit" printf "%s\n" \ " Please use:" \ " git rebase --signoff main && git push --force-with-lease" \ " See https://developercertificate.org/ for more details." exit 1; elif ! codespell --config "$CODESPELL_RC" - <<< "$MSG"; then printf "The spelling in commit %s needs improvement.\n" "$commit" exit 1; else printf "Commit %s is good.\n" "$commit" fi done # Fixes common issues. Files need to be git add'ed fix: #!/usr/bin/env bash if ! git diff-files --quiet ; then echo "Working tree has changes. Please stage them: git add ." exit 1 fi codespell --write-changes just --unstable --fmt cargo clippy --fix --allow-staged # fmt must be last as clippy's changes may break formatting cargo +nightly fmt --all service-binding-3.0.0/.rustfmt.toml000064400000000000000000000001061046102023000153500ustar 00000000000000group_imports = "StdExternalCrate" format_code_in_doc_comments = true service-binding-3.0.0/CONTRIBUTING.md000064400000000000000000000051561046102023000151340ustar 00000000000000# Contributing Thanks for taking the time to contribute to this project! All changes need to: - pass basic checks, including tests, formatting and lints, - be signed-off. ## Basic checks We are using standard Rust ecosystem tools including `rustfmt` and `clippy` with one minor difference. Due to a couple of `rustfmt` features being available only in nightly (see the `.rustfmt.toml` file) nightly `rustfmt` is necessary. All of these details are captured in a `.justfile` and can be checked by running [`just`'](https://just.systems/). To run all checks locally before sending them to CI you can set your git hooks directory: ```sh git config core.hooksPath scripts/hooks/ ``` ## Developer Certificate of Origin The sign-off is a simple line at the end of the git commit message, which certifies that you wrote it or otherwise have the right to pass it on as a open-source patch. The rules are pretty simple: if you can [certify the below][DCO]: ``` Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ``` then you just add a line saying Signed-off-by: Random J Developer using your name. If you set your `user.name` and `user.email`, you can sign your commit automatically with [`git commit --signoff`][GSO]. To sign-off your last commit: git commit --amend --signoff [DCO]: https://developercertificate.org [GSO]: https://git-scm.com/docs/git-commit#git-commit---signoff If you want to fix multiple commits use: git rebase --signoff main To check if your commits are correctly signed-off locally use `just check-commits`. service-binding-3.0.0/Cargo.toml0000644000000026650000000000100121130ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "service-binding" version = "3.0.0" authors = ["Wiktor Kwapisiewicz "] build = false exclude = [".github"] autobins = false autoexamples = false autotests = false autobenches = false description = "Automates parsing and binding to TCP, Unix sockets and Windows Named Pipes" readme = "README.md" keywords = [ "sockets", "systemd", "listenfd", "unix", "binding", ] categories = ["parsing"] license = "MIT OR Apache-2.0" repository = "https://github.com/wiktor-k/service-binding" [lib] name = "service_binding" path = "src/lib.rs" [dev-dependencies.actix-web] version = "4" features = ["macros"] default-features = false [dev-dependencies.clap] version = "4" features = [ "derive", "env", ] [dev-dependencies.serial_test] version = "3.1.1" [dev-dependencies.tempfile] version = "3.10.1" [dev-dependencies.testresult] version = "0.4.0" [target."cfg(target_os = \"macos\")".dependencies.raunch] version = "1" service-binding-3.0.0/Cargo.toml.orig000064400000000000000000000012721046102023000155650ustar 00000000000000[package] name = "service-binding" version = "3.0.0" edition = "2021" authors = ["Wiktor Kwapisiewicz "] description = "Automates parsing and binding to TCP, Unix sockets and Windows Named Pipes" repository = "https://github.com/wiktor-k/service-binding" license = "MIT OR Apache-2.0" keywords = ["sockets", "systemd", "listenfd", "unix", "binding"] categories = ["parsing"] exclude = [".github"] [target.'cfg(target_os = "macos")'.dependencies] raunch = "1" [dev-dependencies] actix-web = { version = "4", default-features = false, features = ["macros"] } clap = { version = "4", features = ["derive", "env"] } serial_test = "3.1.1" tempfile = "3.10.1" testresult = "0.4.0" service-binding-3.0.0/LICENSE-APACHE000064400000000000000000000251371046102023000146300ustar 00000000000000 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. service-binding-3.0.0/LICENSE-MIT000064400000000000000000000017771046102023000143440ustar 00000000000000Permission 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. service-binding-3.0.0/README.md000064400000000000000000000152031046102023000141540ustar 00000000000000# service-binding [![CI](https://github.com/wiktor-k/service-binding/actions/workflows/rust.yml/badge.svg)](https://github.com/wiktor-k/service-binding/actions/workflows/rust.yml) [![Crates.io](https://img.shields.io/crates/v/service-binding)](https://crates.io/crates/service-binding) Provides a way for servers and clients to describe their service bindings and client endpoints in a structured format. This crate automates parsing and binding to TCP sockets, Unix sockets and [Windows Named Pipes][WNP]. [WNP]: https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipes By design this crate is very lean and mostly relies on what is in `std` (with an exception of macOS launchd service binding). The URI scheme bindings have been heavily inspired by how [Docker Engine] specifies them. [Docker Engine]: https://docs.docker.com/desktop/faqs/general/#how-do-i-connect-to-the-remote-docker-engine-api ```rust use service_binding::{Binding, Listener}; let host = "tcp://127.0.0.1:8080"; // or "unix:///tmp/socket" // parse the URI into a Binding let binding: Binding = host.parse().unwrap(); // convert the binding into a Listener match binding.try_into().unwrap() { #[cfg(unix)] Listener::Unix(listener) => { // bind to a unix domain socket }, Listener::Tcp(listener) => { // bind to a TCP socket } Listener::NamedPipe(pipe) => { // bind to a Windows Named Pipe } } ``` ## Supported schemes | URI format | Example | Description | Binding | Listener / Stream | | ------------ | ---------------- | -------------------------- | ---- | --- | | `tcp://ip:port` | `tcp://127.0.0.1:8080` | TCP IP address | `Sockets` | `Tcp` | | `tcp://address:port` |`tcp://localhost:8080` | Hostname with address resolution [^1] | `Sockets` | `Tcp` | | `unix://path` | `unix:///run/user/1000/test.sock` | Unix domain sockets [^2] | `FilePath` | `Unix` | | `fd://` | `fd://` | systemd first socket activation [^3][^4] | `FileDescriptor` | `Unix` | | `fd://` | `fd://3` | exact number file descriptor | `FileDescriptor` | `Unix` | | `fd://` | `fd://http` | socket activation by name [^4] | `FileDescriptor` | `Unix` | | `npipe://` | `npipe://agent` | Windows Named Pipe [^5] | `NamedPipe` | `NamedPipe` | [^1]: binds to the first address that succeeds (see [`TcpListener::bind`](https://doc.rust-lang.org/std/net/struct.TcpListener.html#method.bind)) [^2]: not available on Windows through the `std` right now (see [#271] and [#56533]) [^3]: equivalent of `fd://3` but fails if more sockets have been passed [^4]: *listener only* [^5]: translates to `\\.\pipe\test` [#271]: https://github.com/rust-lang/libs-team/issues/271 [#56533]: https://github.com/rust-lang/rust/issues/56533 ## Example The following example uses `clap` and `actix-web` and makes it possible to run the server using any combination of Unix domain sockets (including systemd socket activation) and regular TCP socket bound to a TCP port: ```rust,no_run use actix_web::{web, App, HttpServer, Responder}; use clap::Parser; use service_binding::{Binding, Listener}; #[derive(Parser, Debug)] struct Args { #[clap( env = "HOST", short = 'H', long, default_value = "tcp://127.0.0.1:8080" )] host: Binding, } async fn greet() -> impl Responder { "Hello!" } #[actix_web::main] async fn main() -> std::io::Result<()> { let server = HttpServer::new(move || { App::new().route("/", web::get().to(greet)) }); match Args::parse().host.try_into()? { #[cfg(unix)] Listener::Unix(listener) => server.listen_uds(listener), Listener::Tcp(listener) => server.listen(listener), _ => Err(std::io::Error::other("Unsupported listener type")), }?.run().await } ``` ## systemd Socket Activation This crate also supports systemd's [Socket Activation][]. If the argument to be parsed is `fd://` the `Listener` object returned will be a `Unix` variant containing the listener provided by systemd. [Socket Activation]: https://0pointer.de/blog/projects/socket-activation.html For example the following file defines a socket unit: `~/.config/systemd/user/app.socket`: ```ini [Socket] ListenStream=%t/app.sock FileDescriptorName=service-name [Install] WantedBy=sockets.target ``` When enabled it will create a new socket file in `$XDG_RUNTIME_DIR` directory. When this socket is connected to systemd will start the service; `fd://` reads the correct systemd environment variable and returns the Unix domain socket. The service unit file `~/.config/systemd/user/app.service`: ```ini [Service] ExecStart=/usr/bin/app -H fd:// ``` Since the socket is named (`FileDescriptorName=service-name`) it can also be selected using its explicit name: `fd://service-name`. ## launchd Socket Activation On macOS [launchd socket activation][LSA] is also available although the socket needs to be explicitly named through the `fd://socket-name` syntax. [LSA]: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html The corresponding `plist` file (which can be placed in `~/Library/LaunchAgents` and loaded via `launchctl load ~/Library/LaunchAgents/service.plist`): ```xml EnvironmentVariables RUST_LOG debug KeepAlive Label com.example.service OnDemand ProgramArguments /path/to/service -H fd://socket-name RunAtLoad Sockets socket-name SockPathName /path/to/socket SockFamily Unix StandardErrorPath /Users/test/Library/Logs/service/stderr.log StandardOutPath /Users/test/Library/Logs/service/stdout.log ``` ## License This project is licensed under either of: - [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0), - [MIT license](https://opensource.org/licenses/MIT). at your option. ### Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. service-binding-3.0.0/SECURITY.md000064400000000000000000000023121046102023000144630ustar 00000000000000# Security policy If you have discovered a security vulnerability in this project, please report it privately. Do not disclose it as a public issue. This gives us time to work with you to fix the issue before public exposure, reducing the chance that the exploit will be used before a patch is released. This project is maintained by a team of volunteers on a reasonable-effort basis. As such, please give us at least 90 days to work on a fix before public exposure. We will contact you back within 2 business days after reporting the issue. Thanks for helping make the project safe for everyone! ## Reporting a vulnerability Please, report the vulnerability either through [new security advisory form][ADV] or by directly contacting our security contacts. [ADV]: https://github.com/wiktor-k/service-binding/security/advisories/new Security contacts: - [Wiktor Kwapisiewicz][WK], preferably encrypted with the following OpenPGP certificate: [`6539 09A2 F0E3 7C10 6F5F AF54 6C88 57E0 D8E8 F074`][KEY]. [WK]: https://github.com/wiktor-k [KEY]: https://keys.openpgp.org/vks/v1/by-fingerprint/653909A2F0E37C106F5FAF546C8857E0D8E8F074 ## Supported Versions Security updates are applied only to the most recent release. service-binding-3.0.0/deny.toml000064400000000000000000000002421046102023000145260ustar 00000000000000[advisories] version = 2 yanked = "deny" ignore = [ ] [bans] deny = [ ] multiple-versions = "allow" [licenses] version = 2 allow = [ "Apache-2.0", "MIT", ] service-binding-3.0.0/scripts/hooks/pre-commit000075500000000000000000000042441046102023000175140ustar 00000000000000#!/usr/bin/env -S just --working-directory . --justfile # Since this is a first recipe it's being run by default. # Faster checks need to be executed first for better UX. For example # codespell is very fast. cargo fmt does not need to download crates etc. check: spelling formatting docs lints dependencies tests # Checks common spelling mistakes spelling: codespell # Checks source code formatting formatting: just --unstable --fmt --check # We're using nightly to properly group imports, see .rustfmt.toml cargo +nightly fmt --all -- --check # Lints the source code lints: cargo clippy --workspace --no-deps --all-targets -- -D warnings # Checks for issues with dependencies dependencies: cargo deny check # Runs all unit tests. By default ignored tests are not run. Run with `ignored=true` to run only ignored tests tests: cargo test --all # Build docs for this crate only docs: cargo doc --no-deps # Checks for commit messages check-commits REFS='main..': #!/usr/bin/env bash set -euo pipefail for commit in $(git rev-list "{{ REFS }}"); do MSG="$(git show -s --format=%B "$commit")" CODESPELL_RC="$(mktemp)" git show "$commit:.codespellrc" > "$CODESPELL_RC" if ! grep -q "Signed-off-by: " <<< "$MSG"; then printf "Commit %s lacks \"Signed-off-by\" line.\n" "$commit" printf "%s\n" \ " Please use:" \ " git rebase --signoff main && git push --force-with-lease" \ " See https://developercertificate.org/ for more details." exit 1; elif ! codespell --config "$CODESPELL_RC" - <<< "$MSG"; then printf "The spelling in commit %s needs improvement.\n" "$commit" exit 1; else printf "Commit %s is good.\n" "$commit" fi done # Fixes common issues. Files need to be git add'ed fix: #!/usr/bin/env bash if ! git diff-files --quiet ; then echo "Working tree has changes. Please stage them: git add ." exit 1 fi codespell --write-changes just --unstable --fmt cargo clippy --fix --allow-staged # fmt must be last as clippy's changes may break formatting cargo +nightly fmt --all service-binding-3.0.0/scripts/hooks/pre-push000075500000000000000000000000711046102023000171750ustar 00000000000000#!/usr/bin/env sh set -euo pipefail just check-commits service-binding-3.0.0/src/lib.rs000064400000000000000000000023101046102023000145730ustar 00000000000000#![doc = include_str!("../README.md")] #![deny(missing_debug_implementations)] #![deny(missing_docs)] mod service; use std::io; use std::num::ParseIntError; pub use service::Binding; pub use service::Listener; pub use service::Stream; /// Errors while processing service listeners. #[derive(Debug)] #[non_exhaustive] pub enum Error { /// Address cannot be parsed and did not resolve to a known domain BadAddress(io::Error), /// Descriptor value cannot be parsed to a number. BadDescriptor(ParseIntError), /// Descriptor value exceeds acceptable range. DescriptorOutOfRange(i32), /// Descriptor environment variable (`LISTEN_FDS`) is missing. DescriptorsMissing, /// Specified URI scheme is not supported. UnsupportedScheme, } impl From for Error { fn from(error: ParseIntError) -> Self { Error::BadDescriptor(error) } } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) } } impl std::error::Error for Error {} #[cfg(test)] mod tests { use super::*; #[test] fn test_display() { format!("{}", Error::UnsupportedScheme); } } service-binding-3.0.0/src/service.rs000064400000000000000000000374031046102023000155000ustar 00000000000000use std::env::var; use std::net::SocketAddr; use std::net::TcpListener; use std::net::TcpStream; use std::net::ToSocketAddrs; #[cfg(unix)] use std::os::unix::net::UnixListener; #[cfg(unix)] use std::os::unix::net::UnixStream; use std::path::PathBuf; use super::Error; const SD_LISTEN_FDS_START: i32 = 3; /// Service binding. /// /// Indicates which mechanism should the service take to bind its /// listener to. /// /// # Examples /// /// Note that since the `tcp` protocol can use an address the `Sockets` /// binding will contain all IP addresses that the address resolves to. /// /// ``` /// # use service_binding::Binding; /// # fn main() -> testresult::TestResult { /// let binding = "tcp://127.0.0.1:8080".try_into()?; /// assert_eq!( /// Binding::Sockets(vec![([127, 0, 0, 1], 8080).into()]), /// binding /// ); /// # Ok(()) } /// ``` #[derive(Debug, PartialEq, Eq, Clone)] pub enum Binding { /// The service should be bound to this explicit, opened file /// descriptor. This mechanism is used by the socket activation. FileDescriptor(i32), /// The service should be bound to a Unix domain socket file under /// specified path. FilePath(PathBuf), /// The service should be bound to the first TCP socket that succeed /// the binding. Sockets(Vec), /// Windows Named Pipe. NamedPipe(std::ffi::OsString), } impl From for Binding { fn from(value: PathBuf) -> Self { Binding::FilePath(value) } } impl From for Binding { fn from(value: SocketAddr) -> Self { Binding::Sockets(vec![value]) } } /// Opened service listener. /// /// This structure contains an already open listener. Note that the /// listeners are set to non-blocking mode. /// /// # Examples /// /// ``` /// # use service_binding::{Binding, Listener}; /// # fn main() -> testresult::TestResult { /// let binding: Binding = "tcp://127.0.0.1:8080".parse()?; /// let listener = binding.try_into()?; /// assert!(matches!(listener, Listener::Tcp(_))); /// # Ok(()) } /// ``` #[derive(Debug)] pub enum Listener { /// Listener for a Unix domain socket. #[cfg(unix)] Unix(UnixListener), /// Listener for a TCP socket. Tcp(TcpListener), /// Named Pipe. NamedPipe(std::ffi::OsString), } #[cfg(unix)] impl From for Listener { fn from(listener: UnixListener) -> Self { while let Err(e) = listener.set_nonblocking(true) { // retry WouldBlock errors if e.kind() != std::io::ErrorKind::WouldBlock { break; } } Listener::Unix(listener) } } impl From for Listener { fn from(listener: TcpListener) -> Self { while let Err(e) = listener.set_nonblocking(true) { // retry WouldBlock errors if e.kind() != std::io::ErrorKind::WouldBlock { break; } } Listener::Tcp(listener) } } /// Client service connection. /// /// This structure contains an already open stream. Note that the /// streams are set to non-blocking mode. /// /// # Examples /// /// ```no_run /// # use service_binding::{Binding, Stream}; /// # fn main() -> testresult::TestResult { /// let binding: Binding = "tcp://127.0.0.1:8080".parse()?; /// let stream = binding.try_into()?; /// assert!(matches!(stream, Stream::Tcp(_))); /// # Ok(()) } /// ``` #[derive(Debug)] pub enum Stream { /// Stream for a Unix domain socket. #[cfg(unix)] Unix(UnixStream), /// Stream for a TCP socket. Tcp(TcpStream), /// Named Pipe. NamedPipe(std::ffi::OsString), } #[cfg(unix)] impl From for Stream { fn from(stream: UnixStream) -> Self { while let Err(e) = stream.set_nonblocking(true) { // retry WouldBlock errors if e.kind() != std::io::ErrorKind::WouldBlock { break; } } Stream::Unix(stream) } } impl From for Stream { fn from(stream: TcpStream) -> Self { while let Err(e) = stream.set_nonblocking(true) { // retry WouldBlock errors if e.kind() != std::io::ErrorKind::WouldBlock { break; } } Stream::Tcp(stream) } } impl<'a> std::convert::TryFrom<&'a str> for Binding { type Error = Error; fn try_from(s: &'a str) -> Result { if let Some(name) = s.strip_prefix("fd://") { if name.is_empty() { if let Ok(fds) = var("LISTEN_FDS") { let fds: i32 = fds.parse()?; // we support only one socket for now if fds != 1 { return Err(Error::DescriptorOutOfRange(fds)); } return Ok(Binding::FileDescriptor(SD_LISTEN_FDS_START)); } else { return Err(Error::DescriptorsMissing); } } if let Ok(fd) = name.parse() { return Ok(Binding::FileDescriptor(fd)); } #[cfg(target_os = "macos")] { let fds = raunch::activate_socket(name).map_err(|_| Error::DescriptorsMissing)?; if fds.len() == 1 { Ok(Binding::FileDescriptor(fds[0])) } else { Err(Error::DescriptorOutOfRange(fds.len() as i32)) } } #[cfg(not(target_os = "macos"))] { if let (Ok(names), Ok(fds)) = (var("LISTEN_FDNAMES"), var("LISTEN_FDS")) { let fds: usize = fds.parse()?; for (fd_index, fd_name) in names.split(':').enumerate() { if fd_name == name && fd_index < fds { return Ok(Binding::FileDescriptor( SD_LISTEN_FDS_START + fd_index as i32, )); } } } Err(Error::DescriptorsMissing) } } else if let Some(file) = s.strip_prefix("unix://") { Ok(Binding::FilePath(file.into())) } else if let Some(file) = s.strip_prefix("npipe://") { if let Some('.' | '/' | '\\') = file.chars().next() { Ok(Binding::NamedPipe(file.replace('/', "\\").into())) } else { Ok(Binding::NamedPipe(format!(r"\\.\pipe\{file}").into())) } } else if let Some(addr) = s.strip_prefix("tcp://") { match addr.to_socket_addrs() { Ok(addrs) => Ok(Binding::Sockets(addrs.collect())), Err(err) => return Err(Error::BadAddress(err)), } } else if s.starts_with(r"\\") { Ok(Binding::NamedPipe(s.into())) } else { Err(Error::UnsupportedScheme) } } } impl std::str::FromStr for Binding { type Err = Error; fn from_str(s: &str) -> Result { s.try_into() } } impl TryFrom for Listener { type Error = std::io::Error; fn try_from(value: Binding) -> Result { match value { #[cfg(unix)] Binding::FileDescriptor(descriptor) => { use std::os::unix::io::FromRawFd; Ok(unsafe { UnixListener::from_raw_fd(descriptor) }.into()) } #[cfg(unix)] Binding::FilePath(path) => { // ignore errors if the file does not exist let _ = std::fs::remove_file(&path); Ok(UnixListener::bind(path)?.into()) } Binding::Sockets(sockets) => Ok(std::net::TcpListener::bind(&*sockets)?.into()), Binding::NamedPipe(pipe) => Ok(Listener::NamedPipe(pipe)), #[cfg(not(unix))] _ => Err(std::io::Error::new( std::io::ErrorKind::Other, Error::UnsupportedScheme, )), } } } impl TryFrom for Stream { type Error = std::io::Error; fn try_from(value: Binding) -> Result { match value { #[cfg(unix)] Binding::FileDescriptor(descriptor) => { use std::os::unix::io::FromRawFd; Ok(unsafe { UnixStream::from_raw_fd(descriptor) }.into()) } #[cfg(unix)] Binding::FilePath(path) => Ok(UnixStream::connect(path)?.into()), Binding::Sockets(sockets) => Ok(std::net::TcpStream::connect(&*sockets)?.into()), Binding::NamedPipe(pipe) => Ok(Self::NamedPipe(pipe)), #[cfg(not(unix))] _ => Err(std::io::Error::new( std::io::ErrorKind::Other, Error::UnsupportedScheme, )), } } } #[cfg(test)] mod tests { #[cfg(unix)] use std::os::fd::IntoRawFd; use std::str::FromStr; use serial_test::serial; use super::*; type TestResult = Result<(), Box>; #[test] #[serial] fn parse_fd() -> TestResult { std::env::set_var("LISTEN_FDS", "1"); let binding = "fd://".parse()?; assert_eq!(Binding::FileDescriptor(3), binding); Ok(()) } #[test] #[cfg(unix)] #[serial] fn fd_to_listener() -> TestResult { let file = tempfile::tempfile()?; let binding = Binding::FileDescriptor(file.into_raw_fd()); let result: Result = binding.try_into(); // UnixListener is supported only on Unix platforms assert_eq!(cfg!(unix), result.is_ok()); Ok(()) } #[test] // on non-macOS systems this reads environment variables #[cfg(not(target_os = "macos"))] #[serial] fn parse_fd_named() -> TestResult { std::env::set_var("LISTEN_FDS", "2"); std::env::set_var("LISTEN_FDNAMES", "other:service-name"); let binding = "fd://service-name".parse()?; assert_eq!(Binding::FileDescriptor(4), binding); std::env::remove_var("LISTEN_FDNAMES"); Ok(()) } #[test] // on macOS the test will attempt launchd system activation but since // the plist file is not present it will fail #[cfg(target_os = "macos")] #[serial] fn parse_fd_named() -> TestResult { assert!(matches!( Binding::from_str("fd://service-name"), Err(Error::DescriptorsMissing) )); Ok(()) } #[test] #[serial] fn parse_fd_bad() -> TestResult { std::env::set_var("LISTEN_FDS", "1"); // should be "2" std::env::set_var("LISTEN_FDNAMES", "other:service-name"); assert!(matches!( Binding::from_str("fd://service-name"), Err(Error::DescriptorsMissing) )); std::env::remove_var("LISTEN_FDNAMES"); Ok(()) } #[test] #[cfg(unix)] #[serial] fn parse_fd_explicit() -> TestResult { let file = tempfile::tempfile()?; let raw_fd = file.into_raw_fd(); let binding = format!("fd://{raw_fd}").parse()?; assert_eq!(Binding::FileDescriptor(raw_fd), binding); let result: Result = binding.try_into(); // UnixListener is supported only on Unix platforms assert_eq!(cfg!(unix), result.is_ok()); Ok(()) } #[test] #[serial] fn parse_fd_fail_unsupported_fds_count() -> TestResult { std::env::set_var("LISTEN_FDS", "3"); assert!(matches!( Binding::from_str("fd://"), Err(Error::DescriptorOutOfRange(3)) )); Ok(()) } #[test] #[serial] fn parse_fd_fail_not_a_number() -> TestResult { std::env::set_var("LISTEN_FDS", "3a"); assert!(matches!( Binding::from_str("fd://"), Err(Error::BadDescriptor(_)) )); Ok(()) } #[test] #[serial] fn parse_fd_fail() -> TestResult { std::env::remove_var("LISTEN_FDS"); assert!(matches!( Binding::from_str("fd://"), Err(Error::DescriptorsMissing) )); Ok(()) } #[test] fn parse_unix() -> TestResult { let binding = "unix:///tmp/test".try_into()?; assert_eq!(Binding::FilePath("/tmp/test".into()), binding); let result: Result = binding.try_into(); // UnixListener is supported only on Unix platforms if cfg!(unix) { assert!(result.is_ok()); } else { assert!(result.is_err()); } Ok(()) } #[test] fn parse_tcp() -> TestResult { let binding = "tcp://127.0.0.1:8081".try_into()?; assert_eq!( Binding::from(SocketAddr::from(([127, 0, 0, 1], 8081))), binding ); let _: Listener = binding.try_into()?; Ok(()) } #[test] fn parse_tcp_localhost() -> TestResult { let mut binding = "tcp://localhost:8081".try_into()?; let Binding::Sockets(addrs) = &mut binding else { panic!("Address should be parsed to Sockets"); }; let mut expected = vec![ SocketAddr::from(([127, 0, 0, 1], 8081)), SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], 8081)), ]; // Sort both vectors for testing equality as the ordering may be different addrs.sort(); expected.sort(); assert_eq!(addrs, &expected); let _: Listener = binding.try_into()?; Ok(()) } #[test] fn parse_tcp_fail() -> TestResult { assert!(matches!( Binding::try_from("tcp://::8080"), Err(Error::BadAddress(_)) )); assert!(matches!( Binding::try_from("tcp://an-unknown-hostname:8080"), Err(Error::BadAddress(_)) )); Ok(()) } #[test] fn parse_pipe() -> TestResult { let binding = r"\\.\pipe\test".try_into()?; assert_eq!(Binding::NamedPipe(r"\\.\pipe\test".into()), binding); let _: Listener = binding.try_into()?; Ok(()) } #[test] fn parse_pipe_short() -> TestResult { let binding = r"npipe://test".try_into()?; assert_eq!(Binding::NamedPipe(r"\\.\pipe\test".into()), binding); let _: Listener = binding.try_into()?; Ok(()) } #[test] fn parse_pipe_long() -> TestResult { let binding = r"npipe:////./pipe/test".try_into()?; assert_eq!(Binding::NamedPipe(r"\\.\pipe\test".into()), binding); let _: Listener = binding.try_into()?; Ok(()) } #[test] fn parse_pipe_fail() -> TestResult { assert!(matches!( Binding::try_from(r"\test"), Err(Error::UnsupportedScheme) )); Ok(()) } #[test] fn parse_unknown_fail() -> TestResult { assert!(matches!( Binding::try_from("unknown://test"), Err(Error::UnsupportedScheme) )); Ok(()) } #[test] #[cfg(unix)] #[serial] fn listen_on_socket_cleans_the_socket_file() -> TestResult { let dir = std::env::temp_dir().join("temp-socket"); let binding = Binding::FilePath(dir); let listener: Listener = binding.try_into().unwrap(); drop(listener); // create a second listener from the same path let dir = std::env::temp_dir().join("temp-socket"); let binding = Binding::FilePath(dir); let listener: Listener = binding.try_into().unwrap(); drop(listener); Ok(()) } #[test] #[cfg(unix)] fn convert_from_pathbuf() { let path = std::path::PathBuf::from("/tmp"); let binding: Binding = path.into(); assert!(matches!(binding, Binding::FilePath(_))); } #[test] fn convert_from_socket() { let socket: SocketAddr = ([127, 0, 0, 1], 8080).into(); let binding: Binding = socket.into(); assert!(matches!(binding, Binding::Sockets(_))); } }