pax_global_header 0000666 0000000 0000000 00000000064 14564660433 0014525 g ustar 00root root 0000000 0000000 52 comment=10197acd5f1d544294dee78a6e8135fa71aeeac5 unixtransport-0.0.4/ 0000775 0000000 0000000 00000000000 14564660433 0014466 5 ustar 00root root 0000000 0000000 unixtransport-0.0.4/.github/ 0000775 0000000 0000000 00000000000 14564660433 0016026 5 ustar 00root root 0000000 0000000 unixtransport-0.0.4/.github/workflows/ 0000775 0000000 0000000 00000000000 14564660433 0020063 5 ustar 00root root 0000000 0000000 unixtransport-0.0.4/.github/workflows/test.yaml 0000664 0000000 0000000 00000001723 14564660433 0021731 0 ustar 00root root 0000000 0000000 name: Test on: pull_request: types: [opened, synchronize] push: branches: [main] jobs: test: strategy: matrix: platform: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.platform }} steps: - name: Install Go uses: actions/setup-go@v3 with: go-version: 1.x - name: Check out repo uses: actions/checkout@v3 with: ref: ${{ github.event.pull_request.head.sha }} # default is a pseudo 'merge' commit - name: Install tools run: | go install honnef.co/go/tools/cmd/staticcheck@latest go install mvdan.cc/gofumpt@latest - name: gofmt run: diff <(gofmt -d . 2>/dev/null) <(printf '') - name: go vet run: go vet ./... - name: staticcheck run: staticcheck ./... - name: gofumpt run: diff <(gofumpt -d -e -l . 2>/dev/null) <(printf '') - name: go test run: go test -v ./... unixtransport-0.0.4/.gitignore 0000664 0000000 0000000 00000000415 14564660433 0016456 0 ustar 00root root 0000000 0000000 # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ unixtransport-0.0.4/LICENSE 0000664 0000000 0000000 00000026135 14564660433 0015502 0 ustar 00root root 0000000 0000000 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. unixtransport-0.0.4/README.md 0000664 0000000 0000000 00000001557 14564660433 0015755 0 ustar 00root root 0000000 0000000 # unixtransport [](https://pkg.go.dev/github.com/peterbourgon/unixtransport)   This package adds support for Unix domain sockets in Go HTTP clients. ```go t := &http.Transport{...} unixtransport.Register(t) client := &http.Client{Transport: t} ``` Now you can make requests with URLs like this: ```go resp, err := client.Get("https+unix:///path/to/socket:/request/path?a=b") ``` Use scheme `http+unix` or `https+unix`. Inspiration taken from, and thanks given to, both [tv42/httpunix](https://github.com/tv42/httpunix) and [agorman/httpunix](https://github.com/agorman/httpunix). unixtransport-0.0.4/doc.go 0000664 0000000 0000000 00000000134 14564660433 0015560 0 ustar 00root root 0000000 0000000 // package unixtransport allows HTTP client requests to Unix sockets. package unixtransport unixtransport-0.0.4/example_test.go 0000664 0000000 0000000 00000001373 14564660433 0017513 0 ustar 00root root 0000000 0000000 package unixtransport_test import ( "net/http" "github.com/peterbourgon/unixtransport" ) func ExampleRegister_default() { // Register the "http+unix" and "https+unix" protocols in the default client transport. unixtransport.Register(http.DefaultTransport.(*http.Transport)) // This will issue a GET request to an HTTP server listening at /tmp/my.sock. // Note the three '/' characters between 'http+unix:' and 'tmp'. http.Get("http+unix:///tmp/my.sock") // This shows how to include a request path and query. http.Get("http+unix:///tmp/my.sock:/users/123?q=abc") } func ExampleRegister_custom() { t := &http.Transport{ // ... } unixtransport.Register(t) c := &http.Client{ Transport: t, // ... } c.Get("https+unix:///tmp/my.sock") } unixtransport-0.0.4/go.mod 0000664 0000000 0000000 00000000244 14564660433 0015574 0 ustar 00root root 0000000 0000000 module github.com/peterbourgon/unixtransport go 1.16 require ( github.com/miekg/dns v1.1.54 github.com/oklog/run v1.1.0 github.com/peterbourgon/ff/v3 v3.3.1 ) unixtransport-0.0.4/go.sum 0000664 0000000 0000000 00000007730 14564660433 0015630 0 ustar 00root root 0000000 0000000 github.com/miekg/dns v1.1.54 h1:5jon9mWcb0sFJGpnI99tOMhCPyJ+RPVz5b63MQG0VWI= github.com/miekg/dns v1.1.54/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/peterbourgon/ff/v3 v3.3.1 h1:XSWvXxeNdgeppLNGGJEAOiXRdX2YMF/LuZfdnqQ1SNc= github.com/peterbourgon/ff/v3 v3.3.1/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= unixtransport-0.0.4/register.go 0000664 0000000 0000000 00000007241 14564660433 0016645 0 ustar 00root root 0000000 0000000 package unixtransport import ( "context" "encoding/base64" "errors" "fmt" "net" "net/http" "strings" ) // Register adds a protocol handler to the provided transport that can serve // requests to Unix domain sockets via the "http+unix" or "https+unix" schemes. // Request URLs should have the following form: // // https+unix:///path/to/socket:/request/path?query=val&... // // The registered transport is based on a clone of the provided transport, and // so uses the same configuration: timeouts, TLS settings, and so on. Connection // pooling should also work as normal. One caveat: only the DialContext and // DialTLSContext dialers are respected; the Dial and DialTLS dialers are // explicitly removed and ignored. Any configured Proxy is also discarded. func Register(t *http.Transport) { copy := t.Clone() copy.Dial = nil //lint:ignore SA1019 yes, it's deprecated, that's the point copy.DialTLS = nil //lint:ignore SA1019 yes, it's deprecated, that's the point copy.Proxy = nil // Proxy doesn't support Unix sockets, so drop it switch { case copy.DialContext == nil && copy.DialTLSContext == nil: copy.DialContext = dialContextAdapter(defaultDialContextFunc) case copy.DialContext == nil && copy.DialTLSContext != nil: copy.DialContext = dialContextAdapter(defaultDialContextFunc) copy.DialTLSContext = dialContextAdapter(copy.DialTLSContext) case copy.DialContext != nil && copy.DialTLSContext == nil: copy.DialContext = dialContextAdapter(copy.DialContext) case copy.DialContext != nil && copy.DialTLSContext != nil: copy.DialContext = dialContextAdapter(copy.DialContext) copy.DialTLSContext = dialContextAdapter(copy.DialTLSContext) } tt := roundTripAdapter(copy) t.RegisterProtocol("http+unix", tt) t.RegisterProtocol("https+unix", tt) } // dialContextAdapter decorates the provided DialContext function by trying to base64 decode // the provided address. If successful, the network is changed to "unix" and the address // is changed to the decoded value. func dialContextAdapter(next dialContextFunc) dialContextFunc { return func(ctx context.Context, network, address string) (net.Conn, error) { host, _, err := net.SplitHostPort(address) if err != nil { host = address } filepath, err := base64.RawURLEncoding.DecodeString(host) if err == nil { network, address = "unix", string(filepath) } return next(ctx, network, address) } } // roundTripAdapter returns an http.RoundTripper which, when used in combination // with the dialContextAdapter, supports Unix sockets via any scheme with a // "+unix" suffix. func roundTripAdapter(next http.RoundTripper) http.RoundTripper { return roundTripFunc(func(req *http.Request) (*http.Response, error) { if req.URL == nil { return nil, fmt.Errorf("unix transport: no request URL") } scheme := strings.TrimSuffix(req.URL.Scheme, "+unix") if scheme == req.URL.Scheme { return nil, fmt.Errorf("unix transport: missing '+unix' suffix in scheme %s", req.URL.Scheme) } parts := strings.SplitN(req.URL.Path, ":", 2) if len(parts) != 2 { return nil, errors.New("unix transport: invalid path") } var ( socketPath = parts[0] requestPath = parts[1] encodedHost = base64.RawURLEncoding.EncodeToString([]byte(socketPath)) ) req = req.Clone(req.Context()) req.URL.Scheme = scheme req.URL.Host = encodedHost req.URL.Path = requestPath return next.RoundTrip(req) }) } type dialContextFunc func(ctx context.Context, network, address string) (net.Conn, error) var defaultDialContextFunc = (&net.Dialer{}).DialContext type roundTripFunc func(req *http.Request) (*http.Response, error) func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } unixtransport-0.0.4/register_test.go 0000664 0000000 0000000 00000006032 14564660433 0017701 0 ustar 00root root 0000000 0000000 package unixtransport_test import ( "crypto/tls" "crypto/x509" "fmt" "io" "net" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "github.com/peterbourgon/unixtransport" ) func TestBasics(t *testing.T) { t.Parallel() // This first server will do HTTP. var ( tempdir = t.TempDir() socket1 = filepath.Join(tempdir, "1") ) { ln, err := net.Listen("unix", socket1) if err != nil { t.Fatal(err) } defer ln.Close() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, 1, r.URL.Path) }) server := httptest.NewUnstartedServer(handler) server.Listener = ln server.Start() defer server.Close() } // This second server will speak HTTPS. The httptest.Server can do TLS, but // it uses a hard-coded cert with "example.com" as a server name. We'll get // that cert in the config's pool after we start the server. var ( socket2 = filepath.Join(tempdir, "2") tlsClientConfig = &tls.Config{ServerName: "example.com"} ) { ln, err := net.Listen("unix", socket2) if err != nil { t.Fatal(err) } defer ln.Close() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, 2, r.URL.Path) }) server := httptest.NewUnstartedServer(handler) server.Listener = ln server.StartTLS() defer server.Close() certpool := x509.NewCertPool() certpool.AddCert(server.Certificate()) tlsClientConfig.RootCAs = certpool } // We could just use a plain http.Client, but for the TLS config required by // the second server. Create the transport with the TLS config, and a client // that utilizes that transport. transport := &http.Transport{TLSClientConfig: tlsClientConfig} client := &http.Client{Transport: transport} // The magic. unixtransport.Register(transport) // http+unix should work. { var ( rawurl = "http+unix://" + socket1 + ":/foo?a=1" want = "1 /foo" have = get(t, client, rawurl) ) if want != have { t.Errorf("%s: want %q, have %q", rawurl, want, have) } } // https+unix should also work. { var ( rawurl = "https+unix://" + socket2 + ":/bar#fragment" want = "2 /bar" have = get(t, client, rawurl) ) if want != have { t.Errorf("%s: want %q, have %q", rawurl, want, have) } } // Do another http+unix request, to kind of verify the connection pool // didn't mix things up too badly. { var ( rawurl = "http+unix://" + socket1 + ":/baz:baz:baz" want = "1 /baz:baz:baz" have = get(t, client, rawurl) ) if want != have { t.Errorf("%s: want %q, have %q", rawurl, want, have) } } } func get(t *testing.T, client *http.Client, rawurl string) string { t.Helper() req, err := http.NewRequest("GET", rawurl, nil) if err != nil { t.Errorf("GET %s: %v", rawurl, err) return "" } resp, err := client.Do(req) if err != nil { t.Errorf("GET %s: %v", rawurl, err) return "" } defer resp.Body.Close() buf, err := io.ReadAll(resp.Body) if err != nil { t.Errorf("GET %s: %v", rawurl, err) return "" } return strings.TrimSpace(string(buf)) } unixtransport-0.0.4/unixproxy/ 0000775 0000000 0000000 00000000000 14564660433 0016553 5 ustar 00root root 0000000 0000000 unixtransport-0.0.4/unixproxy/README.md 0000664 0000000 0000000 00000000653 14564660433 0020036 0 ustar 00root root 0000000 0000000 # package unixproxy Note: this package is experimental and potentially insecure. Package unixproxy provides a reverse proxy to local Unix sockets. The intent is to facilitate local development of distributed systems, by creating semantically meaningful addresses for arbitrary localhost HTTP servers. See [package documentation][docs] for details. [docs]: https://pkg.go.dev/github.com/peterbourgon/unixtransport/unixproxy unixtransport-0.0.4/unixproxy/cmd/ 0000775 0000000 0000000 00000000000 14564660433 0017316 5 ustar 00root root 0000000 0000000 unixtransport-0.0.4/unixproxy/cmd/unixproxy/ 0000775 0000000 0000000 00000000000 14564660433 0021403 5 ustar 00root root 0000000 0000000 unixtransport-0.0.4/unixproxy/cmd/unixproxy/main.go 0000664 0000000 0000000 00000005627 14564660433 0022670 0 ustar 00root root 0000000 0000000 package main import ( "bytes" "context" "errors" "flag" "fmt" "io" "log" "net/http" "os" "syscall" "text/tabwriter" "github.com/oklog/run" "github.com/peterbourgon/ff/v3" "github.com/peterbourgon/unixtransport/unixproxy" ) func main() { err := exe( context.Background(), os.Stdin, os.Stdout, os.Stderr, os.Args[1:], ) switch { case err == nil: os.Exit(0) case errors.Is(err, flag.ErrHelp): os.Exit(1) case isSignalError(err): fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(0) case err != nil: fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } } func exe(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, args []string) error { fs := flag.NewFlagSet("unixproxy", flag.ContinueOnError) var ( addrFlag = fs.String("addr", ":80", "listen endpoint for HTTP reverse proxy server") hostFlag = fs.String("host", "unixproxy.localhost", "Host header where this service is reachable") rootFlag = fs.String("root", ".", "root path to look for Unix sockets") dnsFlag = fs.String("dns", "", "listen endpoint for localhost DNS resolver (optional)") ) fs.Usage = usageFor(fs) if err := ff.Parse(fs, args); err != nil { return fmt.Errorf("parse flags: %w", err) } logger := log.New(stderr, "", 0) proxyListener, err := unixproxy.ListenURI(ctx, *addrFlag) if err != nil { return fmt.Errorf("listen on proxy addr: %w", err) } proxyHandler := &unixproxy.Handler{ Host: *hostFlag, Root: *rootFlag, ErrorLogWriter: logger.Writer(), } logger.Printf("serving host http://%s", *hostFlag) logger.Printf("sockets root %s", *rootFlag) var g run.Group { logger.Printf("proxy listening on %s", proxyListener.Addr()) server := &http.Server{Handler: proxyHandler} g.Add(func() error { return server.Serve(proxyListener) }, func(error) { server.Close() }) } if *dnsFlag != "" { logger.Printf("DNS resolver listening on %s", *dnsFlag) server := unixproxy.NewDNSServer(*dnsFlag, logger) g.Add(func() error { return server.ListenAndServe() }, func(error) { server.ShutdownContext(ctx) }) } { g.Add(run.SignalHandler(ctx, syscall.SIGINT, syscall.SIGTERM)) } return g.Run() } func isSignalError(err error) bool { var sig run.SignalError return errors.As(err, &sig) } func usageFor(fs *flag.FlagSet) func() { return func() { buf := &bytes.Buffer{} fmt.Fprintf(buf, "USAGE\n") fmt.Fprintf(buf, " %s [flags]\n", fs.Name()) fmt.Fprintf(buf, "\n") fmt.Fprintf(buf, "FLAGS\n") tw := tabwriter.NewWriter(buf, 0, 4, 2, ' ', 0) fs.VisitAll(func(f *flag.Flag) { def := f.DefValue if def == "" { def = "..." } fmt.Fprintf(tw, " --%s=%s\t%s\n", f.Name, def, f.Usage) }) tw.Flush() fmt.Fprintf(buf, "\n") fmt.Fprintf(buf, "DOCUMENTATION\n") fmt.Fprintf(buf, " https://github.com/peterbourgon/unixtransport/tree/main/unixproxy\n") fmt.Fprintf(buf, "\n") fmt.Fprint(os.Stdout, buf.String()) } } unixtransport-0.0.4/unixproxy/dns.go 0000664 0000000 0000000 00000005204 14564660433 0017667 0 ustar 00root root 0000000 0000000 package unixproxy import ( "fmt" "io" "log" "github.com/miekg/dns" ) // NewDNSServer returns a DNS server which will listen on addr, and resolve all // incoming A, AAAA, and HTTPS requests to localhost. Specifically, it resolves // all A and HTTPS queries to the IPv4 address 127.0.0.1, and all AAAA queries // to the IPv6 address ::1. It ignores all other request types. // // A nil logger parameter is valid and will result in no log output. // // This is intended for use on macOS systems, where many applications (including // Safari and cURL) perform DNS lookups through a system resolver that ignores // /etc/hosts. As a workaround, users can run this (limited) DNS resolver on a // specific local port, and configure the system resolver to use it when // resolving hosts matching the relevant host string. // // Assuming the default host of unixproxy.localhost, and assuming this resolver // runs on 127.0.0.1:5354, create /etc/resolver/localhost with the following // content. // // nameserver 127.0.0.1 // port 5354 // // Then e.g. Safari will resolve any URL ending in .localhost by querying the // resolver running on 127.0.0.1:5354. See `man 5 resolver` for more information // on the /etc/resolver file format. func NewDNSServer(addr string, logger *log.Logger) *dns.Server { if logger == nil { logger = log.New(io.Discard, "", 0) } mux := dns.NewServeMux() mux.HandleFunc(".", func(w dns.ResponseWriter, request *dns.Msg) { for i, q := range request.Question { logger.Printf("-> DNS %d/%d: %s", i+1, len(request.Question), q.String()) } response := getResponse(request, logger) for i, a := range response.Answer { logger.Printf("<- DNS %d/%d: %s", i+1, len(response.Answer), a.String()) } w.WriteMsg(response) }) return &dns.Server{ Addr: addr, Net: "udp", Handler: mux, } } func getResponse(request *dns.Msg, logger *log.Logger) *dns.Msg { var response dns.Msg response.SetReply(request) response.Compress = false if request.Opcode != dns.OpcodeQuery { return &response } var answer []dns.RR for _, q := range response.Question { var ( typ = dns.TypeToString[q.Qtype] rr dns.RR err error ) switch q.Qtype { case dns.TypeA: rr, err = dns.NewRR(fmt.Sprintf("%s A 127.0.0.1", q.Name)) case dns.TypeAAAA: rr, err = dns.NewRR(fmt.Sprintf("%s AAAA ::1", q.Name)) case dns.TypeHTTPS: rr, err = dns.NewRR(fmt.Sprintf("%s HTTPS 1 127.0.0.1", q.Name)) default: err = fmt.Errorf("unsupported question type %s", typ) } if err != nil { logger.Printf("%s %s: %v", typ, q.Name, err) return &response } answer = append(answer, rr) } response.Answer = answer return &response } unixtransport-0.0.4/unixproxy/doc.go 0000664 0000000 0000000 00000002612 14564660433 0017650 0 ustar 00root root 0000000 0000000 // package unixproxy provides an EXPERIMENTAL reverse proxy to Unix sockets. // // The intent of this package is to facilitate local development of distributed // systems, by allowing normal HTTP clients that assume TCP (cURL, browsers, // etc.) to address localhost servers via semantically-meaningful subdomains // rather than opaque port numbers. // // For example, rather than addressing your application server as localhost:8081 // and your Prometheus instance as localhost:9090, you could use // // http://myapp.unixproxy.localhost // http://prometheus.unixproxy.localhost // // Or, you could have 3 clusters of 3 instances each, addressed as // // http://{nyc,lax,fra}.{1,2,3}.unixproxy.localhost // // The intermediating reverse-proxy, provided by [Handler], works dynamically, // without any explicit configuration. See documentation on that type for usage // information. // // Application servers need to be able to listen on Unix sockets. The [ParseURI] // and [ListenURI] helpers exist for this purpose. They accept both typical // listen addresses e.g. "localhost:8081" or ":8081" as well as e.g. // "unix:///tmp/my.sock" as input. Applications can use these helpers to create // listeners, and bind HTTP servers to those listeners, rather than using // default helpers that assume TCP, like [http.ListenAndServe]. // // cmd/unixproxy is an example program utilizing package unixproxy. package unixproxy unixtransport-0.0.4/unixproxy/handler.go 0000664 0000000 0000000 00000013315 14564660433 0020522 0 ustar 00root root 0000000 0000000 package unixproxy import ( "bytes" "context" "encoding/json" "fmt" "io" "io/fs" "log" "net" "net/http" "net/http/httputil" "os" "path/filepath" "strings" "sync" "text/template" ) // Handler is a reverse proxy to Unix sockets on the local filesystem. // // Requests are mapped to sockets based on their Host header. Each sub-domain // element underneath the configured Host domain is parsed as a filepath element // relative to Root directory. If the resulting filepath identifies a valid Unix // socket, the request is proxied to that socket. // // As an example, a Handler configured with Host "unixproxy.localhost" and Root // "/tmp/abc" would map a request with Host header "foo.bar.unixproxy.localhost" // to a socket at "/tmp/abc/foo/bar". // // Parameters are evaluated during ServeHTTP. type Handler struct { // Root is a valid directory on the local filesystem. The handler will look // in this directory tree, recursively, for destination Unix sockets, when // proxying an incoming request. See [Handler] for more detail. // // Required. Root string // Host is the base/apex domain which the Handler expects to receive as part // of all request Host headers. It should end in .localhost per RFC2606, and // the system should resolve that domain, and all subdomains, to a localhost // IP. Typically, this is done by adding an entry to /etc/hosts as follows. // // 127.0.0.1 localhost # note: separator must be a literal tab // // Modern macOS systems will ignore /etc/hosts in many contexts, see // [NewDNSServer] for a workaround. // // Optional. The default value is "unixproxy.localhost". Host string // ErrorLogWriter is used as the destination writer for the ErrorLog of the // [http.ReverseProxy] used to proxy individual requests. // // Optional. By default, each [http.ReverseProxy] has a nil ErrorLog. ErrorLogWriter io.Writer once sync.Once } const defaultHost = "unixproxy.localhost" func (h *Handler) validate() error { h.once.Do(func() { if h.Host == "" { h.Host = defaultHost } }) if h.Root == "" { return fmt.Errorf("invalid Root: not specified") } if fi, err := os.Stat(h.Root); err != nil { return fmt.Errorf("invalid Root: %w", err) } else if !fi.IsDir() { return fmt.Errorf("invalid Root: %s: not a directory", h.Root) } return nil } // ServeHTTP implements http.Handler. If the request Host header is equal to the // Host field (i.e. has no subdomains), ServeHTTP will serve a list of valid // subdomains. Otherwise, the request will be proxied to a local Unix domain // socket based on its subdomain. func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := h.validate(); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } switch { case r.URL.Path == "/favicon.ico": http.NotFound(w, r) case r.Host == h.Host: h.handleIndex(w, r) default: h.handleProxy(w, r) } } func (h *Handler) handleIndex(w http.ResponseWriter, r *http.Request) { domains, err := h.domains() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } accept := strings.ToLower(r.Header.Get("accept")) switch { case strings.Contains(accept, "text/html"): var buf bytes.Buffer if err := indexTemplate.Execute(&buf, struct{ Domains []string }{domains}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("content-type", "text/html; charset=utf-8") buf.WriteTo(w) case strings.Contains(accept, "application/json"): w.Header().Set("content-type", "application/json; charset=utf-8") enc := json.NewEncoder(w) enc.SetIndent("", " ") enc.Encode(domains) default: w.Header().Set("content-type", "text/plain; charset=utf-8") for _, s := range domains { fmt.Fprintln(w, s) } } } func (h *Handler) domains() ([]string, error) { var domains []string if err := filepath.WalkDir(h.Root, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.Type()&os.ModeSocket == 0 { return nil } relpath, err := filepath.Rel(h.Root, path) if err != nil { return err } subdomain := strings.Replace(relpath, string(filepath.Separator), ".", -1) domain := strings.Trim(subdomain, ".") + "." + strings.Trim(h.Host, ".") domains = append(domains, domain) return nil }); err != nil { return nil, err } return domains, nil } func (h *Handler) handleProxy(w http.ResponseWriter, r *http.Request) { var ( clean = strings.TrimSuffix(r.Host, h.Host) elements = strings.Split(clean, ".") relative = filepath.Join(elements...) socket = filepath.Join(h.Root, relative) ) fi, err := os.Stat(socket) // TODO: sanitize, chroot, etc. if err != nil || fi.Mode()&os.ModeSocket == 0 { http.Error(w, fmt.Sprintf("target socket %s invalid", socket), http.StatusNotFound) return } director := func(req *http.Request) { req.URL.Scheme = "http" req.URL.Host = socket req.URL.Path = r.URL.Path } var proxyLog *log.Logger if h.ErrorLogWriter != nil { proxyLog = log.New(h.ErrorLogWriter, fmt.Sprintf("unixproxy: %s: ", relative), 0) } rp := &httputil.ReverseProxy{ Transport: onlyUnixTransport, ErrorLog: proxyLog, Director: director, } rp.ServeHTTP(w, r) } var onlyUnixTransport = &http.Transport{ DialContext: func(ctx context.Context, _, address string) (net.Conn, error) { host, _, err := net.SplitHostPort(address) if err == nil { address = host } return (&net.Dialer{}).DialContext(ctx, "unix", address) }, } var indexTemplate = template.Must(template.New("").Parse(`