refactor, initial work for ssh driver (still wip)
This commit is contained in:
parent
7c443887dc
commit
d3029d09e7
@ -36,7 +36,6 @@ var rootCmd = &cobra.Command{
|
||||
for id, item := range docker.Default.Containers() {
|
||||
err := server.DefaultManager.ProcessEvent(dto.Event{
|
||||
Type: dto.EventStart,
|
||||
ID: id,
|
||||
Container: item,
|
||||
})
|
||||
if err != nil {
|
||||
@ -54,7 +53,8 @@ var rootCmd = &cobra.Command{
|
||||
for event := range events {
|
||||
err := server.DefaultManager.ProcessEvent(event)
|
||||
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
1
go.mod
@ -6,6 +6,7 @@ require (
|
||||
github.com/docker/docker v24.0.7+incompatible
|
||||
github.com/docker/go-connections v0.4.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/spf13/cast v1.5.1
|
||||
github.com/spf13/cobra v1.8.0
|
||||
|
2
go.sum
2
go.sum
@ -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/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/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/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
"github.com/Neur0toxine/sshpoke/internal/logger"
|
||||
"github.com/Neur0toxine/sshpoke/internal/server"
|
||||
"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"
|
||||
"github.com/Neur0toxine/sshpoke/pkg/plugin/pb"
|
||||
"google.golang.org/grpc"
|
||||
@ -44,10 +44,7 @@ func (p *pluginAPI) EventStatus(ctx context.Context, msg *pb.EventStatusMessage)
|
||||
if pl == nil {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
pl.HandleStatus(dto.EventStatus{
|
||||
ID: msg.Id,
|
||||
Error: msg.Error,
|
||||
})
|
||||
pl.PushEventStatus(convert.MessageToAppEventStatus(msg))
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
|
@ -50,6 +50,21 @@ func (d *Docker) Containers() map[string]dto.Container {
|
||||
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) {
|
||||
cli, err := client.NewClientWithOpts(config.Default.Docker.Opts)
|
||||
if err != nil {
|
||||
@ -83,7 +98,6 @@ func (d *Docker) Listen() (chan dto.Event, error) {
|
||||
}
|
||||
newEvent := dto.Event{
|
||||
Type: eventType,
|
||||
ID: event.Actor.ID,
|
||||
Container: converted,
|
||||
}
|
||||
msg := "exposing container"
|
||||
|
@ -77,6 +77,8 @@ func dockerContainerToInternal(container types.Container) (result dto.Container,
|
||||
}
|
||||
|
||||
return dto.Container{
|
||||
ID: container.ID,
|
||||
Names: container.Names,
|
||||
IP: ip,
|
||||
Port: uint16(port),
|
||||
Server: labels.Server,
|
||||
|
30
internal/server/container_list.go
Normal file
30
internal/server/container_list.go
Normal 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
|
||||
}
|
46
internal/server/driver/base/base.go
Normal file
46
internal/server/driver/base/base.go
Normal 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
|
||||
}
|
@ -1,17 +1,22 @@
|
||||
package iface
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/Neur0toxine/sshpoke/internal/config"
|
||||
"github.com/Neur0toxine/sshpoke/pkg/dto"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type DriverConstructor func(ctx context.Context, name string, params config.DriverParams) (Driver, error)
|
||||
type EventStatusCallback func(status dto.EventStatus)
|
||||
|
||||
type Driver interface {
|
||||
Name() string
|
||||
SetEventStatusCallback(callback EventStatusCallback)
|
||||
PushEventStatus(status dto.EventStatus)
|
||||
Handle(event dto.Event) error
|
||||
Driver() config.DriverType
|
||||
Log() *zap.SugaredLogger
|
||||
WaitForShutdown()
|
||||
}
|
@ -4,13 +4,13 @@ import (
|
||||
"context"
|
||||
|
||||
"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/plugin"
|
||||
"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 {
|
||||
case config.DriverSSH:
|
||||
return ssh.New(ctx, name, params)
|
||||
|
@ -4,30 +4,40 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/Neur0toxine/sshpoke/internal/config"
|
||||
"github.com/Neur0toxine/sshpoke/internal/logger"
|
||||
"github.com/Neur0toxine/sshpoke/internal/server/driver/iface"
|
||||
"github.com/Neur0toxine/sshpoke/internal/server/driver/base"
|
||||
"github.com/Neur0toxine/sshpoke/pkg/dto"
|
||||
)
|
||||
|
||||
// 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?
|
||||
type Null struct {
|
||||
name string
|
||||
base.Base
|
||||
}
|
||||
|
||||
func New(ctx context.Context, name string, params config.DriverParams) (iface.Driver, error) {
|
||||
return &Null{name: name}, nil
|
||||
func New(ctx context.Context, name string, params config.DriverParams) (base.Driver, error) {
|
||||
return &Null{
|
||||
Base: base.New(ctx, name),
|
||||
}, nil
|
||||
}
|
||||
|
||||
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)
|
||||
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 {
|
||||
return config.DriverNull
|
||||
}
|
||||
|
@ -7,8 +7,7 @@ import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/Neur0toxine/sshpoke/internal/config"
|
||||
"github.com/Neur0toxine/sshpoke/internal/logger"
|
||||
"github.com/Neur0toxine/sshpoke/internal/server/driver/iface"
|
||||
"github.com/Neur0toxine/sshpoke/internal/server/driver/base"
|
||||
"github.com/Neur0toxine/sshpoke/internal/server/driver/util"
|
||||
"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.
|
||||
type Driver struct {
|
||||
ctx context.Context
|
||||
name string
|
||||
base.Base
|
||||
params Params
|
||||
send *Queue[dto.Event]
|
||||
listening atomic.Bool
|
||||
@ -29,16 +27,14 @@ type EventStream interface {
|
||||
}
|
||||
|
||||
type Plugin interface {
|
||||
iface.Driver
|
||||
base.Driver
|
||||
Token() string
|
||||
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{
|
||||
name: name,
|
||||
ctx: ctx,
|
||||
Base: base.New(ctx, name),
|
||||
send: NewQueue[dto.Event](),
|
||||
}
|
||||
if err := util.UnmarshalParams(params, &drv.params); err != nil {
|
||||
@ -56,10 +52,6 @@ func (d *Driver) Handle(event dto.Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) Name() string {
|
||||
return d.name
|
||||
}
|
||||
|
||||
func (d *Driver) Driver() config.DriverType {
|
||||
return config.DriverPlugin
|
||||
}
|
||||
@ -88,21 +80,17 @@ func (d *Driver) Listen(ctx context.Context, stream EventStream) error {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
logger.Sugar.Errorw("error writing event to plugin",
|
||||
"server", d.name, "error", err)
|
||||
d.Log().Errorw("error writing event to plugin",
|
||||
"server", d.Name(), "error", 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 {
|
||||
select {
|
||||
case <-d.ctx.Done():
|
||||
case <-d.Context().Done():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@ -110,6 +98,6 @@ func (d *Driver) isDone() bool {
|
||||
}
|
||||
|
||||
func (d *Driver) WaitForShutdown() {
|
||||
<-d.ctx.Done()
|
||||
<-d.Context().Done()
|
||||
return
|
||||
}
|
||||
|
@ -3,20 +3,26 @@ package ssh
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"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/proto/sshtun"
|
||||
"github.com/Neur0toxine/sshpoke/pkg/dto"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type SSH struct {
|
||||
ctx context.Context
|
||||
name string
|
||||
base.Base
|
||||
params Params
|
||||
sessions map[string]conn
|
||||
keys []ssh.Signer
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
@ -25,21 +31,47 @@ type conn struct {
|
||||
tun *sshtun.Tunnel
|
||||
}
|
||||
|
||||
func New(ctx context.Context, name string, params config.DriverParams) (iface.Driver, error) {
|
||||
drv := &SSH{ctx: ctx, name: name, sessions: make(map[string]conn)}
|
||||
func New(ctx context.Context, name string, params config.DriverParams) (base.Driver, error) {
|
||||
drv := &SSH{
|
||||
Base: base.New(ctx, name),
|
||||
sessions: make(map[string]conn),
|
||||
}
|
||||
if err := util.UnmarshalParams(params, &drv.params); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
drv.populateFromSSHConfig()
|
||||
if err := drv.parseKeys(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return drv, nil
|
||||
}
|
||||
|
||||
func (d *SSH) Handle(event dto.Event) error {
|
||||
// TODO: Implement event handling & connections management.
|
||||
return errors.New(d.name + " server handler is not implemented yet")
|
||||
func (d *SSH) populateFromSSHConfig() {
|
||||
if d.params.Auth.Directory == "" {
|
||||
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 {
|
||||
return d.name
|
||||
func (d *SSH) Handle(event dto.Event) error {
|
||||
// TODO: Implement event handling & connections management.
|
||||
return errors.New("server handler is not implemented yet")
|
||||
}
|
||||
|
||||
func (d *SSH) Driver() config.DriverType {
|
||||
@ -49,3 +81,79 @@ func (d *SSH) Driver() config.DriverType {
|
||||
func (d *SSH) WaitForShutdown() {
|
||||
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
|
||||
}
|
||||
|
@ -1,63 +1,24 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Neur0toxine/sshpoke/internal/server/driver/ssh/types"
|
||||
"github.com/Neur0toxine/sshpoke/internal/server/driver/util"
|
||||
)
|
||||
|
||||
type Params struct {
|
||||
Address string `mapstructure:"address" validate:"required"`
|
||||
Auth Auth `mapstructure:"auth"`
|
||||
KeepAlive KeepAlive `mapstructure:"keepalive"`
|
||||
Domain string `mapstructure:"domain"`
|
||||
DomainProto string `mapstructure:"domain_proto"`
|
||||
DomainExtractRegex string `mapstructure:"domain_extract_regex" validate:"validregexp"`
|
||||
Mode DomainMode `mapstructure:"mode" validate:"required,oneof=single multi"`
|
||||
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"`
|
||||
Address string `mapstructure:"address" validate:"required"`
|
||||
Auth types.Auth `mapstructure:"auth"`
|
||||
KeepAlive types.KeepAlive `mapstructure:"keepalive"`
|
||||
Domain string `mapstructure:"domain"`
|
||||
DomainProto string `mapstructure:"domain_proto"`
|
||||
DomainExtractRegex string `mapstructure:"domain_extract_regex" validate:"validregexp"`
|
||||
Mode types.DomainMode `mapstructure:"mode" validate:"required,oneof=single multi"`
|
||||
Prefix bool `mapstructure:"prefix"`
|
||||
}
|
||||
|
||||
func (p *Params) Validate() error {
|
||||
if err := util.Validator.Struct(p); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.Auth.validate()
|
||||
return p.Auth.Validate()
|
||||
}
|
||||
|
21
internal/server/driver/ssh/sshconfig.go
Normal file
21
internal/server/driver/ssh/sshconfig.go
Normal 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))
|
||||
}
|
73
internal/server/driver/ssh/types/auth.go
Normal file
73
internal/server/driver/ssh/types/auth.go
Normal 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
|
||||
}
|
8
internal/server/driver/ssh/types/domain_mode.go
Normal file
8
internal/server/driver/ssh/types/domain_mode.go
Normal file
@ -0,0 +1,8 @@
|
||||
package types
|
||||
|
||||
type DomainMode string
|
||||
|
||||
const (
|
||||
DomainModeSingle DomainMode = "single"
|
||||
DomainModeMulti DomainMode = "multi"
|
||||
)
|
6
internal/server/driver/ssh/types/keep_alive.go
Normal file
6
internal/server/driver/ssh/types/keep_alive.go
Normal 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"`
|
||||
}
|
@ -6,20 +6,28 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/Neur0toxine/sshpoke/internal/config"
|
||||
"github.com/Neur0toxine/sshpoke/internal/docker"
|
||||
"github.com/Neur0toxine/sshpoke/internal/logger"
|
||||
"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/pkg/dto"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
rw sync.RWMutex
|
||||
servers map[string]iface.Driver
|
||||
servers map[string]base.Driver
|
||||
plugins map[string]plugin.Plugin
|
||||
statusMap map[string]serverStatus
|
||||
statusLock sync.RWMutex
|
||||
defaultServer string
|
||||
}
|
||||
|
||||
type serverStatus struct {
|
||||
Name string `json:"name"`
|
||||
Connections Connections `json:"connections"`
|
||||
}
|
||||
|
||||
var DefaultManager *Manager
|
||||
var (
|
||||
ErrNoServer = errors.New("server is not specified")
|
||||
@ -28,7 +36,7 @@ var (
|
||||
|
||||
func NewManager(ctx context.Context, servers []config.Server, defaultServer string) *Manager {
|
||||
m := &Manager{
|
||||
servers: make(map[string]iface.Driver),
|
||||
servers: make(map[string]base.Driver),
|
||||
plugins: make(map[string]plugin.Plugin),
|
||||
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)
|
||||
continue
|
||||
}
|
||||
server.SetEventStatusCallback(m.eventStatusCallback(server.Name()))
|
||||
if server.Driver() == config.DriverPlugin {
|
||||
pl := server.(plugin.Plugin)
|
||||
if pl.Token() == "" {
|
||||
@ -73,6 +82,37 @@ func (m *Manager) ProcessEvent(event dto.Event) error {
|
||||
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 {
|
||||
server, ok := m.plugins[token]
|
||||
if !ok {
|
||||
|
@ -9,14 +9,15 @@ import (
|
||||
|
||||
func MessageToAppEvent(event *pb.EventMessage) dto.Event {
|
||||
return dto.Event{
|
||||
Type: MessageEventTypeToApp(event.Type),
|
||||
ID: event.Id,
|
||||
Type: MessageEventTypeToApp(event.GetType()),
|
||||
Container: dto.Container{
|
||||
IP: net.ParseIP(event.Container.Ip),
|
||||
Port: uint16(event.Container.Port),
|
||||
Server: event.Container.Server,
|
||||
Prefix: event.Container.Prefix,
|
||||
Domain: event.Container.Domain,
|
||||
ID: event.GetContainer().GetId(),
|
||||
Names: event.GetContainer().GetNames(),
|
||||
IP: net.ParseIP(event.GetContainer().GetIp()),
|
||||
Port: uint16(event.GetContainer().GetPort()),
|
||||
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 {
|
||||
return &pb.EventMessage{
|
||||
Type: AppEventTypeToMessage(event.Type),
|
||||
Id: event.ID,
|
||||
Container: &pb.Container{
|
||||
Id: event.Container.ID,
|
||||
Names: event.Container.Names,
|
||||
Ip: event.Container.IP.String(),
|
||||
Port: uint32(event.Container.Port),
|
||||
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 {
|
||||
return &pb.EventStatusMessage{
|
||||
Type: AppEventTypeToMessage(status.Type),
|
||||
Id: status.ID,
|
||||
Error: status.Error,
|
||||
Domain: status.Domain,
|
||||
@ -58,7 +70,9 @@ func AppEventTypeToMessage(typ dto.EventType) pb.EventType {
|
||||
case 1:
|
||||
return pb.EventType_EVENT_STOP
|
||||
case 2:
|
||||
fallthrough
|
||||
return pb.EventType_EVENT_SHUTDOWN
|
||||
case 3:
|
||||
return pb.EventType_EVENT_ERROR
|
||||
default:
|
||||
return pb.EventType_EVENT_UNKNOWN
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ const (
|
||||
EventStart EventType = iota
|
||||
EventStop
|
||||
EventShutdown
|
||||
EventError
|
||||
EventUnknown
|
||||
)
|
||||
|
||||
@ -24,20 +25,22 @@ func TypeFromAction(action string) EventType {
|
||||
|
||||
type Event struct {
|
||||
Type EventType
|
||||
ID string
|
||||
Container Container
|
||||
}
|
||||
|
||||
type EventStatus struct {
|
||||
Type EventType
|
||||
ID string
|
||||
Error string
|
||||
Domain string
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
IP net.IP
|
||||
Port uint16
|
||||
Server string
|
||||
Prefix string
|
||||
Domain string
|
||||
ID string `json:"id"`
|
||||
Names []string `json:"names"`
|
||||
IP net.IP `json:"ip"`
|
||||
Port uint16 `json:"port"`
|
||||
Server string `json:"-"`
|
||||
Prefix string `json:"prefix"`
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
@ -13,15 +13,18 @@ enum EventType {
|
||||
EVENT_START = 0;
|
||||
EVENT_STOP = 1;
|
||||
EVENT_SHUTDOWN = 2;
|
||||
EVENT_UNKNOWN = 3;
|
||||
EVENT_ERROR = 3;
|
||||
EVENT_UNKNOWN = 4;
|
||||
}
|
||||
|
||||
message Container {
|
||||
string ip = 1;
|
||||
uint32 port = 2;
|
||||
string server = 3;
|
||||
string prefix = 4;
|
||||
string domain = 5;
|
||||
string id = 1;
|
||||
repeated string names = 2;
|
||||
string ip = 3;
|
||||
uint32 port = 4;
|
||||
string server = 5;
|
||||
string prefix = 6;
|
||||
string domain = 7;
|
||||
}
|
||||
|
||||
message EventMessage {
|
||||
@ -31,7 +34,8 @@ message EventMessage {
|
||||
}
|
||||
|
||||
message EventStatusMessage {
|
||||
string id = 1;
|
||||
string error = 2;
|
||||
string domain = 3;
|
||||
EventType type = 1;
|
||||
string id = 2;
|
||||
string error = 3;
|
||||
string domain = 4;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user