more work on websocket support

This commit is contained in:
Shivaram Lingamneni
2020-05-04 22:29:10 -04:00
parent 25813f6d3a
commit 3dc5c8de78
17 changed files with 830 additions and 444 deletions
+34
View File
@@ -5,12 +5,16 @@ package utils
import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"crypto/tls"
"encoding/base32"
"encoding/base64"
"encoding/hex"
"errors"
"net"
"strings"
"time"
)
var (
@@ -18,6 +22,10 @@ var (
B32Encoder = base32.NewEncoding("abcdefghijkmnpqrstuvwxyz23456789").WithPadding(base32.NoPadding)
ErrInvalidCertfp = errors.New("Invalid certfp")
ErrNoPeerCerts = errors.New("No certfp available")
ErrNotTLS = errors.New("Connection is not TLS")
)
const (
@@ -83,3 +91,29 @@ func NormalizeCertfp(certfp string) (result string, err error) {
}
return
}
func GetCertFP(conn net.Conn, handshakeTimeout time.Duration) (result string, err error) {
tlsConn, isTLS := conn.(*tls.Conn)
if !isTLS {
return "", ErrNotTLS
}
// ensure handshake is performed
tlsConn.SetDeadline(time.Now().Add(handshakeTimeout))
err = tlsConn.Handshake()
tlsConn.SetDeadline(time.Time{})
if err != nil {
return "", err
}
peerCerts := tlsConn.ConnectionState().PeerCertificates
if len(peerCerts) < 1 {
return "", ErrNoPeerCerts
}
rawCert := sha256.Sum256(peerCerts[0].Raw)
fingerprint := hex.EncodeToString(rawCert[:])
return fingerprint, nil
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license
package utils
import (
"bytes"
"regexp"
"strings"
)
// yet another glob implementation in Go
func CompileGlob(glob string) (result *regexp.Regexp, err error) {
var buf bytes.Buffer
buf.WriteByte('^')
for {
i := strings.IndexByte(glob, '*')
if i == -1 {
buf.WriteString(regexp.QuoteMeta(glob))
break
} else {
buf.WriteString(regexp.QuoteMeta(glob[:i]))
buf.WriteString(".*")
glob = glob[i+1:]
}
}
buf.WriteByte('$')
return regexp.Compile(buf.String())
}
+37
View File
@@ -0,0 +1,37 @@
// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license
package utils
import (
"regexp"
"testing"
)
func globMustCompile(glob string) *regexp.Regexp {
re, err := CompileGlob(glob)
if err != nil {
panic(err)
}
return re
}
func assertMatches(glob, str string, match bool, t *testing.T) {
re := globMustCompile(glob)
if re.MatchString(str) != match {
t.Errorf("should %s match %s? %t, but got %t instead", glob, str, match, !match)
}
}
func TestGlob(t *testing.T) {
assertMatches("https://testnet.oragono.io", "https://testnet.oragono.io", true, t)
assertMatches("https://*.oragono.io", "https://testnet.oragono.io", true, t)
assertMatches("*://*.oragono.io", "https://testnet.oragono.io", true, t)
assertMatches("*://*.oragono.io", "https://oragono.io", false, t)
assertMatches("*://*.oragono.io", "https://githubusercontent.com", false, t)
assertMatches("", "", true, t)
assertMatches("", "x", false, t)
assertMatches("*", "", true, t)
assertMatches("*", "x", true, t)
}
+42 -7
View File
@@ -22,19 +22,13 @@ var (
func AddrToIP(addr net.Addr) net.IP {
if tcpaddr, ok := addr.(*net.TCPAddr); ok {
return tcpaddr.IP.To16()
} else if AddrIsUnix(addr) {
} else if _, ok := addr.(*net.UnixAddr); ok {
return IPv4LoopbackAddress
} else {
return nil
}
}
// AddrIsUnix returns whether the address is a unix domain socket.
func AddrIsUnix(addr net.Addr) bool {
_, ok := addr.(*net.UnixAddr)
return ok
}
// IPStringToHostname converts a string representation of an IP address to an IRC-ready hostname
func IPStringToHostname(ipStr string) string {
if 0 < len(ipStr) && ipStr[0] == ':' {
@@ -158,3 +152,44 @@ func ParseNetList(netList []string) (nets []net.IPNet, err error) {
}
return
}
// Process the X-Forwarded-For header, validating against a list of trusted IPs.
// Returns the address that the request was forwarded for, or nil if no trustworthy
// data was available.
func HandleXForwardedFor(remoteAddr string, xForwardedFor string, whitelist []net.IPNet) (result net.IP) {
// http.Request.RemoteAddr "has no defined format". with TCP it's typically "127.0.0.1:23784",
// with unix domain it's typically "@"
var remoteIP net.IP
host, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
remoteIP = IPv4LoopbackAddress
} else {
remoteIP = net.ParseIP(host)
}
if remoteIP == nil || !IPInNets(remoteIP, whitelist) {
return remoteIP
}
// walk backwards through the X-Forwarded-For chain looking for an IP
// that is *not* trusted. that means it was added by one of our trusted
// forwarders (either remoteIP or something ahead of it in the chain)
// and we can trust it:
result = remoteIP
forwardedIPs := strings.Split(xForwardedFor, ",")
for i := len(forwardedIPs) - 1; i >= 0; i-- {
proxiedIP := net.ParseIP(strings.TrimSpace(forwardedIPs[i]))
if proxiedIP == nil {
return
} else if !IPInNets(proxiedIP, whitelist) {
return proxiedIP
} else {
result = proxiedIP
}
}
// no valid untrusted IPs were found in the chain;
// return either the last valid and trusted IP (which must be the origin),
// or nil:
return
}
+36
View File
@@ -159,3 +159,39 @@ func TestNormalizedNetFromString(t *testing.T) {
assertEqual(NetToNormalizedString(network), "2001:db8::1", t)
assertEqual(network.Contains(net.ParseIP("2001:0db8::1")), true, t)
}
func checkXFF(remoteAddr, forwardedHeader string, expectedStr string, t *testing.T) {
whitelistCIDRs := []string{"10.0.0.0/8", "127.0.0.1/8"}
var whitelist []net.IPNet
for _, str := range whitelistCIDRs {
_, wlNet, err := net.ParseCIDR(str)
if err != nil {
panic(err)
}
whitelist = append(whitelist, *wlNet)
}
expected := net.ParseIP(expectedStr)
actual := HandleXForwardedFor(remoteAddr, forwardedHeader, whitelist)
if !actual.Equal(expected) {
t.Errorf("handling %s and %s, expected %s, got %s", remoteAddr, forwardedHeader, expected, actual)
}
}
func TestXForwardedFor(t *testing.T) {
checkXFF("8.8.4.4:9999", "", "8.8.4.4", t)
// forged XFF header from untrustworthy external IP, should be ignored:
checkXFF("8.8.4.4:9999", "1.1.1.1", "8.8.4.4", t)
checkXFF("10.0.0.4:28432", "", "10.0.0.4", t)
checkXFF("10.0.0.4:28432", "8.8.4.4", "8.8.4.4", t)
checkXFF("10.0.0.4:28432", "10.0.0.3", "10.0.0.3", t)
checkXFF("10.0.0.4:28432", "1.1.1.1, 8.8.4.4", "8.8.4.4", t)
checkXFF("10.0.0.4:28432", "8.8.4.4, 1.1.1.1, 10.0.0.3", "1.1.1.1", t)
checkXFF("10.0.0.4:28432", "10.0.0.1, 10.0.0.2, 10.0.0.3", "10.0.0.1", t)
checkXFF("@", "8.8.4.4, 1.1.1.1, 10.0.0.3", "1.1.1.1", t)
}
+174
View File
@@ -0,0 +1,174 @@
// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license
package utils
import (
"crypto/tls"
"errors"
"net"
"strings"
"sync"
"time"
)
// TODO: handle PROXY protocol v2 (the binary protocol)
const (
// https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
// "a 108-byte buffer is always enough to store all the line and a trailing zero
// for string processing."
maxProxyLineLen = 107
)
var (
ErrBadProxyLine = errors.New("invalid PROXY line")
// TODO(golang/go#4373): replace this with the stdlib ErrNetClosing
ErrNetClosing = errors.New("use of closed network connection")
)
// ListenerConfig is all the information about how to process
// incoming IRC connections on a listener.
type ListenerConfig struct {
TLSConfig *tls.Config
ProxyDeadline time.Duration
RequireProxy bool
// these are just metadata for easier tracking,
// they are not used by ReloadableListener:
Tor bool
STSOnly bool
WebSocket bool
}
// read a PROXY line one byte at a time, to ensure we don't read anything beyond
// that into a buffer, which would break the TLS handshake
func readRawProxyLine(conn net.Conn, deadline time.Duration) (result string) {
// normally this is covered by ping timeouts, but we're doing this outside
// of the normal client goroutine:
conn.SetDeadline(time.Now().Add(deadline))
defer conn.SetDeadline(time.Time{})
var buf [maxProxyLineLen]byte
oneByte := make([]byte, 1)
i := 0
for i < maxProxyLineLen {
n, err := conn.Read(oneByte)
if err != nil {
return
} else if n == 1 {
buf[i] = oneByte[0]
if buf[i] == '\n' {
candidate := string(buf[0 : i+1])
if strings.HasPrefix(candidate, "PROXY") {
return candidate
} else {
return
}
}
i += 1
}
}
// no \r\n, fail out
return
}
// ParseProxyLine parses a PROXY protocol (v1) line and returns the remote IP.
func ParseProxyLine(line string) (ip net.IP, err error) {
params := strings.Fields(line)
if len(params) != 6 || params[0] != "PROXY" {
return nil, ErrBadProxyLine
}
ip = net.ParseIP(params[2])
if ip == nil {
return nil, ErrBadProxyLine
}
return ip.To16(), nil
}
/// ProxiedConnection is a net.Conn with some additional data stapled to it;
// the proxied IP, if one was read via the PROXY protocol, and the listener
// configuration.
type ProxiedConnection struct {
net.Conn
ProxiedIP net.IP
Config ListenerConfig
}
// ReloadableListener is a wrapper for net.Listener that allows reloading
// of config data for postprocessing connections (TLS, PROXY protocol, etc.)
type ReloadableListener struct {
// TODO: make this lock-free
sync.Mutex
realListener net.Listener
config ListenerConfig
isClosed bool
}
func NewReloadableListener(realListener net.Listener, config ListenerConfig) *ReloadableListener {
return &ReloadableListener{
realListener: realListener,
config: config,
}
}
func (rl *ReloadableListener) Reload(config ListenerConfig) {
rl.Lock()
rl.config = config
rl.Unlock()
}
func (rl *ReloadableListener) Accept() (conn net.Conn, err error) {
conn, err = rl.realListener.Accept()
rl.Lock()
config := rl.config
isClosed := rl.isClosed
rl.Unlock()
if isClosed {
if err == nil {
conn.Close()
}
err = ErrNetClosing
}
if err != nil {
return nil, err
}
var proxiedIP net.IP
if config.RequireProxy {
// this will occur synchronously on the goroutine calling Accept(),
// but that's OK because this listener *requires* a PROXY line,
// therefore it must be used with proxies that always send the line
// and we won't get slowloris'ed waiting for the client response
proxyLine := readRawProxyLine(conn, config.ProxyDeadline)
proxiedIP, err = ParseProxyLine(proxyLine)
if err != nil {
conn.Close()
return nil, err
}
}
if config.TLSConfig != nil {
conn = tls.Server(conn, config.TLSConfig)
}
return &ProxiedConnection{
Conn: conn,
ProxiedIP: proxiedIP,
Config: config,
}, nil
}
func (rl *ReloadableListener) Close() error {
rl.Lock()
rl.isClosed = true
rl.Unlock()
return rl.realListener.Close()
}
func (rl *ReloadableListener) Addr() net.Addr {
return rl.realListener.Addr()
}