diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ed3b07 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.test diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..33aec14 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013-2015 Tommi Virtanen. + +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. diff --git a/httpunix.go b/httpunix.go new file mode 100644 index 0000000..95f5e95 --- /dev/null +++ b/httpunix.go @@ -0,0 +1,95 @@ +// Package httpunix provides a HTTP transport (net/http.RoundTripper) +// that uses Unix domain sockets instead of HTTP. +// +// This is useful for non-browser connections within the same host, as +// it allows using the file system for credentials of both client +// and server, and guaranteeing unique names. +// +// The URLs look like this: +// +// http+unix://LOCATION/PATH_ETC +// +// where LOCATION is translated to a file system path with +// Transport.RegisterLocation, and PATH_ETC follow normal http: scheme +// conventions. +package httpunix + +import ( + "bufio" + "errors" + "net" + "net/http" + "sync" + "time" +) + +// Scheme is the URL scheme used for HTTP over UNIX domain sockets. +const Scheme = "http+unix" + +// Transport is a http.RoundTripper that connects to Unix domain +// sockets. +type Transport struct { + DialTimeout time.Duration + RequestTimeout time.Duration + ResponseHeaderTimeout time.Duration + + mu sync.Mutex + // map a URL "hostname" to a UNIX domain socket path + loc map[string]string +} + +// RegisterLocation registers an URL location and maps it to the given +// file system path. +// +// Calling RegisterLocation twice for the same location is a +// programmer error, and causes a panic. +func (t *Transport) RegisterLocation(loc string, path string) { + t.mu.Lock() + defer t.mu.Unlock() + if t.loc == nil { + t.loc = make(map[string]string) + } + if _, exists := t.loc[loc]; exists { + panic("location " + loc + " already registered") + } + t.loc[loc] = path +} + +var _ http.RoundTripper = (*Transport)(nil) + +// RoundTrip executes a single HTTP transaction. See +// net/http.RoundTripper. +func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { + if req.URL == nil { + return nil, errors.New("http+unix: nil Request.URL") + } + if req.URL.Scheme != Scheme { + return nil, errors.New("unsupported protocol scheme: " + req.URL.Scheme) + } + if req.URL.Host == "" { + return nil, errors.New("http+unix: no Host in request URL") + } + t.mu.Lock() + path, ok := t.loc[req.URL.Host] + t.mu.Unlock() + if !ok { + return nil, errors.New("unknown location: " + req.Host) + } + + c, err := net.DialTimeout("unix", path, t.DialTimeout) + if err != nil { + return nil, err + } + r := bufio.NewReader(c) + if t.RequestTimeout > 0 { + c.SetWriteDeadline(time.Now().Add(t.RequestTimeout)) + } + if err := req.Write(c); err != nil { + return nil, err + } + if t.ResponseHeaderTimeout > 0 { + c.SetReadDeadline(time.Now().Add(t.ResponseHeaderTimeout)) + } + resp, err := http.ReadResponse(r, req) + return resp, err +} diff --git a/httpunix_test.go b/httpunix_test.go new file mode 100644 index 0000000..adc9c38 --- /dev/null +++ b/httpunix_test.go @@ -0,0 +1,78 @@ +package httpunix_test + +import ( + "fmt" + "log" + "net" + "net/http" + "net/http/httputil" + "time" + + "github.com/tv42/httpunix" +) + +func Example_clientStandalone() { + // This example shows using a customized http.Client. + u := &httpunix.Transport{ + DialTimeout: 100 * time.Millisecond, + RequestTimeout: 1 * time.Second, + ResponseHeaderTimeout: 1 * time.Second, + } + u.RegisterLocation("myservice", "/path/to/socket") + + var client = http.Client{ + Transport: u, + } + + resp, err := client.Get("http+unix://myservice/urlpath/as/seen/by/server") + if err != nil { + log.Fatal(err) + } + buf, err := httputil.DumpResponse(resp, true) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s", buf) + resp.Body.Close() +} + +func Example_clientIntegrated() { + // This example shows handling all net/http requests for the + // http+unix URL scheme. + u := &httpunix.Transport{ + DialTimeout: 100 * time.Millisecond, + RequestTimeout: 1 * time.Second, + ResponseHeaderTimeout: 1 * time.Second, + } + u.RegisterLocation("myservice", "/path/to/socket") + + // If you want to use http: with the same client: + t := &http.Transport{} + t.RegisterProtocol(httpunix.Scheme, u) + var client = http.Client{ + Transport: t, + } + + resp, err := client.Get("http+unix://myservice/urlpath/as/seen/by/server") + if err != nil { + log.Fatal(err) + } + buf, err := httputil.DumpResponse(resp, true) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s", buf) + resp.Body.Close() +} + +func Example_server() { + l, err := net.Listen("unix", "/path/to/socket") + if err != nil { + log.Fatal(err) + } + defer l.Close() + + if err := http.Serve(l, nil); err != nil { + log.Fatal(err) + } +}