simplify plugin api, better settings validation

This commit is contained in:
Pavel 2023-11-18 12:36:17 +03:00
parent fc81ebe650
commit 7c443887dc
19 changed files with 269 additions and 139 deletions

View File

@ -6,12 +6,12 @@ import (
"os/signal" "os/signal"
"syscall" "syscall"
"github.com/Neur0toxine/sshpoke/internal/api"
"github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/config"
"github.com/Neur0toxine/sshpoke/internal/docker" "github.com/Neur0toxine/sshpoke/internal/docker"
"github.com/Neur0toxine/sshpoke/internal/logger" "github.com/Neur0toxine/sshpoke/internal/logger"
"github.com/Neur0toxine/sshpoke/internal/model"
"github.com/Neur0toxine/sshpoke/internal/plugin"
"github.com/Neur0toxine/sshpoke/internal/server" "github.com/Neur0toxine/sshpoke/internal/server"
"github.com/Neur0toxine/sshpoke/pkg/dto"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -24,7 +24,7 @@ var rootCmd = &cobra.Command{
Short: "Expose your Docker services to the Internet via SSH.", Short: "Expose your Docker services to the Internet via SSH.",
Long: `sshpoke is a CLI application that listens to the docker socket and automatically exposes relevant services to the Internet.`, Long: `sshpoke is a CLI application that listens to the docker socket and automatically exposes relevant services to the Internet.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
go plugin.StartAPIServer() go api.StartPluginAPI()
var err error var err error
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
server.DefaultManager = server.NewManager(ctx, config.Default.Servers, config.Default.DefaultServer) server.DefaultManager = server.NewManager(ctx, config.Default.Servers, config.Default.DefaultServer)
@ -34,8 +34,8 @@ var rootCmd = &cobra.Command{
} }
for id, item := range docker.Default.Containers() { for id, item := range docker.Default.Containers() {
err := server.DefaultManager.ProcessEvent(model.Event{ err := server.DefaultManager.ProcessEvent(dto.Event{
Type: model.EventStart, Type: dto.EventStart,
ID: id, ID: id,
Container: item, Container: item,
}) })

View File

@ -1,4 +1,4 @@
package plugin package api
import ( import (
"context" "context"
@ -8,10 +8,11 @@ import (
"github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/config"
"github.com/Neur0toxine/sshpoke/internal/logger" "github.com/Neur0toxine/sshpoke/internal/logger"
"github.com/Neur0toxine/sshpoke/internal/model"
"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"
pb "github.com/Neur0toxine/sshpoke/pkg/plugin" "github.com/Neur0toxine/sshpoke/pkg/dto"
plugin2 "github.com/Neur0toxine/sshpoke/pkg/plugin"
"github.com/Neur0toxine/sshpoke/pkg/plugin/pb"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
@ -43,14 +44,14 @@ func (p *pluginAPI) EventStatus(ctx context.Context, msg *pb.EventStatusMessage)
if pl == nil { if pl == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
pl.HandleStatus(model.EventRequest{ pl.HandleStatus(dto.EventStatus{
ID: msg.Id, ID: msg.Id,
Error: msg.Error, Error: msg.Error,
}) })
return &emptypb.Empty{}, nil return &emptypb.Empty{}, nil
} }
func (p *pluginAPI) receiverForContext(ctx context.Context) *plugin.Plugin { func (p *pluginAPI) receiverForContext(ctx context.Context) plugin.Plugin {
md, ok := metadata.FromIncomingContext(ctx) md, ok := metadata.FromIncomingContext(ctx)
if !ok { if !ok {
return nil return nil
@ -62,10 +63,10 @@ func (p *pluginAPI) receiverForContext(ctx context.Context) *plugin.Plugin {
return server.DefaultManager.PluginByToken(tokens[0]) return server.DefaultManager.PluginByToken(tokens[0])
} }
func StartAPIServer() { func StartPluginAPI() {
port := config.Default.PluginAPIPort port := config.Default.PluginAPIPort
if port == 0 { if port == 0 {
port = 3000 port = plugin2.DefaultPort
} }
socket, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) socket, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil { if err != nil {

15
internal/api/stream.go Normal file
View File

@ -0,0 +1,15 @@
package api
import (
"github.com/Neur0toxine/sshpoke/pkg/convert"
"github.com/Neur0toxine/sshpoke/pkg/dto"
"github.com/Neur0toxine/sshpoke/pkg/plugin/pb"
)
type Stream struct {
stream pb.PluginService_EventServer
}
func (s *Stream) Send(event dto.Event) error {
return s.stream.Send(convert.AppEventToMessage(event))
}

View File

@ -7,7 +7,7 @@ import (
"github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/config"
"github.com/Neur0toxine/sshpoke/internal/logger" "github.com/Neur0toxine/sshpoke/internal/logger"
"github.com/Neur0toxine/sshpoke/internal/model" "github.com/Neur0toxine/sshpoke/pkg/dto"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client" "github.com/docker/docker/client"
@ -31,7 +31,7 @@ func New(ctx context.Context) (*Docker, error) {
}, nil }, nil
} }
func (d *Docker) Containers() map[string]model.Container { func (d *Docker) Containers() map[string]dto.Container {
items, err := d.cli.ContainerList(d.ctx, types.ContainerListOptions{ items, err := d.cli.ContainerList(d.ctx, types.ContainerListOptions{
Filters: filters.NewArgs(filters.Arg("status", "running")), Filters: filters.NewArgs(filters.Arg("status", "running")),
}) })
@ -39,7 +39,7 @@ func (d *Docker) Containers() map[string]model.Container {
logger.Sugar.Errorf("cannot get containers list: %s", err) logger.Sugar.Errorf("cannot get containers list: %s", err)
return nil return nil
} }
containers := map[string]model.Container{} containers := map[string]dto.Container{}
for _, item := range items { for _, item := range items {
container, ok := dockerContainerToInternal(item) container, ok := dockerContainerToInternal(item)
if !ok { if !ok {
@ -50,13 +50,13 @@ func (d *Docker) Containers() map[string]model.Container {
return containers return containers
} }
func (d *Docker) Listen() (chan model.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 {
return nil, err return nil, err
} }
output := make(chan model.Event) output := make(chan dto.Event)
go func() { go func() {
for { for {
eventSource, errSource := cli.Events(d.ctx, types.EventsOptions{ eventSource, errSource := cli.Events(d.ctx, types.EventsOptions{
@ -64,8 +64,8 @@ func (d *Docker) Listen() (chan model.Event, error) {
}) })
select { select {
case event := <-eventSource: case event := <-eventSource:
eventType := model.TypeFromAction(event.Action) eventType := dto.TypeFromAction(event.Action)
if (eventType != model.EventStart && eventType != model.EventStop) || !actorEnabled(event.Actor) { if (eventType != dto.EventStart && eventType != dto.EventStop) || !actorEnabled(event.Actor) {
continue continue
} }
container, err := d.cli.ContainerList(d.ctx, types.ContainerListOptions{ container, err := d.cli.ContainerList(d.ctx, types.ContainerListOptions{
@ -81,13 +81,13 @@ func (d *Docker) Listen() (chan model.Event, error) {
if !ok { if !ok {
continue continue
} }
newEvent := model.Event{ newEvent := dto.Event{
Type: eventType, Type: eventType,
ID: event.Actor.ID, ID: event.Actor.ID,
Container: converted, Container: converted,
} }
msg := "exposing container" msg := "exposing container"
if eventType == model.EventStop { if eventType == dto.EventStop {
msg = "stopping container" msg = "stopping container"
} }
logger.Sugar.Debugw(msg, logger.Sugar.Debugw(msg,
@ -96,8 +96,7 @@ func (d *Docker) Listen() (chan model.Event, error) {
"container.ip", converted.IP.String(), "container.ip", converted.IP.String(),
"container.port", converted.Port, "container.port", converted.Port,
"container.server", converted.Server, "container.server", converted.Server,
"container.prefix", converted.Prefix, "container.prefix", converted.Prefix)
"container.domain", converted.Domain)
output <- newEvent output <- newEvent
case err := <-errSource: case err := <-errSource:
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {

View File

@ -5,7 +5,7 @@ import (
"strconv" "strconv"
"github.com/Neur0toxine/sshpoke/internal/logger" "github.com/Neur0toxine/sshpoke/internal/logger"
"github.com/Neur0toxine/sshpoke/internal/model" "github.com/Neur0toxine/sshpoke/pkg/dto"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
@ -18,7 +18,6 @@ type labelsConfig struct {
Server string `mapstructure:"sshpoke.server"` Server string `mapstructure:"sshpoke.server"`
Port string `mapstructure:"sshpoke.port"` Port string `mapstructure:"sshpoke.port"`
Prefix string `mapstructure:"sshpoke.prefix"` Prefix string `mapstructure:"sshpoke.prefix"`
Domain string `mapstructure:"sshpoke.domain"`
} }
type boolStr string type boolStr string
@ -35,7 +34,7 @@ func actorEnabled(actor events.Actor) bool {
return boolStr(label).Bool() return boolStr(label).Bool()
} }
func dockerContainerToInternal(container types.Container) (result model.Container, ok bool) { func dockerContainerToInternal(container types.Container) (result dto.Container, ok bool) {
var labels labelsConfig var labels labelsConfig
if err := mapstructure.Decode(container.Labels, &labels); err != nil { if err := mapstructure.Decode(container.Labels, &labels); err != nil {
logger.Sugar.Debugf("skipping container %s because configuration is invalid: %s", container.ID, err) logger.Sugar.Debugf("skipping container %s because configuration is invalid: %s", container.ID, err)
@ -77,12 +76,11 @@ func dockerContainerToInternal(container types.Container) (result model.Containe
return result, false return result, false
} }
return model.Container{ return dto.Container{
IP: ip, IP: ip,
Port: uint16(port), Port: uint16(port),
Server: labels.Server, Server: labels.Server,
Prefix: labels.Prefix, Prefix: labels.Prefix,
Domain: labels.Domain,
}, true }, true
} }

View File

@ -1,65 +0,0 @@
package plugin
import (
"net"
"github.com/Neur0toxine/sshpoke/internal/model"
pb "github.com/Neur0toxine/sshpoke/pkg/plugin"
)
type Stream struct {
stream pb.PluginService_EventServer
}
func (s *Stream) Send(event model.Event) error {
return s.stream.Send(s.eventToMessage(event))
}
func (s *Stream) messageToEvent(event *pb.EventMessage) model.Event {
return model.Event{
Type: s.pbEventTypeToApp(event.Type),
ID: event.Id,
Container: model.Container{
IP: net.ParseIP(event.Container.Ip),
Port: uint16(event.Container.Port),
Server: event.Container.Server,
Prefix: event.Container.Prefix,
Domain: event.Container.Domain,
},
}
}
func (s *Stream) eventToMessage(event model.Event) *pb.EventMessage {
return &pb.EventMessage{
Type: s.appEventTypeToPB(event.Type),
Id: event.ID,
Container: &pb.Container{
Ip: event.Container.IP.String(),
Port: uint32(event.Container.Port),
Server: event.Container.Server,
Prefix: event.Container.Prefix,
Domain: event.Container.Domain,
},
}
}
func (s *Stream) pbEventTypeToApp(typ pb.EventType) model.EventType {
val := model.EventType(typ.Number())
if val > model.EventStart {
return model.EventUnknown
}
return val
}
func (s *Stream) appEventTypeToPB(typ model.EventType) pb.EventType {
switch typ {
case 0:
return pb.EventType_EVENT_START
case 1:
return pb.EventType_EVENT_STOP
case 2:
fallthrough
default:
return pb.EventType_EVENT_UNKNOWN
}
}

View File

@ -4,13 +4,14 @@ import (
"context" "context"
"github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/config"
"github.com/Neur0toxine/sshpoke/internal/model" "github.com/Neur0toxine/sshpoke/pkg/dto"
) )
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 Driver interface { type Driver interface {
Handle(event model.Event) error Name() string
Handle(event dto.Event) error
Driver() config.DriverType Driver() config.DriverType
WaitForShutdown() WaitForShutdown()
} }

View File

@ -5,8 +5,8 @@ import (
"github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/config"
"github.com/Neur0toxine/sshpoke/internal/logger" "github.com/Neur0toxine/sshpoke/internal/logger"
"github.com/Neur0toxine/sshpoke/internal/model"
"github.com/Neur0toxine/sshpoke/internal/server/driver/iface" "github.com/Neur0toxine/sshpoke/internal/server/driver/iface"
"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.
@ -19,11 +19,15 @@ func New(ctx context.Context, name string, params config.DriverParams) (iface.Dr
return &Null{name: name}, nil return &Null{name: name}, nil
} }
func (d *Null) Handle(event model.Event) error { func (d *Null) Handle(event dto.Event) error {
logger.Sugar.Debugw("handling event with null driver", "serverName", d.name, "event", event) logger.Sugar.Debugw("handling event with null driver", "serverName", d.name, "event", event)
return nil return nil
} }
func (d *Null) Name() string {
return d.name
}
func (d *Null) Driver() config.DriverType { func (d *Null) Driver() config.DriverType {
return config.DriverNull return config.DriverNull
} }

View File

@ -8,31 +8,38 @@ import (
"github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/config"
"github.com/Neur0toxine/sshpoke/internal/logger" "github.com/Neur0toxine/sshpoke/internal/logger"
"github.com/Neur0toxine/sshpoke/internal/model"
"github.com/Neur0toxine/sshpoke/internal/server/driver/iface" "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"
) )
var ErrAlreadyConnected = errors.New("already connected") var ErrAlreadyConnected = errors.New("already connected")
// Plugin driver uses RPC to communicate with external plugin. // Driver plugin uses RPC to communicate with external plugin.
type Plugin struct { type Driver struct {
ctx context.Context ctx context.Context
name string name string
params Params params Params
send *Queue[model.Event] send *Queue[dto.Event]
listening atomic.Bool listening atomic.Bool
} }
type EventStream interface { type EventStream interface {
Send(event model.Event) error Send(event dto.Event) error
}
type Plugin interface {
iface.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) (iface.Driver, error) {
drv := &Plugin{ drv := &Driver{
name: name, name: name,
ctx: ctx, ctx: ctx,
send: NewQueue[model.Event](), send: NewQueue[dto.Event](),
} }
if err := util.UnmarshalParams(params, &drv.params); err != nil { if err := util.UnmarshalParams(params, &drv.params); err != nil {
return nil, err return nil, err
@ -40,27 +47,28 @@ func New(ctx context.Context, name string, params config.DriverParams) (iface.Dr
return drv, nil return drv, nil
} }
func (d *Plugin) Handle(event model.Event) error { func (d *Driver) Handle(event dto.Event) error {
if d.isDone() { if d.isDone() {
d.send.Enqueue(dto.Event{Type: dto.EventShutdown})
return nil return nil
} }
d.send.Enqueue(event) d.send.Enqueue(event)
return nil return nil
} }
func (d *Plugin) Name() string { func (d *Driver) Name() string {
return d.name return d.name
} }
func (d *Plugin) Driver() config.DriverType { func (d *Driver) Driver() config.DriverType {
return config.DriverPlugin return config.DriverPlugin
} }
func (d *Plugin) Token() string { func (d *Driver) Token() string {
return d.params.Token return d.params.Token
} }
func (d *Plugin) Listen(ctx context.Context, stream EventStream) error { func (d *Driver) Listen(ctx context.Context, stream EventStream) error {
if d.listening.Load() { if d.listening.Load() {
return ErrAlreadyConnected return ErrAlreadyConnected
} }
@ -88,11 +96,11 @@ func (d *Plugin) Listen(ctx context.Context, stream EventStream) error {
} }
} }
func (d *Plugin) HandleStatus(event model.EventRequest) { func (d *Driver) HandleStatus(event dto.EventStatus) {
logger.Sugar.Errorw("plugin error", "serverName", d.name, "id", event.ID, "error", event.Error) logger.Sugar.Errorw("plugin error", "serverName", d.name, "id", event.ID, "error", event.Error)
} }
func (d *Plugin) isDone() bool { func (d *Driver) isDone() bool {
select { select {
case <-d.ctx.Done(): case <-d.ctx.Done():
return true return true
@ -101,7 +109,7 @@ func (d *Plugin) isDone() bool {
} }
} }
func (d *Plugin) WaitForShutdown() { func (d *Driver) WaitForShutdown() {
<-d.ctx.Done() <-d.ctx.Done()
return return
} }

View File

@ -6,10 +6,10 @@ import (
"sync" "sync"
"github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/config"
"github.com/Neur0toxine/sshpoke/internal/model"
"github.com/Neur0toxine/sshpoke/internal/server/driver/iface" "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/internal/server/proto/sshtun" "github.com/Neur0toxine/sshpoke/internal/server/proto/sshtun"
"github.com/Neur0toxine/sshpoke/pkg/dto"
) )
type SSH struct { type SSH struct {
@ -21,7 +21,7 @@ type SSH struct {
} }
type conn struct { type conn struct {
container model.Container container dto.Container
tun *sshtun.Tunnel tun *sshtun.Tunnel
} }
@ -33,11 +33,15 @@ func New(ctx context.Context, name string, params config.DriverParams) (iface.Dr
return drv, nil return drv, nil
} }
func (d *SSH) Handle(event model.Event) error { func (d *SSH) Handle(event dto.Event) error {
// TODO: Implement event handling & connections management. // TODO: Implement event handling & connections management.
return errors.New(d.name + " server handler is not implemented yet") return errors.New(d.name + " server handler is not implemented yet")
} }
func (d *SSH) Name() string {
return d.name
}
func (d *SSH) Driver() config.DriverType { func (d *SSH) Driver() config.DriverType {
return config.DriverSSH return config.DriverSSH
} }

View File

@ -7,15 +7,16 @@ import (
"github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/config"
"github.com/Neur0toxine/sshpoke/internal/logger" "github.com/Neur0toxine/sshpoke/internal/logger"
"github.com/Neur0toxine/sshpoke/internal/model"
"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/iface"
"github.com/Neur0toxine/sshpoke/internal/server/driver/plugin" "github.com/Neur0toxine/sshpoke/internal/server/driver/plugin"
"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]iface.Driver
plugins map[string]plugin.Plugin
defaultServer string defaultServer string
} }
@ -28,6 +29,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]iface.Driver),
plugins: make(map[string]plugin.Plugin),
defaultServer: defaultServer, defaultServer: defaultServer,
} }
for _, serverConfig := range servers { for _, serverConfig := range servers {
@ -36,12 +38,25 @@ 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
} }
if server.Driver() == config.DriverPlugin {
pl := server.(plugin.Plugin)
if pl.Token() == "" {
logger.Sugar.Warnf("server '%s' will not work because it doesn't have a token", pl.Name())
continue
}
existing, found := m.plugins[pl.Token()]
if found {
logger.Sugar.Fatalw("two plugins cannot have the same token",
"plugin1", existing.Name(), "plugin2", pl.Name(), "token", pl.Token())
}
m.plugins[pl.Token()] = pl
}
m.servers[serverConfig.Name] = server m.servers[serverConfig.Name] = server
} }
return m return m
} }
func (m *Manager) ProcessEvent(event model.Event) error { func (m *Manager) ProcessEvent(event dto.Event) error {
serverName := event.Container.Server serverName := event.Container.Server
if serverName == "" { if serverName == "" {
serverName = m.defaultServer serverName = m.defaultServer
@ -58,21 +73,13 @@ func (m *Manager) ProcessEvent(event model.Event) error {
return srv.Handle(event) return srv.Handle(event)
} }
func (m *Manager) PluginByToken(token string) *plugin.Plugin { func (m *Manager) PluginByToken(token string) plugin.Plugin {
defer m.rw.RUnlock() server, ok := m.plugins[token]
m.rw.RLock() if !ok {
for _, srv := range m.servers {
if srv.Driver() != config.DriverPlugin {
continue
}
pl := srv.(*plugin.Plugin)
if pl.Token() != token {
continue
}
return pl
}
return nil return nil
} }
return server
}
func (m *Manager) WaitForShutdown() { func (m *Manager) WaitForShutdown() {
defer m.rw.RUnlock() defer m.rw.RUnlock()

View File

@ -121,7 +121,7 @@ func (t Tunnel) Bind(ctx context.Context, wg *sync.WaitGroup) {
ln.Close() ln.Close()
}() }()
t.Logger.Printf("(%v) binded Tunnel", t) t.Logger.Printf("(%v) bound Tunnel", t)
defer t.Logger.Printf("(%v) collapsed Tunnel", t) defer t.Logger.Printf("(%v) collapsed Tunnel", t)
// Accept all incoming connections. // Accept all incoming connections.

65
pkg/convert/convert.go Normal file
View File

@ -0,0 +1,65 @@
package convert
import (
"net"
"github.com/Neur0toxine/sshpoke/pkg/dto"
"github.com/Neur0toxine/sshpoke/pkg/plugin/pb"
)
func MessageToAppEvent(event *pb.EventMessage) dto.Event {
return dto.Event{
Type: MessageEventTypeToApp(event.Type),
ID: event.Id,
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,
},
}
}
func AppEventToMessage(event dto.Event) *pb.EventMessage {
return &pb.EventMessage{
Type: AppEventTypeToMessage(event.Type),
Id: event.ID,
Container: &pb.Container{
Ip: event.Container.IP.String(),
Port: uint32(event.Container.Port),
Server: event.Container.Server,
Prefix: event.Container.Prefix,
Domain: event.Container.Domain,
},
}
}
func AppEventStatusToMessage(status dto.EventStatus) *pb.EventStatusMessage {
return &pb.EventStatusMessage{
Id: status.ID,
Error: status.Error,
Domain: status.Domain,
}
}
func MessageEventTypeToApp(typ pb.EventType) dto.EventType {
val := dto.EventType(typ.Number())
if val < dto.EventStart || val > dto.EventUnknown {
return dto.EventUnknown
}
return val
}
func AppEventTypeToMessage(typ dto.EventType) pb.EventType {
switch typ {
case 0:
return pb.EventType_EVENT_START
case 1:
return pb.EventType_EVENT_STOP
case 2:
fallthrough
default:
return pb.EventType_EVENT_UNKNOWN
}
}

View File

@ -1,4 +1,4 @@
package model package dto
import "net" import "net"
@ -7,6 +7,7 @@ type EventType uint8
const ( const (
EventStart EventType = iota EventStart EventType = iota
EventStop EventStop
EventShutdown
EventUnknown EventUnknown
) )
@ -27,9 +28,10 @@ type Event struct {
Container Container Container Container
} }
type EventRequest struct { type EventStatus struct {
ID string ID string
Error string Error string
Domain string
} }
type Container struct { type Container struct {

68
pkg/plugin/client.go Normal file
View File

@ -0,0 +1,68 @@
package plugin
import (
"context"
"net"
"runtime"
"strconv"
"strings"
"github.com/Neur0toxine/sshpoke/pkg/convert"
"github.com/Neur0toxine/sshpoke/pkg/dto"
"github.com/Neur0toxine/sshpoke/pkg/plugin/pb"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/types/known/emptypb"
)
type Client struct {
parent pb.PluginServiceClient
token string
close func() error
}
func NewClient(addr, token string) (*Client, error) {
conn, err := grpc.Dial(normalizeAddr(addr), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, err
}
c := &Client{
parent: pb.NewPluginServiceClient(conn),
token: token,
close: conn.Close,
}
runtime.SetFinalizer(c, connCloser)
return c, nil
}
func (c *Client) Event(ctx context.Context) (*Stream, error) {
stream, err := c.parent.Event(ctx, &emptypb.Empty{})
if err != nil {
return nil, err
}
return &Stream{stream: stream}, nil
}
func (c *Client) EventStatus(ctx context.Context, status dto.EventStatus) error {
_, err := c.parent.EventStatus(ctx, convert.AppEventStatusToMessage(status))
return err
}
func connCloser(c *Client) {
_ = c.close()
}
func normalizeAddr(addr string) string {
addr = strings.TrimSpace(addr)
if strings.HasPrefix(addr, "grpc://") {
addr = addr[7:]
}
host, port, err := net.SplitHostPort(addr)
if err != nil && err.Error() == "missing port in address" {
host, port, err = net.SplitHostPort(addr + ":" + strconv.Itoa(DefaultPort))
}
if err != nil {
return ""
}
return host + ":" + port
}

View File

@ -1,2 +1,2 @@
//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative pb.proto //go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative pb/pb.proto
package plugin package plugin

View File

@ -1,19 +1,19 @@
syntax = "proto3"; syntax = "proto3";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
option go_package = "github.com/Neur0toxine/sshpoke/pkg/plugin"; option go_package = "github.com/Neur0toxine/sshpoke/pkg/plugin/pb";
option java_multiple_files = true; option java_multiple_files = true;
service PluginService { service PluginService {
rpc Event (google.protobuf.Empty) returns (stream EventMessage); rpc Event (google.protobuf.Empty) returns (stream EventMessage);
rpc EventStatus (EventStatusMessage) returns (google.protobuf.Empty); rpc EventStatus (EventStatusMessage) returns (google.protobuf.Empty);
rpc Shutdown (stream google.protobuf.Empty) returns (google.protobuf.Empty);
} }
enum EventType { enum EventType {
EVENT_START = 0; EVENT_START = 0;
EVENT_STOP = 1; EVENT_STOP = 1;
EVENT_UNKNOWN = 2; EVENT_SHUTDOWN = 2;
EVENT_UNKNOWN = 3;
} }
message Container { message Container {
@ -33,4 +33,5 @@ message EventMessage {
message EventStatusMessage { message EventStatusMessage {
string id = 1; string id = 1;
string error = 2; string error = 2;
string domain = 3;
} }

3
pkg/plugin/port.go Normal file
View File

@ -0,0 +1,3 @@
package plugin
const DefaultPort = 25681

19
pkg/plugin/stream.go Normal file
View File

@ -0,0 +1,19 @@
package plugin
import (
"github.com/Neur0toxine/sshpoke/pkg/convert"
"github.com/Neur0toxine/sshpoke/pkg/dto"
"github.com/Neur0toxine/sshpoke/pkg/plugin/pb"
)
type Stream struct {
stream pb.PluginService_EventClient
}
func (s *Stream) Receive() (dto.Event, error) {
data, err := s.stream.Recv()
if err != nil {
return dto.Event{}, err
}
return convert.MessageToAppEvent(data), nil
}