diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..f559d22 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,32 @@ +kind: pipeline +name: default +steps: +- name: build + image: golang:1.17.2 + commands: + - git tag $DRONE_TAG + - bash release.sh + - echo -n "latest,${DRONE_TAG#v}" > .tags +- name: gitea_release + image: plugins/gitea-release + settings: + api_key: + from_secret: gitea_api_key + base_url: https://kumoly.io + files: dist/* + checksum: + - sha256 +- name: docker + image: plugins/docker + settings: + username: + from_secret: hub_user + password: + from_secret: hub_passwd + # auto_tag: true + mtu: 1000 + # purge: true + repo: hub.kumoly.io/tools/configui + registry: hub.kumoly.io +trigger: + event: tag \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e853c41 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM golang:1.17.2-alpine3.14 as builder +RUN apk update && apk add --no-cache git tzdata +WORKDIR /src + +COPY go.mod go.sum /src/ +RUN go mod download +COPY . . +RUN VERSION=$(git describe --tags --abbrev=0) BUILD=$(git rev-parse --short HEAD) && \ + GOOS=linux GOARCH=amd64 \ + go build -ldflags "-X main.Version=${VERSION} -X main.Build=${BUILD} -w" \ + -o /go/bin/breacher + +FROM alpine:3.14 +ENV PATH="/go/bin:${PATH}" +COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=builder /go/bin/breacher /go/bin/breacher +ENTRYPOINT ["/go/bin/breacher"] \ No newline at end of file diff --git a/breacher/ssh.go b/breacher/ssh.go index 09084ce..c8572db 100644 --- a/breacher/ssh.go +++ b/breacher/ssh.go @@ -1,12 +1,18 @@ package breacher import ( + "fmt" + "io" + "io/ioutil" "log" "net" + "os" "strconv" "strings" "github.com/spf13/cobra" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" ) var ( @@ -14,14 +20,19 @@ var ( sKey string ) +func init() { + sshCmd.Flags().StringVarP(&sPasswd, "password", "p", "", "login using password") + sshCmd.Flags().StringVarP(&sKey, "keyfile", "i", "", "login keyfile (path/to/file)") +} + var sshCmd = &cobra.Command{ Use: "tunnel [from address] [to address] [user@host:port]", Short: "ssh tunneling to access remote services", Long: `ssh tunneling to access remote services ex. -breacher forward :8080 kumoly.io:5080 -breacher forward :8080 :8000 -breacher forward --udp :8080 192.168.51.211:53 +breacher tunnel :8080 host:80 user@example.com -p paswd +breacher tunnel :8080 :80 user@example.com -i ~/.ssh/id_rsa +breacher tunnel :8080 kumoly.io:443 user@example.com `, Args: cobra.ExactArgs(3), Run: func(cmd *cobra.Command, args []string) { @@ -42,12 +53,11 @@ breacher forward --udp :8080 192.168.51.211:53 log.Fatalln(err) } if localHost == "" { - localHost = "localhost" + localHost = "0.0.0.0" } if remoteHost == "" { remoteHost = "localhost" } - st := NewSSHTunnel(localHost, localPort, remoteHost, remotePort) split := strings.Split(args[2], "@") if len(split) != 2 { log.Fatalln("ssh host name not valid") @@ -73,22 +83,179 @@ breacher forward --udp :8080 192.168.51.211:53 } } } + var auth ssh.AuthMethod + if sPasswd != "" { + auth = ssh.Password(sPasswd) + } else if sKey != "" { + auth = PrivateKeyFile(sKey) + } else { + auth = SSHAgent() + } + st := NewSSHTunnel( + &Endpoint{localHost, localPort, ""}, + &Endpoint{remoteHost, remotePort, ""}, + &Endpoint{sshHost, sshPort, usr}, + auth, + ) - st.server.Host = sshHost - st.SetPort(sshPort) - st.SetUser(usr) - st.SetPassword("ubuntu") - st.SetDebug(true) - st.SetConnState(func(tun *SSHTun, state ConnState) { - switch state { - case StateStarting: - log.Printf("STATE is Starting") - case StateStarted: - log.Printf("STATE is Started") - case StateStopped: - log.Printf("STATE is Stopped") - } - }) log.Fatalln(st.Start()) }, } + +type Endpoint struct { + Host string + Port int + User string +} + +func (endpoint *Endpoint) String() string { + return fmt.Sprintf("%s:%d", endpoint.Host, endpoint.Port) +} + +type SSHTunnel struct { + Local *Endpoint + Server *Endpoint + Remote *Endpoint + Config *ssh.ClientConfig + Conns []net.Conn + SvrConns []*ssh.Client + isOpen bool + close chan interface{} +} + +func newConnectionWaiter(listener net.Listener, c chan net.Conn) { + conn, err := listener.Accept() + if err != nil { + fmt.Println(err) + return + } + c <- conn +} + +func (tunnel *SSHTunnel) Start() error { + listener, err := net.Listen("tcp", tunnel.Local.String()) + if err != nil { + return err + } + tunnel.isOpen = true + tunnel.Local.Port = listener.Addr().(*net.TCPAddr).Port + + for { + if !tunnel.isOpen { + break + } + + c := make(chan net.Conn) + go newConnectionWaiter(listener, c) + log.Println("listening for new connections...") + + select { + case <-tunnel.close: + log.Println("close signal received, closing...") + tunnel.isOpen = false + case conn := <-c: + tunnel.Conns = append(tunnel.Conns, conn) + log.Println("accepted connection") + go tunnel.forward(conn) + } + } + var total int + total = len(tunnel.Conns) + for i, conn := range tunnel.Conns { + log.Printf("closing the netConn (%d of %d)\n", i+1, total) + err := conn.Close() + if err != nil { + log.Println(err.Error()) + } + } + total = len(tunnel.SvrConns) + for i, conn := range tunnel.SvrConns { + log.Printf("closing the serverConn (%d of %d)\n", i+1, total) + err := conn.Close() + if err != nil { + log.Println(err.Error()) + } + } + err = listener.Close() + if err != nil { + return err + } + log.Println("tunnel closed") + return nil +} + +func (tunnel *SSHTunnel) forward(localConn net.Conn) { + serverConn, err := ssh.Dial("tcp", tunnel.Server.String(), tunnel.Config) + if err != nil { + log.Printf("server dial error: %s\n", err) + return + } + log.Printf("connected to %s (1 of 2)\n", tunnel.Server.String()) + tunnel.SvrConns = append(tunnel.SvrConns, serverConn) + + remoteConn, err := serverConn.Dial("tcp", tunnel.Remote.String()) + if err != nil { + log.Printf("remote dial error: %s\n", err) + return + } + tunnel.Conns = append(tunnel.Conns, remoteConn) + log.Printf("connected to %s (2 of 2)\n", tunnel.Remote.String()) + copyConn := func(writer, reader net.Conn) { + _, err := io.Copy(writer, reader) + if err != nil { + log.Printf("io.Copy error: %s\n", err) + } + } + go copyConn(localConn, remoteConn) + go copyConn(remoteConn, localConn) + +} + +func (tunnel *SSHTunnel) Close() { + tunnel.close <- struct{}{} +} + +// NewSSHTunnel creates a new single-use tunnel. Supplying "0" for localport will use a random port. +func NewSSHTunnel(from, to, server *Endpoint, auth ssh.AuthMethod) *SSHTunnel { + if server.Port == 0 { + server.Port = 22 + } + + sshTunnel := &SSHTunnel{ + Config: &ssh.ClientConfig{ + User: server.User, + Auth: []ssh.AuthMethod{auth}, + HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { + // Always accept key. + return nil + }, + }, + Local: from, + Server: server, + Remote: to, + close: make(chan interface{}), + } + + return sshTunnel +} + +func PrivateKeyFile(file string) ssh.AuthMethod { + buffer, err := ioutil.ReadFile(file) + if err != nil { + return nil + } + + key, err := ssh.ParsePrivateKey(buffer) + if err != nil { + return nil + } + + return ssh.PublicKeys(key) +} + +func SSHAgent() ssh.AuthMethod { + if sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil { + return ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers) + } + return nil +} diff --git a/breacher/ssh_lib.go b/breacher/ssh_lib.go deleted file mode 100644 index 682506a..0000000 --- a/breacher/ssh_lib.go +++ /dev/null @@ -1,499 +0,0 @@ -package breacher - -import ( - "context" - "fmt" - "io" - "log" - "net" - "os" - "sync" - "time" - - "io/ioutil" - "os/user" - - "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/agent" -) - -type Endpoint struct { - Host string - Port int - UnixSocket string -} - -func (e *Endpoint) connectionString() string { - if e.UnixSocket != "" { - return e.UnixSocket - } - return fmt.Sprintf("%s:%d", e.Host, e.Port) -} - -func (e *Endpoint) connectionType() string { - if e.UnixSocket != "" { - return "unix" - } - return "tcp" -} - -// AuthType is the type of authentication to use for SSH. -type AuthType int - -const ( - // AuthTypeKeyFile uses the keys from a SSH key file read from the system. - AuthTypeKeyFile AuthType = iota - // AuthTypeEncryptedKeyFile uses the keys from an encrypted SSH key file read from the system. - AuthTypeEncryptedKeyFile - // AuthTypeKeyReader uses the keys from a SSH key reader. - AuthTypeKeyReader - // AuthTypeEncryptedKeyReader uses the keys from an encrypted SSH key reader. - AuthTypeEncryptedKeyReader - // AuthTypePassword uses a password directly. - AuthTypePassword - // AuthTypeSSHAgent will use registered users in the ssh-agent. - AuthTypeSSHAgent - // AuthTypeAuto tries to get the authentication method automatically. See SSHTun.Start for details on - // this. - AuthTypeAuto -) - -// SSHTun represents a SSH tunnel -type SSHTun struct { - *sync.Mutex - ctx context.Context - cancel context.CancelFunc - errCh chan error - user string - authType AuthType - authKeyFile string - authKeyReader io.Reader - authPassword string - server Endpoint - local Endpoint - remote Endpoint - started bool - timeout time.Duration - debug bool - connState func(*SSHTun, ConnState) -} - -// ConnState represents the state of the SSH tunnel. It's returned to an optional function provided to SetConnState. -type ConnState int - -const ( - // StateStopped represents a stopped tunnel. A call to Start will make the state to transition to StateStarting. - StateStopped ConnState = iota - - // StateStarting represents a tunnel initializing and preparing to listen for connections. - // A successful initialization will make the state to transition to StateStarted, otherwise it will transition to StateStopped. - StateStarting - - // StateStarted represents a tunnel ready to accept connections. - // A call to stop or an error will make the state to transition to StateStopped. - StateStarted -) - -// New creates a new SSH tunnel to the specified server redirecting a port on local localhost to a port on remote localhost. -// By default the SSH connection is made to port 22 as root and using automatic detection of the authentication -// method (see Start for details on this). -// Calling SetPassword will change the authentication to password based. -// Calling SetKeyFile will change the authentication to keyfile based with an optional key file. -// The SSH user and port can be changed with SetUser and SetPort. -// The local and remote hosts can be changed to something different than localhost with SetLocalHost and SetRemoteHost. -// The states of the tunnel can be received throgh a callback function with SetConnState. -func NewSSHTunnel(localHost string, localPort int, remoteHost string, remotePort int) *SSHTun { - return &SSHTun{ - Mutex: &sync.Mutex{}, - server: Endpoint{ - Host: "", - Port: 22, - }, - user: "root", - authType: AuthTypeAuto, - authKeyFile: "", - authPassword: "", - local: Endpoint{ - Host: localHost, - Port: localPort, - }, - remote: Endpoint{ - Host: remoteHost, - Port: remotePort, - }, - started: false, - timeout: time.Second * 15, - debug: false, - } -} - -func NewUnix(localUnixSocket string, server string, remoteUnixSocket string) *SSHTun { - return &SSHTun{ - Mutex: &sync.Mutex{}, - server: Endpoint{ - Host: server, - Port: 22, - }, - user: "root", - authType: AuthTypeAuto, - authKeyFile: "", - authPassword: "", - local: Endpoint{ - UnixSocket: localUnixSocket, - }, - remote: Endpoint{ - UnixSocket: remoteUnixSocket, - }, - started: false, - timeout: time.Second * 15, - debug: false, - } -} - -// SetPort changes the port where the SSH connection will be made. -func (tun *SSHTun) SetPort(port int) { - tun.server.Port = port -} - -// SetUser changes the user used to make the SSH connection. -func (tun *SSHTun) SetUser(user string) { - tun.user = user -} - -// SetKeyFile changes the authentication to key-based and uses the specified file. -// Leaving it empty defaults to the default linux private key location ($HOME/.ssh/id_rsa). -func (tun *SSHTun) SetKeyFile(file string) { - tun.authType = AuthTypeKeyFile - tun.authKeyFile = file -} - -// SetEncryptedKeyFile changes the authentication to encrypted key-based and uses the specified file and password. -// Leaving it empty defaults to the default linux private key location ($HOME/.ssh/id_rsa). -func (tun *SSHTun) SetEncryptedKeyFile(file string, password string) { - tun.authType = AuthTypeEncryptedKeyFile - tun.authKeyFile = file - tun.authPassword = password -} - -// SetKeyReader changes the authentication to key-based and uses the specified reader. -// Leaving it empty defaults to the default linux private key location ($HOME/.ssh/id_rsa). -func (tun *SSHTun) SetKeyReader(reader io.Reader) { - tun.authType = AuthTypeKeyReader - tun.authKeyReader = reader -} - -// SetEncryptedKeyReader changes the authentication to encrypted key-based and uses the specified reader and password. -// Leaving it empty defaults to the default linux private key location ($HOME/.ssh/id_rsa). -func (tun *SSHTun) SetEncryptedKeyReader(reader io.Reader, password string) { - tun.authType = AuthTypeEncryptedKeyReader - tun.authKeyReader = reader - tun.authPassword = password -} - -// SetSSHAgent changes the authentication to ssh-agent. -func (tun *SSHTun) SetSSHAgent() { - tun.authType = AuthTypeSSHAgent -} - -// SetPassword changes the authentication to password-based and uses the specified password. -func (tun *SSHTun) SetPassword(password string) { - tun.authType = AuthTypePassword - tun.authPassword = password -} - -// SetLocalHost sets the local host to redirect (defaults to localhost) -func (tun *SSHTun) SetLocalHost(host string) { - tun.local.Host = host -} - -// SetRemoteHost sets the remote host to redirect (defaults to localhost) -func (tun *SSHTun) SetRemoteHost(host string) { - tun.remote.Host = host -} - -// SetTimeout sets the connection timeouts (defaults to 15 seconds). -func (tun *SSHTun) SetTimeout(timeout time.Duration) { - tun.timeout = timeout -} - -// SetDebug enables or disables log messages (disabled by default). -func (tun *SSHTun) SetDebug(debug bool) { - tun.debug = debug -} - -// SetConnState specifies an optional callback function that is called when a SSH tunnel changes state. -// See the ConnState type and associated constants for details. -func (tun *SSHTun) SetConnState(connStateFun func(*SSHTun, ConnState)) { - tun.connState = connStateFun -} - -// Start starts the SSH tunnel. After this call, all Set* methods will have no effect until Close is called. -// Note on SSH authentication: in case the tunnel's authType is set to AuthTypeAuto the following will happen: -// The default key file will be used, if that doesn't succeed it will try to use the SSH agent. -// If that fails the whole authentication fails. -// That means if you want to use password or encrypted key file authentication, you have to specify that explicitly. -func (tun *SSHTun) Start() error { - tun.Lock() - - if tun.connState != nil { - tun.connState(tun, StateStarting) - } - - // SSH config - config, err := tun.initSSHConfig() - if err != nil { - return tun.errNotStarted(err) - } - - local := tun.local.connectionString() - // Local listener - localList, err := net.Listen(tun.local.connectionType(), local) - if err != nil { - return tun.errNotStarted(fmt.Errorf("local listen on %s failed: %s", local, err.Error())) - } - - // Context and error channel - tun.ctx, tun.cancel = context.WithCancel(context.Background()) - tun.errCh = make(chan error) - - // Accept connections - go func() { - for { - localConn, err := localList.Accept() - if err != nil { - tun.errStarted(fmt.Errorf("local accept on %s failed: %s", local, err.Error())) - break - } - if tun.debug { - log.Printf("Accepted connection from %s", localConn.RemoteAddr().String()) - } - - // Launch the forward - go tun.forward(localConn, config) - } - }() - - // Wait until someone cancels the context and stop accepting connections - go func() { - <-tun.ctx.Done() - localList.Close() - }() - - // Now others can call Stop or fail - if tun.debug { - log.Printf("Listening on %s", local) - } - tun.started = true - if tun.connState != nil { - tun.connState(tun, StateStarted) - } - tun.Unlock() - - // Wait to exit - errFromCh := <-tun.errCh - return errFromCh - -} - -func (tun *SSHTun) errNotStarted(err error) error { - tun.started = false - if tun.connState != nil { - tun.connState(tun, StateStopped) - } - tun.Unlock() - return err -} - -func (tun *SSHTun) errStarted(err error) { - tun.Lock() - if tun.started { - tun.cancel() - if tun.connState != nil { - tun.connState(tun, StateStopped) - } - tun.started = false - tun.errCh <- err - } - tun.Unlock() -} - -func (tun *SSHTun) initSSHConfig() (*ssh.ClientConfig, error) { - config := &ssh.ClientConfig{ - User: tun.user, - HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { - return nil - }, - Timeout: tun.timeout, - } - - authMethod, err := tun.getSSHAuthMethod() - if err != nil { - return nil, err - } - - config.Auth = []ssh.AuthMethod{authMethod} - - return config, nil -} - -func (tun *SSHTun) getSSHAuthMethod() (ssh.AuthMethod, error) { - switch tun.authType { - case AuthTypeKeyFile: - return tun.getSSHAuthMethodForKeyFile(false) - case AuthTypeEncryptedKeyFile: - return tun.getSSHAuthMethodForKeyFile(true) - case AuthTypeKeyReader: - return tun.getSSHAuthMethodForKeyReader(false) - case AuthTypeEncryptedKeyReader: - return tun.getSSHAuthMethodForKeyReader(true) - case AuthTypePassword: - return ssh.Password(tun.authPassword), nil - case AuthTypeSSHAgent: - return tun.getSSHAuthMethodForSSHAgent() - case AuthTypeAuto: - method, err := tun.getSSHAuthMethodForKeyFile(false) - if err != nil { - return tun.getSSHAuthMethodForSSHAgent() - } - return method, nil - default: - return nil, fmt.Errorf("unknown auth type: %d", tun.authType) - } -} - -func (tun *SSHTun) getSSHAuthMethodForKeyFile(encrypted bool) (ssh.AuthMethod, error) { - if tun.authKeyFile == "" { - usr, _ := user.Current() - if usr != nil { - tun.authKeyFile = usr.HomeDir + "/.ssh/id_rsa" - } else { - tun.authKeyFile = "/root/.ssh/id_rsa" - } - } - buf, err := ioutil.ReadFile(tun.authKeyFile) - if err != nil { - return nil, fmt.Errorf("error reading SSH key file %s: %s", tun.authKeyFile, err.Error()) - } - key, err := tun.parsePrivateKey(buf, encrypted) - if err != nil { - return nil, fmt.Errorf("error reading SSH key file %s: %s", tun.authKeyFile, err.Error()) - } - return key, nil -} - -func (tun *SSHTun) getSSHAuthMethodForKeyReader(encrypted bool) (ssh.AuthMethod, error) { - buf, err := ioutil.ReadAll(tun.authKeyReader) - if err != nil { - return nil, fmt.Errorf("error reading from SSH key reader: %s", err.Error()) - } - key, err := tun.parsePrivateKey(buf, encrypted) - if err != nil { - return nil, fmt.Errorf("error reading from SSH key reader: %s", err.Error()) - } - return key, nil -} - -func (tun *SSHTun) parsePrivateKey(buf []byte, encrypted bool) (ssh.AuthMethod, error) { - var key ssh.Signer - var err error - if encrypted { - key, err = ssh.ParsePrivateKeyWithPassphrase(buf, []byte(tun.authPassword)) - if err != nil { - return nil, fmt.Errorf("error parsing encrypted key: %s", err.Error()) - } - } else { - key, err = ssh.ParsePrivateKey(buf) - if err != nil { - return nil, fmt.Errorf("error parsing key: %s", err.Error()) - } - } - return ssh.PublicKeys(key), nil -} - -func (tun *SSHTun) getSSHAuthMethodForSSHAgent() (ssh.AuthMethod, error) { - conn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")) - if err != nil { - return nil, fmt.Errorf("error opening unix socket: %s", err) - } - - agentClient := agent.NewClient(conn) - - signers, err := agentClient.Signers() - if err != nil { - return nil, fmt.Errorf("error getting ssh-agent signers: %s", err) - } - - if len(signers) == 0 { - return nil, fmt.Errorf("no signers from ssh-agent. Use 'ssh-add' to add keys to agent") - } - - return ssh.PublicKeys(signers...), nil -} - -func (tun *SSHTun) forward(localConn net.Conn, config *ssh.ClientConfig) { - defer localConn.Close() - - local := tun.local.connectionString() - server := tun.server.connectionString() - remote := tun.remote.connectionString() - - sshConn, err := ssh.Dial(tun.server.connectionType(), server, config) - if err != nil { - tun.errStarted(fmt.Errorf("SSH connection to %s failed: %s", server, err.Error())) - return - } - defer sshConn.Close() - if tun.debug { - log.Printf("SSH connection to %s done", server) - } - - remoteConn, err := sshConn.Dial(tun.remote.connectionType(), remote) - if err != nil { - if tun.debug { - log.Printf("Remote dial to %s failed: %s", remote, err.Error()) - } - return - } - defer remoteConn.Close() - if tun.debug { - log.Printf("Remote connection to %s done", remote) - } - - connStr := fmt.Sprintf("%s -(tcp)> %s -(ssh)> %s -(tcp)> %s", localConn.RemoteAddr().String(), local, server, remote) - if tun.debug { - log.Printf("SSH tunnel OPEN: %s", connStr) - } - - myCtx, myCancel := context.WithCancel(tun.ctx) - - go func() { - _, err = io.Copy(remoteConn, localConn) - if err != nil { - //log.Printf("Error on io.Copy remote->local on connection %s: %s", connStr, err.Error()) - myCancel() - return - } - }() - - go func() { - _, err = io.Copy(localConn, remoteConn) - if err != nil { - //log.Printf("Error on io.Copy local->remote on connection %s: %s", connStr, err.Error()) - myCancel() - return - } - }() - - <-myCtx.Done() - myCancel() - if tun.debug { - log.Printf("SSH tunnel CLOSE: %s", connStr) - } - -} - -// Stop closes the SSH tunnel and its connections. -// After this call all Set* methods will have effect and Start can be called again. -func (tun *SSHTun) Stop() { - tun.errStarted(nil) -} diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..9d4d306 --- /dev/null +++ b/build.sh @@ -0,0 +1,34 @@ +VERSION=$(git describe --tags --abbrev=0) +if [ $? -ne 0 ]; then VERSION=$DRONE_TAG; fi +BUILD=$(git rev-parse --short HEAD) +if [ $? -ne 0 ]; then BUILD=${DRONE_COMMIT:0:7}; fi + +PROJ=breacher +HUB=hub.kumoly.io +HUB_PROJECT=tools +DIST=dist + +LDFLAGS="-ldflags \"-X main.Version=${VERSION} -X main.Build=${BUILD} -w\"" +FAILURES="" + +PLATFORMS="darwin/amd64 darwin/arm64" +PLATFORMS="$PLATFORMS windows/amd64" +PLATFORMS="$PLATFORMS linux/amd64" + + +for PLATFORM in $PLATFORMS; do + GOOS=${PLATFORM%/*} + GOARCH=${PLATFORM#*/} + BIN_FILENAME="${PROJ}" + if [[ "${GOOS}" == "windows" ]]; then BIN_FILENAME="${BIN_FILENAME}.exe"; fi + CMD="GOOS=${GOOS} GOARCH=${GOARCH} go build ${LDFLAGS} -o ${DIST}/${BIN_FILENAME} $@" + echo "${CMD}" + eval $CMD || FAILURES="${FAILURES} ${PLATFORM}" + sh -c "cd ${DIST} && tar -czf ${PROJ}-${VERSION}-${GOOS}-${GOARCH}.tar.gz ${BIN_FILENAME} && rm ${BIN_FILENAME}" +done + +if [[ "${FAILURES}" != "" ]]; then + echo "" + echo "${SCRIPT_NAME} failed on: ${FAILURES}" + exit 1 +fi \ No newline at end of file