mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Scaffold admin app
This commit is contained in:
176
pkg/proxy/cert.go
Normal file
176
pkg/proxy/cert.go
Normal file
@ -0,0 +1,176 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"errors"
|
||||
"math/big"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MaxSerialNumber is the upper boundary that is used to create unique serial
|
||||
// numbers for the certificate. This can be any unsigned integer up to 20
|
||||
// bytes (2^(8*20)-1).
|
||||
var MaxSerialNumber = big.NewInt(0).SetBytes(bytes.Repeat([]byte{255}, 20))
|
||||
|
||||
// CertConfig is a set of configuration values that are used to build TLS configs
|
||||
// capable of MITM
|
||||
type CertConfig struct {
|
||||
ca *x509.Certificate
|
||||
caPriv crypto.PrivateKey
|
||||
priv *rsa.PrivateKey
|
||||
keyID []byte
|
||||
}
|
||||
|
||||
// NewCertConfig creates a MITM config using the CA certificate and
|
||||
// private key to generate on-the-fly certificates.
|
||||
func NewCertConfig(ca *x509.Certificate, caPrivKey crypto.PrivateKey) (*CertConfig, error) {
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pub := priv.Public()
|
||||
|
||||
// Subject Key Identifier support for end entity certificate.
|
||||
// https://www.ietf.org/rfc/rfc3280.txt (section 4.2.1.2)
|
||||
pkixPubKey, err := x509.MarshalPKIXPublicKey(pub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h := sha1.New()
|
||||
h.Write(pkixPubKey)
|
||||
keyID := h.Sum(nil)
|
||||
|
||||
return &CertConfig{
|
||||
ca: ca,
|
||||
caPriv: caPrivKey,
|
||||
priv: priv,
|
||||
keyID: keyID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewCA creates a new CA certificate and associated private key.
|
||||
func NewCA(name, organization string, validity time.Duration) (*x509.Certificate, *rsa.PrivateKey, error) {
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pub := priv.Public()
|
||||
|
||||
// Subject Key Identifier support for end entity certificate.
|
||||
// https://www.ietf.org/rfc/rfc3280.txt (section 4.2.1.2)
|
||||
pkixpub, err := x509.MarshalPKIXPublicKey(pub)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
h := sha1.New()
|
||||
h.Write(pkixpub)
|
||||
keyID := h.Sum(nil)
|
||||
|
||||
// TODO: keep a map of used serial numbers to avoid potentially reusing a
|
||||
// serial multiple times.
|
||||
serial, err := rand.Int(rand.Reader, MaxSerialNumber)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{
|
||||
CommonName: name,
|
||||
Organization: []string{organization},
|
||||
},
|
||||
SubjectKeyId: keyID,
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
NotBefore: time.Now().Add(-24 * time.Hour),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
DNSNames: []string{name},
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
raw, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, pub, priv)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Parse certificate bytes so that we have a leaf certificate.
|
||||
x509c, err := x509.ParseCertificate(raw)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return x509c, priv, nil
|
||||
}
|
||||
|
||||
// TLSConfig returns a *tls.Config that will generate certificates on-the-fly using
|
||||
// the SNI extension in the TLS ClientHello.
|
||||
func (c *CertConfig) TLSConfig() *tls.Config {
|
||||
return &tls.Config{
|
||||
GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if clientHello.ServerName == "" {
|
||||
return nil, errors.New("missing server name (SNI)")
|
||||
}
|
||||
return c.cert(clientHello.ServerName)
|
||||
},
|
||||
NextProtos: []string{"http/1.1"},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CertConfig) cert(hostname string) (*tls.Certificate, error) {
|
||||
// Remove the port if it exists.
|
||||
host, _, err := net.SplitHostPort(hostname)
|
||||
if err == nil {
|
||||
hostname = host
|
||||
}
|
||||
|
||||
serial, err := rand.Int(rand.Reader, MaxSerialNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{
|
||||
CommonName: hostname,
|
||||
Organization: []string{"Gurp"},
|
||||
},
|
||||
SubjectKeyId: c.keyID,
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
NotBefore: time.Now().Add(-24 * time.Hour),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
|
||||
if ip := net.ParseIP(hostname); ip != nil {
|
||||
tmpl.IPAddresses = []net.IP{ip}
|
||||
} else {
|
||||
tmpl.DNSNames = []string{hostname}
|
||||
}
|
||||
|
||||
raw, err := x509.CreateCertificate(rand.Reader, tmpl, c.ca, c.priv.Public(), c.caPriv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse certificate bytes so that we have a leaf certificate.
|
||||
x509c, err := x509.ParseCertificate(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tls.Certificate{
|
||||
Certificate: [][]byte{raw, c.ca.Raw},
|
||||
PrivateKey: c.priv,
|
||||
Leaf: x509c,
|
||||
}, nil
|
||||
}
|
24
pkg/proxy/modify.go
Normal file
24
pkg/proxy/modify.go
Normal file
@ -0,0 +1,24 @@
|
||||
package proxy
|
||||
|
||||
import "net/http"
|
||||
|
||||
var (
|
||||
nopReqModifier = func(req *http.Request) {}
|
||||
nopResModifier = func(res *http.Response) error { return nil }
|
||||
)
|
||||
|
||||
// RequestModifyFunc defines a type for a function that can modify a HTTP
|
||||
// request before it's proxied.
|
||||
type RequestModifyFunc func(req *http.Request)
|
||||
|
||||
// RequestModifyMiddleware defines a type for chaining request modifier
|
||||
// middleware.
|
||||
type RequestModifyMiddleware func(next RequestModifyFunc) RequestModifyFunc
|
||||
|
||||
// ResponseModifyFunc defines a type for a function that can modify a HTTP
|
||||
// response before it's written back to the client.
|
||||
type ResponseModifyFunc func(res *http.Response) error
|
||||
|
||||
// ResponseModifyMiddleware defines a type for chaining response modifier
|
||||
// middleware.
|
||||
type ResponseModifyMiddleware func(ResponseModifyFunc) ResponseModifyFunc
|
47
pkg/proxy/net.go
Normal file
47
pkg/proxy/net.go
Normal file
@ -0,0 +1,47 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
var ErrAlreadyAccepted = errors.New("listener already accepted")
|
||||
|
||||
// OnceListener implements net.Listener.
|
||||
//
|
||||
// Accepts a connection once and returns an error on subsequent
|
||||
// attempts.
|
||||
type OnceAcceptListener struct {
|
||||
c net.Conn
|
||||
}
|
||||
|
||||
func (l *OnceAcceptListener) Accept() (net.Conn, error) {
|
||||
if l.c == nil {
|
||||
return nil, ErrAlreadyAccepted
|
||||
}
|
||||
|
||||
c := l.c
|
||||
l.c = nil
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (l *OnceAcceptListener) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *OnceAcceptListener) Addr() net.Addr {
|
||||
return l.c.LocalAddr()
|
||||
}
|
||||
|
||||
// ConnNotify embeds net.Conn and adds a channel field for notifying
|
||||
// that the connection was closed.
|
||||
type ConnNotify struct {
|
||||
net.Conn
|
||||
closed chan struct{}
|
||||
}
|
||||
|
||||
func (c *ConnNotify) Close() {
|
||||
c.Conn.Close()
|
||||
c.closed <- struct{}{}
|
||||
}
|
151
pkg/proxy/proxy.go
Normal file
151
pkg/proxy/proxy.go
Normal file
@ -0,0 +1,151 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
)
|
||||
|
||||
// Proxy implements http.Handler and offers MITM behaviour for modifying
|
||||
// HTTP requests and responses.
|
||||
type Proxy struct {
|
||||
certConfig *CertConfig
|
||||
handler http.Handler
|
||||
|
||||
// TODO: Add mutex for modifier funcs.
|
||||
reqModifiers []RequestModifyMiddleware
|
||||
resModifiers []ResponseModifyMiddleware
|
||||
}
|
||||
|
||||
// NewProxy returns a new Proxy.
|
||||
func NewProxy(ca *x509.Certificate, key crypto.PrivateKey) (*Proxy, error) {
|
||||
certConfig, err := NewCertConfig(ca, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := &Proxy{
|
||||
certConfig: certConfig,
|
||||
reqModifiers: make([]RequestModifyMiddleware, 0),
|
||||
resModifiers: make([]ResponseModifyMiddleware, 0),
|
||||
}
|
||||
|
||||
p.handler = &httputil.ReverseProxy{
|
||||
Director: p.modifyRequest,
|
||||
ModifyResponse: p.modifyResponse,
|
||||
ErrorHandler: errorHandler,
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodConnect {
|
||||
p.handleConnect(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
p.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *Proxy) UseRequestModifier(fn ...RequestModifyMiddleware) {
|
||||
p.reqModifiers = append(p.reqModifiers, fn...)
|
||||
}
|
||||
|
||||
func (p *Proxy) UseResponseModifier(fn ...ResponseModifyMiddleware) {
|
||||
p.resModifiers = append(p.resModifiers, fn...)
|
||||
}
|
||||
|
||||
func (p *Proxy) modifyRequest(r *http.Request) {
|
||||
// Fix r.URL for HTTPS requests after CONNECT.
|
||||
if r.URL.Scheme == "" {
|
||||
r.URL.Host = r.Host
|
||||
r.URL.Scheme = "https"
|
||||
}
|
||||
|
||||
fn := nopReqModifier
|
||||
|
||||
for i := len(p.reqModifiers) - 1; i >= 0; i-- {
|
||||
fn = p.reqModifiers[i](fn)
|
||||
}
|
||||
|
||||
fn(r)
|
||||
}
|
||||
|
||||
func (p *Proxy) modifyResponse(res *http.Response) error {
|
||||
fn := nopResModifier
|
||||
|
||||
for i := len(p.resModifiers) - 1; i >= 0; i-- {
|
||||
fn = p.resModifiers[i](fn)
|
||||
}
|
||||
|
||||
return fn(res)
|
||||
}
|
||||
|
||||
// handleConnect hijacks the incoming HTTP request and sets up an HTTP tunnel.
|
||||
// During the TLS handshake with the client, we use the proxy's CA config to
|
||||
// create a certificate on-the-fly.
|
||||
func (p *Proxy) handleConnect(w http.ResponseWriter, r *http.Request) {
|
||||
hj, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
log.Printf("[ERROR] handleConnect: ResponseWriter is not a http.Hijacker (type: %T)", w)
|
||||
writeError(w, r, http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
clientConn, _, err := hj.Hijack()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Hijacking client connection failed: %v", err)
|
||||
writeError(w, r, http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
defer clientConn.Close()
|
||||
|
||||
// Secure connection to client.
|
||||
clientConn, err = p.clientTLSConn(clientConn)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Securing client connection failed: %v", err)
|
||||
return
|
||||
}
|
||||
clientConnNotify := ConnNotify{clientConn, make(chan struct{})}
|
||||
|
||||
l := &OnceAcceptListener{clientConnNotify.Conn}
|
||||
|
||||
err = http.Serve(l, p.handler)
|
||||
if err != nil && err != ErrAlreadyAccepted {
|
||||
log.Printf("[ERROR] Serving HTTP request failed: %v", err)
|
||||
}
|
||||
<-clientConnNotify.closed
|
||||
}
|
||||
|
||||
func (p *Proxy) clientTLSConn(conn net.Conn) (*tls.Conn, error) {
|
||||
tlsConfig := p.certConfig.TLSConfig()
|
||||
|
||||
tlsConn := tls.Server(conn, tlsConfig)
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
tlsConn.Close()
|
||||
return nil, fmt.Errorf("handshake error: %v", err)
|
||||
}
|
||||
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
func errorHandler(w http.ResponseWriter, r *http.Request, err error) {
|
||||
if err == context.Canceled {
|
||||
return
|
||||
}
|
||||
log.Printf("[ERROR]: Proxy error: %v", err)
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, r *http.Request, code int) {
|
||||
http.Error(w, http.StatusText(code), code)
|
||||
}
|
Reference in New Issue
Block a user