From d49fe57776e6d19181c8ccb8d5332ed2c62d5ca8 Mon Sep 17 00:00:00 2001 From: V Date: Fri, 27 Aug 2021 06:08:09 +0200 Subject: Root commit Co-authored-by: edef --- proxy.go | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 proxy.go (limited to 'proxy.go') diff --git a/proxy.go b/proxy.go new file mode 100644 index 0000000..dfee27e --- /dev/null +++ b/proxy.go @@ -0,0 +1,145 @@ +// SPDX-FileCopyrightText: V +// SPDX-FileCopyrightText: edef +// SPDX-License-Identifier: OSL-3.0 + +package main + +import ( + "bufio" + "bytes" + "crypto/tls" + "io" + "log" + "net" + "net/http" + "sync" +) + +type sidedConn struct { + net.Conn + Side +} + +type Proxy struct { + store *Store + tlsConfig *tls.Config + + exiting chan struct{} + wg sync.WaitGroup +} + +func NewProxy(store *Store, tlsConfig *tls.Config) *Proxy { + return &Proxy{ + store: store, + tlsConfig: tlsConfig, + + exiting: make(chan struct{}), + } +} + +func (p *Proxy) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodConnect { + log.Printf("%s - invalid request %s %s", req.RemoteAddr, req.Method, req.RequestURI) + http.Error(resp, "405 I'm a proxy", http.StatusMethodNotAllowed) + return + } + + host := req.URL.Hostname() + log.Printf("%s - new connection to %s", req.RemoteAddr, host) + + server, err := tls.DialWithDialer( + &net.Dialer{Cancel: req.Context().Done()}, + "tcp", net.JoinHostPort(host, "6697"), + p.tlsConfig, + ) + if err != nil { + log.Printf("%s - failed connection %v", req.RemoteAddr, err) + resp.WriteHeader(http.StatusBadGateway) + return + } + + session := OpenSession(p.store, host) + resp.WriteHeader(http.StatusOK) + + // http.Server's Shutdown "does not attempt to close nor wait for hijacked connections", + // so we have to bump the waitgroup prior to calling Hijack() + p.wg.Add(1) + defer p.wg.Done() + + // XXX: bufio.ReadWriter might still contain data + // I think it's impossible for err to be non-nil + client, _, _ := resp.(http.Hijacker).Hijack() + p.proxy(session, sidedConn{client, SideClient}, sidedConn{server, SideServer}) +} + +func (p *Proxy) proxy(session *Session, a, b sidedConn) { + ch := make(chan func()) + + pipe := func(r, w sidedConn) { + scanner := bufio.NewScanner(r) + scanner.Split(scanIRCLines) + for scanner.Scan() { + session.Write(r.Side, string(dropLineEnding(scanner.Bytes()))) + _, err := w.Write(scanner.Bytes()) + if err != nil { + ch <- func() { session.Close(w.Side, err.Error()) } + return + } + } + + err := scanner.Err() + if err == nil { + err = io.EOF + } + + ch <- func() { session.Close(r.Side, err.Error()) } + } + + go pipe(a, b) + go pipe(b, a) + + done := func() { session.Close(SideProxy, "shutting down") } + + select { + case <-p.exiting: + a.Close() + b.Close() + <-ch + <-ch + case done = <-ch: + a.Close() + b.Close() + <-ch + } + + done() +} + +func (p *Proxy) Shutdown() { + close(p.exiting) + p.wg.Wait() + p.store.Close() +} + +func scanIRCLines(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := bytes.IndexByte(data, '\n'); i >= 0 { + // we have a full newline-terminated line + return i + 1, data[:i+1], nil + } + if atEOF { + return 0, nil, io.ErrUnexpectedEOF + } + return 0, nil, nil // request more data +} + +// on a buffer known to end in \n, drop \n or \r\n +func dropLineEnding(data []byte) []byte { + n := len(data) + if n > 1 && data[n-1] == '\r' { + return data[:n-2] + } + return data[:n-1] +} -- cgit 1.4.1