1/*
2 * Copyright (c) 2014, Yawning Angel <yawning at schwanenlied dot me>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 *  * Redistributions of source code must retain the above copyright notice,
9 *    this list of conditions and the following disclaimer.
10 *
11 *  * Redistributions in binary form must reproduce the above copyright notice,
12 *    this list of conditions and the following disclaimer in the documentation
13 *    and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25 * POSSIBILITY OF SUCH DAMAGE.
26 */
27
28package main
29
30import (
31	"bufio"
32	"encoding/base64"
33	"fmt"
34	"net"
35	"net/http"
36	"net/http/httputil"
37	"net/url"
38	"time"
39
40	"golang.org/x/net/proxy"
41)
42
43// httpProxy is a HTTP connect proxy.
44type httpProxy struct {
45	hostPort string
46	haveAuth bool
47	username string
48	password string
49	forward  proxy.Dialer
50}
51
52func newHTTP(uri *url.URL, forward proxy.Dialer) (proxy.Dialer, error) {
53	s := new(httpProxy)
54	s.hostPort = uri.Host
55	s.forward = forward
56	if uri.User != nil {
57		s.haveAuth = true
58		s.username = uri.User.Username()
59		s.password, _ = uri.User.Password()
60	}
61
62	return s, nil
63}
64
65func (s *httpProxy) Dial(network, addr string) (net.Conn, error) {
66	// Dial and create the http client connection.
67	c, err := s.forward.Dial("tcp", s.hostPort)
68	if err != nil {
69		return nil, err
70	}
71	conn := new(httpConn)
72	conn.httpConn = httputil.NewClientConn(c, nil) // nolint: staticcheck
73	conn.remoteAddr, err = net.ResolveTCPAddr(network, addr)
74	if err != nil {
75		conn.httpConn.Close()
76		return nil, err
77	}
78
79	// HACK HACK HACK HACK.  http.ReadRequest also does this.
80	reqURL, err := url.Parse("http://" + addr)
81	if err != nil {
82		conn.httpConn.Close()
83		return nil, err
84	}
85	reqURL.Scheme = ""
86
87	req, err := http.NewRequest("CONNECT", reqURL.String(), nil)
88	if err != nil {
89		conn.httpConn.Close()
90		return nil, err
91	}
92	req.Close = false
93	if s.haveAuth {
94		// SetBasicAuth doesn't quite do what is appropriate, because
95		// the correct header is `Proxy-Authorization`.
96		req.Header.Set("Proxy-Authorization", "Basic " + base64.StdEncoding.EncodeToString([]byte(s.username+":"+s.password)))
97	}
98	req.Header.Set("User-Agent", "")
99
100	resp, err := conn.httpConn.Do(req)
101	if err != nil && err != httputil.ErrPersistEOF { // nolint: staticcheck
102		conn.httpConn.Close()
103		return nil, err
104	}
105	if resp.StatusCode != 200 {
106		conn.httpConn.Close()
107		return nil, fmt.Errorf("proxy error: %s", resp.Status)
108	}
109
110	conn.hijackedConn, conn.staleReader = conn.httpConn.Hijack()
111	return conn, nil
112}
113
114type httpConn struct {
115	remoteAddr   *net.TCPAddr
116	httpConn     *httputil.ClientConn // nolint: staticcheck
117	hijackedConn net.Conn
118	staleReader  *bufio.Reader
119}
120
121func (c *httpConn) Read(b []byte) (int, error) {
122	if c.staleReader != nil {
123		if c.staleReader.Buffered() > 0 {
124			return c.staleReader.Read(b)
125		}
126		c.staleReader = nil
127	}
128	return c.hijackedConn.Read(b)
129}
130
131func (c *httpConn) Write(b []byte) (int, error) {
132	return c.hijackedConn.Write(b)
133}
134
135func (c *httpConn) Close() error {
136	return c.hijackedConn.Close()
137}
138
139func (c *httpConn) LocalAddr() net.Addr {
140	return c.hijackedConn.LocalAddr()
141}
142
143func (c *httpConn) RemoteAddr() net.Addr {
144	return c.remoteAddr
145}
146
147func (c *httpConn) SetDeadline(t time.Time) error {
148	return c.hijackedConn.SetDeadline(t)
149}
150
151func (c *httpConn) SetReadDeadline(t time.Time) error {
152	return c.hijackedConn.SetReadDeadline(t)
153}
154
155func (c *httpConn) SetWriteDeadline(t time.Time) error {
156	return c.hijackedConn.SetWriteDeadline(t)
157}
158
159func init() {
160	proxy.RegisterDialerType("http", newHTTP)
161}
162