refactor, initial work for ssh driver (still wip)

This commit is contained in:
Pavel 2023-11-18 16:14:39 +03:00
parent 7c443887dc
commit d3029d09e7
22 changed files with 461 additions and 128 deletions

View File

@ -36,7 +36,6 @@ var rootCmd = &cobra.Command{
for id, item := range docker.Default.Containers() { for id, item := range docker.Default.Containers() {
err := server.DefaultManager.ProcessEvent(dto.Event{ err := server.DefaultManager.ProcessEvent(dto.Event{
Type: dto.EventStart, Type: dto.EventStart,
ID: id,
Container: item, Container: item,
}) })
if err != nil { if err != nil {
@ -54,7 +53,8 @@ var rootCmd = &cobra.Command{
for event := range events { for event := range events {
err := server.DefaultManager.ProcessEvent(event) err := server.DefaultManager.ProcessEvent(event)
if err != nil { if err != nil {
logger.Sugar.Errorw("cannot expose container", "id", event.ID, "error", err) logger.Sugar.Errorw("cannot expose container",
"id", event.Container.ID, "error", err)
} }
} }
}() }()

1
go.mod
View File

@ -6,6 +6,7 @@ require (
github.com/docker/docker v24.0.7+incompatible github.com/docker/docker v24.0.7+incompatible
github.com/docker/go-connections v0.4.0 github.com/docker/go-connections v0.4.0
github.com/go-playground/validator/v10 v10.16.0 github.com/go-playground/validator/v10 v10.16.0
github.com/kevinburke/ssh_config v1.2.0
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/spf13/cast v1.5.1 github.com/spf13/cast v1.5.1
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0

2
go.sum
View File

@ -162,6 +162,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=

View File

@ -10,7 +10,7 @@ import (
"github.com/Neur0toxine/sshpoke/internal/logger" "github.com/Neur0toxine/sshpoke/internal/logger"
"github.com/Neur0toxine/sshpoke/internal/server" "github.com/Neur0toxine/sshpoke/internal/server"
"github.com/Neur0toxine/sshpoke/internal/server/driver/plugin" "github.com/Neur0toxine/sshpoke/internal/server/driver/plugin"
"github.com/Neur0toxine/sshpoke/pkg/dto" "github.com/Neur0toxine/sshpoke/pkg/convert"
plugin2 "github.com/Neur0toxine/sshpoke/pkg/plugin" plugin2 "github.com/Neur0toxine/sshpoke/pkg/plugin"
"github.com/Neur0toxine/sshpoke/pkg/plugin/pb" "github.com/Neur0toxine/sshpoke/pkg/plugin/pb"
"google.golang.org/grpc" "google.golang.org/grpc"
@ -44,10 +44,7 @@ func (p *pluginAPI) EventStatus(ctx context.Context, msg *pb.EventStatusMessage)
if pl == nil { if pl == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
pl.HandleStatus(dto.EventStatus{ pl.PushEventStatus(convert.MessageToAppEventStatus(msg))
ID: msg.Id,
Error: msg.Error,
})
return &emptypb.Empty{}, nil return &emptypb.Empty{}, nil
} }

View File

@ -50,6 +50,21 @@ func (d *Docker) Containers() map[string]dto.Container {
return containers return containers
} }
func (d *Docker) GetContainer(id string) (dto.Container, bool) {
container, err := d.cli.ContainerList(d.ctx, types.ContainerListOptions{
Filters: filters.NewArgs(filters.Arg("id", id)),
All: true,
})
if err != nil || len(container) != 1 {
return dto.Container{}, false
}
converted, ok := dockerContainerToInternal(container[0])
if !ok {
return dto.Container{}, false
}
return converted, true
}
func (d *Docker) Listen() (chan dto.Event, error) { func (d *Docker) Listen() (chan dto.Event, error) {
cli, err := client.NewClientWithOpts(config.Default.Docker.Opts) cli, err := client.NewClientWithOpts(config.Default.Docker.Opts)
if err != nil { if err != nil {
@ -83,7 +98,6 @@ func (d *Docker) Listen() (chan dto.Event, error) {
} }
newEvent := dto.Event{ newEvent := dto.Event{
Type: eventType, Type: eventType,
ID: event.Actor.ID,
Container: converted, Container: converted,
} }
msg := "exposing container" msg := "exposing container"

View File

@ -77,6 +77,8 @@ func dockerContainerToInternal(container types.Container) (result dto.Container,
} }
return dto.Container{ return dto.Container{
ID: container.ID,
Names: container.Names,
IP: ip, IP: ip,
Port: uint16(port), Port: uint16(port),
Server: labels.Server, Server: labels.Server,

View File

@ -0,0 +1,30 @@
package server
import (
"encoding/json"
"github.com/Neur0toxine/sshpoke/pkg/dto"
)
type Connections map[string]dto.Container
func (c Connections) MarshalJSON() ([]byte, error) {
items := []dto.Container{}
for _, item := range c {
items = append(items, item)
}
return json.Marshal(items)
}
func (c *Connections) UnmarshalJSON(data []byte) error {
var items []dto.Container
if err := json.Unmarshal(data, &items); err != nil {
return err
}
m := make(map[string]dto.Container, len(items))
for _, item := range items {
m[item.ID] = item
}
*c = m
return nil
}

View File

@ -0,0 +1,46 @@
package base
import (
"context"
"github.com/Neur0toxine/sshpoke/internal/logger"
"github.com/Neur0toxine/sshpoke/pkg/dto"
"go.uber.org/zap"
)
type Base struct {
ctx context.Context
name string
log *zap.SugaredLogger
eventStatusCb EventStatusCallback
}
func New(ctx context.Context, name string) Base {
return Base{
ctx: ctx,
name: name,
log: logger.Default.With(zap.String("serverName", name)).Sugar(),
}
}
func (b *Base) SetEventStatusCallback(callback EventStatusCallback) {
b.eventStatusCb = callback
}
func (b *Base) PushEventStatus(status dto.EventStatus) {
if b.eventStatusCb != nil {
b.eventStatusCb(status)
}
}
func (b *Base) Context() context.Context {
return b.ctx
}
func (b *Base) Name() string {
return b.name
}
func (b *Base) Log() *zap.SugaredLogger {
return b.log
}

View File

@ -1,17 +1,22 @@
package iface package base
import ( import (
"context" "context"
"github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/config"
"github.com/Neur0toxine/sshpoke/pkg/dto" "github.com/Neur0toxine/sshpoke/pkg/dto"
"go.uber.org/zap"
) )
type DriverConstructor func(ctx context.Context, name string, params config.DriverParams) (Driver, error) type DriverConstructor func(ctx context.Context, name string, params config.DriverParams) (Driver, error)
type EventStatusCallback func(status dto.EventStatus)
type Driver interface { type Driver interface {
Name() string Name() string
SetEventStatusCallback(callback EventStatusCallback)
PushEventStatus(status dto.EventStatus)
Handle(event dto.Event) error Handle(event dto.Event) error
Driver() config.DriverType Driver() config.DriverType
Log() *zap.SugaredLogger
WaitForShutdown() WaitForShutdown()
} }

View File

@ -4,13 +4,13 @@ import (
"context" "context"
"github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/config"
"github.com/Neur0toxine/sshpoke/internal/server/driver/iface" "github.com/Neur0toxine/sshpoke/internal/server/driver/base"
"github.com/Neur0toxine/sshpoke/internal/server/driver/null" "github.com/Neur0toxine/sshpoke/internal/server/driver/null"
"github.com/Neur0toxine/sshpoke/internal/server/driver/plugin" "github.com/Neur0toxine/sshpoke/internal/server/driver/plugin"
"github.com/Neur0toxine/sshpoke/internal/server/driver/ssh" "github.com/Neur0toxine/sshpoke/internal/server/driver/ssh"
) )
func New(ctx context.Context, name string, driver config.DriverType, params config.DriverParams) (iface.Driver, error) { func New(ctx context.Context, name string, driver config.DriverType, params config.DriverParams) (base.Driver, error) {
switch driver { switch driver {
case config.DriverSSH: case config.DriverSSH:
return ssh.New(ctx, name, params) return ssh.New(ctx, name, params)

View File

@ -4,28 +4,38 @@ import (
"context" "context"
"github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/config"
"github.com/Neur0toxine/sshpoke/internal/logger" "github.com/Neur0toxine/sshpoke/internal/server/driver/base"
"github.com/Neur0toxine/sshpoke/internal/server/driver/iface"
"github.com/Neur0toxine/sshpoke/pkg/dto" "github.com/Neur0toxine/sshpoke/pkg/dto"
) )
// Null driver only logs container events to debug log. It is used when user provides invalid driver type. // Null driver only logs container events to debug log. It is used when user provides invalid driver type.
// You can use it directly, but it won't do anything, so... why bother? // You can use it directly, but it won't do anything, so... why bother?
type Null struct { type Null struct {
name string base.Base
} }
func New(ctx context.Context, name string, params config.DriverParams) (iface.Driver, error) { func New(ctx context.Context, name string, params config.DriverParams) (base.Driver, error) {
return &Null{name: name}, nil return &Null{
Base: base.New(ctx, name),
}, nil
} }
func (d *Null) Handle(event dto.Event) error { func (d *Null) Handle(event dto.Event) error {
logger.Sugar.Debugw("handling event with null driver", "serverName", d.name, "event", event) d.Log().Debugw("handling event with null driver", "serverName", d.Name(), "event", event)
return nil switch event.Type {
case dto.EventStart:
d.PushEventStatus(dto.EventStatus{
Type: dto.EventStart,
ID: event.Container.ID,
Domain: "https://" + event.Container.ID + "null.dev",
})
case dto.EventStop, dto.EventShutdown:
d.PushEventStatus(dto.EventStatus{
Type: event.Type,
ID: event.Container.ID,
})
} }
return nil
func (d *Null) Name() string {
return d.name
} }
func (d *Null) Driver() config.DriverType { func (d *Null) Driver() config.DriverType {

View File

@ -7,8 +7,7 @@ import (
"sync/atomic" "sync/atomic"
"github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/config"
"github.com/Neur0toxine/sshpoke/internal/logger" "github.com/Neur0toxine/sshpoke/internal/server/driver/base"
"github.com/Neur0toxine/sshpoke/internal/server/driver/iface"
"github.com/Neur0toxine/sshpoke/internal/server/driver/util" "github.com/Neur0toxine/sshpoke/internal/server/driver/util"
"github.com/Neur0toxine/sshpoke/pkg/dto" "github.com/Neur0toxine/sshpoke/pkg/dto"
) )
@ -17,8 +16,7 @@ var ErrAlreadyConnected = errors.New("already connected")
// Driver plugin uses RPC to communicate with external plugin. // Driver plugin uses RPC to communicate with external plugin.
type Driver struct { type Driver struct {
ctx context.Context base.Base
name string
params Params params Params
send *Queue[dto.Event] send *Queue[dto.Event]
listening atomic.Bool listening atomic.Bool
@ -29,16 +27,14 @@ type EventStream interface {
} }
type Plugin interface { type Plugin interface {
iface.Driver base.Driver
Token() string Token() string
Listen(ctx context.Context, stream EventStream) error Listen(ctx context.Context, stream EventStream) error
HandleStatus(event dto.EventStatus)
} }
func New(ctx context.Context, name string, params config.DriverParams) (iface.Driver, error) { func New(ctx context.Context, name string, params config.DriverParams) (base.Driver, error) {
drv := &Driver{ drv := &Driver{
name: name, Base: base.New(ctx, name),
ctx: ctx,
send: NewQueue[dto.Event](), send: NewQueue[dto.Event](),
} }
if err := util.UnmarshalParams(params, &drv.params); err != nil { if err := util.UnmarshalParams(params, &drv.params); err != nil {
@ -56,10 +52,6 @@ func (d *Driver) Handle(event dto.Event) error {
return nil return nil
} }
func (d *Driver) Name() string {
return d.name
}
func (d *Driver) Driver() config.DriverType { func (d *Driver) Driver() config.DriverType {
return config.DriverPlugin return config.DriverPlugin
} }
@ -88,21 +80,17 @@ func (d *Driver) Listen(ctx context.Context, stream EventStream) error {
return nil return nil
} }
if err != nil { if err != nil {
logger.Sugar.Errorw("error writing event to plugin", d.Log().Errorw("error writing event to plugin",
"server", d.name, "error", err) "server", d.Name(), "error", err)
return err return err
} }
} }
} }
} }
func (d *Driver) HandleStatus(event dto.EventStatus) {
logger.Sugar.Errorw("plugin error", "serverName", d.name, "id", event.ID, "error", event.Error)
}
func (d *Driver) isDone() bool { func (d *Driver) isDone() bool {
select { select {
case <-d.ctx.Done(): case <-d.Context().Done():
return true return true
default: default:
return false return false
@ -110,6 +98,6 @@ func (d *Driver) isDone() bool {
} }
func (d *Driver) WaitForShutdown() { func (d *Driver) WaitForShutdown() {
<-d.ctx.Done() <-d.Context().Done()
return return
} }

View File

@ -3,20 +3,26 @@ package ssh
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"os"
"path"
"strings"
"sync" "sync"
"github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/config"
"github.com/Neur0toxine/sshpoke/internal/server/driver/iface" "github.com/Neur0toxine/sshpoke/internal/server/driver/base"
"github.com/Neur0toxine/sshpoke/internal/server/driver/ssh/types"
"github.com/Neur0toxine/sshpoke/internal/server/driver/util" "github.com/Neur0toxine/sshpoke/internal/server/driver/util"
"github.com/Neur0toxine/sshpoke/internal/server/proto/sshtun" "github.com/Neur0toxine/sshpoke/internal/server/proto/sshtun"
"github.com/Neur0toxine/sshpoke/pkg/dto" "github.com/Neur0toxine/sshpoke/pkg/dto"
"golang.org/x/crypto/ssh"
) )
type SSH struct { type SSH struct {
ctx context.Context base.Base
name string
params Params params Params
sessions map[string]conn sessions map[string]conn
keys []ssh.Signer
wg sync.WaitGroup wg sync.WaitGroup
} }
@ -25,21 +31,47 @@ type conn struct {
tun *sshtun.Tunnel tun *sshtun.Tunnel
} }
func New(ctx context.Context, name string, params config.DriverParams) (iface.Driver, error) { func New(ctx context.Context, name string, params config.DriverParams) (base.Driver, error) {
drv := &SSH{ctx: ctx, name: name, sessions: make(map[string]conn)} drv := &SSH{
Base: base.New(ctx, name),
sessions: make(map[string]conn),
}
if err := util.UnmarshalParams(params, &drv.params); err != nil { if err := util.UnmarshalParams(params, &drv.params); err != nil {
return nil, err return nil, err
} }
drv.populateFromSSHConfig()
if err := drv.parseKeys(); err != nil {
return nil, err
}
return drv, nil return drv, nil
} }
func (d *SSH) Handle(event dto.Event) error { func (d *SSH) populateFromSSHConfig() {
// TODO: Implement event handling & connections management. if d.params.Auth.Directory == "" {
return errors.New(d.name + " server handler is not implemented yet") return
}
cfg, err := parseSSHConfig(types.SmartPath(path.Join(string(d.params.Auth.Directory), "config")))
if err != nil {
return
}
if user, err := cfg.Get(d.params.Address, "User"); err == nil && user != "" {
d.params.Auth.User = user
}
if usePass, err := cfg.Get(d.params.Address, "PasswordAuthentication"); err == nil && usePass == "yes" {
d.params.Auth.Type = types.AuthTypePassword
}
if keyfile, err := cfg.Get(d.params.Address, "IdentityFile"); err == nil && keyfile != "" {
resolvedKeyFile, err := types.SmartPath(keyfile).Resolve(false)
if err == nil {
d.params.Auth.Type = types.AuthTypeKey
d.params.Auth.Keyfile = resolvedKeyFile
}
}
} }
func (d *SSH) Name() string { func (d *SSH) Handle(event dto.Event) error {
return d.name // TODO: Implement event handling & connections management.
return errors.New("server handler is not implemented yet")
} }
func (d *SSH) Driver() config.DriverType { func (d *SSH) Driver() config.DriverType {
@ -49,3 +81,79 @@ func (d *SSH) Driver() config.DriverType {
func (d *SSH) WaitForShutdown() { func (d *SSH) WaitForShutdown() {
d.wg.Wait() d.wg.Wait()
} }
func (d *SSH) parseKeys() error {
if d.params.Auth.Type != types.AuthTypeKey {
return nil
}
dir, err := d.params.Auth.Directory.Resolve(true)
if err != nil {
return fmt.Errorf("cannot parse keys: %s", err)
}
if d.params.Auth.Keyfile != "" {
key, err := parseKey(path.Join(dir, d.params.Auth.Keyfile))
if err != nil {
return err
}
d.keys = []ssh.Signer{key}
return nil
}
entries, err := os.ReadDir(dir)
if err != nil {
return fmt.Errorf("cannot read key directory: %s", err)
}
keys := []ssh.Signer{}
for _, entry := range entries {
if entry.IsDir() {
d.Log().Debugf("skipping '%s' because it's a directory", entry.Name())
continue
}
info, err := entry.Info()
if err != nil {
d.Log().Debugf("skipping '%s' because stat failed: %s", entry.Name(), err)
continue
}
if strings.HasSuffix(entry.Name(), ".pub") {
d.Log().Debugf("skipping '%s' because it's probably a public key", entry.Name())
continue
}
if entry.Name() == "config" {
d.Log().Debugf("skipping '%s' because it's probably a ssh-config file", entry.Name())
continue
}
if entry.Name() == "known_hosts" {
d.Log().Debugf(
"skipping '%s' because it's probably a list of hosts generated by OpenSSH", entry.Name())
continue
}
// this file is too small to be a private key
if info.Size() < 256 {
d.Log().Debugf("skipping '%s' because the file is smaller than 256 bytes", entry.Name())
continue
}
key, err := parseKey(path.Join(dir, entry.Name()))
if err != nil {
d.Log().Debugf("skipping '%s' because it's probably not a key: %s", entry.Name(), err)
continue
}
d.Log().Debugf("loading key '%s', type: %s", entry.Name(), key.PublicKey().Type())
keys = append(keys, key)
}
if len(keys) == 0 {
return errors.New("no keys in the provided directory")
}
d.keys = keys
return nil
}
func parseKey(keyFile string) (ssh.Signer, error) {
keyData, err := os.ReadFile(keyFile)
if err != nil {
return nil, err
}
key, err := ssh.ParsePrivateKey(keyData)
if err != nil {
return nil, err
}
return key, nil
}

View File

@ -1,63 +1,24 @@
package ssh package ssh
import ( import (
"fmt" "github.com/Neur0toxine/sshpoke/internal/server/driver/ssh/types"
"github.com/Neur0toxine/sshpoke/internal/server/driver/util" "github.com/Neur0toxine/sshpoke/internal/server/driver/util"
) )
type Params struct { type Params struct {
Address string `mapstructure:"address" validate:"required"` Address string `mapstructure:"address" validate:"required"`
Auth Auth `mapstructure:"auth"` Auth types.Auth `mapstructure:"auth"`
KeepAlive KeepAlive `mapstructure:"keepalive"` KeepAlive types.KeepAlive `mapstructure:"keepalive"`
Domain string `mapstructure:"domain"` Domain string `mapstructure:"domain"`
DomainProto string `mapstructure:"domain_proto"` DomainProto string `mapstructure:"domain_proto"`
DomainExtractRegex string `mapstructure:"domain_extract_regex" validate:"validregexp"` DomainExtractRegex string `mapstructure:"domain_extract_regex" validate:"validregexp"`
Mode DomainMode `mapstructure:"mode" validate:"required,oneof=single multi"` Mode types.DomainMode `mapstructure:"mode" validate:"required,oneof=single multi"`
Prefix bool `mapstructure:"prefix"` Prefix bool `mapstructure:"prefix"`
} }
type AuthType string
const (
AuthTypePasswordless AuthType = "passwordless"
AuthTypePassword AuthType = "password"
AuthTypeKey AuthType = "key"
)
type DomainMode string
const (
DomainModeSingle DomainMode = "single"
DomainModeMulti DomainMode = "multi"
)
type Auth struct {
Type AuthType `mapstructure:"type" validate:"required,oneof=passwordless password key"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Directory string `mapstructure:"directory"`
Keyfile string `mapstructure:"keyfile"`
}
func (a Auth) validate() error {
if a.Type == AuthTypePassword && a.Password == "" {
return fmt.Errorf("password must be provided for authentication type '%s'", AuthTypePassword)
}
if a.Type == AuthTypeKey && a.Directory == "" {
return fmt.Errorf("password must be provided for authentication type '%s'", AuthTypePassword)
}
return nil
}
type KeepAlive struct {
Interval int `mapstructure:"interval" validate:"gte=0"`
MaxAttempts int `mapstructure:"max_attempts" validate:"gte=1"`
}
func (p *Params) Validate() error { func (p *Params) Validate() error {
if err := util.Validator.Struct(p); err != nil { if err := util.Validator.Struct(p); err != nil {
return err return err
} }
return p.Auth.validate() return p.Auth.Validate()
} }

View File

@ -0,0 +1,21 @@
package ssh
import (
"bytes"
"os"
"github.com/Neur0toxine/sshpoke/internal/server/driver/ssh/types"
"github.com/kevinburke/ssh_config"
)
func parseSSHConfig(filePath types.SmartPath) (*ssh_config.Config, error) {
fileName, err := filePath.Resolve(false)
if err != nil {
return nil, err
}
file, err := os.ReadFile(fileName)
if err != nil {
return nil, err
}
return ssh_config.Decode(bytes.NewReader(file))
}

View File

@ -0,0 +1,73 @@
package types
import (
"errors"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"strings"
)
var envMatcherRegExp = regexp.MustCompile(`\$[\w\d\_]`)
type (
AuthType string
SmartPath string
)
const (
AuthTypePasswordless AuthType = "passwordless"
AuthTypePassword AuthType = "password"
AuthTypeKey AuthType = "key"
)
type Auth struct {
Type AuthType `mapstructure:"type" validate:"required,oneof=passwordless password key"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Directory SmartPath `mapstructure:"directory"`
Keyfile string `mapstructure:"keyfile"`
}
func (k SmartPath) Resolve(shouldBeDirectory bool) (result string, err error) {
result = strings.TrimSpace(string(k))
if strings.HasPrefix(result, "~/") {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
result = path.Join(homeDir, result[2:])
}
for _, match := range envMatcherRegExp.FindAllString(string(k), -1) {
envVar := match[1:]
if envVar == "" {
continue
}
envVar = os.Getenv(envVar)
result = strings.ReplaceAll(result, match, envVar)
}
result, err = filepath.Abs(result)
if err != nil {
return
}
stat, err := os.Stat(result)
if err != nil {
return
}
if !stat.IsDir() && shouldBeDirectory {
err = errors.New("is not a directory")
}
return
}
func (a Auth) Validate() error {
if a.Type == AuthTypePassword && a.Password == "" {
return fmt.Errorf("password must be provided for authentication type '%s'", AuthTypePassword)
}
if a.Type == AuthTypeKey && a.Directory == "" {
return fmt.Errorf("password must be provided for authentication type '%s'", AuthTypePassword)
}
return nil
}

View File

@ -0,0 +1,8 @@
package types
type DomainMode string
const (
DomainModeSingle DomainMode = "single"
DomainModeMulti DomainMode = "multi"
)

View File

@ -0,0 +1,6 @@
package types
type KeepAlive struct {
Interval int `mapstructure:"interval" validate:"gte=0"`
MaxAttempts int `mapstructure:"max_attempts" validate:"gte=1"`
}

View File

@ -6,20 +6,28 @@ import (
"sync" "sync"
"github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/config"
"github.com/Neur0toxine/sshpoke/internal/docker"
"github.com/Neur0toxine/sshpoke/internal/logger" "github.com/Neur0toxine/sshpoke/internal/logger"
"github.com/Neur0toxine/sshpoke/internal/server/driver" "github.com/Neur0toxine/sshpoke/internal/server/driver"
"github.com/Neur0toxine/sshpoke/internal/server/driver/iface" "github.com/Neur0toxine/sshpoke/internal/server/driver/base"
"github.com/Neur0toxine/sshpoke/internal/server/driver/plugin" "github.com/Neur0toxine/sshpoke/internal/server/driver/plugin"
"github.com/Neur0toxine/sshpoke/pkg/dto" "github.com/Neur0toxine/sshpoke/pkg/dto"
) )
type Manager struct { type Manager struct {
rw sync.RWMutex rw sync.RWMutex
servers map[string]iface.Driver servers map[string]base.Driver
plugins map[string]plugin.Plugin plugins map[string]plugin.Plugin
statusMap map[string]serverStatus
statusLock sync.RWMutex
defaultServer string defaultServer string
} }
type serverStatus struct {
Name string `json:"name"`
Connections Connections `json:"connections"`
}
var DefaultManager *Manager var DefaultManager *Manager
var ( var (
ErrNoServer = errors.New("server is not specified") ErrNoServer = errors.New("server is not specified")
@ -28,7 +36,7 @@ var (
func NewManager(ctx context.Context, servers []config.Server, defaultServer string) *Manager { func NewManager(ctx context.Context, servers []config.Server, defaultServer string) *Manager {
m := &Manager{ m := &Manager{
servers: make(map[string]iface.Driver), servers: make(map[string]base.Driver),
plugins: make(map[string]plugin.Plugin), plugins: make(map[string]plugin.Plugin),
defaultServer: defaultServer, defaultServer: defaultServer,
} }
@ -38,6 +46,7 @@ func NewManager(ctx context.Context, servers []config.Server, defaultServer stri
logger.Sugar.Errorf("cannot initialize server '%s': %s", serverConfig.Name, err) logger.Sugar.Errorf("cannot initialize server '%s': %s", serverConfig.Name, err)
continue continue
} }
server.SetEventStatusCallback(m.eventStatusCallback(server.Name()))
if server.Driver() == config.DriverPlugin { if server.Driver() == config.DriverPlugin {
pl := server.(plugin.Plugin) pl := server.(plugin.Plugin)
if pl.Token() == "" { if pl.Token() == "" {
@ -73,6 +82,37 @@ func (m *Manager) ProcessEvent(event dto.Event) error {
return srv.Handle(event) return srv.Handle(event)
} }
func (m *Manager) eventStatusCallback(serverName string) base.EventStatusCallback {
return func(status dto.EventStatus) {
m.processEventStatus(serverName, status)
}
}
func (m *Manager) processEventStatus(serverName string, event dto.EventStatus) {
m.statusLock.RLock()
_, exists := m.statusMap[serverName]
if !exists {
return
}
m.statusLock.RUnlock()
defer m.statusLock.Unlock()
m.statusLock.Lock()
item, found := docker.Default.GetContainer(event.ID)
if !found {
return
}
switch event.Type {
case dto.EventStart:
item.Domain = event.Domain
case dto.EventStop, dto.EventShutdown, dto.EventError:
item.Domain = ""
default:
return
}
m.statusMap[serverName].Connections[item.ID] = item
}
func (m *Manager) PluginByToken(token string) plugin.Plugin { func (m *Manager) PluginByToken(token string) plugin.Plugin {
server, ok := m.plugins[token] server, ok := m.plugins[token]
if !ok { if !ok {

View File

@ -9,14 +9,15 @@ import (
func MessageToAppEvent(event *pb.EventMessage) dto.Event { func MessageToAppEvent(event *pb.EventMessage) dto.Event {
return dto.Event{ return dto.Event{
Type: MessageEventTypeToApp(event.Type), Type: MessageEventTypeToApp(event.GetType()),
ID: event.Id,
Container: dto.Container{ Container: dto.Container{
IP: net.ParseIP(event.Container.Ip), ID: event.GetContainer().GetId(),
Port: uint16(event.Container.Port), Names: event.GetContainer().GetNames(),
Server: event.Container.Server, IP: net.ParseIP(event.GetContainer().GetIp()),
Prefix: event.Container.Prefix, Port: uint16(event.GetContainer().GetPort()),
Domain: event.Container.Domain, Server: event.GetContainer().GetServer(),
Prefix: event.GetContainer().GetPrefix(),
Domain: event.GetContainer().GetDomain(),
}, },
} }
} }
@ -24,8 +25,9 @@ func MessageToAppEvent(event *pb.EventMessage) dto.Event {
func AppEventToMessage(event dto.Event) *pb.EventMessage { func AppEventToMessage(event dto.Event) *pb.EventMessage {
return &pb.EventMessage{ return &pb.EventMessage{
Type: AppEventTypeToMessage(event.Type), Type: AppEventTypeToMessage(event.Type),
Id: event.ID,
Container: &pb.Container{ Container: &pb.Container{
Id: event.Container.ID,
Names: event.Container.Names,
Ip: event.Container.IP.String(), Ip: event.Container.IP.String(),
Port: uint32(event.Container.Port), Port: uint32(event.Container.Port),
Server: event.Container.Server, Server: event.Container.Server,
@ -35,8 +37,18 @@ func AppEventToMessage(event dto.Event) *pb.EventMessage {
} }
} }
func MessageToAppEventStatus(val *pb.EventStatusMessage) dto.EventStatus {
return dto.EventStatus{
Type: MessageEventTypeToApp(val.GetType()),
ID: val.GetId(),
Error: val.GetError(),
Domain: val.GetDomain(),
}
}
func AppEventStatusToMessage(status dto.EventStatus) *pb.EventStatusMessage { func AppEventStatusToMessage(status dto.EventStatus) *pb.EventStatusMessage {
return &pb.EventStatusMessage{ return &pb.EventStatusMessage{
Type: AppEventTypeToMessage(status.Type),
Id: status.ID, Id: status.ID,
Error: status.Error, Error: status.Error,
Domain: status.Domain, Domain: status.Domain,
@ -58,7 +70,9 @@ func AppEventTypeToMessage(typ dto.EventType) pb.EventType {
case 1: case 1:
return pb.EventType_EVENT_STOP return pb.EventType_EVENT_STOP
case 2: case 2:
fallthrough return pb.EventType_EVENT_SHUTDOWN
case 3:
return pb.EventType_EVENT_ERROR
default: default:
return pb.EventType_EVENT_UNKNOWN return pb.EventType_EVENT_UNKNOWN
} }

View File

@ -8,6 +8,7 @@ const (
EventStart EventType = iota EventStart EventType = iota
EventStop EventStop
EventShutdown EventShutdown
EventError
EventUnknown EventUnknown
) )
@ -24,20 +25,22 @@ func TypeFromAction(action string) EventType {
type Event struct { type Event struct {
Type EventType Type EventType
ID string
Container Container Container Container
} }
type EventStatus struct { type EventStatus struct {
Type EventType
ID string ID string
Error string Error string
Domain string Domain string
} }
type Container struct { type Container struct {
IP net.IP ID string `json:"id"`
Port uint16 Names []string `json:"names"`
Server string IP net.IP `json:"ip"`
Prefix string Port uint16 `json:"port"`
Domain string Server string `json:"-"`
Prefix string `json:"prefix"`
Domain string `json:"domain"`
} }

View File

@ -13,15 +13,18 @@ enum EventType {
EVENT_START = 0; EVENT_START = 0;
EVENT_STOP = 1; EVENT_STOP = 1;
EVENT_SHUTDOWN = 2; EVENT_SHUTDOWN = 2;
EVENT_UNKNOWN = 3; EVENT_ERROR = 3;
EVENT_UNKNOWN = 4;
} }
message Container { message Container {
string ip = 1; string id = 1;
uint32 port = 2; repeated string names = 2;
string server = 3; string ip = 3;
string prefix = 4; uint32 port = 4;
string domain = 5; string server = 5;
string prefix = 6;
string domain = 7;
} }
message EventMessage { message EventMessage {
@ -31,7 +34,8 @@ message EventMessage {
} }
message EventStatusMessage { message EventStatusMessage {
string id = 1; EventType type = 1;
string error = 2; string id = 2;
string domain = 3; string error = 3;
string domain = 4;
} }