From 01af95dd4f5763aa25d7617496df66423040d38a Mon Sep 17 00:00:00 2001 From: David Juhasz Date: Tue, 17 Oct 2023 17:45:59 -0700 Subject: [PATCH] Add an SFTP client package - Add an SFTP client interface definition - Implement an SFTP client upload method using native Go SSH and SFTP packages - Add SFTP configuration with some default values - Generate a client mock with mockgen - Add SFTP client tests against a test SFTP server --- Makefile | 5 +- go.mod | 7 +- go.sum | 10 + go.work.sum | 16 - internal/sftp/client.go | 17 ++ internal/sftp/config.go | 62 ++++ internal/sftp/fake/mock_sftp.go | 78 +++++ internal/sftp/goclient.go | 96 ++++++ internal/sftp/goclient_test.go | 287 ++++++++++++++++++ internal/sftp/ssh.go | 63 ++++ internal/sftp/testdata/authorized_keys | 2 + .../sftp/testdata/clientkeys/test_ed25519 | 7 + .../sftp/testdata/clientkeys/test_ed25519.pub | 1 + .../sftp/testdata/clientkeys/test_pass_rsa | 50 +++ .../testdata/clientkeys/test_pass_rsa.pub | 1 + .../sftp/testdata/clientkeys/test_unk_ed25519 | 7 + .../testdata/clientkeys/test_unk_ed25519.pub | 1 + internal/sftp/testdata/empty_file | 0 internal/sftp/testdata/known_hosts | 1 + internal/sftp/testdata/serverkeys/test_rsa | 49 +++ .../sftp/testdata/serverkeys/test_rsa.pub | 1 + 21 files changed, 741 insertions(+), 20 deletions(-) create mode 100644 internal/sftp/client.go create mode 100644 internal/sftp/config.go create mode 100644 internal/sftp/fake/mock_sftp.go create mode 100644 internal/sftp/goclient.go create mode 100644 internal/sftp/goclient_test.go create mode 100644 internal/sftp/ssh.go create mode 100644 internal/sftp/testdata/authorized_keys create mode 100644 internal/sftp/testdata/clientkeys/test_ed25519 create mode 100644 internal/sftp/testdata/clientkeys/test_ed25519.pub create mode 100644 internal/sftp/testdata/clientkeys/test_pass_rsa create mode 100644 internal/sftp/testdata/clientkeys/test_pass_rsa.pub create mode 100644 internal/sftp/testdata/clientkeys/test_unk_ed25519 create mode 100644 internal/sftp/testdata/clientkeys/test_unk_ed25519.pub create mode 100644 internal/sftp/testdata/empty_file create mode 100644 internal/sftp/testdata/known_hosts create mode 100644 internal/sftp/testdata/serverkeys/test_rsa create mode 100644 internal/sftp/testdata/serverkeys/test_rsa.pub diff --git a/Makefile b/Makefile index e2d21a571..55fb92440 100644 --- a/Makefile +++ b/Makefile @@ -101,13 +101,14 @@ gen-dashboard-client: gen-mock: # @HELP Generate mocks. gen-mock: $(MOCKGEN) + mockgen -typed -destination=./internal/api/auth/fake/mock_ticket_store.go -package=fake github.com/artefactual-sdps/enduro/internal/api/auth TicketStore mockgen -typed -destination=./internal/package_/fake/mock_package_.go -package=fake github.com/artefactual-sdps/enduro/internal/package_ Service + mockgen -typed -destination=./internal/persistence/fake/mock_persistence.go -package=fake github.com/artefactual-sdps/enduro/internal/persistence Service + mockgen -typed -destination=./internal/sftp/fake/mock_sftp.go -package=fake github.com/artefactual-sdps/enduro/internal/sftp Service mockgen -typed -destination=./internal/storage/fake/mock_storage.go -package=fake github.com/artefactual-sdps/enduro/internal/storage Service mockgen -typed -destination=./internal/storage/persistence/fake/mock_persistence.go -package=fake github.com/artefactual-sdps/enduro/internal/storage/persistence Storage mockgen -typed -destination=./internal/upload/fake/mock_upload.go -package=fake github.com/artefactual-sdps/enduro/internal/upload Service mockgen -typed -destination=./internal/watcher/fake/mock_watcher.go -package=fake github.com/artefactual-sdps/enduro/internal/watcher Service - mockgen -typed -destination=./internal/api/auth/fake/mock_ticket_store.go -package=fake github.com/artefactual-sdps/enduro/internal/api/auth TicketStore - mockgen -typed -destination=./internal/persistence/fake/mock_persistence.go -package=fake github.com/artefactual-sdps/enduro/internal/persistence Service gen-ent: # @HELP Generate Ent assets. gen-ent: $(ENT) diff --git a/go.mod b/go.mod index 218e823a3..c8231548f 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,9 @@ require ( github.com/aws/aws-sdk-go v1.45.25 github.com/coreos/go-oidc/v3 v3.7.0 github.com/cyphar/filepath-securejoin v0.2.4 + github.com/dolmen-go/contextio v1.0.0 github.com/fsnotify/fsnotify v1.6.0 + github.com/gliderlabs/ssh v0.3.5 github.com/go-logr/logr v1.2.4 github.com/go-sql-driver/mysql v1.7.1 github.com/golang-migrate/migrate/v4 v4.16.2 @@ -27,6 +29,7 @@ require ( github.com/nyudlts/go-bagit v0.2.0-alpha github.com/oklog/run v1.1.0 github.com/otiai10/copy v1.14.0 + github.com/pkg/sftp v1.13.1 github.com/prometheus/client_golang v1.17.0 github.com/radovskyb/watcher v1.0.7 github.com/redis/go-redis/v9 v9.2.1 @@ -42,6 +45,7 @@ require ( goa.design/goa/v3 v3.13.2 goa.design/plugins/v3 v3.13.2 gocloud.dev v0.34.0 + golang.org/x/crypto v0.14.0 google.golang.org/grpc v1.59.0 gopkg.in/square/go-jose.v2 v2.6.0 gotest.tools/v3 v3.5.1 @@ -52,6 +56,7 @@ require ( github.com/agext/levenshtein v1.2.3 // indirect github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect github.com/andybalholm/brotli v1.0.4 // indirect + github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/aws/aws-sdk-go-v2 v1.20.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.11 // indirect @@ -113,7 +118,6 @@ require ( github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pierrec/lz4/v4 v4.1.16 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pkg/sftp v1.13.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect github.com/prometheus/common v0.44.0 // indirect @@ -136,7 +140,6 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/net v0.17.0 // indirect diff --git a/go.sum b/go.sum index 072afa95d..9d798a6f4 100644 --- a/go.sum +++ b/go.sum @@ -800,6 +800,8 @@ github.com/alicebob/miniredis/v2 v2.31.0/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CAS github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= @@ -905,6 +907,8 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dolmen-go/contextio v1.0.0 h1:bNfCo4gsRIhMeo6Z1ImXzkxZG81B6I5t2fUFJjphdAU= +github.com/dolmen-go/contextio v1.0.0/go.mod h1:cxc20xI7fOgsFHWgt+PenlDDnMcrvh7Ocuj5hEFIdEk= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= @@ -938,6 +942,8 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS github.com/getkin/kin-openapi v0.120.0 h1:MqJcNJFrMDFNc07iwE8iFC5eT2k/NPUFDIpNeiZv8Jg= github.com/getkin/kin-openapi v0.120.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= +github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= @@ -1403,6 +1409,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= @@ -1519,6 +1526,7 @@ golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= @@ -1657,6 +1665,7 @@ golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1672,6 +1681,7 @@ golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= diff --git a/go.work.sum b/go.work.sum index 8364948c1..23ddc4be3 100644 --- a/go.work.sum +++ b/go.work.sum @@ -193,7 +193,6 @@ github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HR github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI= github.com/apache/arrow/go/v11 v11.0.0 h1:hqauxvFQxww+0mEU/2XHG6LT7eZternCZq+A5Yly2uM= @@ -303,7 +302,6 @@ github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkF github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= github.com/getkin/kin-openapi v0.114.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= -github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ= github.com/go-fonts/latin-modern v0.2.0 h1:5/Tv1Ek/QCr20C6ZOz15vw3g7GELYL98KWr8Hgo+3vk= github.com/go-fonts/liberation v0.2.0 h1:jAkAWJP4S+OsrPLZM4/eC9iW7CtHy+HBXrEwZXWo5VM= @@ -344,7 +342,6 @@ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2V github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= -github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -380,8 +377,6 @@ github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMW github.com/hashicorp/consul/api v1.20.0/go.mod h1:nR64eD44KQ59Of/ECwt2vUmIK2DKsDzAwTmwmLl8Wpo= github.com/hashicorp/consul/api v1.25.1 h1:CqrdhYzc8XZuPnhIYZWH45toM0LB9ZeYr/gvpLVI3PE= github.com/hashicorp/consul/api v1.25.1/go.mod h1:iiLVwR/htV7mas/sy0O+XSuEnrdBUUydemjxcUrAt4g= -github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= -github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= @@ -467,11 +462,6 @@ github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= @@ -659,7 +649,6 @@ go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= @@ -673,7 +662,6 @@ golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= @@ -684,11 +672,9 @@ golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E= @@ -702,8 +688,6 @@ google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go. google.golang.org/genproto/googleapis/bytestream v0.0.0-20230720185612-659f7aaaa771/go.mod h1:3QoBVwTHkXbY1oRGzlhwhOykfcATQN43LJ6iT8Wy8kE= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230920204549-e6e6cdab5c13 h1:AzcXcS6RbpBm65S0+/F78J9hFCL0/GZWp8oCRZod780= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:qDbnxtViX5J6CvFbxeNUSzKgVlDLJ/6L+caxye9+Flo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230720185612-659f7aaaa771/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= diff --git a/internal/sftp/client.go b/internal/sftp/client.go new file mode 100644 index 000000000..5050eec75 --- /dev/null +++ b/internal/sftp/client.go @@ -0,0 +1,17 @@ +package sftp + +import ( + "context" + "io" +) + +// A Client manages the transmission of data over SFTP. +// +// Implementations of the Client interface handle the connection details, +// authentication, and other intricacies associated with different SFTP +// servers and protocols. +type Client interface { + // Upload transfers data from the provided source reader to a specified + // destination on the SFTP server. + Upload(ctx context.Context, src io.Reader, dest string) (bytes int64, err error) +} diff --git a/internal/sftp/config.go b/internal/sftp/config.go new file mode 100644 index 000000000..e3cb84389 --- /dev/null +++ b/internal/sftp/config.go @@ -0,0 +1,62 @@ +package sftp + +import ( + "os" + "path/filepath" +) + +type Config struct { + // Host address, e.g. 127.0.0.1 (default), sftp.example.org. + Host string + + // User name. + User string + + // Host port (default: 22). + Port string + + // Path to known_hosts file as per https://linux.die.net/man/8/sshd + // "SSH_KNOWN_HOSTS FILE FORMAT" (default: "$HOME/.ssh/known_hosts"). The + // known_hosts file must include the public key of the SFTP server for + // authentication to succeed. + KnownHostsFile string + + // Private key used for authentication. + PrivateKey PrivateKey + + // Default directory on SFTP server for file transfers. + RemoteDir string +} + +type PrivateKey struct { + // Path to private key file used for authentication (default: + // "$HOME/.ssh/id_rsa") + Path string + + // Passphrase (if any) used to decrypt private key. + Passphrase string +} + +// SetDefaults sets default values for some configs. +func (c *Config) SetDefaults() { + if c.Host == "" { + c.Host = "localhost" + } + + if c.Port == "" { + c.Port = "22" + } + + home, err := os.UserHomeDir() + if err != nil { + return // Don't set default paths if homedir is unknown. + } + + if c.KnownHostsFile == "" { + c.KnownHostsFile = filepath.Join(home, ".ssh", "known_hosts") + } + + if c.PrivateKey.Path == "" { + c.PrivateKey.Path = filepath.Join(home, ".ssh", "id_rsa") + } +} diff --git a/internal/sftp/fake/mock_sftp.go b/internal/sftp/fake/mock_sftp.go new file mode 100644 index 000000000..82b2dfb80 --- /dev/null +++ b/internal/sftp/fake/mock_sftp.go @@ -0,0 +1,78 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/artefactual-sdps/enduro/internal/sftp (interfaces: Service) +// +// Generated by this command: +// +// mockgen -typed -destination=./internal/sftp/fake/mock_sftp.go -package=fake github.com/artefactual-sdps/enduro/internal/sftp Service +// +// Package fake is a generated GoMock package. +package fake + +import ( + io "io" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockService is a mock of Service interface. +type MockService struct { + ctrl *gomock.Controller + recorder *MockServiceMockRecorder +} + +// MockServiceMockRecorder is the mock recorder for MockService. +type MockServiceMockRecorder struct { + mock *MockService +} + +// NewMockService creates a new mock instance. +func NewMockService(ctrl *gomock.Controller) *MockService { + mock := &MockService{ctrl: ctrl} + mock.recorder = &MockServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockService) EXPECT() *MockServiceMockRecorder { + return m.recorder +} + +// Upload mocks base method. +func (m *MockService) Upload(arg0 io.Reader, arg1 string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Upload", arg0, arg1) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Upload indicates an expected call of Upload. +func (mr *MockServiceMockRecorder) Upload(arg0, arg1 any) *ServiceUploadCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upload", reflect.TypeOf((*MockService)(nil).Upload), arg0, arg1) + return &ServiceUploadCall{Call: call} +} + +// ServiceUploadCall wrap *gomock.Call +type ServiceUploadCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *ServiceUploadCall) Return(arg0 int64, arg1 error) *ServiceUploadCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *ServiceUploadCall) Do(f func(io.Reader, string) (int64, error)) *ServiceUploadCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *ServiceUploadCall) DoAndReturn(f func(io.Reader, string) (int64, error)) *ServiceUploadCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/internal/sftp/goclient.go b/internal/sftp/goclient.go new file mode 100644 index 000000000..3c6772edd --- /dev/null +++ b/internal/sftp/goclient.go @@ -0,0 +1,96 @@ +package sftp + +import ( + "context" + "errors" + "fmt" + "io" + "os" + + "github.com/dolmen-go/contextio" + "github.com/pkg/sftp" + "golang.org/x/crypto/ssh" +) + +// GoClient implements the SFTP service using native Go SSH and SFTP packages. +type GoClient struct { + cfg Config + + ssh *ssh.Client + sftp *sftp.Client +} + +var _ Client = (*GoClient)(nil) + +// NewGoClient returns a new GoSFTP client with the given configuration. +func NewGoClient(cfg Config) *GoClient { + cfg.SetDefaults() + + return &GoClient{cfg: cfg} +} + +// Upload writes the data from src to the remote file at dest and returns the +// number of bytes written. A new SFTP connection is opened before writing, and +// closed when the upload is complete or cancelled. +// +// Upload is not thread safe. +func (c *GoClient) Upload(ctx context.Context, src io.Reader, dest string) (int64, error) { + if err := c.dial(); err != nil { + return 0, err + } + defer c.close() + + // Note: Some SFTP servers don't support O_RDWR mode. + w, err := c.sftp.OpenFile(dest, (os.O_WRONLY | os.O_CREATE | os.O_TRUNC)) + if err != nil { + return 0, fmt.Errorf("SFTP: open remote file %q: %v", dest, err) + } + defer w.Close() + + // Use contextio to stop the upload if a context cancellation signal is + // received. + bytes, err := io.Copy(contextio.NewWriter(ctx, w), contextio.NewReader(ctx, src)) + if err != nil { + return 0, fmt.Errorf("SFTP: upload to %q: %v", dest, err) + } + + return bytes, nil +} + +// Dial connects to an SSH host then creates an SFTP client on the connection. +// When the clients are no longer needed, close() must be called to prevent +// leaks. +func (c *GoClient) dial() error { + var err error + + c.ssh, err = sshConnect(c.cfg) + if err != nil { + return fmt.Errorf("SSH: %v", err) + } + + c.sftp, err = sftp.NewClient(c.ssh) + if err != nil { + return fmt.Errorf("start SFTP subsystem: %v", err) + } + + return nil +} + +// Close closes the SFTP client first, then the SSH client. +func (c *GoClient) close() error { + var errs error + + if c.sftp != nil { + if err := c.sftp.Close(); err != nil { + errs = errors.Join(err, errs) + } + } + + if c.ssh != nil { + if err := c.ssh.Close(); err != nil { + errs = errors.Join(err, errs) + } + } + + return errs +} diff --git a/internal/sftp/goclient_test.go b/internal/sftp/goclient_test.go new file mode 100644 index 000000000..bfb1692b1 --- /dev/null +++ b/internal/sftp/goclient_test.go @@ -0,0 +1,287 @@ +package sftp_test + +import ( + "bufio" + "context" + "fmt" + "io" + "log" + "net" + "os" + "strings" + "testing" + "time" + + "github.com/gliderlabs/ssh" + gosftp "github.com/pkg/sftp" + gossh "golang.org/x/crypto/ssh" + "gotest.tools/v3/assert" + tfs "gotest.tools/v3/fs" + + "github.com/artefactual-sdps/enduro/internal/sftp" +) + +// ServerAddress is the test SFTP server address. +const serverAddress = "127.0.0.1:2222" + +// PubkeyHandler handles checking the client's public key against the keys in +// the authorized_keys file. +func pubkeyHandler(ctx ssh.Context, key ssh.PublicKey) bool { + file, err := os.Open("./testdata/authorized_keys") + if err != nil { + log.Fatalf("SFTP server: couldn't open authorized_keys file: %s", err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + allowed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(scanner.Text())) + if err != nil { + log.Fatalf("SFTP server: couldn't parse authorized keys: %s", err) + } + if ssh.KeysEqual(key, allowed) { + return true + } + } + + log.Println("SFTP server: unknown key provided.") + return false +} + +// HostKeySigner signs messages from the server to the client and allows the +// client to confirm the host key signature. +func hostKeySigner() (gossh.Signer, error) { + keyfile := "./testdata/serverkeys/test_rsa" + + key, err := os.ReadFile(keyfile) + if err != nil { + return nil, fmt.Errorf("read keyfile %q, %v\n", keyfile, err) + } + + signer, err := gossh.ParsePrivateKey(key) + if err != nil { + return nil, fmt.Errorf("parse private key: %v\n", err) + } + + return signer, nil +} + +// SftpHandler starts the SFTP subsystem. +func sftpHandler(sess ssh.Session) { + debugStream := io.Discard + serverOptions := []gosftp.ServerOption{ + gosftp.WithDebug(debugStream), + } + server, err := gosftp.NewServer( + sess, + serverOptions..., + ) + if err != nil { + log.Fatalf("SFTP server init error: %s", err) + } + if err := server.Serve(); err == io.EOF { + server.Close() + fmt.Println("SFTP client exited session.") + } else if err != nil { + fmt.Println("SFTP server completed with error:", err) + } +} + +// StartSFTPServer starts a test SFTP server, and returns a pointer to the +// server. +func startSFTPServer(t *testing.T, addr string) *ssh.Server { + t.Helper() + + var err error + + srv := ssh.Server{ + Addr: addr, + Handler: func(s ssh.Session) { + authorizedKey := gossh.MarshalAuthorizedKey(s.PublicKey()) + io.WriteString(s, fmt.Sprintf("public key used by %s:\n", s.User())) + s.Write(authorizedKey) + }, + PublicKeyHandler: pubkeyHandler, + SubsystemHandlers: map[string]ssh.SubsystemHandler{ + "sftp": sftpHandler, + }, + } + + signer, err := hostKeySigner() + if err != nil { + t.Fatalf("SFTP server: couldn't create host key signer: %v", err) + } + srv.AddHostKey(signer) + + errCh := make(chan error, 1) + go func() { + errCh <- srv.ListenAndServe() + }() + + // Wait for the server to be ready + func() { + for { + select { + case err := <-errCh: + t.Fatalf("SFTP server: failed to start: %v", err) + default: + conn, err := net.DialTimeout("tcp", addr, 1*time.Second) + if err == nil { + conn.Close() + return + } + time.Sleep(10 * time.Millisecond) + } + } + }() + + t.Cleanup(func() { srv.Close() }) + return &srv +} + +func TestGoClient(t *testing.T) { + host, port, err := net.SplitHostPort(serverAddress) + if err != nil { + t.Fatalf("Bad server address: %s", serverAddress) + } + + _ = startSFTPServer(t, serverAddress) + + // Start a listener on an open port and use the address to test a bad SFTP + // server address. + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Couldn't start listener: %v", err) + } + defer listener.Close() + badHost, badPort, _ := net.SplitHostPort(listener.Addr().String()) + + type results struct { + Bytes int64 + Paths []tfs.PathOp + } + + type test struct { + name string + cfg sftp.Config + want results + wantErr string + } + for _, tc := range []test{ + { + name: "Uploads a file using a key with no passphrase", + cfg: sftp.Config{ + Host: host, + Port: port, + KnownHostsFile: "./testdata/known_hosts", + PrivateKey: sftp.PrivateKey{ + Path: "./testdata/clientkeys/test_ed25519", + }, + }, + want: results{ + Bytes: 13, + Paths: []tfs.PathOp{tfs.WithFile("test.txt", "Testing 1-2-3")}, + }, + }, + { + name: "Uploads a file using a key with a passphrase", + cfg: sftp.Config{ + Host: host, + Port: port, + KnownHostsFile: "./testdata/known_hosts", + PrivateKey: sftp.PrivateKey{ + Path: "./testdata/clientkeys/test_pass_rsa", + Passphrase: "Backpack-Spirits6-Bronzing", + }, + }, + want: results{ + Bytes: 13, + Paths: []tfs.PathOp{tfs.WithFile("test.txt", "Testing 1-2-3")}, + }, + }, + { + name: "Errors when the key passphrase is wrong", + cfg: sftp.Config{ + Host: host, + Port: port, + KnownHostsFile: "./testdata/known_hosts", + PrivateKey: sftp.PrivateKey{ + Path: "./testdata/clientkeys/test_pass_rsa", + Passphrase: "wrong", + }, + }, + wantErr: "SSH: parse private key with passphrase: x509: decryption password incorrect", + }, + { + name: "Errors when the SFTP server isn't there", + cfg: sftp.Config{ + Host: badHost, + Port: badPort, + KnownHostsFile: "./testdata/known_hosts", + PrivateKey: sftp.PrivateKey{ + Path: "./testdata/clientkeys/test_ed25519", + }, + }, + wantErr: fmt.Sprintf( + "SSH: connect: dial tcp %s:%s: connect: connection refused", + badHost, badPort, + ), + }, + { + name: "Errors when the private key is not recognized", + cfg: sftp.Config{ + Host: host, + Port: port, + KnownHostsFile: "./testdata/known_hosts", + PrivateKey: sftp.PrivateKey{ + Path: "./testdata/clientkeys/test_unk_ed25519", + }, + }, + wantErr: "SSH: connect: ssh: handshake failed: ssh: unable to authenticate, attempted methods [none publickey], no supported methods remain", + }, + { + name: "Errors when the host key is not in known_hosts", + cfg: sftp.Config{ + Host: host, + Port: port, + KnownHostsFile: "./testdata/empty_file", + PrivateKey: sftp.PrivateKey{ + Path: "./testdata/clientkeys/test_ed25519", + }, + }, + wantErr: "SSH: connect: ssh: handshake failed: knownhosts: key is unknown", + }, + { + name: "Errors when the known_hosts file doesn't exist", + cfg: sftp.Config{ + Host: host, + Port: port, + KnownHostsFile: "./testdata/missing", + PrivateKey: sftp.PrivateKey{ + Path: "./testdata/clientkeys/test_ed25519", + }, + }, + wantErr: "SSH: parse known_hosts: open testdata/missing: no such file or directory", + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + sftpc := sftp.NewGoClient(tc.cfg) + src := strings.NewReader("Testing 1-2-3") + dest := tfs.NewDir(t, "sftp_test") + + bytes, err := sftpc.Upload(context.Background(), src, dest.Join("test.txt")) + + if tc.wantErr != "" { + assert.Error(t, err, tc.wantErr) + return + } + + assert.NilError(t, err) + assert.Equal(t, bytes, tc.want.Bytes) + assert.Assert(t, tfs.Equal(dest.Path(), tfs.Expected(t, tc.want.Paths...))) + }) + } +} diff --git a/internal/sftp/ssh.go b/internal/sftp/ssh.go new file mode 100644 index 000000000..6596bd62a --- /dev/null +++ b/internal/sftp/ssh.go @@ -0,0 +1,63 @@ +package sftp + +import ( + "fmt" + "net" + "os" + "path/filepath" + "time" + + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/knownhosts" +) + +// sshConnect connects to an SSH server using the given configuration and +// returns a client connection. +// +// Only private key authentication is currently supported, with or without a +// passphrase. +func sshConnect(cfg Config) (*ssh.Client, error) { + // Load private key for authentication. + keyBytes, err := os.ReadFile(filepath.Clean(cfg.PrivateKey.Path)) // #nosec G304 -- File data is validated below + if err != nil { + return nil, fmt.Errorf("read private key: %v", err) + } + + // Create a signer from the private key, with or without a passphrase. + var signer ssh.Signer + if cfg.PrivateKey.Passphrase != "" { + signer, err = ssh.ParsePrivateKeyWithPassphrase(keyBytes, []byte(cfg.PrivateKey.Passphrase)) + if err != nil { + return nil, fmt.Errorf("parse private key with passphrase: %v", err) + } + } else { + signer, err = ssh.ParsePrivateKey(keyBytes) + if err != nil { + return nil, fmt.Errorf("parse private key: %v", err) + } + } + + // Check that the host key is in the client's known_hosts file. + hostcallback, err := knownhosts.New(filepath.Clean(cfg.KnownHostsFile)) + if err != nil { + return nil, fmt.Errorf("parse known_hosts: %v", err) + } + + // Configure the SSH client. + sshConfig := &ssh.ClientConfig{ + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(signer), + }, + HostKeyCallback: hostcallback, + Timeout: 5 * time.Second, + } + + // Connect to the server. + address := net.JoinHostPort(cfg.Host, cfg.Port) + conn, err := ssh.Dial("tcp", address, sshConfig) + if err != nil { + return nil, fmt.Errorf("connect: %v", err) + } + + return conn, nil +} diff --git a/internal/sftp/testdata/authorized_keys b/internal/sftp/testdata/authorized_keys new file mode 100644 index 000000000..a4bbad255 --- /dev/null +++ b/internal/sftp/testdata/authorized_keys @@ -0,0 +1,2 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHdtD23HlmYgcqkRlzQDsLOL1T3tMnJCU4MobvgujR1K your_email@example.com +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC6EyloxKiIXy2TTJltJ4hOpaBX3/QSZH7CxrVf15yZ6pltUN8UOJBVmY/uDHv8B5Qey+W88/vAYhdi4ZPPGIDBZGxtB3X1VFh7BE0xx2Y0I8mrBS2QRy0daWOzaVtgkZHZFmp5wXHGjci4yv+197C8dMIdTU6U68u27VK4ojE03VAIAQcgRa8sLlM5S9nB2gjBLdP8IzvkRsxOLwm6aTghh4i2YTkb9aQL92dsp1u9kxXXE4B82ia5NZ8ZF6L6DCsjJjytoOzHhmCTrSdGF/BTUruOX/iVADBZi49q/xmJdmd+HD70b67quREF4edkw78UIHqvm1bTZFuF8E2vCJQQfH39/o7QUVf6eLzKD5kpz/oMT8MC8CsY0BIgRZLmX9VZA7zu9eoGgmjIuFYpPOpwI4pCjLm9hxzG981AjCUiQ4GJRNoFlsaHTRjnhqMISWU3r+rNAF6IrP5E30m8EYgsLqQbzNZchBOguOdNKrb00tTuFj9hrKICpAu4PN8xm0HQCNBrBtqRjxZg8jPRd4P9FrmTJVFqZuWeomLllN5hvgPBrdj0oZARp8ZOVLVsBTEiBDAA28iqXiI3ljIVYF5kbsqcnSMTA17ftkNZYIsfLKq8fPrf5vcRUuKmgyrtYA78lOw1+/bMIMKK7UoD2MjEz5rbIGpJlQJJBRortO7ROQ== your_email@example.com diff --git a/internal/sftp/testdata/clientkeys/test_ed25519 b/internal/sftp/testdata/clientkeys/test_ed25519 new file mode 100644 index 000000000..e2a8c96a0 --- /dev/null +++ b/internal/sftp/testdata/clientkeys/test_ed25519 @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACB3bQ9tx5ZmIHKpEZc0A7Czi9U97TJyQlODKG74Lo0dSgAAAKCfil5rn4pe +awAAAAtzc2gtZWQyNTUxOQAAACB3bQ9tx5ZmIHKpEZc0A7Czi9U97TJyQlODKG74Lo0dSg +AAAEAM2BZXDS+Oe0Zm9ha/CWEX+n7D8ra9f3lbwcKLSnIS/XdtD23HlmYgcqkRlzQDsLOL +1T3tMnJCU4MobvgujR1KAAAAFnlvdXJfZW1haWxAZXhhbXBsZS5jb20BAgMEBQYH +-----END OPENSSH PRIVATE KEY----- diff --git a/internal/sftp/testdata/clientkeys/test_ed25519.pub b/internal/sftp/testdata/clientkeys/test_ed25519.pub new file mode 100644 index 000000000..46fde37db --- /dev/null +++ b/internal/sftp/testdata/clientkeys/test_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHdtD23HlmYgcqkRlzQDsLOL1T3tMnJCU4MobvgujR1K your_email@example.com diff --git a/internal/sftp/testdata/clientkeys/test_pass_rsa b/internal/sftp/testdata/clientkeys/test_pass_rsa new file mode 100644 index 000000000..0b2303a42 --- /dev/null +++ b/internal/sftp/testdata/clientkeys/test_pass_rsa @@ -0,0 +1,50 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBRZFxI6p +4Jui2kmXYIadGzAAAAEAAAAAEAAAIXAAAAB3NzaC1yc2EAAAADAQABAAACAQC6EyloxKiI +Xy2TTJltJ4hOpaBX3/QSZH7CxrVf15yZ6pltUN8UOJBVmY/uDHv8B5Qey+W88/vAYhdi4Z +PPGIDBZGxtB3X1VFh7BE0xx2Y0I8mrBS2QRy0daWOzaVtgkZHZFmp5wXHGjci4yv+197C8 +dMIdTU6U68u27VK4ojE03VAIAQcgRa8sLlM5S9nB2gjBLdP8IzvkRsxOLwm6aTghh4i2YT +kb9aQL92dsp1u9kxXXE4B82ia5NZ8ZF6L6DCsjJjytoOzHhmCTrSdGF/BTUruOX/iVADBZ +i49q/xmJdmd+HD70b67quREF4edkw78UIHqvm1bTZFuF8E2vCJQQfH39/o7QUVf6eLzKD5 +kpz/oMT8MC8CsY0BIgRZLmX9VZA7zu9eoGgmjIuFYpPOpwI4pCjLm9hxzG981AjCUiQ4GJ +RNoFlsaHTRjnhqMISWU3r+rNAF6IrP5E30m8EYgsLqQbzNZchBOguOdNKrb00tTuFj9hrK +ICpAu4PN8xm0HQCNBrBtqRjxZg8jPRd4P9FrmTJVFqZuWeomLllN5hvgPBrdj0oZARp8ZO +VLVsBTEiBDAA28iqXiI3ljIVYF5kbsqcnSMTA17ftkNZYIsfLKq8fPrf5vcRUuKmgyrtYA +78lOw1+/bMIMKK7UoD2MjEz5rbIGpJlQJJBRortO7ROQAAB1DKfMC3AeEhJ42g3GS4q6gw +c3xyKckUcEbjlqgEhJkECrNDNlHZon8937yfuW5zYIKFgFln+XJtaX/FyXiIWnxwiOJX+7 +cjAobTCFogI1tO5p9+QghHMVRWcd32TnmP622UriMPEMakzdH3hQQKGqvUSsEKkWn/fgHp +35jTHmCcmqaRlj/HOK8dAJMViACehR1c50rx0FPPHXdhBiOI4WQQOcpgAcD4ixi685MUwh +WHUmq1TLOYl5xZpt2culURlNoXc8pb/zRIPjaNFK7wy6yjhINKOnB4/fQOooCzpVAJRrkE +ljYnpce9x385JUVwoAgrgVb0I6TZYdLdi4sQZ+gytxkJTnI3quwvRfYFDUxa5r74bXVEhw +5J5yDbvTScxK8Q/m42OWJBAdr+oVpjuNZhwhqGNAeTe5pb9bssNnUn3CvMiyyS/xqWqXU8 +wYDoU1q92ubTzMWwdvoKjBEKVG/9H6w6nUd+jQO+36a/S83z0sUUpfCBexT09/L7a0PdG2 +P6Px7ZGdR2UGz6bMc80NZebhvLHdcu5OPsG3csu6CRRxvXUX2p44Q4ojYimehiWbhsyI3Y +/wM6J/47zVvMxDnc0H4CmKGis56iaEvUYRsz5Gwxohen0fVu8pLb/9exORJAAz52Q/Do5j +EyQKrh9DyVEFCM5SGwE13OWuH39KlDkf8dWVeqIhDd/ysli7GErcPMVAOZKG91SYpTeBvb +xQXtozGS2dt8W0VSOooel17LhMjXumhuWtkW36BUNGMZiT4eXkgkbyfdvQqW8gGXfaSeNg +F44le4LKYuMHbXn9nm8AYWcx/pvpbLxeqUnRZueEtyC6/O5K5qUL5U+M9nlujjueFHgYRZ +DOF8RHKkYHjATtCg9Aa37muooIsJ3utvqXwuD3b/A+hbIDPBllSgrDhq6kLDvomDRg9Unj +8r7KTquA1gLMGNuai3GhpwY4PD30g3mKB5m8FsDw/BbPuw+/6YNqi+4UyrVWoNPmDrmGRk +jYzwGL9etfrR+jFcN9xMxP+W3zRXEA+l/azfmwN1mJy7HkdCBEJoQ24Z5eIGhAOP0kQVmd +OHNbkehQIJ+VkYebHUgbbHW+VONPFwFXBy5rt4qTSdwzye0hzXmll2u31NhlwaMKGulza7 +TFwRkXGfFK2L2FDpJZ1ZRJ2o+j03vft3CIMXt7H+73XGNQqjeIJ9ttaRtPOho6wu+fTsax +ZLOz60K2HGtBJtXhVh+bVmBxa4TSuxYP6uhyATZqKlZwLZmF8RVTibaz9HqTX6cFi4ONx3 +bvEwa/rycbG/QaBBmP/9//83Rbeu4gpugsf+FTOC0nGPiCanIz2D7yLN3i0uxoI4GruLXi +mIj91JnACuf4jg0Askmpw57UGXfCH6NZuWff+/taUqtYddJZ6lPc6JV+SfArzL3zL+x9rZ +7Lss6PdVxmErPTlpXXG/U2qnOS7laDA5NWov3XPCbTCCadPpxccw1uYdITgCNo9OwhJlIQ +1OxdXgIRoqYtZVxZGhTFehfNqqijcRCsAXUttweM9LrvP+5CrevPe4D0LcYt34tQk51cbz +wfQeJRQhpFKVqviPyNACLVFKxV03NwzDdbRc+SOZopA9KX+fLQiVduTGjQbg8+IsAjubsg +UZLAUqVGQ4C7yxr3f4CLrYqZgXuYipTwn2iJ9YdHTW3tT2VM+ch93hYvJ3QEvlp+gQcb+k +dW7eO1fLth2ArXlpN6CO2sn4a7bBtc75Rr69Q4KxZZsysxH5MTZ+qsx+uhoiHE4KQOBWpB +3Dg1xZ7cdLStBJyelZrS0K9DJW1HCxKoglV2rDuz8ccuA41bQwK3nwXgfbHATCpVnNjavg +hRtzDS/5KeWwQVNbIPWuaJgr8u/vI/dC0QuzdSepumjJE2tQkvBBNJV5CPrykvcPGmKmWr +Sxi+6c80mg55fmX4Yys89Ul1MmRH9cvr+dAbd7BUBAYI3dmd6ejsrD1wX1C1X8+kt6Q+2v +62WWXwVXqI6k9z4sXBaxr6DKongcpvXrqGEkASl13eqov41zyhP5W7x4ahEsXGtUGAEoML +Vr6FNjjgi0x1Ks8uo1ECIIqjpq+dyOFIsg91llrKdQt2N0ctfLi/ZssQ2+s2bnAAQeOOSI +h4rGXu+NVN9PQJmhGxXt5y7c0u3ljk6NllndiWnbIduHNSi7ZPXU81la0okrec70Y8VKWh +HcAl7IrlxTjaEWTxKUD2MerZ7GcRZHieuTGIiW4su3b5ipAtGGEks2txZ1tTUPmp+s+EWU +nBHxirFaPIIBv6suf3sfwcD4FHJ5cW5at3SEszTSe/tb141/a/h1K8lVGgPKE+cihaq5vj +Nu+1wtS9ywL3hl2TB+BEI6zMBuRepsdnncbgz0C2i/3wddKshDDVzQOpbXECYdqFpp/CKg +QvxCZfJ2lAMn2rBAEdyAMML+oz26mzkjaHjX+LIso41ZKJ26YdKI9xQJYwrdtVYwC/T20x +s7aw6IaspoWOda0HfHn9e8/jo= +-----END OPENSSH PRIVATE KEY----- diff --git a/internal/sftp/testdata/clientkeys/test_pass_rsa.pub b/internal/sftp/testdata/clientkeys/test_pass_rsa.pub new file mode 100644 index 000000000..187f0c63a --- /dev/null +++ b/internal/sftp/testdata/clientkeys/test_pass_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC6EyloxKiIXy2TTJltJ4hOpaBX3/QSZH7CxrVf15yZ6pltUN8UOJBVmY/uDHv8B5Qey+W88/vAYhdi4ZPPGIDBZGxtB3X1VFh7BE0xx2Y0I8mrBS2QRy0daWOzaVtgkZHZFmp5wXHGjci4yv+197C8dMIdTU6U68u27VK4ojE03VAIAQcgRa8sLlM5S9nB2gjBLdP8IzvkRsxOLwm6aTghh4i2YTkb9aQL92dsp1u9kxXXE4B82ia5NZ8ZF6L6DCsjJjytoOzHhmCTrSdGF/BTUruOX/iVADBZi49q/xmJdmd+HD70b67quREF4edkw78UIHqvm1bTZFuF8E2vCJQQfH39/o7QUVf6eLzKD5kpz/oMT8MC8CsY0BIgRZLmX9VZA7zu9eoGgmjIuFYpPOpwI4pCjLm9hxzG981AjCUiQ4GJRNoFlsaHTRjnhqMISWU3r+rNAF6IrP5E30m8EYgsLqQbzNZchBOguOdNKrb00tTuFj9hrKICpAu4PN8xm0HQCNBrBtqRjxZg8jPRd4P9FrmTJVFqZuWeomLllN5hvgPBrdj0oZARp8ZOVLVsBTEiBDAA28iqXiI3ljIVYF5kbsqcnSMTA17ftkNZYIsfLKq8fPrf5vcRUuKmgyrtYA78lOw1+/bMIMKK7UoD2MjEz5rbIGpJlQJJBRortO7ROQ== your_email@example.com diff --git a/internal/sftp/testdata/clientkeys/test_unk_ed25519 b/internal/sftp/testdata/clientkeys/test_unk_ed25519 new file mode 100644 index 000000000..29b70d047 --- /dev/null +++ b/internal/sftp/testdata/clientkeys/test_unk_ed25519 @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACDpVMal3wdha1AJDcMFatYKyFhuNU9J+uSjPUSnIXzN0QAAAKC/R+6Rv0fu +kQAAAAtzc2gtZWQyNTUxOQAAACDpVMal3wdha1AJDcMFatYKyFhuNU9J+uSjPUSnIXzN0Q +AAAEAyiM7W/kJtxp4l8V0vbWqXGsmhAu8THeEJtOxE/HyPI+lUxqXfB2FrUAkNwwVq1grI +WG41T0n65KM9RKchfM3RAAAAFnlvdXJfZW1haWxAZXhhbXBsZS5jb20BAgMEBQYH +-----END OPENSSH PRIVATE KEY----- diff --git a/internal/sftp/testdata/clientkeys/test_unk_ed25519.pub b/internal/sftp/testdata/clientkeys/test_unk_ed25519.pub new file mode 100644 index 000000000..3870cd3d1 --- /dev/null +++ b/internal/sftp/testdata/clientkeys/test_unk_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOlUxqXfB2FrUAkNwwVq1grIWG41T0n65KM9RKchfM3R your_email@example.com diff --git a/internal/sftp/testdata/empty_file b/internal/sftp/testdata/empty_file new file mode 100644 index 000000000..e69de29bb diff --git a/internal/sftp/testdata/known_hosts b/internal/sftp/testdata/known_hosts new file mode 100644 index 000000000..b9efb6f73 --- /dev/null +++ b/internal/sftp/testdata/known_hosts @@ -0,0 +1 @@ +[127.0.0.1]:2222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDqWdPfdbCwxGRwX5rWAuOWTA9UpB73odP/IsSk3Ir1lkL1wPSQQKppCvl8MHcXGGojrkCg5fSiwBZ0UgKK0u7mntn71S6K3xHcL5vDSLEMxjgygDU7mdaJpc6W1uk96nrFdkRx7kFm+lEClWy/kDhj7TxacHxrWg2mQetYlsGVMmRt+d2hJMafDOenCv3pQWal/bsjS4rtpXe6Sm4Y+YT4jV8WO8MNClNrDYEfZac1l9ZY87wFMUpWq7lJBEumAFpppbOm6uCuL9Rb8bfi/TXgWowMfyvCOChUzbnStaia6slo75Gia2YTmSLqTThmw3LdQJP8PFG4aa7IIDBVpEw7137CA5DFev6yWLA6I4yylT5fxw01Ikvcx7rd+gCzSDtYEwmbnM2jpQRRzKwyvUb65LHttgGXMiUMh/rOHeZk6o3nDPy3AHNvmIKScl9V0P01MJhssMhhfGnoBOO7zJmONnG8ICto8Wyx/qzrpPROKK6MJCV/nD6kBpc1JTANqn1ftsCOXJf85sZnDZ0k9bpoEN5rRbd68Aq3Z+/oyaCZkpR++lE7ogQIJPzUmQed67gF1kg958rAWdnuUPA8NPcoVCSPivT3L85rsZckSiOGTIv9WR9hJuMQWQ5/ij1MXMUhWo+daH/4/8m6OQyCaaPq6Dgdt9Cn8gvvwAE0+XGLUQ== diff --git a/internal/sftp/testdata/serverkeys/test_rsa b/internal/sftp/testdata/serverkeys/test_rsa new file mode 100644 index 000000000..8925a701d --- /dev/null +++ b/internal/sftp/testdata/serverkeys/test_rsa @@ -0,0 +1,49 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/internal/sftp/testdata/serverkeys/test_rsa.pub b/internal/sftp/testdata/serverkeys/test_rsa.pub new file mode 100644 index 000000000..d19b6d508 --- /dev/null +++ b/internal/sftp/testdata/serverkeys/test_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDqWdPfdbCwxGRwX5rWAuOWTA9UpB73odP/IsSk3Ir1lkL1wPSQQKppCvl8MHcXGGojrkCg5fSiwBZ0UgKK0u7mntn71S6K3xHcL5vDSLEMxjgygDU7mdaJpc6W1uk96nrFdkRx7kFm+lEClWy/kDhj7TxacHxrWg2mQetYlsGVMmRt+d2hJMafDOenCv3pQWal/bsjS4rtpXe6Sm4Y+YT4jV8WO8MNClNrDYEfZac1l9ZY87wFMUpWq7lJBEumAFpppbOm6uCuL9Rb8bfi/TXgWowMfyvCOChUzbnStaia6slo75Gia2YTmSLqTThmw3LdQJP8PFG4aa7IIDBVpEw7137CA5DFev6yWLA6I4yylT5fxw01Ikvcx7rd+gCzSDtYEwmbnM2jpQRRzKwyvUb65LHttgGXMiUMh/rOHeZk6o3nDPy3AHNvmIKScl9V0P01MJhssMhhfGnoBOO7zJmONnG8ICto8Wyx/qzrpPROKK6MJCV/nD6kBpc1JTANqn1ftsCOXJf85sZnDZ0k9bpoEN5rRbd68Aq3Z+/oyaCZkpR++lE7ogQIJPzUmQed67gF1kg958rAWdnuUPA8NPcoVCSPivT3L85rsZckSiOGTIv9WR9hJuMQWQ5/ij1MXMUhWo+daH/4/8m6OQyCaaPq6Dgdt9Cn8gvvwAE0+XGLUQ== my_email@example.com