Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f92963c74 | |||
| 51f6d7c74d | |||
| 67bf26c5da | |||
| 62c3db88ef | |||
| bb7749efd1 | |||
| a3a8115279 | |||
| 8ca8538268 | |||
| 155c49cc5e | |||
| dda9fd9f07 | |||
| c251c1e1b5 | |||
| 6eb533f79b |
@@ -419,14 +419,9 @@ builds:
|
||||
|
||||
archives:
|
||||
- formats: [tar.gz]
|
||||
# this name template makes the OS and Arch compatible with the results of `uname`.
|
||||
# archive filename: name_version_os_arch
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_
|
||||
{{- title .Os }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}
|
||||
# use zip for windows archives
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
@@ -439,13 +434,10 @@ changelog:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
|
||||
gitea_urls:
|
||||
api: https://git.wntrmute.dev/api/v1
|
||||
download: https://git.wntrmute.dev
|
||||
# set to true if you use a self-signed certificate
|
||||
skip_tls_verify: false
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: kisom
|
||||
name: goutils
|
||||
footer: >-
|
||||
|
||||
---
|
||||
|
||||
51
CHANGELOG
51
CHANGELOG
@@ -1,5 +1,56 @@
|
||||
CHANGELOG
|
||||
|
||||
v1.14.1 - 2025-11-18
|
||||
|
||||
Added:
|
||||
- build: add missing Dockerfile.
|
||||
|
||||
v1.14.0 - 2025-11-18
|
||||
|
||||
Added:
|
||||
- lib/dialer: introduce proxy-aware dialers and helpers:
|
||||
- NewNetDialer and NewTLSDialer honoring SOCKS5_PROXY, HTTPS_PROXY, HTTP_PROXY
|
||||
(case-insensitive) with precedence SOCKS5 > HTTPS > HTTP.
|
||||
- DialTCP and DialTLS convenience functions; DialTLS performs a TLS handshake
|
||||
and returns a concrete *tls.Conn.
|
||||
- NewHTTPClient: returns a proxy-aware *http.Client. Uses SOCKS5 proxy when
|
||||
configured (disables HTTP(S) proxying to avoid double-proxying); otherwise
|
||||
relies on http.ProxyFromEnvironment (respects HTTP(S)_PROXY and NO_PROXY).
|
||||
- build: the releasse-docker.sh builds and pushes the correct Docker images.
|
||||
|
||||
Changed:
|
||||
- cmd: migrate tools to new proxy-aware helpers where appropriate:
|
||||
- certchain, stealchain, tlsinfo: use lib.DialTLS.
|
||||
- cert-revcheck: use lib.DialTLS for site connects and a proxy-aware
|
||||
HTTP client for OCSP/CRL fetches.
|
||||
- rhash: use proxy-aware HTTP client for downloads.
|
||||
- lib/fetch: migrate from certlib/fetch.go to lib/fetch.go and use DialTLS
|
||||
under the hood.
|
||||
- go.mod: add golang.org/x/net dependency (for SOCKS5 support) and align x/crypto.
|
||||
|
||||
Notes:
|
||||
- HTTP(S) proxy CONNECT supports optional basic auth via proxy URL credentials.
|
||||
- HTTPS proxies are TLS-wrapped prior to CONNECT.
|
||||
- Timeouts apply to TCP connects, proxy handshakes, and TLS handshakes; context
|
||||
cancellation is honored.
|
||||
- Some commands retain bespoke dialing (e.g., IPv6-only or unix sockets) and
|
||||
were intentionally left unchanged.
|
||||
|
||||
v1.13.6 - 2025-11-18
|
||||
|
||||
Changed:
|
||||
- build: removing gitea stuff.
|
||||
|
||||
v1.13.5 - 2025-11-18
|
||||
|
||||
Changed:
|
||||
- build: updating goreleaser config.
|
||||
|
||||
v1.13.4 - 2025-11-18
|
||||
|
||||
Changed:
|
||||
- build: updating goreleaser config.
|
||||
|
||||
v1.13.3 - 2025-11-18
|
||||
|
||||
Added:
|
||||
|
||||
38
Dockerfile
Normal file
38
Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# ----- Builder stage: build all cmd/... tools -----
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
# Install necessary build dependencies for fetching modules
|
||||
RUN apk add --no-cache git ca-certificates && update-ca-certificates
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
# Cache modules
|
||||
COPY go.mod go.sum ./
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
go mod download
|
||||
|
||||
# Copy the rest of the source
|
||||
COPY . .
|
||||
|
||||
# Build and install all commands under ./cmd/... into /out
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOBIN=/out
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
go install ./cmd/...
|
||||
|
||||
# ----- Final runtime image: minimal alpine with tools installed -----
|
||||
FROM alpine:3.20
|
||||
|
||||
# Ensure common utilities are present
|
||||
RUN apk add --no-cache bash curl ca-certificates && update-ca-certificates
|
||||
|
||||
# Copy binaries from builder
|
||||
COPY --from=builder /out/ /usr/local/bin/
|
||||
|
||||
# Working directory for mounting the host CWD
|
||||
WORKDIR /work
|
||||
|
||||
# Default command shows available tools if run without args
|
||||
CMD ["/bin/sh", "-lc", "echo 'Tools installed:' && ls -1 /usr/local/bin && echo '\nMount your project with: docker run --rm -it -v $PWD:/work IMAGE <tool> ...'"]
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -16,6 +15,7 @@ import (
|
||||
hosts "git.wntrmute.dev/kyle/goutils/certlib/hosts"
|
||||
"git.wntrmute.dev/kyle/goutils/certlib/revoke"
|
||||
"git.wntrmute.dev/kyle/goutils/fileutil"
|
||||
"git.wntrmute.dev/kyle/goutils/lib"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -38,8 +38,10 @@ func main() {
|
||||
flag.Parse()
|
||||
|
||||
revoke.HardFail = hardfail
|
||||
// Set HTTP client timeout for revocation library
|
||||
revoke.HTTPClient.Timeout = timeout
|
||||
// Build a proxy-aware HTTP client for OCSP/CRL fetches
|
||||
if httpClient, err := lib.NewHTTPClient(lib.DialerOpts{Timeout: timeout}); err == nil {
|
||||
revoke.HTTPClient = httpClient
|
||||
}
|
||||
|
||||
if flag.NArg() == 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
|
||||
}
|
||||
|
||||
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)
|
||||
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 {
|
||||
return strUnknown, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
tconn, ok := conn.(*tls.Conn)
|
||||
if !ok {
|
||||
return strUnknown, errors.New("connection is not TLS")
|
||||
}
|
||||
|
||||
state := tconn.ConnectionState()
|
||||
state := conn.ConnectionState()
|
||||
if len(state.PeerCertificates) == 0 {
|
||||
return strUnknown, errors.New("no peer certificates presented")
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/die"
|
||||
"git.wntrmute.dev/kyle/goutils/lib"
|
||||
)
|
||||
|
||||
var hasPort = regexp.MustCompile(`:\d+$`)
|
||||
@@ -23,13 +24,9 @@ func main() {
|
||||
server += ":443"
|
||||
}
|
||||
|
||||
d := &tls.Dialer{Config: &tls.Config{}} // #nosec G402
|
||||
nc, err := d.DialContext(context.Background(), "tcp", server)
|
||||
// Use proxy-aware TLS dialer
|
||||
conn, err := lib.DialTLS(context.Background(), server, lib.DialerOpts{TLSConfig: &tls.Config{}}) // #nosec G402
|
||||
die.If(err)
|
||||
conn, ok := nc.(*tls.Conn)
|
||||
if !ok {
|
||||
die.With("invalid TLS connection (not a *tls.Conn)")
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
|
||||
"github.com/kr/text"
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||
"git.wntrmute.dev/kyle/goutils/lib"
|
||||
)
|
||||
|
||||
@@ -351,14 +350,14 @@ func main() {
|
||||
flag.BoolVar(&leafOnly, "l", false, "only show the leaf certificate")
|
||||
flag.Parse()
|
||||
|
||||
opts := &certlib.FetcherOpts{
|
||||
opts := &lib.FetcherOpts{
|
||||
SkipVerify: true,
|
||||
Roots: nil,
|
||||
}
|
||||
|
||||
for _, filename := range flag.Args() {
|
||||
fmt.Fprintf(os.Stdout, "--%s ---%s", filename, "\n")
|
||||
certs, err := certlib.GetCertificateChain(filename, opts)
|
||||
certs, err := lib.GetCertificateChain(filename, opts)
|
||||
if err != nil {
|
||||
_, _ = lib.Warn(err, "couldn't read certificate")
|
||||
continue
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||
"git.wntrmute.dev/kyle/goutils/die"
|
||||
"git.wntrmute.dev/kyle/goutils/lib"
|
||||
)
|
||||
@@ -75,7 +74,7 @@ func checkCert(cert *x509.Certificate) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
opts := &certlib.FetcherOpts{}
|
||||
opts := &lib.FetcherOpts{}
|
||||
|
||||
flag.BoolVar(&opts.SkipVerify, "k", false, "skip server verification")
|
||||
flag.BoolVar(&warnOnly, "q", false, "only warn about expiring certs")
|
||||
@@ -83,7 +82,7 @@ func main() {
|
||||
flag.Parse()
|
||||
|
||||
for _, file := range flag.Args() {
|
||||
certs, err := certlib.GetCertificateChain(file, opts)
|
||||
certs, err := lib.GetCertificateChain(file, opts)
|
||||
if err != nil {
|
||||
_, _ = lib.Warn(err, "while parsing certificates")
|
||||
continue
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||
"git.wntrmute.dev/kyle/goutils/die"
|
||||
"git.wntrmute.dev/kyle/goutils/lib"
|
||||
)
|
||||
@@ -32,7 +31,7 @@ func serialString(cert *x509.Certificate, mode lib.HexEncodeMode) string {
|
||||
}
|
||||
|
||||
func main() {
|
||||
opts := &certlib.FetcherOpts{}
|
||||
opts := &lib.FetcherOpts{}
|
||||
displayAs := flag.String("d", "int", "display mode (int, hex, uhex)")
|
||||
showExpiry := flag.Bool("e", false, "show expiry date")
|
||||
flag.BoolVar(&opts.SkipVerify, "k", false, "skip server verification")
|
||||
@@ -41,7 +40,7 @@ func main() {
|
||||
displayMode := parseDisplayMode(*displayAs)
|
||||
|
||||
for _, arg := range flag.Args() {
|
||||
cert, err := certlib.GetCertificate(arg, opts)
|
||||
cert, err := lib.GetCertificate(arg, opts)
|
||||
die.If(err)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
opts := &certlib.FetcherOpts{
|
||||
opts := &lib.FetcherOpts{
|
||||
Roots: combinedPool,
|
||||
SkipVerify: cfg.skipVerify,
|
||||
}
|
||||
|
||||
chain, err := certlib.GetCertificateChain(flag.Arg(0), opts)
|
||||
chain, err := lib.GetCertificateChain(flag.Arg(0), opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/ahash"
|
||||
"git.wntrmute.dev/kyle/goutils/die"
|
||||
@@ -82,8 +83,13 @@ func main() {
|
||||
_, _ = lib.Warn(reqErr, "building request for %s", remote)
|
||||
continue
|
||||
}
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
// Use proxy-aware HTTP client with a reasonable timeout for connects/handshakes
|
||||
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 {
|
||||
_, _ = lib.Warn(err, "fetching %s", remote)
|
||||
continue
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/die"
|
||||
"git.wntrmute.dev/kyle/goutils/lib"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -44,15 +45,10 @@ func main() {
|
||||
if err != nil {
|
||||
site += ":443"
|
||||
}
|
||||
d := &tls.Dialer{Config: cfg}
|
||||
nc, err := d.DialContext(context.Background(), "tcp", site)
|
||||
// Use proxy-aware TLS dialer
|
||||
conn, err := lib.DialTLS(context.Background(), site, lib.DialerOpts{TLSConfig: cfg})
|
||||
die.If(err)
|
||||
|
||||
conn, ok := nc.(*tls.Conn)
|
||||
if !ok {
|
||||
die.With("invalid TLS connection (not a *tls.Conn)")
|
||||
}
|
||||
|
||||
cs := conn.ConnectionState()
|
||||
var chain []byte
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/certlib/hosts"
|
||||
"git.wntrmute.dev/kyle/goutils/die"
|
||||
"git.wntrmute.dev/kyle/goutils/lib"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -20,18 +21,14 @@ func main() {
|
||||
hostPort, err := hosts.ParseHost(os.Args[1])
|
||||
die.If(err)
|
||||
|
||||
d := &tls.Dialer{Config: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}} // #nosec G402
|
||||
|
||||
nc, err := d.DialContext(context.Background(), "tcp", hostPort.String())
|
||||
// Use proxy-aware TLS dialer; skip verification as before
|
||||
conn, err := lib.DialTLS(
|
||||
context.Background(),
|
||||
hostPort.String(),
|
||||
lib.DialerOpts{TLSConfig: &tls.Config{InsecureSkipVerify: true}},
|
||||
) // #nosec G402
|
||||
die.If(err)
|
||||
|
||||
conn, ok := nc.(*tls.Conn)
|
||||
if !ok {
|
||||
die.With("invalid TLS connection (not a *tls.Conn)")
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
state := conn.ConnectionState()
|
||||
|
||||
3
go.mod
3
go.mod
@@ -6,7 +6,8 @@ require (
|
||||
github.com/hashicorp/go-syslog v1.0.0
|
||||
github.com/kr/text v0.2.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
|
||||
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=
|
||||
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.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/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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/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/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
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 (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||
"git.wntrmute.dev/kyle/goutils/certlib/hosts"
|
||||
"git.wntrmute.dev/kyle/goutils/fileutil"
|
||||
"git.wntrmute.dev/kyle/goutils/lib"
|
||||
)
|
||||
|
||||
// FetcherOpts are options for fetching certificates. They are only applicable to ServerFetcher.
|
||||
@@ -21,6 +20,13 @@ type FetcherOpts struct {
|
||||
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
|
||||
// currently supports fetching from a server or a file.
|
||||
type Fetcher interface {
|
||||
@@ -65,29 +71,20 @@ func ParseServer(host string) (*ServerFetcher, error) {
|
||||
}
|
||||
|
||||
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) {
|
||||
config := &tls.Config{
|
||||
InsecureSkipVerify: sf.insecure, // #nosec G402 - no shit sherlock
|
||||
RootCAs: sf.roots,
|
||||
opts := DialerOpts{
|
||||
TLSConfig: &tls.Config{
|
||||
InsecureSkipVerify: sf.insecure, // #nosec G402 - no shit sherlock
|
||||
RootCAs: sf.roots,
|
||||
},
|
||||
}
|
||||
|
||||
dialer := &tls.Dialer{
|
||||
Config: config,
|
||||
}
|
||||
|
||||
hostSpec := net.JoinHostPort(sf.host, lib.Itoa(sf.port, -1))
|
||||
|
||||
netConn, err := dialer.DialContext(context.Background(), "tcp", hostSpec)
|
||||
conn, err := DialTLS(context.Background(), net.JoinHostPort(sf.host, Itoa(sf.port, -1)), opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dialing server: %w", err)
|
||||
}
|
||||
|
||||
conn, ok := netConn.(*tls.Conn)
|
||||
if !ok {
|
||||
return nil, errors.New("connection is not TLS")
|
||||
return nil, fmt.Errorf("failed to dial server: %w", err)
|
||||
}
|
||||
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 ParseCertificatesPEM(certData)
|
||||
return certlib.ParseCertificatesPEM(certData)
|
||||
}
|
||||
|
||||
certs, err := LoadCertificates(ff.path)
|
||||
certs, err := certlib.LoadCertificates(ff.path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load chain: %w", err)
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// Package lib contains functions useful for most programs.
|
||||
package lib
|
||||
|
||||
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