2 Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, version 3 of the License.
8 This program is distributed in the hope that it will be useful,
9 but WITHOUT ANY WARRANTY; without even the implied warranty of
10 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 GNU General Public License for more details.
13 You should have received a copy of the GNU General Public License
14 along with this program. If not, see <http://www.gnu.org/licenses/>.
38 "github.com/dustin/go-humanize"
39 "go.cypherpunks.ru/ucspi"
43 tlsNextProtoS = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
44 tlsNextProtoC = make(map[string]func(string, *tls.Conn) http.RoundTripper)
45 caCert *x509.Certificate
46 caPrv crypto.PrivateKey
49 transport = http.Transport{
50 ForceAttemptHTTP2: false,
51 DisableKeepAlives: true,
52 MaxIdleConnsPerHost: 2,
53 TLSNextProto: tlsNextProtoC,
54 DialTLSContext: dialTLS,
57 accepted = make(map[string]string)
58 acceptedM sync.RWMutex
59 rejected = make(map[string]string)
60 rejectedM sync.RWMutex
63 func spkiHash(cert *x509.Certificate) string {
64 hsh := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
65 return hex.EncodeToString(hsh[:])
68 func certInfo(certRaw []byte) string {
69 cmd := exec.Command("certtool", "--certificate-info", "--inder")
70 cmd.Stdin = bytes.NewReader(certRaw)
71 out, err := cmd.Output()
78 func acceptedAdd(addr, h string) {
84 func rejectedAdd(addr, h string) {
90 type ErrRejected struct {
94 func (err ErrRejected) Error() string { return err.addr + " was rejected" }
96 func dialTLS(ctx context.Context, network, addr string) (net.Conn, error) {
97 host := strings.TrimSuffix(addr, ":443")
99 VerifyPeerCertificate: func(
101 verifiedChains [][]*x509.Certificate,
103 return verifyCert(host, nil, rawCerts, verifiedChains)
106 conn, dialErr := tls.Dial(network, addr, &cfg)
108 if _, ok := dialErr.(ErrRejected); ok {
111 cfg.InsecureSkipVerify = true
112 cfg.VerifyPeerCertificate = func(
114 verifiedChains [][]*x509.Certificate,
116 return verifyCert(host, dialErr, rawCerts, verifiedChains)
119 conn, err = tls.Dial(network, addr, &cfg)
121 sinkErr <- fmt.Sprintf("%s\t%s", addr, dialErr.Error())
125 connState := conn.ConnectionState()
126 sinkTLS <- fmt.Sprintf(
128 strings.TrimSuffix(addr, ":443"),
129 ucspi.TLSVersion(connState.Version),
130 tls.CipherSuiteName(connState.CipherSuite),
131 spkiHash(connState.PeerCertificates[0]),
136 func roundTrip(w http.ResponseWriter, req *http.Request) {
137 if req.Method == http.MethodHead {
138 http.Error(w, "go away", http.StatusMethodNotAllowed)
141 sinkReq <- fmt.Sprintf("%s %s", req.Method, req.URL.String())
142 host := strings.TrimSuffix(req.URL.Host, ":443")
143 for _, spy := range SpyDomains {
144 if strings.HasSuffix(host, spy) {
145 http.NotFound(w, req)
146 sinkOther <- fmt.Sprintf(
147 "%s %s\t%d\tspy one",
155 if strings.HasPrefix(req.URL.Host, "www.reddit.com") {
156 req.URL.Host = "old.reddit.com"
157 http.Redirect(w, req, req.URL.String(), http.StatusMovedPermanently)
160 resp, err := transport.RoundTrip(req)
162 sinkErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error())
163 w.WriteHeader(http.StatusBadGateway)
164 w.Write([]byte(err.Error()))
167 for k, vs := range resp.Header {
168 if k == "Location" || k == "Content-Type" || k == "Content-Length" {
171 for _, v := range vs {
175 if req.Method == http.MethodGet {
177 switch resp.StatusCode {
178 case http.StatusMovedPermanently, http.StatusPermanentRedirect:
179 redirType = "permanent"
181 case http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect:
182 if strings.Contains(req.Header.Get("User-Agent"), "newsboat/") {
185 redirType = "temporary"
191 w.Header().Add("Content-Type", "text/html")
192 w.WriteHeader(http.StatusOK)
193 location := resp.Header.Get("Location")
197 <head><title>%d %s: %s redirection</title></head>
198 <body>Redirection to <a href="%s">%s</a>
200 resp.StatusCode, http.StatusText(resp.StatusCode),
201 redirType, location, location,
203 sinkRedir <- fmt.Sprintf(
204 "%s %s\t%s\t%s", req.Method, resp.Status, req.URL.String(), location,
209 for _, h := range []string{"Location", "Content-Type", "Content-Length"} {
210 if v := resp.Header.Get(h); v != "" {
214 w.WriteHeader(resp.StatusCode)
215 n, err := io.Copy(w, resp.Body)
217 log.Printf("Error during %s: %+v\n", req.URL, err)
225 resp.Header.Get("Content-Type"),
226 humanize.IBytes(uint64(n)),
228 if resp.StatusCode == http.StatusOK {
235 type Handler struct{}
237 func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
238 if req.Method != http.MethodConnect {
242 hj, ok := w.(http.Hijacker)
244 log.Fatalln("no hijacking")
246 conn, _, err := hj.Hijack()
251 conn.Write([]byte(fmt.Sprintf(
254 http.StatusOK, http.StatusText(http.StatusOK),
256 host := strings.Split(req.Host, ":")[0]
258 keypair, ok := hostCerts[host]
259 if !ok || !keypair.cert.NotAfter.After(time.Now().Add(time.Hour)) {
260 keypair = newKeypair(host, caCert, caPrv)
261 hostCerts[host] = keypair
264 tlsConn := tls.Server(conn, &tls.Config{
265 Certificates: []tls.Certificate{{
266 Certificate: [][]byte{keypair.cert.Raw},
267 PrivateKey: keypair.prv,
270 if err = tlsConn.Handshake(); err != nil {
271 log.Printf("TLS error %s: %+v\n", host, err)
275 Handler: &HTTPSHandler{host: req.Host},
276 TLSNextProto: tlsNextProtoS,
278 err = srv.Serve(&SingleListener{conn: tlsConn})
280 if _, ok := err.(AlreadyAccepted); !ok {
281 log.Printf("TLS serve error %s: %+v\n", host, err)
287 type HTTPSHandler struct {
291 func (h *HTTPSHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
292 req.URL.Scheme = "https"
293 req.URL.Host = h.host
298 crtPath := flag.String("cert", "cert.pem", "Path to server X.509 certificate")
299 prvPath := flag.String("key", "prv.pem", "Path to server PKCS#8 private key")
300 bind := flag.String("bind", "[::1]:8080", "Bind address")
301 certs = flag.String("certs", "certs", "Directory with pinned certificates")
302 dnsSrv = flag.String("dns", "[::1]:53", "DNS server")
303 fifos = flag.String("fifos", "fifos", "Directory with FIFOs")
304 notai = flag.Bool("notai", false, "Do not prepend TAI64N to logs")
306 log.SetFlags(log.Lshortfile)
310 _, caCert, err = ucspi.CertificateFromFile(*crtPath)
314 caPrv, err = ucspi.PrivateKeyFromFile(*prvPath)
319 ln, err := net.Listen("tcp", *bind)
325 TLSNextProto: tlsNextProtoS,
327 srv.SetKeepAlivesEnabled(false)
328 log.Println("listening:", *bind)
329 if err := srv.Serve(ln); err != nil {