1// Package httpunix provides a HTTP transport (net/http.RoundTripper)
2// that uses Unix domain sockets instead of HTTP.
3//
4// This is useful for non-browser connections within the same host, as
5// it allows using the file system for credentials of both client
6// and server, and guaranteeing unique names.
7//
8// The URLs look like this:
9//
10//     http+unix://LOCATION/PATH_ETC
11//
12// where LOCATION is translated to a file system path with
13// Transport.RegisterLocation, and PATH_ETC follow normal http: scheme
14// conventions.
15package httpunix
16
17import (
18	"context"
19	"errors"
20	"net"
21	"net/http"
22	"sync"
23	"time"
24)
25
26// Scheme is the URL scheme used for HTTP over UNIX domain sockets.
27const Scheme = "http+unix"
28
29// Transport is a http.RoundTripper that connects to Unix domain
30// sockets.
31type Transport struct {
32	// DialTimeout is deprecated. Use context instead.
33	DialTimeout time.Duration
34	// RequestTimeout is deprecated and has no effect.
35	RequestTimeout time.Duration
36	// ResponseHeaderTimeout is deprecated. Use context instead.
37	ResponseHeaderTimeout time.Duration
38
39	onceInit  sync.Once
40	transport http.Transport
41
42	mu sync.Mutex
43	// map a URL "hostname" to a UNIX domain socket path
44	loc map[string]string
45}
46
47func (t *Transport) initTransport() {
48	t.transport.DialContext = t.dialContext
49	t.transport.DialTLS = t.dialTLS
50	t.transport.DisableCompression = true
51	t.transport.ResponseHeaderTimeout = t.ResponseHeaderTimeout
52}
53
54func (t *Transport) getTransport() *http.Transport {
55	t.onceInit.Do(t.initTransport)
56	return &t.transport
57}
58
59func (t *Transport) dialContext(ctx context.Context, network, addr string) (net.Conn, error) {
60	if network != "tcp" {
61		return nil, errors.New("httpunix internals are confused: network=" + network)
62	}
63	host, port, err := net.SplitHostPort(addr)
64	if err != nil {
65		return nil, err
66	}
67	if port != "80" {
68		return nil, errors.New("httpunix internals are confused: port=" + port)
69	}
70	t.mu.Lock()
71	path, ok := t.loc[host]
72	t.mu.Unlock()
73	if !ok {
74		return nil, errors.New("unknown location: " + host)
75	}
76	d := net.Dialer{
77		Timeout: t.DialTimeout,
78	}
79	return d.DialContext(ctx, "unix", path)
80}
81
82func (t *Transport) dialTLS(network, addr string) (net.Conn, error) {
83	return nil, errors.New("httpunix: TLS over UNIX domain sockets is not supported")
84}
85
86// RegisterLocation registers an URL location and maps it to the given
87// file system path.
88//
89// Calling RegisterLocation twice for the same location is a
90// programmer error, and causes a panic.
91func (t *Transport) RegisterLocation(loc string, path string) {
92	t.mu.Lock()
93	defer t.mu.Unlock()
94	if t.loc == nil {
95		t.loc = make(map[string]string)
96	}
97	if _, exists := t.loc[loc]; exists {
98		panic("location " + loc + " already registered")
99	}
100	t.loc[loc] = path
101}
102
103var _ http.RoundTripper = (*Transport)(nil)
104
105// RoundTrip executes a single HTTP transaction. See
106// net/http.RoundTripper.
107func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
108	if req.URL == nil {
109		return nil, errors.New("http+unix: nil Request.URL")
110	}
111	if req.URL.Scheme != Scheme {
112		return nil, errors.New("unsupported protocol scheme: " + req.URL.Scheme)
113	}
114	if req.URL.Host == "" {
115		return nil, errors.New("http+unix: no Host in request URL")
116	}
117
118	tt := t.getTransport()
119	req = req.Clone(req.Context())
120	// get http.Transport to cooperate
121	req.URL.Scheme = "http"
122	return tt.RoundTrip(req)
123}
124