Skip to content

Commit

Permalink
Merge pull request #12 from openziti-incubator/add-tests-minor-refactor
Browse files Browse the repository at this point in the history
merging current zscp to main - zscp needs more testing forthcoming.
dovholuknf authored Jul 22, 2021
2 parents 0697cc8 + f2bf027 commit d19312c
Showing 17 changed files with 702 additions and 279 deletions.
40 changes: 22 additions & 18 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -13,14 +13,20 @@ jobs:
matrix:
include:
- os: ubuntu-latest
artifact_name: zssh
asset_name: zssh-linux-amd64
zssh_artifact_name: zssh
zssh_asset_name: zssh-linux-amd64
zscp_artifact_name: zscp
zscp_asset_name: zscp-linux-amd64
- os: windows-latest
artifact_name: zssh.exe
asset_name: zssh-windows-amd64.exe
zssh_artifact_name: zssh.exe
zssh_asset_name: zssh-windows-amd64.exe
zscp_artifact_name: zscp.exe
zscp_asset_name: zscp-windows-amd64.exe
- os: macos-latest
artifact_name: zssh
asset_name: zssh-macos-amd64
zssh_artifact_name: zssh
zssh_asset_name: zssh-macos-amd64
zscp_artifact_name: zscp
zscp_asset_name: zscp-macos-amd64
steps:
- uses: actions/checkout@v2

@@ -33,22 +39,20 @@ jobs:
run: mkdir "${{ runner.workspace }}/build"

- name: Build
run: go build -o ${{ runner.workspace }}/build ./...
run: go build -o ${{ runner.workspace }}/build ./...

- name: Upload binaries to release
- name: Upload zssh binaries to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ runner.workspace }}/build/${{ matrix.artifact_name }}
asset_name: ${{ matrix.asset_name }}
tag: latest-tag
overwrite: true
release_name: latest-tagged-release

- name: Upload binaries to release
file: ${{ runner.workspace }}/build/${{ matrix.zssh_artifact_name }}
asset_name: ${{ matrix.zssh_asset_name }}
tag: ${{ github.ref }}

- name: Upload zscp binaries to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ runner.workspace }}/build/${{ matrix.artifact_name }}
asset_name: ${{ matrix.asset_name }}
tag: ${{ github.ref }}
file: ${{ runner.workspace }}/build/${{ matrix.zscp_artifact_name }}
asset_name: ${{ matrix.zscp_asset_name }}
tag: ${{ github.ref }}
23 changes: 23 additions & 0 deletions .github/workflows/mattermost-ziti-webhook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: mattermost-ziti-webhook
on:
create:
delete:
issues:
issue_comment:
pull_request_review:
pull_request_review_comment:
pull_request:
push:
fork:
release:

jobs:
mattermost-ziti-webhook:
runs-on: macos-latest
name: POST Webhook
steps:
- uses: openziti/ziti-webhook-action@main
with:
ziti-id: ${{ secrets.ZITI_MATTERMOST_IDENTITY }}
webhook-url: ${{ secrets.ZITI_MATTERMOST_WEBHOOK_URL }}
webhook-secret: ${{ secrets.ZITI_MATTERMOSTI_WEBHOOK_SECRET }}
17 changes: 17 additions & 0 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Code of Conduct

All open source projects managed by NetFoundry share a common [code of
conduct](https://netfoundry.github.io/policies/CODE_OF_CONDUCT.html) which all contributors are expected to follow.
Please be sure you read, understand and adhere to the thoughts expressed therein.

Individuals acting in any way that is considered in violation of the Code of Conduct will receive corrective actions
appropriate for the violation. It is possible to act in ways which technically do not violate the Code of Conduct but
are clearly against the spirit of the project. These situations will also be considered on a case by case basis and if
necessary the Code of Conduct will be revised accordingly and approrpiate notifications will be made to the community
and to the violator.

## Violations

Contributors violating the Code of Conduct will receive public notification of the violation. Continued violations or
aggregious violations of the Code of Conduct will result in the contributor being removed from the contributors list as
well as further contributions being declined.
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Contributing

NetFoundry welcomes all and any contributions. All open source projects managed by NetFoundry share a common
[guide for contributions](https://netfoundry.github.io/policies/CONTRIBUTING.html).

If you are eager to contribute to a NetFoundry-managed open source project please read and act accordingly.
201 changes: 201 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

1. Definitions.

"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.

"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.

"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.

"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.

"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.

"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.

"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).

"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.

"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."

"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.

2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.

3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.

4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:

(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and

(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and

(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and

(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.

You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.

5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.

6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.

7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.

8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.

9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

END OF TERMS AND CONDITIONS

APPENDIX: How to apply the Apache License to your work.

To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright 2019 NetFoundry, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -6,13 +6,14 @@ require (
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
github.com/natefinch/npipe v0.0.0-20160621034901-c1b8fa8bdcce
github.com/openziti/foundation v0.15.53
github.com/openziti/foundation v0.15.53 // indirect
github.com/openziti/sdk-golang v0.15.47
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231 // indirect
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.1
github.com/pkg/sftp v1.13.2
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.1.3
github.com/stretchr/testify v1.7.0
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
7 changes: 5 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -185,6 +185,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
@@ -244,6 +245,7 @@ github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJE
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/netfoundry/secretstream v0.1.2 h1:NgqrYytDnjKbOfWI29TT0SJM+RwB3yf9MIkJVJaU+J0=
github.com/netfoundry/secretstream v0.1.2/go.mod h1:uasYkYSp0MmNSlKOWJ2sVzxPms8e58TS4ENq4yro86k=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
@@ -269,8 +271,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pkg/sftp v1.13.2 h1:taJnKntsWgU+qae21Rx52lIwndAdKrj0mfUNQsz1z4Q=
github.com/pkg/sftp v1.13.2/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
@@ -563,6 +565,7 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
261 changes: 111 additions & 150 deletions zssh/zscp/main.go
Original file line number Diff line number Diff line change
@@ -1,155 +1,117 @@
/*
Copyright NetFoundry, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"fmt"
"github.com/openziti/sdk-golang/ziti"
"github.com/openziti/sdk-golang/ziti/config"
"github.com/pkg/sftp"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"io/fs"
"log"
"os"
"os/user"
"path/filepath"
"runtime"
"strings"
"zssh/zsshlib"
)

const ExpectedServiceAndExeName = "zssh"

var (
ZConfig string
SshKeyPath string
debug bool
recursive bool
serviceName string

rootCmd = &cobra.Command{
Use: "Remote to Local: zscp <remoteUsername>@<targetIdentity>:[Remote Path] [Local Path]\n" +
"Local to Remote: zscp [Local Path] <remoteUsername>@<targetIdentity>:[Remote Path]",
Short: "Z(iti)scp, Carb-loaded ssh performs faster and stronger than ssh",
Long: "Z(iti)scp is a version of ssh that utilizes a ziti network to provide a faster and more secure remote connection. A ziti connection must be established before use",
Args: cobra.ExactValidArgs(2),
Run: func(cmd *cobra.Command, args []string) {
if SshKeyPath == "" {
userHome, err := os.UserHomeDir()
if err != nil {
logrus.Fatalf("could not find UserHomeDir? %v", err)
}
SshKeyPath = filepath.Join(userHome, ".ssh", "id_rsa")
}
if debug {
logrus.Infof(" sshKeyPath set to: %s", SshKeyPath)
}

if ZConfig == "" {
userHome, err := os.UserHomeDir()
if err != nil {
logrus.Fatalf("could not find UserHomeDir: %v", err)
}
ZConfig = filepath.Join(userHome, ".ziti", fmt.Sprintf("%s.json", ExpectedServiceAndExeName))
}
if debug {
logrus.Infof(" ZConfig set to: %s", ZConfig)
}

var username string
var targetIdentity string
var remoteFilePath string
var localFilePath string
var isCopyToRemote bool

if strings.ContainsAny(args[0], ":") {
remoteFilePath = args[0]
localFilePath = args[1]
isCopyToRemote = false

} else if strings.ContainsAny(args[1], ":") {
remoteFilePath = args[1]
localFilePath = args[0]
isCopyToRemote = true
} else {
logrus.Fatal(`cannot determine remote file PATH use ":" for remote path`)
}

localFilePath, err := filepath.Abs(localFilePath)
var flags = &zsshlib.ScpFlags{}
var rootCmd = &cobra.Command{
Use: "Remote to Local: zscp <remoteUsername>@<targetIdentity>:[Remote Path] [Local Path]\n" +
"Local to Remote: zscp [Local Path][...] <remoteUsername>@<targetIdentity>:[Remote Path]",
Short: "Z(iti)scp, Carb-loaded ssh performs faster and stronger than ssh",
Long: "Z(iti)scp is a version of ssh that utilizes a ziti network to provide a faster and more secure remote connection. A ziti connection must be established before use",
Args: cobra.MinimumNArgs(2),
Run: func(cmd *cobra.Command, args []string) {
var remoteFilePath string
var localFilePaths []string
var isCopyToRemote bool
var err error

if strings.ContainsAny(args[0], ":") {
remoteFilePath = args[0]
localFilePaths = args[1:]
if len(localFilePaths) > 1 {
logrus.Fatalf("remote to local cannot have more than two arguments")
}
isCopyToRemote = false

} else if strings.ContainsAny(args[len(args)-1], ":") {
remoteFilePath = args[len(args)-1]
localFilePaths = args[0 : len(args)-1]
isCopyToRemote = true
} else {
logrus.Fatal(`cannot determine remote file PATH use ":" for remote path`)
}

for i, path := range localFilePaths {
localFilePaths[i], err = filepath.Abs(path)
if err != nil {
logrus.Fatalf("cannot determine absolute local file path, unrecognized file name: %s", localFilePath)
logrus.Fatalf("cannot determine absolute local file path, unrecognized file name: %s", path)
}
if _, err := os.Stat(localFilePath); err != nil {
if _, err := os.Stat(localFilePaths[i]); err != nil {
logrus.Fatal(err)
}

if debug {
logrus.Infof(" local path: %s", localFilePath)
}

fullRemoteFilePath := strings.Split(remoteFilePath, ":")
remoteFilePath = fullRemoteFilePath[1]

if strings.ContainsAny(fullRemoteFilePath[0], "@") {
userServiceName := strings.Split(fullRemoteFilePath[0], "@")
username = userServiceName[0]
targetIdentity = userServiceName[1]

} else {
curUser, err := user.Current()
if err != nil {
logrus.Fatal(err)
}
username = curUser.Username
if strings.Contains(username, "\\") && runtime.GOOS == "windows" {
username = strings.Split(username, "\\")[1]
}
targetIdentity = args[0]
}
if debug {
logrus.Infof(" username set to: %s", username)
logrus.Infof("targetIdentity set to: %s", targetIdentity)
}

ctx := ziti.NewContextWithConfig(getConfig(ZConfig))

_, ok := ctx.GetService(serviceName)
if !ok {
logrus.Fatalf("error when dialing service name %s. %v", serviceName, err)
}

dialOptions := &ziti.DialOptions{
ConnectTimeout: 0,
Identity: targetIdentity,
AppData: nil,
}
svc, err := ctx.DialWithOptions(serviceName, dialOptions)
defer func() { _ = svc.Close() }()
if err != nil {
logrus.Fatal(fmt.Sprintf("error when dialing service name %s. %v", serviceName, err))
}
factory := zsshlib.NewSshConfigFactoryImpl(username, SshKeyPath)
config := factory.Config()
sshConn, err := zsshlib.Dial(config, svc)
if err != nil {
logrus.Fatalf("error dialing SSH Conn: %v", err)
}
client, err := sftp.NewClient(sshConn)
if err != nil {
logrus.Fatalf("error creating sftp client: %v", err)
}
defer func() { _ = client.Close() }()

if isCopyToRemote {
if recursive {
flags.DebugLog(" local path: %s", localFilePaths[i])
}

username, targetIdentity := flags.GetUserAndIdentity(remoteFilePath)
remoteFilePath = zsshlib.ParseFilePath(remoteFilePath)

sshConn := zsshlib.EstablishClient(flags.SshFlags, username, targetIdentity)
defer func() { _ = sshConn.Close() }()

client, err := sftp.NewClient(sshConn)
if err != nil {
logrus.Fatalf("error creating sftp client: %v", err)
}
defer func() { _ = client.Close() }()

if remoteFilePath == "~" {
remoteFilePath = ""
} else if len(remoteFilePath) > 1 && remoteFilePath[:2] == "~/" {
remoteFilePath = after(remoteFilePath, "~/")
}

remoteFilePath, err = client.RealPath(remoteFilePath)
if err != nil {
logrus.Fatalf("cannot find remote file path: %s [%v]", remoteFilePath, err)
}

remoteGlob, err := client.Glob(remoteFilePath)
if err != nil {
logrus.Fatalf("file pattern [%s] not recognized [%v]", remoteFilePath, err)
} else if remoteGlob == nil {
remoteGlob = append(remoteGlob, remoteFilePath)
}

if isCopyToRemote { //local to remote
for i, localFilePath := range localFilePaths {
if flags.Recursive {
baseDir := filepath.Base(localFilePath)
err := filepath.WalkDir(localFilePath, func(path string, info fs.DirEntry, err error) error {
remotePath := filepath.Join(remoteFilePath, baseDir, after(path, baseDir))
if info.IsDir() {
err = client.Mkdir(remotePath)
if err != nil && debug {
logrus.Error(err) //occurs when directories exist already. Is not fatal. Only logs when debug flag is set.
} else if debug {
logrus.Infof("made directory: %s", remotePath)
if err != nil {
flags.DebugLog("%s", err) //occurs when directories exist already. Is not fatal. Only logs when debug flag is set.
} else {
flags.DebugLog("made directory: %s", remotePath)
}
} else {
err = zsshlib.SendFile(client, path, remotePath)
@@ -165,25 +127,32 @@ var (
logrus.Fatal(err)
}
} else {
if i > 0 {
remoteFilePath = filepath.Join(filepath.Dir(remoteFilePath), filepath.Base(localFilePath))
}
remoteFilePath = zsshlib.AppendBaseName(client, remoteFilePath, localFilePath, flags.Debug)
err = zsshlib.SendFile(client, localFilePath, remoteFilePath)
if err != nil {
logrus.Errorf("could not send file: %s [%v]", localFilePath, err)
} else {
logrus.Infof("sent file: %s ==> %s", localFilePath, remoteFilePath)
}
}
} else {
if recursive {
}
} else { //remote to local
localFilePath := localFilePaths[0]
for _, remoteFilePath = range remoteGlob {
if flags.Recursive {
baseDir := filepath.Base(remoteFilePath)
walker := client.Walk(remoteFilePath)
for walker.Step() {
localPath := filepath.Join(localFilePath, baseDir, after(walker.Path(), baseDir))
localPath := filepath.Join(localFilePath, baseDir, after(walker.Path(), baseDir)) //saves base directory to cut remote directory after it to append to localpath
if walker.Stat().IsDir() {
err = os.Mkdir(localPath, os.ModePerm)
if debug && err != nil {
logrus.Errorf("failed to make directory: %s [%v]", localPath, err) //occurs when directories exist already. Is not fatal. Only logs when debug flag is set.
} else if debug {
logrus.Infof("made directory: %s", localPath)
if err != nil {
flags.DebugLog("failed to make directory: %s [%v]", localPath, err) //occurs when directories exist already. Is not fatal. Only logs when debug flag is set.
} else {
flags.DebugLog("made directory: %s", localPath)
}
} else {
err = zsshlib.RetrieveRemoteFiles(client, localPath, walker.Path())
@@ -193,30 +162,22 @@ var (
}
}
} else {
if info, _ := os.Lstat(localFilePaths[0]); info.IsDir() {
localFilePath = filepath.Join(localFilePaths[0], filepath.Base(remoteFilePath))
}
err = zsshlib.RetrieveRemoteFiles(client, localFilePath, remoteFilePath)
if err != nil {
logrus.Fatalf("failed to retrieve file: %s [%v]", remoteFilePath, err)
}
}
}
},
}
)

func init() {
rootCmd.Flags().StringVarP(&ZConfig, "ZConfig", "c", "", fmt.Sprintf("path to ziti config file. default: $HOME/.ziti/%s.json", serviceName))
rootCmd.Flags().StringVarP(&SshKeyPath, "SshKeyPath", "i", "", "path to ssh key. default: $HOME/.ssh/id_rsa")
rootCmd.Flags().BoolVarP(&debug, "debug", "d", false, "pass to enable additional debug information")
rootCmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "pass to enable recursive file transfer")
rootCmd.Flags().StringVarP(&serviceName, "service", "s", ExpectedServiceAndExeName, fmt.Sprintf("service name. default: %s", ExpectedServiceAndExeName))
}
},
}

func getConfig(cfgFile string) (zitiCfg *config.Config) {
zitiCfg, err := config.NewFromFile(cfgFile)
if err != nil {
log.Fatalf("failed to load ziti configuration file: %v", err)
}
return zitiCfg
func init() {
flags.InitFlags(rootCmd, ExpectedServiceAndExeName)
rootCmd.Flags().BoolVarP(&flags.Recursive, "recursive", "r", false, "pass to enable recursive file transfer")
}

func after(value string, a string) string {
136 changes: 35 additions & 101 deletions zssh/zssh/main.go
Original file line number Diff line number Diff line change
@@ -1,117 +1,51 @@
/*
Copyright NetFoundry, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"fmt"
"github.com/openziti/sdk-golang/ziti"
"github.com/openziti/sdk-golang/ziti/config"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"log"
"os"
"os/user"
"path/filepath"
"runtime"
"strings"
"zssh/zsshlib"
)

const ExpectedServiceAndExeName = "zssh"

var (
ZConfig string
SshKeyPath string
debug bool
serviceName string

rootCmd = &cobra.Command{
Use: fmt.Sprintf("%s <remoteUsername>@<targetIdentity>", serviceName),
Short: "Z(iti)ssh, Carb-loaded ssh performs faster and stronger than ssh",
Long: "Z(iti)ssh is a version of ssh that utilizes a ziti network to provide a faster and more secure remote connection. A ziti connection must be established before use",
Args: cobra.ExactValidArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if SshKeyPath == "" {
userHome, err := os.UserHomeDir()
if err != nil {
logrus.Fatalf("could not find UserHomeDir? %v", err)
}
SshKeyPath = filepath.Join(userHome, ".ssh", "id_rsa")
}
if debug {
logrus.Infof(" sshKeyPath set to: %s", SshKeyPath)
}

if ZConfig == "" {
userHome, err := os.UserHomeDir()
if err != nil {
logrus.Fatalf("could not find UserHomeDir? %v", err)
}
ZConfig = filepath.Join(userHome, ".ziti", fmt.Sprintf("%s.json", ExpectedServiceAndExeName))
}
if debug {
logrus.Infof(" ZConfig set to: %s", ZConfig)
}

var username string
var targetIdentity string
if strings.ContainsAny(args[0], "@") {
userServiceName := strings.Split(args[0], "@")
username = userServiceName[0]
targetIdentity = userServiceName[1]
} else {
curUser, err := user.Current()
if err != nil {
logrus.Fatal(err)
}
username = curUser.Username
if strings.Contains(username, "\\") && runtime.GOOS == "windows" {
username = strings.Split(username, "\\")[1]
}
targetIdentity = args[0]
}
if debug {
logrus.Infof(" username set to: %s", username)
logrus.Infof("targetIdentity set to: %s", targetIdentity)
}

ctx := ziti.NewContextWithConfig(getConfig(ZConfig))

_, ok := ctx.GetService(serviceName)
if !ok {
logrus.Fatalf("could not find service: %s", serviceName)
}

dialOptions := &ziti.DialOptions{
ConnectTimeout: 0,
Identity: targetIdentity,
AppData: nil,
}
svc, err := ctx.DialWithOptions(serviceName, dialOptions)
if err != nil {
logrus.Fatalf("error when dialing service name %s. %v", serviceName, err)
}
factory := zsshlib.NewSshConfigFactoryImpl(username, SshKeyPath)
zclient, err := zsshlib.Dial(factory.Config(), svc)
if err != nil {
logrus.Fatal(err)
}
err = zsshlib.RemoteShell(zclient)
},
}
)

func init() {
rootCmd.Flags().StringVarP(&serviceName, "service", "s", ExpectedServiceAndExeName, fmt.Sprintf("service name. default: %s", ExpectedServiceAndExeName))
rootCmd.Flags().StringVarP(&ZConfig, "ZConfig", "c", "", fmt.Sprintf("Path to ziti config file. default: $HOME/.ziti/%s.json", serviceName))
rootCmd.Flags().StringVarP(&SshKeyPath, "SshKeyPath", "i", "", "Path to ssh key. default: $HOME/.ssh/id_rsa")
rootCmd.Flags().BoolVarP(&debug, "debug", "d", false, "pass to enable additional debug information")
var flags = zsshlib.SshFlags{}

var rootCmd = &cobra.Command{
Use: fmt.Sprintf("%s <remoteUsername>@<targetIdentity>", flags.ServiceName),
Short: "Z(iti)ssh, Carb-loaded ssh performs faster and stronger than ssh",
Long: "Z(iti)ssh is a version of ssh that utilizes a ziti network to provide a faster and more secure remote connection. A ziti connection must be established before use",
Args: cobra.ExactValidArgs(1),
Run: func(cmd *cobra.Command, args []string) {
username, targetIdentity := flags.GetUserAndIdentity(args[0])

sshConn := zsshlib.EstablishClient(flags, username, targetIdentity)
defer func() { _ = sshConn.Close() }()
err := zsshlib.RemoteShell(sshConn)
if err != nil {
logrus.Fatalf("error opening remote shell: %v", err)
}
},
}

func getConfig(cfgFile string) (zitiCfg *config.Config) {
zitiCfg, err := config.NewFromFile(cfgFile)
if err != nil {
log.Fatalf("failed to load ziti configuration file: %v", err)
}
return zitiCfg
func init() {
flags.InitFlags(rootCmd, ExpectedServiceAndExeName)
}

func main() {
97 changes: 97 additions & 0 deletions zsshlib/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package zsshlib

import (
"fmt"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"os"
"os/user"
"path/filepath"
"runtime"
"strings"
)

type SshFlags struct {
ZConfig string
SshKeyPath string
Debug bool
ServiceName string
}

type ScpFlags struct {
SshFlags
Recursive bool
}

func (f *SshFlags) GetUserAndIdentity(input string) (string, string) {
username := ParseUserName(input)
f.DebugLog(" username set to: %s", username)
targetIdentity := ParseTargetIdentity(input)
f.DebugLog("targetIdentity set to: %s", targetIdentity)
return username, targetIdentity
}

func ParseUserName(input string) string {
var username string
if strings.ContainsAny(input, "@") {
userServiceName := strings.Split(input, "@")
username = userServiceName[0]
} else {
curUser, err := user.Current()
if err != nil {
logrus.Fatal(err)
}
username = curUser.Username
if strings.Contains(username, "\\") && runtime.GOOS == "windows" {
username = strings.Split(username, "\\")[1]
}
}
return username
}

func ParseTargetIdentity(input string) string {
var targetIdentity string
if strings.ContainsAny(input, "@") {
targetIdentity = strings.Split(input, "@")[1]
} else {
targetIdentity = input
}

if strings.Contains(targetIdentity, ":") {
return strings.Split(targetIdentity, ":")[0]
}
return targetIdentity
}

func ParseFilePath(input string) string {
if strings.Contains(input, ":") {
colPos := strings.Index(input, ":") + 1
return input[colPos:]
}
return input
}

func (f *SshFlags) InitFlags(cmd *cobra.Command, exeName string) {
cmd.Flags().StringVarP(&f.ServiceName, "service", "s", exeName, fmt.Sprintf("service name. default: %s", exeName))
cmd.Flags().StringVarP(&f.ZConfig, "ZConfig", "c", "", fmt.Sprintf("Path to ziti config file. default: $HOME/.ziti/%s.json", f.ServiceName))
cmd.Flags().StringVarP(&f.SshKeyPath, "SshKeyPath", "i", "", "Path to ssh key. default: $HOME/.ssh/id_rsa")
cmd.Flags().BoolVarP(&f.Debug, "debug", "d", false, "pass to enable additional debug information")

if f.SshKeyPath == "" {
userHome, err := os.UserHomeDir()
if err != nil {
logrus.Fatalf("could not find UserHomeDir? %v", err)
}
f.SshKeyPath = filepath.Join(userHome, SSH_DIR, ID_RSA)
}
f.DebugLog(" flags.SshKeyPath set to: %s", f.SshKeyPath)

if f.ZConfig == "" {
userHome, err := os.UserHomeDir()
if err != nil {
logrus.Fatalf("could not find UserHomeDir? %v", err)
}
f.ZConfig = filepath.Join(userHome, ".ziti", fmt.Sprintf("%s.json", exeName))
}
f.DebugLog(" ZConfig set to: %s", f.ZConfig)
}
61 changes: 61 additions & 0 deletions zsshlib/flags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package zsshlib

import (
"os/user"
"runtime"
"strings"
"testing"
)
import "github.com/stretchr/testify/assert"

func TestParseTargetIdentity(t *testing.T) {
result := ParseTargetIdentity("user@hostname:port")
assert.Equal(t, result, "hostname", "user not correct")

result = ParseTargetIdentity("hostname:port")
assert.Equal(t, result, "hostname", "user not correct")

result = ParseTargetIdentity("hostname")
assert.Equal(t, result, "hostname", "user not correct")

result = ParseTargetIdentity("user@hostname")
assert.Equal(t, result, "hostname", "user not correct")
}

func getOsUser() string {
u, _ := user.Current()
return u.Username
}
func TestParseUserName(t *testing.T) {
result := ParseUserName("user@hostname:port")
assert.Equal(t, result, "user", "user not correct")

var osUser string
if runtime.GOOS == "windows" {
osUser = strings.Split(getOsUser(), `\`)[1]
} else {
osUser = getOsUser()
}

result = ParseUserName("hostname:port")
assert.Equal(t, result, osUser, "user not correct")

result = ParseUserName("hostname")
assert.Equal(t, result, osUser, "user not correct")

result = ParseUserName("user@hostname")
assert.Equal(t, result, "user", "user not correct")
}
func TestParseFilePath(t *testing.T) {
result := ParseFilePath("user@hostname:/*/bob")
assert.Equal(t, result, "/*/bob", "user not correct")

result = ParseFilePath("user@hostname:/bob")
assert.Equal(t, result, "/bob", "user not correct")

result = ParseFilePath("user@hostname:")
assert.Equal(t, result, "", "user not correct")

result = ParseFilePath(`user@hostname:/haha://two\:colons`)
assert.Equal(t, result, `/haha://two\:colons`, "user not correct")
}
16 changes: 16 additions & 0 deletions zsshlib/logrus_setup.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
Copyright NetFoundry, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package zsshlib

import (
71 changes: 68 additions & 3 deletions zsshlib/ssh.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2019 NetFoundry, Inc.
Copyright NetFoundry, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -18,12 +18,16 @@ package zsshlib

import (
"fmt"
"github.com/openziti/sdk-golang/ziti"
"github.com/openziti/sdk-golang/ziti/config"
"github.com/pkg/errors"
"github.com/pkg/sftp"
"io"
"io/ioutil"
"log"
"net"
"os"
"path/filepath"
"strconv"
"sync"

@@ -32,6 +36,11 @@ import (
"golang.org/x/crypto/ssh/terminal"
)

const (
ID_RSA = "id_rsa"
SSH_DIR = ".ssh"
)

func RemoteShell(client *ssh.Client) error {
session, err := client.NewSession()
if err != nil {
@@ -174,7 +183,6 @@ func sshAuthMethodFromFile(keyPath string) (ssh.AuthMethod, error) {
}

func SendFile(client *sftp.Client, localPath string, remotePath string) error {

localFile, err := ioutil.ReadFile(localPath)

if err != nil {
@@ -206,7 +214,7 @@ func RetrieveRemoteFiles(client *sftp.Client, localPath string, remotePath strin

lf, err := os.OpenFile(localPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm)
if err != nil {
return fmt.Errorf("error opening local file [%s] (%w)", remotePath, err)
return fmt.Errorf("error opening local file [%s] (%w)", localPath, err)
}
defer func() { _ = lf.Close() }()

@@ -218,3 +226,60 @@ func RetrieveRemoteFiles(client *sftp.Client, localPath string, remotePath strin

return nil
}

func EstablishClient(f SshFlags, userName string, targetIdentity string) *ssh.Client {
ctx := ziti.NewContextWithConfig(getConfig(f.ZConfig))
_, ok := ctx.GetService(f.ServiceName)
if !ok {
logrus.Fatalf("service not found: %s", f.ServiceName)
}

dialOptions := &ziti.DialOptions{
ConnectTimeout: 0,
Identity: targetIdentity,
AppData: nil,
}
svc, err := ctx.DialWithOptions(f.ServiceName, dialOptions)

if err != nil {
logrus.Fatalf("error when dialing service name %s. %v", f.ServiceName, err)
}

factory := NewSshConfigFactoryImpl(userName, f.SshKeyPath)
config := factory.Config()
sshConn, err := Dial(config, svc)
if err != nil {
logrus.Fatalf("error dialing SSH Conn: %v", err)
}
return sshConn
}

func (f *SshFlags) DebugLog(msg string, args ...interface{}) {
if f.Debug {
logrus.Infof(msg, args...)
}
}

func getConfig(cfgFile string) (zitiCfg *config.Config) {
zitiCfg, err := config.NewFromFile(cfgFile)
if err != nil {
log.Fatalf("failed to load ziti configuration file: %v", err)
}
return zitiCfg
}

// AppendBaseName tags file name on back of remotePath if the path is blank or a directory/*
func AppendBaseName(c *sftp.Client, remotePath string, localPath string, debug bool) string {
localPath = filepath.Base(localPath)
if remotePath == "" {
remotePath = filepath.Base(localPath)
} else {
info, err := c.Lstat(remotePath)
if err == nil && info.IsDir() {
remotePath = filepath.Join(remotePath, localPath)
} else if debug {
logrus.Infof("Remote File/Directory: %s doesn't exist [%v]", remotePath, err)
}
}
return remotePath
}
2 changes: 1 addition & 1 deletion zsshlib/ssh_darwin.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2019 NetFoundry, Inc.
Copyright NetFoundry, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
2 changes: 1 addition & 1 deletion zsshlib/ssh_linux.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2019 NetFoundry, Inc.
Copyright NetFoundry, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
34 changes: 34 additions & 0 deletions zsshlib/ssh_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package zsshlib

import (
"github.com/pkg/sftp"
"github.com/stretchr/testify/assert"
"net"
"os"
"path/filepath"
"testing"
)

func TestAppendBaseName(t *testing.T) {
conn, _ := net.Dial("tcp", "localhost:3838")
userHome, _ := os.UserHomeDir()
factory := NewSshConfigFactoryImpl(getOsUser(), filepath.Join(userHome, SSH_DIR, ID_RSA))
factory.port = 3838
config := factory.Config()
sshConn, _ := Dial(config, conn)

client, _ := sftp.NewClient(sshConn)
defer func() { _ = client.Close() }()

result := appendBaseName(client, "", "message.txt", false)
assert.Equal(t, result, "message.txt", "Path not correct")

result = appendBaseName(client, "~", "message.txt", false)
assert.Equal(t, result, "message.txt", "Path not correct")

result = appendBaseName(client, "/", "message.txt", false)
assert.Equal(t, result, "message.txt", "Path not correct")

result = appendBaseName(client, "message.txt", "message.txt", false)
assert.Equal(t, result, "message.txt", "Path not correct")
}
2 changes: 1 addition & 1 deletion zsshlib/ssh_windows.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2019 NetFoundry, Inc.
Copyright NetFoundry Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

0 comments on commit d19312c

Please sign in to comment.