Skip to content

Commit

Permalink
Add source code
Browse files Browse the repository at this point in the history
  • Loading branch information
Entscheider committed Aug 21, 2022
1 parent 3630b8d commit 3e76ffc
Show file tree
Hide file tree
Showing 20 changed files with 3,486 additions and 0 deletions.
134 changes: 134 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Introduction

SSHTool is a go application that implements some ssh capabilities in a simple-to-use way. In detail:

1. Generating ed25519 private/public keys.
2. Exposing a terminal program via an ssh connection. Including pty support (on unix).
3. Exposing one or more directories via **sftp**. Using regular expressions, you can set read and write permissions
separately from the operating system as well as hide files from public view.
For application/operating systems which doesn't support sftp connection, SSHTool additionally can start a **webdav**
server which can be forwarded with ssh to localhost in order to connect to it.

Note that SSHTool only supports public key authentication.

# Usage

## Key generation

To generate an ed25519 key pair execute

```bash
sshtool generate sshkey
```

It creates the private file under `sshkey` and the corresponding public key named `sshkey.pub`.

Be aware that you don't need to manually generate the key for SSHTool's remaining commands.
Private keys that were not found will be generated there automatically.

## Program exposing

For exposing another program in an ssh session, execute

```bash
sshtool cmd config.toml
```

where `config.toml` is the configuration file containing the details.
A standard configuration file is created if the given filename does not exist.
In the config file you can set the hostname the ssh server is listen to as well as the port.

### Connecting

For connecting to the ssh server, execute

```bash
ssh username@servername -p 2222
```

where `username` can be arbitrary, `servername` is the ip address or dns name of the host and 2222 is the port set in
the configuration.

### Configuration details

The `ServerKeyFilename` is an array with different private keys the server offers the client and will sign requests with.
Every key must use a different signature scheme (rsa, ed25519, etc.).
If a key is not found, a random ed25519 key is generated and saved at the location automatically.

The parameter `MaxNumberOfConnections` is the maximal number of parallel ssh connection accepted by the server.
A value of 0 means no limit.

`AuthorizedKeys` is a list of public ssh keys accepted from a client.
The format of every entry is the same as in the `authorized_keys` file ssh expects.
E.g. "ssh-ed25519 AAAAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX someone@somewhere".

`Command` is the path of the application to execute and `CommandArgs` a list of arguments to start it with.

## SFTP

For starting the sftp server, call

```bash
sshtool sftp config.toml
```

Note that the `config.toml` is different from the cmd subcommand.
If the config file does not exist, it will be created automatically.

### Connecting

Connecting to the sftp server depends on the client you choose.
For Linux there is [sshfs](https://github.com/libfuse/sshfs) and [oxfs](https://github.com/oxfs/oxfs). So you can run

```bash
sshfs username@servername:/ -p 2222 mount
```

to mount the sftp filesystem under "mount". Here 2222 is the port configured, `servername` is ip address or dns name
of the server this app runs on and `username` is the user you want to connect as.
All accepted users along with the accepted authorized keys are listened in the configuration file.
Every user can be served a different filesystem with different read/write permissions.

This filesystem can also be served as webdav if set in the config. This is done by forwarding the webdav
http port to localhost.
If the server runs in sftp mode, there is no stdin/stdout supported. So you have to forward it with

```bash
ssh -NT -L 8080:localhost:80 username@servername -p 2222
```

where 80 is the port described in the config and 8080 is the port opened on the client a webdav client
can connect to. The webdav server has no login requirement.

### Configuration

Most settings match the one from program exposing. In addition to that we have

* `WebDavPort` which is the port the webdav server listen to and can be forwarded from. Note that
the server listen on a virtual port and not on an actual port on the operating system. Thus, the only
way to connect to it is by ssh tcp/ip forwarding.
* `Users` is a list of allowed usernames along with configuration for this user. In the default config
a standard user with the name `user` is created. You need to rename this phrase for change the name.
* `CanRead` is a list of regular expression for files that can be read from a client.
* `CanWrite` is a list of regular expression for files that can be written from a client.
* `ShouldHide` is a list of regular expression for files that are hidden from a client.
* `WebDav` enables the webdav server that can be forwarded with ssh if true. It exposes the exact same filesystem sftp does.
* `FileSystem` lists all directories that can be accessed from a client. If a directory has the name "" in the config,
it will be exposed directly. If not, a client sees a virtual filesystem containing every directory with the name in
this config.
* `Root` is the path of the directory to expose.
* `ReadOnly` sets that this directory can only be read and not be written. It has some overlaps with the `CanRead` config.

# Building

As SSHTool is written in golang, simple run

```bash
go build -o sshtool
```

for compilation. By setting the "GOOS" and "GOARCH" environment variable, cross compiling is possible.

# License

The code is distributed under AGPL-3.0.
14 changes: 14 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module github.com/Entscheider/sshtool

go 1.16

require (
github.com/BurntSushi/toml v1.0.0
github.com/creack/pty v1.1.17
github.com/gliderlabs/ssh v0.3.3
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a
github.com/pkg/sftp v1.13.4
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2
golang.org/x/sys v0.0.0-20220325203850-36772127a21f // indirect
)
48 changes: 48 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
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/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gliderlabs/ssh v0.3.3 h1:mBQ8NiOgDkINJrZtoizkC3nDNYgSaWtxyem6S2XHBtA=
github.com/gliderlabs/ssh v0.3.3/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE=
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ=
github.com/pkg/sftp v1.13.4 h1:Lb0RYJCmgUcBgZosfoi9Y9sbl6+LJgOIgk/2Y4YjMFg=
github.com/pkg/sftp v1.13.4/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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 h1:S25/rfnfsMVgORT4/J61MJ7rdyseOZOyvLIrZEZ7s6s=
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y=
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220325203850-36772127a21f h1:TrmogKRsSOxRMJbLYGrB4SBbW+LJcEllYBLME5Zk5pU=
golang.org/x/sys v0.0.0-20220325203850-36772127a21f/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 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
178 changes: 178 additions & 0 deletions logger/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package logger

import (
"fmt"
"io"
"strings"
"sync"
"time"
)

// Logger is interface for basic logging methods
type Logger interface {
io.Closer
// Warn prints a warning msg under the given tag
Warn(tag string, msg string)
// Err prints an error under the given tag
Err(tag string, msg string)
// Debug prints a debug output under the given tag
Debug(tag string, msg string)
// Info prints a info output under the given tag
Info(tag string, msg string)
}

// A default implementation of a logger that avoids parallel printing in different thread
type stdLogger struct {
channel chan<- string
waitChannel <-chan bool
wg *sync.WaitGroup
}

// NewLogger creates a new Logger implementation that writes all outputs to the given writer
func NewLogger(writer io.Writer) Logger {
c := make(chan string)
wc := make(chan bool)
wg := sync.WaitGroup{}
go func() {
defer wg.Done()
defer close(wc)
wg.Add(1)
if closer, ok := writer.(io.WriteCloser); ok {
defer closer.Close()
}
for text := range c {
_, _ = fmt.Fprint(writer, text)
wc <- true
}
}()
return &stdLogger{c, wc, &wg}
}

func (l *stdLogger) Close() error {
close(l.channel)
l.wg.Wait()
return nil
}

func (l *stdLogger) print(symbol string, tag string, msg string) {
t := time.Now()
l.channel <- fmt.Sprintf("%s [%s] %s - %s\n", t.Local(), symbol, tag, msg)
<-l.waitChannel
}

func (l *stdLogger) Warn(tag string, msg string) {
l.print("W", tag, msg)
}

func (l *stdLogger) Err(tag string, msg string) {
l.print("E", tag, msg)
}

func (l *stdLogger) Debug(tag string, msg string) {
l.print("D", tag, msg)
}

func (l *stdLogger) Info(tag string, msg string) {
l.print("I", tag, msg)
}

// ConnectionInfo contains meta information about a new ssh connection
type ConnectionInfo struct {
IP string
Username string
}

// AccessLogger is an interface that adds method for logging ssh related actions
type AccessLogger interface {
io.Closer
// NewLogin is called when a new login has happened and was answered with the given status
NewLogin(connection ConnectionInfo, status string)
// Logout is called when a logout happens
Logout(connection ConnectionInfo)
// NewAccess is called when a new file has been requested at the given path
// for the given kind (MkDir, Read, Write, etc.) and was answered with the given status (e.g. granted)
NewAccess(connection ConnectionInfo, path string, kind string, status string)
}

// AccessLogger that prints all output to an io.Writer and prevents multiple writes from different threads.
type stdAccessLogger struct {
channel chan<- string
waitChannel <-chan bool
wg *sync.WaitGroup
}

// NewAccessLogger creates a new standard AccessLogger that prints all output to the given writer
func NewAccessLogger(writer io.Writer) AccessLogger {
c := make(chan string)
wc := make(chan bool)
wg := sync.WaitGroup{}
go func() {
defer wg.Done()
defer close(wc)
wg.Add(1)
if closer, ok := writer.(io.WriteCloser); ok {
defer closer.Close()
}
for text := range c {
_, _ = fmt.Fprintf(writer, "%s\n", text)
wc <- true
}
}()
return &stdAccessLogger{c, wc, &wg}
}

// Collects information about an access log entry
type entry struct {
logType string
connectionInfo ConnectionInfo
path string
kind string
status string
}

func (l *stdAccessLogger) printStrings(entries ...string) {
t := time.Now()
values := make([]string, len(entries)+1)
values[0] = fmt.Sprintf("\"%s\"", t.Local())
for i, e := range entries {
values[i+1] = fmt.Sprintf("\"%s\"", strings.ReplaceAll(e, "\"", "\"\""))
}
l.channel <- strings.Join(values, ",")
<-l.waitChannel
}

func (l *stdAccessLogger) printEntry(e entry) {
l.printStrings(e.logType, e.connectionInfo.IP, e.connectionInfo.Username,
e.path, e.kind, e.status)
}

func (l *stdAccessLogger) NewLogin(connection ConnectionInfo, status string) {
l.printEntry(entry{
logType: "login",
connectionInfo: connection,
status: status,
})
}

func (l *stdAccessLogger) Logout(connection ConnectionInfo) {
l.printEntry(entry{
logType: "logout",
connectionInfo: connection,
})
}

func (l *stdAccessLogger) NewAccess(connection ConnectionInfo, path string, kind string, status string) {
l.printEntry(entry{
logType: "access",
connectionInfo: connection,
path: path,
kind: kind,
status: status,
})
}

func (l *stdAccessLogger) Close() error {
close(l.channel)
l.wg.Wait()
return nil
}
Loading

0 comments on commit 3e76ffc

Please sign in to comment.