diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 15a03e2d..dadeb46d 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -17,7 +17,14 @@ jobs: with: submodules: 'recursive' + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: E2E run: docker-compose up --build --abort-on-container-exit - working-directory: sshpiperd/e2e + working-directory: e2e + env: + COMPOSE_DOCKER_CLI_BUILD: "1" + DOCKER_BUILDKIT: "1" + diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 4b37f566..2216c8db 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -37,11 +37,11 @@ jobs: with: args: --timeout=10m -E gofmt - - name: Test + - name: Test lib ssh run: go test -v -race -cover ./... working-directory: crypto/ssh - - name: Test + - name: Test sshpiper run: go test -v -race -cover ./... - name: create release diff --git a/Dockerfile b/Dockerfile index f3e55293..1e10db6d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,20 @@ -FROM golang:1.18-stretch as builder +FROM golang:1.18-bullseye as builder ARG VER=devel ENV CGO_ENABLED=0 -# thanks to https://github.com/montanaflynn/golang-docker-cache RUN mkdir -p /cache/crypto COPY crypto /cache/crypto COPY go.mod go.sum /cache/ WORKDIR /cache -RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get +RUN go mod download RUN mkdir -p /out/plugins ADD . /src/ WORKDIR /src -RUN go build -o /out -ldflags "-X main.mainver=$VER" ./cmd/... -RUN go build -o /out/plugins -ldflags "-X main.mainver=$VER" ./plugin/... +RUN --mount=type=cache,target=/root/.cache/go-build go build -o /out -ldflags "-X main.mainver=$VER" ./cmd/... +RUN --mount=type=cache,target=/root/.cache/go-build go build -o /out/plugins -ldflags "-X main.mainver=$VER" ./plugin/... FROM busybox LABEL maintainer="Boshi Lian" diff --git a/e2e/Dockerfile_e2e b/e2e/Dockerfile_e2e new file mode 100644 index 00000000..09d17927 --- /dev/null +++ b/e2e/Dockerfile_e2e @@ -0,0 +1,11 @@ +FROM golang:1.18-bullseye as builder + +ARG VER=devel + +ENV CGO_ENABLED=0 + +RUN mkdir -p /cache/crypto +COPY crypto /cache/crypto +COPY go.mod go.sum /cache/ +WORKDIR /cache +RUN go mod download \ No newline at end of file diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml new file mode 100644 index 00000000..06873cf9 --- /dev/null +++ b/e2e/docker-compose.yml @@ -0,0 +1,43 @@ +version: '3' + +services: + host-password: + image: lscr.io/linuxserver/openssh-server:latest + environment: + - PASSWORD_ACCESS=true + - USER_PASSWORD=pass + - USER_NAME=user + volumes: + - shared:/shared + + piper-fixed: + environment: + - "SSHPIPERD_LOG_LEVEL=trace" + build: ../ + command: + - "/sshpiperd/sshpiperd" + - "/sshpiperd/plugins/fixed" + - "--target" + - "host-password:2222" + depends_on: + - host-password + + + testrunner: + build: + context: ../ + dockerfile: e2e/Dockerfile_e2e + volumes: + - ..:/src + - shared:/shared + command: ["go", "test", "-v"] + working_dir: /src/e2e + depends_on: + - host-password + - piper-fixed + +volumes: + shared: + driver_opts: + type: tmpfs + device: tmpfs \ No newline at end of file diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go new file mode 100644 index 00000000..4b6d05c6 --- /dev/null +++ b/e2e/e2e_test.go @@ -0,0 +1,141 @@ +// run with docker-compose up --build --abort-on-container-exit + +package e2e_test + +import ( + "bufio" + "bytes" + "fmt" + "io" + "log" + "net" + "os" + "os/exec" + "strings" + "syscall" + "testing" + "time" + + "github.com/creack/pty" + "github.com/google/uuid" +) + +const waitTimeout = time.Second * 10 + +func waitForEndpointReady(addr string) { + now := time.Now() + for { + if time.Since(now) > waitTimeout { + log.Panic("timeout waiting for endpoint " + addr) + } + + conn, err := net.Dial("tcp", addr) + if err == nil { + log.Printf("endpoint %s is ready", addr) + conn.Close() + break + } + time.Sleep(time.Second) + } +} + +func runCmd(cmd string, args ...string) (*exec.Cmd, io.Writer, io.Reader, error) { + c := exec.Command(cmd, args...) + c.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGTERM} + f, err := pty.Start(c) + if err != nil { + return nil, nil, nil, err + } + + var buf bytes.Buffer + r := io.TeeReader(f, &buf) + go io.Copy(os.Stdout, r) + + log.Printf("starting %v", c.Args) + + go c.Wait() + + return c, f, &buf, nil +} + +func enterPassword(stdin io.Writer, stdout io.Reader, password string) { + st := time.Now() + for { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + line := scanner.Text() + if strings.Contains(line, "'s password") { + stdin.Write([]byte(fmt.Sprintf("%v\n", password))) + log.Printf("got password prompt, sending password") + return + } + } + + if time.Since(st) > waitTimeout { + log.Panic("timeout waiting for password prompt") + return + } + } +} + +func checkSharedFileContent(t *testing.T, targetfie string, expected string) { + f, err := os.Open(fmt.Sprintf("/shared/%v", targetfie)) + if err != nil { + t.Errorf("failed to open shared file, %v", err) + } + defer f.Close() + + b, err := io.ReadAll(f) + if err != nil { + t.Errorf("failed to read shared file, %v", err) + } + + if string(b) != expected { + t.Errorf("shared file content mismathc, expected %v, got %v", expected, string(b)) + } +} + +func TestMain(m *testing.M) { + runCmd("ssh", "-V") + + for _, ep := range []string{ + "host-password:2222", + } { + waitForEndpointReady(ep) + } + + os.Exit(m.Run()) +} + +func TestFixed(t *testing.T) { + waitForEndpointReady("piper-fixed:2222") + + randtext := uuid.New().String() + targetfie := uuid.New().String() + + c, stdin, stdout, err := runCmd( + "ssh", + "-v", + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + "-p", + "2222", + "-l", + "user", + "piper-fixed", + fmt.Sprintf(`sh -c "echo -n %v > /shared/%v"`, randtext, targetfie), + ) + + if err != nil { + t.Errorf("failed to ssh to piper-fixed, %v", err) + } + + defer c.Process.Kill() + enterPassword(stdin, stdout, "pass") + + time.Sleep(time.Second) // wait for file flush + + checkSharedFileContent(t, targetfie, randtext) +} diff --git a/go.mod b/go.mod index c9d7a8a1..bb2a758a 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ replace golang.org/x/crypto => ./crypto require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0 + github.com/creack/pty v1.1.18 github.com/google/uuid v1.3.0 github.com/microsoft/kiota-authentication-azure-go v0.2.1 github.com/microsoftgraph/msgraph-sdk-go v0.23.0 diff --git a/go.sum b/go.sum index bc380f62..6aeab57a 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,8 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=