Add proxy-aware dialing functions, and convert cmd/... tooling over.
This commit is contained in:
@@ -7,7 +7,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -16,6 +15,7 @@ import (
|
|||||||
hosts "git.wntrmute.dev/kyle/goutils/certlib/hosts"
|
hosts "git.wntrmute.dev/kyle/goutils/certlib/hosts"
|
||||||
"git.wntrmute.dev/kyle/goutils/certlib/revoke"
|
"git.wntrmute.dev/kyle/goutils/certlib/revoke"
|
||||||
"git.wntrmute.dev/kyle/goutils/fileutil"
|
"git.wntrmute.dev/kyle/goutils/fileutil"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -38,8 +38,10 @@ func main() {
|
|||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
revoke.HardFail = hardfail
|
revoke.HardFail = hardfail
|
||||||
// Set HTTP client timeout for revocation library
|
// Build a proxy-aware HTTP client for OCSP/CRL fetches
|
||||||
revoke.HTTPClient.Timeout = timeout
|
if httpClient, err := lib.NewHTTPClient(lib.DialerOpts{Timeout: timeout}); err == nil {
|
||||||
|
revoke.HTTPClient = httpClient
|
||||||
|
}
|
||||||
|
|
||||||
if flag.NArg() == 0 {
|
if flag.NArg() == 0 {
|
||||||
fmt.Fprintf(os.Stderr, "Usage: %s [options] <target> [<target>...]\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, "Usage: %s [options] <target> [<target>...]\n", os.Args[0])
|
||||||
@@ -99,28 +101,19 @@ func checkSite(hostport string) (string, error) {
|
|||||||
return strUnknown, err
|
return strUnknown, err
|
||||||
}
|
}
|
||||||
|
|
||||||
d := &net.Dialer{Timeout: timeout}
|
|
||||||
tcfg := &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
ServerName: target.Host,
|
|
||||||
} // #nosec G402 -- CLI tool only verifies revocation
|
|
||||||
td := &tls.Dialer{NetDialer: d, Config: tcfg}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
conn, err := td.DialContext(ctx, "tcp", target.String())
|
// Use proxy-aware TLS dialer
|
||||||
|
conn, err := lib.DialTLS(ctx, target.String(), lib.DialerOpts{Timeout: timeout, TLSConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true, // #nosec G402 -- CLI tool only verifies revocation
|
||||||
|
ServerName: target.Host,
|
||||||
|
}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return strUnknown, err
|
return strUnknown, err
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
state := conn.ConnectionState()
|
||||||
tconn, ok := conn.(*tls.Conn)
|
|
||||||
if !ok {
|
|
||||||
return strUnknown, errors.New("connection is not TLS")
|
|
||||||
}
|
|
||||||
|
|
||||||
state := tconn.ConnectionState()
|
|
||||||
if len(state.PeerCertificates) == 0 {
|
if len(state.PeerCertificates) == 0 {
|
||||||
return strUnknown, errors.New("no peer certificates presented")
|
return strUnknown, errors.New("no peer certificates presented")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/goutils/die"
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
var hasPort = regexp.MustCompile(`:\d+$`)
|
var hasPort = regexp.MustCompile(`:\d+$`)
|
||||||
@@ -23,13 +24,9 @@ func main() {
|
|||||||
server += ":443"
|
server += ":443"
|
||||||
}
|
}
|
||||||
|
|
||||||
d := &tls.Dialer{Config: &tls.Config{}} // #nosec G402
|
// Use proxy-aware TLS dialer
|
||||||
nc, err := d.DialContext(context.Background(), "tcp", server)
|
conn, err := lib.DialTLS(context.Background(), server, lib.DialerOpts{TLSConfig: &tls.Config{}}) // #nosec G402
|
||||||
die.If(err)
|
die.If(err)
|
||||||
conn, ok := nc.(*tls.Conn)
|
|
||||||
if !ok {
|
|
||||||
die.With("invalid TLS connection (not a *tls.Conn)")
|
|
||||||
}
|
|
||||||
|
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import (
|
|||||||
|
|
||||||
"github.com/kr/text"
|
"github.com/kr/text"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/goutils/certlib"
|
|
||||||
"git.wntrmute.dev/kyle/goutils/lib"
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -351,14 +350,14 @@ func main() {
|
|||||||
flag.BoolVar(&leafOnly, "l", false, "only show the leaf certificate")
|
flag.BoolVar(&leafOnly, "l", false, "only show the leaf certificate")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
opts := &certlib.FetcherOpts{
|
opts := &lib.FetcherOpts{
|
||||||
SkipVerify: true,
|
SkipVerify: true,
|
||||||
Roots: nil,
|
Roots: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, filename := range flag.Args() {
|
for _, filename := range flag.Args() {
|
||||||
fmt.Fprintf(os.Stdout, "--%s ---%s", filename, "\n")
|
fmt.Fprintf(os.Stdout, "--%s ---%s", filename, "\n")
|
||||||
certs, err := certlib.GetCertificateChain(filename, opts)
|
certs, err := lib.GetCertificateChain(filename, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = lib.Warn(err, "couldn't read certificate")
|
_, _ = lib.Warn(err, "couldn't read certificate")
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/goutils/certlib"
|
|
||||||
"git.wntrmute.dev/kyle/goutils/die"
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
"git.wntrmute.dev/kyle/goutils/lib"
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
)
|
)
|
||||||
@@ -75,7 +74,7 @@ func checkCert(cert *x509.Certificate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
opts := &certlib.FetcherOpts{}
|
opts := &lib.FetcherOpts{}
|
||||||
|
|
||||||
flag.BoolVar(&opts.SkipVerify, "k", false, "skip server verification")
|
flag.BoolVar(&opts.SkipVerify, "k", false, "skip server verification")
|
||||||
flag.BoolVar(&warnOnly, "q", false, "only warn about expiring certs")
|
flag.BoolVar(&warnOnly, "q", false, "only warn about expiring certs")
|
||||||
@@ -83,7 +82,7 @@ func main() {
|
|||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
for _, file := range flag.Args() {
|
for _, file := range flag.Args() {
|
||||||
certs, err := certlib.GetCertificateChain(file, opts)
|
certs, err := lib.GetCertificateChain(file, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = lib.Warn(err, "while parsing certificates")
|
_, _ = lib.Warn(err, "while parsing certificates")
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/goutils/certlib"
|
|
||||||
"git.wntrmute.dev/kyle/goutils/die"
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
"git.wntrmute.dev/kyle/goutils/lib"
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
)
|
)
|
||||||
@@ -32,7 +31,7 @@ func serialString(cert *x509.Certificate, mode lib.HexEncodeMode) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
opts := &certlib.FetcherOpts{}
|
opts := &lib.FetcherOpts{}
|
||||||
displayAs := flag.String("d", "int", "display mode (int, hex, uhex)")
|
displayAs := flag.String("d", "int", "display mode (int, hex, uhex)")
|
||||||
showExpiry := flag.Bool("e", false, "show expiry date")
|
showExpiry := flag.Bool("e", false, "show expiry date")
|
||||||
flag.BoolVar(&opts.SkipVerify, "k", false, "skip server verification")
|
flag.BoolVar(&opts.SkipVerify, "k", false, "skip server verification")
|
||||||
@@ -41,7 +40,7 @@ func main() {
|
|||||||
displayMode := parseDisplayMode(*displayAs)
|
displayMode := parseDisplayMode(*displayAs)
|
||||||
|
|
||||||
for _, arg := range flag.Args() {
|
for _, arg := range flag.Args() {
|
||||||
cert, err := certlib.GetCertificate(arg, opts)
|
cert, err := lib.GetCertificate(arg, opts)
|
||||||
die.If(err)
|
die.If(err)
|
||||||
|
|
||||||
fmt.Printf("%s: %s", arg, serialString(cert, displayMode))
|
fmt.Printf("%s: %s", arg, serialString(cert, displayMode))
|
||||||
|
|||||||
@@ -108,12 +108,12 @@ func run(cfg appConfig) error {
|
|||||||
return fmt.Errorf("failed to build combined pool: %w", err)
|
return fmt.Errorf("failed to build combined pool: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := &certlib.FetcherOpts{
|
opts := &lib.FetcherOpts{
|
||||||
Roots: combinedPool,
|
Roots: combinedPool,
|
||||||
SkipVerify: cfg.skipVerify,
|
SkipVerify: cfg.skipVerify,
|
||||||
}
|
}
|
||||||
|
|
||||||
chain, err := certlib.GetCertificateChain(flag.Arg(0), opts)
|
chain, err := lib.GetCertificateChain(flag.Arg(0), opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/goutils/ahash"
|
"git.wntrmute.dev/kyle/goutils/ahash"
|
||||||
"git.wntrmute.dev/kyle/goutils/die"
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
@@ -82,8 +83,13 @@ func main() {
|
|||||||
_, _ = lib.Warn(reqErr, "building request for %s", remote)
|
_, _ = lib.Warn(reqErr, "building request for %s", remote)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
client := &http.Client{}
|
// Use proxy-aware HTTP client with a reasonable timeout for connects/handshakes
|
||||||
resp, err := client.Do(req)
|
httpClient, err := lib.NewHTTPClient(lib.DialerOpts{Timeout: 30 * time.Second})
|
||||||
|
if err != nil {
|
||||||
|
_, _ = lib.Warn(err, "building HTTP client for %s", remote)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = lib.Warn(err, "fetching %s", remote)
|
_, _ = lib.Warn(err, "fetching %s", remote)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/goutils/die"
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -44,15 +45,10 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
site += ":443"
|
site += ":443"
|
||||||
}
|
}
|
||||||
d := &tls.Dialer{Config: cfg}
|
// Use proxy-aware TLS dialer
|
||||||
nc, err := d.DialContext(context.Background(), "tcp", site)
|
conn, err := lib.DialTLS(context.Background(), site, lib.DialerOpts{TLSConfig: cfg})
|
||||||
die.If(err)
|
die.If(err)
|
||||||
|
|
||||||
conn, ok := nc.(*tls.Conn)
|
|
||||||
if !ok {
|
|
||||||
die.With("invalid TLS connection (not a *tls.Conn)")
|
|
||||||
}
|
|
||||||
|
|
||||||
cs := conn.ConnectionState()
|
cs := conn.ConnectionState()
|
||||||
var chain []byte
|
var chain []byte
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"git.wntrmute.dev/kyle/goutils/certlib/hosts"
|
"git.wntrmute.dev/kyle/goutils/certlib/hosts"
|
||||||
"git.wntrmute.dev/kyle/goutils/die"
|
"git.wntrmute.dev/kyle/goutils/die"
|
||||||
|
"git.wntrmute.dev/kyle/goutils/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -20,18 +21,14 @@ func main() {
|
|||||||
hostPort, err := hosts.ParseHost(os.Args[1])
|
hostPort, err := hosts.ParseHost(os.Args[1])
|
||||||
die.If(err)
|
die.If(err)
|
||||||
|
|
||||||
d := &tls.Dialer{Config: &tls.Config{
|
// Use proxy-aware TLS dialer; skip verification as before
|
||||||
InsecureSkipVerify: true,
|
conn, err := lib.DialTLS(
|
||||||
}} // #nosec G402
|
context.Background(),
|
||||||
|
hostPort.String(),
|
||||||
nc, err := d.DialContext(context.Background(), "tcp", hostPort.String())
|
lib.DialerOpts{TLSConfig: &tls.Config{InsecureSkipVerify: true}},
|
||||||
|
) // #nosec G402
|
||||||
die.If(err)
|
die.If(err)
|
||||||
|
|
||||||
conn, ok := nc.(*tls.Conn)
|
|
||||||
if !ok {
|
|
||||||
die.With("invalid TLS connection (not a *tls.Conn)")
|
|
||||||
}
|
|
||||||
|
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
state := conn.ConnectionState()
|
state := conn.ConnectionState()
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -6,7 +6,8 @@ require (
|
|||||||
github.com/hashicorp/go-syslog v1.0.0
|
github.com/hashicorp/go-syslog v1.0.0
|
||||||
github.com/kr/text v0.2.0
|
github.com/kr/text v0.2.0
|
||||||
github.com/pkg/sftp v1.12.0
|
github.com/pkg/sftp v1.12.0
|
||||||
golang.org/x/crypto v0.44.0
|
golang.org/x/crypto v0.39.0
|
||||||
|
golang.org/x/net v0.38.0
|
||||||
golang.org/x/sys v0.38.0
|
golang.org/x/sys v0.38.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -27,13 +27,19 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
|
|||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||||
|
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||||
|
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
|
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
|||||||
452
lib/dialer.go
Normal file
452
lib/dialer.go
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
// Package lib contains reusable helpers. This file provides proxy-aware
|
||||||
|
// dialers for plain TCP and TLS connections using environment variables.
|
||||||
|
//
|
||||||
|
// Supported proxy environment variables (checked case-insensitively):
|
||||||
|
// - SOCKS5_PROXY (e.g., socks5://user:pass@host:1080)
|
||||||
|
// - HTTPS_PROXY (e.g., https://user:pass@host:443)
|
||||||
|
// - HTTP_PROXY (e.g., http://user:pass@host:3128)
|
||||||
|
//
|
||||||
|
// Precedence when multiple proxies are set (both for net and TLS dialers):
|
||||||
|
// 1. SOCKS5_PROXY
|
||||||
|
// 2. HTTPS_PROXY
|
||||||
|
// 3. HTTP_PROXY
|
||||||
|
//
|
||||||
|
// Both uppercase and lowercase variable names are honored.
|
||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
xproxy "golang.org/x/net/proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DialerOpts controls creation of proxy-aware dialers.
|
||||||
|
//
|
||||||
|
// Timeout controls the maximum amount of time spent establishing the
|
||||||
|
// underlying TCP connection and any proxy handshake. If zero, a
|
||||||
|
// reasonable default (30s) is used.
|
||||||
|
//
|
||||||
|
// TLSConfig is used by the TLS dialer to configure the TLS handshake to
|
||||||
|
// the target endpoint. If TLSConfig.ServerName is empty, it will be set
|
||||||
|
// from the host portion of the address passed to DialContext.
|
||||||
|
type DialerOpts struct {
|
||||||
|
Timeout time.Duration
|
||||||
|
TLSConfig *tls.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContextDialer matches the common DialContext signature used by net and tls dialers.
|
||||||
|
type ContextDialer interface {
|
||||||
|
DialContext(ctx context.Context, network, address string) (net.Conn, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialTCP is a convenience helper that dials a TCP connection to address
|
||||||
|
// using a proxy-aware dialer derived from opts. It honors SOCKS5_PROXY,
|
||||||
|
// HTTPS_PROXY, and HTTP_PROXY environment variables.
|
||||||
|
func DialTCP(ctx context.Context, address string, opts DialerOpts) (net.Conn, error) {
|
||||||
|
d, err := NewNetDialer(opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return d.DialContext(ctx, "tcp", address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialTLS is a convenience helper that dials a TLS-wrapped TCP connection to
|
||||||
|
// address using a proxy-aware dialer derived from opts. It returns a *tls.Conn.
|
||||||
|
// It honors SOCKS5_PROXY, HTTPS_PROXY, and HTTP_PROXY environment variables and
|
||||||
|
// uses opts.TLSConfig for the handshake (filling ServerName from address if empty).
|
||||||
|
func DialTLS(ctx context.Context, address string, opts DialerOpts) (*tls.Conn, error) {
|
||||||
|
d, err := NewTLSDialer(opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := d.DialContext(ctx, "tcp", address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConn, ok := c.(*tls.Conn)
|
||||||
|
if !ok {
|
||||||
|
_ = c.Close()
|
||||||
|
return nil, fmt.Errorf("DialTLS: expected *tls.Conn, got %T", c)
|
||||||
|
}
|
||||||
|
return tlsConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNetDialer returns a ContextDialer that dials TCP connections using
|
||||||
|
// proxies discovered from the environment (SOCKS5_PROXY, HTTPS_PROXY, HTTP_PROXY).
|
||||||
|
// The returned dialer supports context cancellation for direct and HTTP(S)
|
||||||
|
// proxies and applies the configured timeout to connection/proxy handshake.
|
||||||
|
func NewNetDialer(opts DialerOpts) (ContextDialer, error) {
|
||||||
|
if opts.Timeout <= 0 {
|
||||||
|
opts.Timeout = 30 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
if u := getProxyURLFromEnv("SOCKS5_PROXY"); u != nil {
|
||||||
|
return newSOCKS5Dialer(u, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
if u := getProxyURLFromEnv("HTTPS_PROXY"); u != nil {
|
||||||
|
return &httpProxyDialer{
|
||||||
|
proxyURL: u,
|
||||||
|
timeout: opts.Timeout,
|
||||||
|
secure: true,
|
||||||
|
config: opts.TLSConfig,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if u := getProxyURLFromEnv("HTTP_PROXY"); u != nil {
|
||||||
|
return &httpProxyDialer{
|
||||||
|
proxyURL: u,
|
||||||
|
timeout: opts.Timeout,
|
||||||
|
secure: true,
|
||||||
|
config: opts.TLSConfig,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct dialer
|
||||||
|
return &net.Dialer{Timeout: opts.Timeout}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTLSDialer returns a ContextDialer that establishes a TLS connection to
|
||||||
|
// the destination, while honoring SOCKS5_PROXY/HTTPS_PROXY/HTTP_PROXY.
|
||||||
|
//
|
||||||
|
// The returned dialer performs proxy negotiation (if any), then completes a
|
||||||
|
// TLS handshake to the target using opts.TLSConfig.
|
||||||
|
func NewTLSDialer(opts DialerOpts) (ContextDialer, error) {
|
||||||
|
if opts.Timeout <= 0 {
|
||||||
|
opts.Timeout = 30 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer SOCKS5 if present.
|
||||||
|
if u := getProxyURLFromEnv("SOCKS5_PROXY"); u != nil {
|
||||||
|
base, err := newSOCKS5Dialer(u, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &tlsWrappingDialer{base: base, tcfg: opts.TLSConfig, timeout: opts.Timeout}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// For TLS, prefer HTTPS proxy over HTTP if both set.
|
||||||
|
if u := getProxyURLFromEnv("HTTPS_PROXY"); u != nil {
|
||||||
|
base := &httpProxyDialer{
|
||||||
|
proxyURL: u,
|
||||||
|
timeout: opts.Timeout,
|
||||||
|
secure: true,
|
||||||
|
config: opts.TLSConfig,
|
||||||
|
}
|
||||||
|
return &tlsWrappingDialer{base: base, tcfg: opts.TLSConfig, timeout: opts.Timeout}, nil
|
||||||
|
}
|
||||||
|
if u := getProxyURLFromEnv("HTTP_PROXY"); u != nil {
|
||||||
|
base := &httpProxyDialer{
|
||||||
|
proxyURL: u,
|
||||||
|
timeout: opts.Timeout,
|
||||||
|
secure: true,
|
||||||
|
config: opts.TLSConfig,
|
||||||
|
}
|
||||||
|
return &tlsWrappingDialer{base: base, tcfg: opts.TLSConfig, timeout: opts.Timeout}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct TLS
|
||||||
|
base := &net.Dialer{Timeout: opts.Timeout}
|
||||||
|
return &tlsWrappingDialer{base: base, tcfg: opts.TLSConfig, timeout: opts.Timeout}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Implementation helpers ----
|
||||||
|
|
||||||
|
func getProxyURLFromEnv(name string) *url.URL {
|
||||||
|
// check both upper/lowercase
|
||||||
|
v := os.Getenv(name)
|
||||||
|
if v == "" {
|
||||||
|
v = os.Getenv(strings.ToLower(name))
|
||||||
|
}
|
||||||
|
if v == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// If scheme omitted, infer from env var name.
|
||||||
|
if !strings.Contains(v, "://") {
|
||||||
|
switch strings.ToUpper(name) {
|
||||||
|
case "SOCKS5_PROXY":
|
||||||
|
v = "socks5://" + v
|
||||||
|
case "HTTPS_PROXY":
|
||||||
|
v = "https://" + v
|
||||||
|
default:
|
||||||
|
v = "http://" + v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u, err := url.Parse(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHTTPClient returns an *http.Client that is proxy-aware.
|
||||||
|
//
|
||||||
|
// Behavior:
|
||||||
|
// - If SOCKS5_PROXY is set, the client routes all TCP connections through the
|
||||||
|
// SOCKS5 proxy using a custom DialContext, and disables HTTP(S) proxying in
|
||||||
|
// the transport (per our precedence SOCKS5 > HTTPS > HTTP).
|
||||||
|
// - Otherwise, it uses http.ProxyFromEnvironment which supports HTTP_PROXY,
|
||||||
|
// HTTPS_PROXY, and NO_PROXY/no_proxy.
|
||||||
|
// - Connection and TLS handshake timeouts are derived from opts.Timeout.
|
||||||
|
// - For HTTPS targets, opts.TLSConfig is applied to the transport.
|
||||||
|
func NewHTTPClient(opts DialerOpts) (*http.Client, error) {
|
||||||
|
if opts.Timeout <= 0 {
|
||||||
|
opts.Timeout = 30 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base transport configuration
|
||||||
|
tr := &http.Transport{
|
||||||
|
TLSClientConfig: opts.TLSConfig,
|
||||||
|
TLSHandshakeTimeout: opts.Timeout,
|
||||||
|
// Leave other fields as Go defaults for compatibility.
|
||||||
|
}
|
||||||
|
|
||||||
|
// If SOCKS5 is configured, use our dialer and disable HTTP proxying to
|
||||||
|
// avoid double-proxying. Otherwise, rely on ProxyFromEnvironment for
|
||||||
|
// HTTP(S) proxies and still set a connect timeout via net.Dialer.
|
||||||
|
if u := getProxyURLFromEnv("SOCKS5_PROXY"); u != nil {
|
||||||
|
d, err := newSOCKS5Dialer(u, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tr.Proxy = nil
|
||||||
|
tr.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
return d.DialContext(ctx, network, address)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tr.Proxy = http.ProxyFromEnvironment
|
||||||
|
// Use a standard net.Dialer to ensure we apply a connect timeout.
|
||||||
|
nd := &net.Dialer{Timeout: opts.Timeout}
|
||||||
|
tr.DialContext = nd.DialContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct client; we don't set Client.Timeout here to avoid affecting
|
||||||
|
// streaming responses. Callers can set it if they want an overall deadline.
|
||||||
|
return &http.Client{Transport: tr}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// httpProxyDialer implements CONNECT tunneling over HTTP or HTTPS proxy.
|
||||||
|
type httpProxyDialer struct {
|
||||||
|
proxyURL *url.URL
|
||||||
|
timeout time.Duration
|
||||||
|
secure bool // true for HTTPS proxy
|
||||||
|
config *tls.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *httpProxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
if !strings.HasPrefix(network, "tcp") {
|
||||||
|
return nil, fmt.Errorf("http proxy dialer only supports TCP, got %q", network)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial to proxy
|
||||||
|
var nd = &net.Dialer{Timeout: d.timeout}
|
||||||
|
proxyAddr := d.proxyURL.Host
|
||||||
|
if !strings.Contains(proxyAddr, ":") {
|
||||||
|
if d.secure {
|
||||||
|
proxyAddr += ":443"
|
||||||
|
} else {
|
||||||
|
proxyAddr += ":80"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conn, err := nd.DialContext(ctx, "tcp", proxyAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deadline covering CONNECT and (for TLS wrapper) will be handled by caller too.
|
||||||
|
if d.timeout > 0 {
|
||||||
|
_ = conn.SetDeadline(time.Now().Add(d.timeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If HTTPS proxy, wrap with TLS to the proxy itself.
|
||||||
|
if d.secure {
|
||||||
|
host := d.proxyURL.Hostname()
|
||||||
|
d.config.ServerName = host
|
||||||
|
tlsConn := tls.Client(conn, d.config)
|
||||||
|
if err = tlsConn.HandshakeContext(ctx); err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, fmt.Errorf("tls handshake with https proxy failed: %w", err)
|
||||||
|
}
|
||||||
|
conn = tlsConn
|
||||||
|
}
|
||||||
|
|
||||||
|
req := buildConnectRequest(d.proxyURL, address)
|
||||||
|
if _, err = conn.Write([]byte(req)); err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, fmt.Errorf("failed to write CONNECT request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read proxy response until end of headers
|
||||||
|
br := bufio.NewReader(conn)
|
||||||
|
statusLine, err := br.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, fmt.Errorf("failed to read CONNECT response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(statusLine, "HTTP/") {
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, fmt.Errorf("invalid proxy response: %q", strings.TrimSpace(statusLine))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(statusLine, " 200 ") && !strings.HasSuffix(strings.TrimSpace(statusLine), " 200") {
|
||||||
|
// Drain headers for context
|
||||||
|
_ = drainHeaders(br)
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, fmt.Errorf("proxy CONNECT failed: %s", strings.TrimSpace(statusLine))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = drainHeaders(br); err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear deadline for caller to manage further I/O.
|
||||||
|
_ = conn.SetDeadline(time.Time{})
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildConnectRequest(proxyURL *url.URL, target string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
fmt.Fprintf(&b, "CONNECT %s HTTP/1.1\r\n", target)
|
||||||
|
fmt.Fprintf(&b, "Host: %s\r\n", target)
|
||||||
|
b.WriteString("Proxy-Connection: Keep-Alive\r\n")
|
||||||
|
b.WriteString("User-Agent: goutils-dialer/1\r\n")
|
||||||
|
|
||||||
|
if proxyURL.User != nil {
|
||||||
|
user := proxyURL.User.Username()
|
||||||
|
pass, _ := proxyURL.User.Password()
|
||||||
|
auth := base64.StdEncoding.EncodeToString([]byte(user + ":" + pass))
|
||||||
|
fmt.Fprintf(&b, "Proxy-Authorization: Basic %s\r\n", auth)
|
||||||
|
}
|
||||||
|
b.WriteString("\r\n")
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func drainHeaders(br *bufio.Reader) error {
|
||||||
|
for {
|
||||||
|
line, err := br.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading proxy headers: %w", err)
|
||||||
|
}
|
||||||
|
if line == "\r\n" || line == "\n" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSOCKS5Dialer builds a context-aware wrapper over the x/net/proxy dialer.
|
||||||
|
func newSOCKS5Dialer(u *url.URL, opts DialerOpts) (ContextDialer, error) {
|
||||||
|
var auth *xproxy.Auth
|
||||||
|
if u.User != nil {
|
||||||
|
user := u.User.Username()
|
||||||
|
pass, _ := u.User.Password()
|
||||||
|
auth = &xproxy.Auth{User: user, Password: pass}
|
||||||
|
}
|
||||||
|
forward := &net.Dialer{Timeout: opts.Timeout}
|
||||||
|
d, err := xproxy.SOCKS5("tcp", hostPortWithDefault(u, "1080"), auth, forward)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &socks5ContextDialer{d: d, timeout: opts.Timeout}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type socks5ContextDialer struct {
|
||||||
|
d xproxy.Dialer // lacks context; we wrap it
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *socks5ContextDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
if !strings.HasPrefix(network, "tcp") {
|
||||||
|
return nil, errors.New("socks5 dialer only supports TCP")
|
||||||
|
}
|
||||||
|
// Best-effort context support: run the non-context dial in a goroutine
|
||||||
|
// and respect ctx cancellation/timeout.
|
||||||
|
type result struct {
|
||||||
|
c net.Conn
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
ch := make(chan result, 1)
|
||||||
|
go func() {
|
||||||
|
c, err := s.d.Dial("tcp", address)
|
||||||
|
ch <- result{c: c, err: err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case r := <-ch:
|
||||||
|
return r.c, r.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tlsWrappingDialer performs a TLS handshake over an existing base dialer.
|
||||||
|
type tlsWrappingDialer struct {
|
||||||
|
base ContextDialer
|
||||||
|
tcfg *tls.Config
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tlsWrappingDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
if !strings.HasPrefix(network, "tcp") {
|
||||||
|
return nil, fmt.Errorf("tls dialer only supports TCP, got %q", network)
|
||||||
|
}
|
||||||
|
raw, err := t.base.DialContext(ctx, network, address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply deadline for handshake.
|
||||||
|
if t.timeout > 0 {
|
||||||
|
_ = raw.SetDeadline(time.Now().Add(t.timeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
var h string
|
||||||
|
host := address
|
||||||
|
|
||||||
|
if h, _, err = net.SplitHostPort(address); err == nil {
|
||||||
|
host = h
|
||||||
|
}
|
||||||
|
var cfg *tls.Config
|
||||||
|
if t.tcfg != nil {
|
||||||
|
// Clone to avoid copying internal locks and to prevent mutating caller's config.
|
||||||
|
c := t.tcfg.Clone()
|
||||||
|
if c.ServerName == "" {
|
||||||
|
c.ServerName = host
|
||||||
|
}
|
||||||
|
cfg = c
|
||||||
|
} else {
|
||||||
|
cfg = &tls.Config{ServerName: host} // #nosec G402 - intentional
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConn := tls.Client(raw, cfg)
|
||||||
|
if err = tlsConn.HandshakeContext(ctx); err != nil {
|
||||||
|
_ = raw.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear deadline after successful handshake
|
||||||
|
_ = tlsConn.SetDeadline(time.Time{})
|
||||||
|
return tlsConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostPortWithDefault(u *url.URL, defPort string) string {
|
||||||
|
host := u.Host
|
||||||
|
if !strings.Contains(host, ":") {
|
||||||
|
host += ":" + defPort
|
||||||
|
}
|
||||||
|
return host
|
||||||
|
}
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
package certlib
|
package lib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||||
"git.wntrmute.dev/kyle/goutils/certlib/hosts"
|
"git.wntrmute.dev/kyle/goutils/certlib/hosts"
|
||||||
"git.wntrmute.dev/kyle/goutils/fileutil"
|
"git.wntrmute.dev/kyle/goutils/fileutil"
|
||||||
"git.wntrmute.dev/kyle/goutils/lib"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// FetcherOpts are options for fetching certificates. They are only applicable to ServerFetcher.
|
// FetcherOpts are options for fetching certificates. They are only applicable to ServerFetcher.
|
||||||
@@ -21,6 +20,13 @@ type FetcherOpts struct {
|
|||||||
Roots *x509.CertPool
|
Roots *x509.CertPool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fo *FetcherOpts) TLSConfig() *tls.Config {
|
||||||
|
return &tls.Config{
|
||||||
|
InsecureSkipVerify: fo.SkipVerify, // #nosec G402 - intentional
|
||||||
|
RootCAs: fo.Roots,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fetcher is an interface for fetching certificates from a remote source. It
|
// Fetcher is an interface for fetching certificates from a remote source. It
|
||||||
// currently supports fetching from a server or a file.
|
// currently supports fetching from a server or a file.
|
||||||
type Fetcher interface {
|
type Fetcher interface {
|
||||||
@@ -65,29 +71,20 @@ func ParseServer(host string) (*ServerFetcher, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sf *ServerFetcher) String() string {
|
func (sf *ServerFetcher) String() string {
|
||||||
return fmt.Sprintf("tls://%s", net.JoinHostPort(sf.host, lib.Itoa(sf.port, -1)))
|
return fmt.Sprintf("tls://%s", net.JoinHostPort(sf.host, Itoa(sf.port, -1)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sf *ServerFetcher) GetChain() ([]*x509.Certificate, error) {
|
func (sf *ServerFetcher) GetChain() ([]*x509.Certificate, error) {
|
||||||
config := &tls.Config{
|
opts := DialerOpts{
|
||||||
InsecureSkipVerify: sf.insecure, // #nosec G402 - no shit sherlock
|
TLSConfig: &tls.Config{
|
||||||
RootCAs: sf.roots,
|
InsecureSkipVerify: sf.insecure, // #nosec G402 - no shit sherlock
|
||||||
|
RootCAs: sf.roots,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
dialer := &tls.Dialer{
|
conn, err := DialTLS(context.Background(), net.JoinHostPort(sf.host, Itoa(sf.port, -1)), opts)
|
||||||
Config: config,
|
|
||||||
}
|
|
||||||
|
|
||||||
hostSpec := net.JoinHostPort(sf.host, lib.Itoa(sf.port, -1))
|
|
||||||
|
|
||||||
netConn, err := dialer.DialContext(context.Background(), "tcp", hostSpec)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("dialing server: %w", err)
|
return nil, fmt.Errorf("failed to dial server: %w", err)
|
||||||
}
|
|
||||||
|
|
||||||
conn, ok := netConn.(*tls.Conn)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("connection is not TLS")
|
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
@@ -125,10 +122,10 @@ func (ff *FileFetcher) GetChain() ([]*x509.Certificate, error) {
|
|||||||
return nil, fmt.Errorf("failed to read from stdin: %w", err)
|
return nil, fmt.Errorf("failed to read from stdin: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ParseCertificatesPEM(certData)
|
return certlib.ParseCertificatesPEM(certData)
|
||||||
}
|
}
|
||||||
|
|
||||||
certs, err := LoadCertificates(ff.path)
|
certs, err := certlib.LoadCertificates(ff.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load chain: %w", err)
|
return nil, fmt.Errorf("failed to load chain: %w", err)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
// Package lib contains functions useful for most programs.
|
|
||||||
package lib
|
package lib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
70
release-docker.sh
Executable file
70
release-docker.sh
Executable file
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Release Docker image for kisom/goutils using the Dockerfile in the repo root.
|
||||||
|
#
|
||||||
|
# Behavior:
|
||||||
|
# - Determines the git tag that points to HEAD. If no tag points to HEAD, aborts.
|
||||||
|
# - Builds the Docker image from the top-level Dockerfile.
|
||||||
|
# - Tags the image as kisom/goutils:<TAG> and kisom/goutils:latest.
|
||||||
|
# - Pushes both tags.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./release-docker.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
err() { printf "Error: %s\n" "$*" >&2; }
|
||||||
|
info() { printf "==> %s\n" "$*"; }
|
||||||
|
|
||||||
|
# Ensure we're inside a git repository and operate from the repo root.
|
||||||
|
if ! REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null); then
|
||||||
|
err "This script must be run within a git repository."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
IMAGE_REPO="kisom/goutils"
|
||||||
|
DOCKERFILE_PATH="$REPO_ROOT/Dockerfile"
|
||||||
|
|
||||||
|
if [[ ! -f "$DOCKERFILE_PATH" ]]; then
|
||||||
|
err "Dockerfile not found at repository root: $DOCKERFILE_PATH"
|
||||||
|
err "Create a top-level Dockerfile or adjust this script before releasing."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find tags that point to HEAD.
|
||||||
|
if ! TAGS=$(git tag --points-at HEAD); then
|
||||||
|
err "Unable to query git tags."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$TAGS" ]]; then
|
||||||
|
err "No git tag points at HEAD. Aborting release."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use the first tag if multiple are present; warn the user.
|
||||||
|
# Avoid readarray for broader Bash compatibility (e.g., macOS Bash 3.2).
|
||||||
|
TAG_ARRAY=($TAGS)
|
||||||
|
TAG="${TAG_ARRAY[0]}"
|
||||||
|
|
||||||
|
if (( ${#TAG_ARRAY[@]} > 1 )); then
|
||||||
|
info "Multiple tags point at HEAD: ${TAG_ARRAY[*]}"
|
||||||
|
info "Using first tag: $TAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Releasing Docker image for tag: $TAG"
|
||||||
|
|
||||||
|
IMAGE_TAGGED="$IMAGE_REPO:$TAG"
|
||||||
|
IMAGE_LATEST="$IMAGE_REPO:latest"
|
||||||
|
|
||||||
|
info "Building image from $DOCKERFILE_PATH"
|
||||||
|
docker build -f "$DOCKERFILE_PATH" -t "$IMAGE_TAGGED" -t "$IMAGE_LATEST" "$REPO_ROOT"
|
||||||
|
|
||||||
|
info "Pushing $IMAGE_TAGGED"
|
||||||
|
docker push "$IMAGE_TAGGED"
|
||||||
|
|
||||||
|
info "Pushing $IMAGE_LATEST"
|
||||||
|
docker push "$IMAGE_LATEST"
|
||||||
|
|
||||||
|
info "Release complete."
|
||||||
Reference in New Issue
Block a user