services configuration inside config file
This commit is contained in:
parent
64fa306f8d
commit
f169b7f137
@ -117,7 +117,7 @@ func runRestServer() {
|
||||
|
||||
func runDockerEventListener(ctx context.Context) {
|
||||
var err error
|
||||
docker.Default, err = docker.New(ctx)
|
||||
docker.Default, err = docker.New(ctx, config.Default.Services, config.Default.DefaultServer)
|
||||
if err != nil {
|
||||
logger.Sugar.Fatalf("cannot connect to docker daemon: %s", err)
|
||||
}
|
||||
|
@ -23,10 +23,31 @@ docker:
|
||||
version: ~
|
||||
# Default server to use if `sshpoke.server` is not specified in the target container labels.
|
||||
default_server: mine
|
||||
# Services configuration. These values will have higher priority than the container labels.
|
||||
services:
|
||||
# Container ID, one of container names or matcher regex. This value will be used to determine which container will
|
||||
# receive the labels from 'params' section. Add / at the start and the end of the value to use it as regex.
|
||||
# Examples:
|
||||
# - name: a77eaa474cc634f0e4da4d4b72ebf71c03d1ae69329a07e42b830375e247e613 # Match by container ID (exact matcher).
|
||||
# - name: my-container # Match by container name (exact matcher).
|
||||
# - name: /my-container/ # Match by container name via regular expression (e.g. 'my-container_1' will also match).
|
||||
- name: /service-web/
|
||||
# Same params as container labels but without 'sshpoke.' prefix.
|
||||
params:
|
||||
# Enable sshpoke for service. Replaces 'sshpoke.enable' in container labels.
|
||||
enable: true
|
||||
# Specifies container network. Replaces 'sshpoke.network' in container labels.
|
||||
network: service_default
|
||||
# Specifies server which will be used. Replaces 'sshpoke.server' in container labels.
|
||||
server: ssh-demo-sish
|
||||
# Specifies container port to be shared. Replaces 'sshpoke.port' in container labels.
|
||||
port: 80
|
||||
# Specifies remote host to be used. Replaces 'sshpoke.remote_host' in container labels.
|
||||
remote_host: remotehost
|
||||
# Servers configuration.
|
||||
servers:
|
||||
# Server name.
|
||||
- name: mine
|
||||
- name: ssh-demo-sish
|
||||
# Server driver. Each driver has its own set of params. Supported drivers: ssh, plugin, null.
|
||||
driver: ssh
|
||||
params:
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Neur0toxine/sshpoke/pkg/smarttypes"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
)
|
||||
@ -15,9 +16,23 @@ type Config struct {
|
||||
API API `mapstructure:"api" json:"api"`
|
||||
Docker DockerConfig `mapstructure:"docker" json:"docker"`
|
||||
DefaultServer string `mapstructure:"default_server" json:"default_server"`
|
||||
Services []Service `mapstructure:"services" json:"services,omitempty"`
|
||||
Servers []Server `mapstructure:"servers" json:"servers"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
Name smarttypes.MatchableString `mapstructure:"name" json:"name"`
|
||||
Params ServiceLabels `mapstructure:"params" json:"params"`
|
||||
}
|
||||
|
||||
type ServiceLabels struct {
|
||||
Enable smarttypes.BoolStr `mapstructure:"enable" json:"enable"`
|
||||
Network string `mapstructure:"network" json:"network,omitempty"`
|
||||
Server string `mapstructure:"server" json:"server,omitempty"`
|
||||
Port string `mapstructure:"port" json:"port,omitempty"`
|
||||
RemoteHost string `mapstructure:"remote_host" json:"remote_host,omitempty"`
|
||||
}
|
||||
|
||||
type API struct {
|
||||
Rest WebAPI `mapstructure:"rest" json:"rest"`
|
||||
Plugin PluginAPI `mapstructure:"plugin" json:"plugin"`
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"github.com/Neur0toxine/sshpoke/internal/config"
|
||||
"github.com/Neur0toxine/sshpoke/internal/logger"
|
||||
"github.com/Neur0toxine/sshpoke/pkg/dto"
|
||||
"github.com/Neur0toxine/sshpoke/pkg/smarttypes"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
@ -18,19 +19,41 @@ var Default *Docker
|
||||
type Docker struct {
|
||||
cli *client.Client
|
||||
ctx context.Context
|
||||
services map[smarttypes.MatchableString]config.ServiceLabels
|
||||
defaultServer string
|
||||
}
|
||||
|
||||
func New(ctx context.Context) (*Docker, error) {
|
||||
func New(ctx context.Context, services []config.Service, defaultServer string) (*Docker, error) {
|
||||
cli, err := client.NewClientWithOpts(config.Default.Docker.Opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servicesMap := make(map[smarttypes.MatchableString]config.ServiceLabels)
|
||||
for _, svc := range services {
|
||||
servicesMap[svc.Name] = svc.Params
|
||||
}
|
||||
return &Docker{
|
||||
cli: cli,
|
||||
ctx: ctx,
|
||||
services: servicesMap,
|
||||
defaultServer: defaultServer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Docker) findServiceLabels(id string, names []string) *config.ServiceLabels {
|
||||
if labels, ok := d.services[smarttypes.MatchableString(id)]; ok {
|
||||
return &labels
|
||||
}
|
||||
for matcher, labels := range d.services {
|
||||
for _, name := range names {
|
||||
if matcher.Match(name) {
|
||||
return &labels
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Docker) Containers() map[string]dto.Container {
|
||||
items, err := d.cli.ContainerList(d.ctx, types.ContainerListOptions{
|
||||
Filters: filters.NewArgs(filters.Arg("status", "running")),
|
||||
@ -41,7 +64,7 @@ func (d *Docker) Containers() map[string]dto.Container {
|
||||
}
|
||||
containers := map[string]dto.Container{}
|
||||
for _, item := range items {
|
||||
container, ok := dockerContainerToInternal(item)
|
||||
container, ok := dockerContainerToInternal(item, d.findServiceLabels(item.ID, item.Names), d.defaultServer)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
@ -58,7 +81,8 @@ func (d *Docker) GetContainer(id string, all bool) (dto.Container, bool) {
|
||||
if err != nil || len(container) != 1 {
|
||||
return dto.Container{}, false
|
||||
}
|
||||
converted, ok := dockerContainerToInternal(container[0])
|
||||
converted, ok := dockerContainerToInternal(
|
||||
container[0], d.findServiceLabels(container[0].ID, container[0].Names), d.defaultServer)
|
||||
if !ok {
|
||||
return dto.Container{}, false
|
||||
}
|
||||
@ -92,7 +116,8 @@ func (d *Docker) Listen() (chan dto.Event, error) {
|
||||
"id", event.Actor.ID, "err", err)
|
||||
continue
|
||||
}
|
||||
converted, ok := dockerContainerToInternal(container[0])
|
||||
converted, ok := dockerContainerToInternal(container[0],
|
||||
d.findServiceLabels(container[0].ID, container[0].Names), d.defaultServer)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
@ -5,8 +5,10 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Neur0toxine/sshpoke/internal/config"
|
||||
"github.com/Neur0toxine/sshpoke/internal/logger"
|
||||
"github.com/Neur0toxine/sshpoke/pkg/dto"
|
||||
"github.com/Neur0toxine/sshpoke/pkg/smarttypes"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
@ -14,41 +16,60 @@ import (
|
||||
)
|
||||
|
||||
type labelsConfig struct {
|
||||
Enable boolStr `mapstructure:"sshpoke.enable"`
|
||||
Enable smarttypes.BoolStr `mapstructure:"sshpoke.enable"`
|
||||
Network string `mapstructure:"sshpoke.network"`
|
||||
Server string `mapstructure:"sshpoke.server"`
|
||||
Port string `mapstructure:"sshpoke.port"`
|
||||
RemoteHost string `mapstructure:"sshpoke.remote_host"`
|
||||
}
|
||||
|
||||
type boolStr string
|
||||
|
||||
func (b boolStr) Bool() bool {
|
||||
return string(b) == "true"
|
||||
}
|
||||
|
||||
func actorEnabled(actor events.Actor) bool {
|
||||
func actorEnabled(actor events.Actor) smarttypes.BoolStr {
|
||||
label, ok := actor.Attributes["sshpoke.enable"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return boolStr(label).Bool()
|
||||
return smarttypes.BoolFromStr(label)
|
||||
}
|
||||
|
||||
func dockerContainerToInternal(container types.Container) (result dto.Container, ok bool) {
|
||||
func populateLabelsFromConfig(labels *labelsConfig, config *config.ServiceLabels) {
|
||||
if labels.Enable != config.Enable {
|
||||
labels.Enable = config.Enable
|
||||
}
|
||||
if labels.Server != config.Server {
|
||||
labels.Server = config.Server
|
||||
}
|
||||
if labels.Network != config.Network {
|
||||
labels.Network = config.Network
|
||||
}
|
||||
if labels.Port != config.Port {
|
||||
labels.Port = config.Port
|
||||
}
|
||||
if labels.RemoteHost != config.RemoteHost {
|
||||
labels.RemoteHost = config.RemoteHost
|
||||
}
|
||||
}
|
||||
|
||||
func dockerContainerToInternal(
|
||||
container types.Container, configLabels *config.ServiceLabels, defaultServer string) (result dto.Container, ok bool) {
|
||||
var labels labelsConfig
|
||||
if err := mapstructure.Decode(container.Labels, &labels); err != nil {
|
||||
logger.Sugar.Debugf("skipping container %s because configuration is invalid: %s", container.ID, err)
|
||||
return result, false
|
||||
}
|
||||
if !labels.Enable.Bool() {
|
||||
if configLabels != nil {
|
||||
populateLabelsFromConfig(&labels, configLabels)
|
||||
}
|
||||
if !labels.Enable {
|
||||
logger.Sugar.Debugf("skipping container %s because sshpoke is not enabled for it", container.ID)
|
||||
return result, false
|
||||
}
|
||||
if labels.Server == "" {
|
||||
if defaultServer == "" {
|
||||
logger.Sugar.Debugf("skipping container %s because 'sshpoke.server' is not defined", container.ID)
|
||||
return result, false
|
||||
}
|
||||
labels.Server = defaultServer
|
||||
}
|
||||
if container.NetworkSettings == nil ||
|
||||
container.NetworkSettings.Networks == nil ||
|
||||
len(container.NetworkSettings.Networks) == 0 {
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"github.com/Neur0toxine/sshpoke/pkg/dto"
|
||||
"github.com/Neur0toxine/sshpoke/pkg/proto/ssh"
|
||||
"github.com/Neur0toxine/sshpoke/pkg/proto/ssh/knownhosts"
|
||||
"github.com/Neur0toxine/sshpoke/pkg/smarttypes"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@ -145,8 +146,8 @@ func (d *SSH) buildHostKeyCallback() ssh.HostKeyCallback {
|
||||
return sshtun.FixedHostKeys(d.hostKeys)
|
||||
}()
|
||||
if d.params.Auth.Type == types.AuthTypeKey && d.params.Auth.Directory != "" && len(d.hostKeys) == 0 {
|
||||
knownHostsPath := types.SmartPath(path.Join(string(d.params.Auth.Directory), KnownHostsFile))
|
||||
resolvedPath, err := knownHostsPath.Resolve(false)
|
||||
knownHostsPath := smarttypes.Path(path.Join(string(d.params.Auth.Directory), KnownHostsFile))
|
||||
resolvedPath, err := knownHostsPath.File()
|
||||
if err != nil {
|
||||
return ssh.InsecureIgnoreHostKey()
|
||||
}
|
||||
@ -237,7 +238,7 @@ func (d *SSH) populateFromSSHConfig() {
|
||||
if d.params.Auth.Directory == "" {
|
||||
return
|
||||
}
|
||||
cfg, err := parseSSHConfig(types.SmartPath(path.Join(string(d.params.Auth.Directory), "config")))
|
||||
cfg, err := parseSSHConfig(smarttypes.Path(path.Join(string(d.params.Auth.Directory), "config")))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -255,7 +256,7 @@ func (d *SSH) populateFromSSHConfig() {
|
||||
d.params.Auth.User = user
|
||||
}
|
||||
if keyfile, err := hostCfg.Get("IdentityFile"); err == nil && keyfile != "" {
|
||||
resolvedKeyFile, err := types.SmartPath(keyfile).Resolve(false)
|
||||
resolvedKeyFile, err := smarttypes.Path(keyfile).File()
|
||||
if err == nil {
|
||||
d.params.Auth.Type = types.AuthTypeKey
|
||||
d.params.Auth.Keyfile = resolvedKeyFile
|
||||
@ -354,7 +355,7 @@ func (d *SSH) authenticator() ssh.AuthMethod {
|
||||
case types.AuthTypeKey:
|
||||
if d.params.Auth.Keyfile != "" {
|
||||
keyAuth, err := sshtun.AuthKeyFile(
|
||||
types.SmartPath(path.Join(d.params.Auth.Directory.String(), d.params.Auth.Keyfile)))
|
||||
smarttypes.Path(path.Join(d.params.Auth.Directory.String(), d.params.Auth.Keyfile)))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/Neur0toxine/sshpoke/internal/server/driver/ssh/types"
|
||||
"github.com/Neur0toxine/sshpoke/pkg/smarttypes"
|
||||
"github.com/kevinburke/ssh_config"
|
||||
)
|
||||
|
||||
@ -19,8 +19,8 @@ func (c *hostConfig) Get(key string) (string, error) {
|
||||
return strings.TrimSpace(val), err
|
||||
}
|
||||
|
||||
func parseSSHConfig(filePath types.SmartPath) (*ssh_config.Config, error) {
|
||||
fileName, err := filePath.Resolve(false)
|
||||
func parseSSHConfig(filePath smarttypes.Path) (*ssh_config.Config, error) {
|
||||
fileName, err := filePath.File()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -7,11 +7,11 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/Neur0toxine/sshpoke/internal/server/driver/ssh/types"
|
||||
"github.com/Neur0toxine/sshpoke/pkg/proto/ssh"
|
||||
"github.com/Neur0toxine/sshpoke/pkg/smarttypes"
|
||||
)
|
||||
|
||||
func AuthKeyFile(keyFile types.SmartPath) (ssh.AuthMethod, error) {
|
||||
func AuthKeyFile(keyFile smarttypes.Path) (ssh.AuthMethod, error) {
|
||||
key, err := readKey(keyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -19,7 +19,7 @@ func AuthKeyFile(keyFile types.SmartPath) (ssh.AuthMethod, error) {
|
||||
return ssh.PublicKeys(key), nil
|
||||
}
|
||||
|
||||
func AuthKeyDir(keyDir types.SmartPath) (ssh.AuthMethod, error) {
|
||||
func AuthKeyDir(keyDir smarttypes.Path) (ssh.AuthMethod, error) {
|
||||
keys, err := readKeys(keyDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -31,8 +31,8 @@ func AuthPassword(password string) ssh.AuthMethod {
|
||||
return ssh.Password(password)
|
||||
}
|
||||
|
||||
func readKeys(keyDir types.SmartPath) ([]ssh.Signer, error) {
|
||||
dir, err := keyDir.Resolve(true)
|
||||
func readKeys(keyDir smarttypes.Path) ([]ssh.Signer, error) {
|
||||
dir, err := keyDir.Directory()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse keys: %s", err)
|
||||
}
|
||||
@ -62,7 +62,7 @@ func readKeys(keyDir types.SmartPath) ([]ssh.Signer, error) {
|
||||
if info.Size() < 256 {
|
||||
continue
|
||||
}
|
||||
key, err := readKey(types.SmartPath(path.Join(dir, entry.Name())))
|
||||
key, err := readKey(smarttypes.Path(path.Join(dir, entry.Name())))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@ -74,8 +74,8 @@ func readKeys(keyDir types.SmartPath) ([]ssh.Signer, error) {
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func readKey(keyFile types.SmartPath) (ssh.Signer, error) {
|
||||
fileName, err := keyFile.Resolve(false)
|
||||
func readKey(keyFile smarttypes.Path) (ssh.Signer, error) {
|
||||
fileName, err := keyFile.File()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -4,12 +4,12 @@ import (
|
||||
"bytes"
|
||||
"os"
|
||||
|
||||
"github.com/Neur0toxine/sshpoke/internal/server/driver/ssh/types"
|
||||
"github.com/Neur0toxine/sshpoke/pkg/smarttypes"
|
||||
"github.com/kevinburke/ssh_config"
|
||||
)
|
||||
|
||||
func parseSSHConfig(filePath types.SmartPath) (*ssh_config.Config, error) {
|
||||
fileName, err := filePath.Resolve(false)
|
||||
func parseSSHConfig(filePath smarttypes.Path) (*ssh_config.Config, error) {
|
||||
fileName, err := filePath.File()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1,21 +1,12 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/Neur0toxine/sshpoke/pkg/smarttypes"
|
||||
)
|
||||
|
||||
var envMatcherRegExp = regexp.MustCompile(`\$[\w\d\_]`)
|
||||
|
||||
type (
|
||||
AuthType string
|
||||
SmartPath string
|
||||
)
|
||||
type AuthType string
|
||||
|
||||
const (
|
||||
AuthTypePasswordless AuthType = "passwordless"
|
||||
@ -27,45 +18,10 @@ 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"`
|
||||
Directory smarttypes.Path `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 (k SmartPath) String() string {
|
||||
return string(k)
|
||||
}
|
||||
|
||||
func (a Auth) Validate() error {
|
||||
if a.Type == AuthTypePassword && a.Password == "" {
|
||||
return fmt.Errorf("password must be provided for authentication type '%s'", AuthTypePassword)
|
||||
|
19
pkg/smarttypes/bool_str.go
Normal file
19
pkg/smarttypes/bool_str.go
Normal file
@ -0,0 +1,19 @@
|
||||
package smarttypes
|
||||
|
||||
type BoolStr bool
|
||||
|
||||
func BoolFromStr(str string) BoolStr {
|
||||
return str == "true" || str == "1"
|
||||
}
|
||||
|
||||
func (b BoolStr) MarshalText() ([]byte, error) {
|
||||
if b {
|
||||
return []byte("true"), nil
|
||||
}
|
||||
return []byte("false"), nil
|
||||
}
|
||||
|
||||
func (b *BoolStr) UnmarshalText(src []byte) error {
|
||||
*b = BoolFromStr(string(src))
|
||||
return nil
|
||||
}
|
57
pkg/smarttypes/file_path.go
Normal file
57
pkg/smarttypes/file_path.go
Normal file
@ -0,0 +1,57 @@
|
||||
package smarttypes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var envMatcherRegExp = regexp.MustCompile(`\$[\w\d\_]`)
|
||||
|
||||
type Path string
|
||||
|
||||
func (k Path) File() (string, error) {
|
||||
return k.resolve(false)
|
||||
}
|
||||
|
||||
func (k Path) Directory() (string, error) {
|
||||
return k.resolve(true)
|
||||
}
|
||||
|
||||
func (k Path) 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 (k Path) String() string {
|
||||
return string(k)
|
||||
}
|
47
pkg/smarttypes/matchable_string.go
Normal file
47
pkg/smarttypes/matchable_string.go
Normal file
@ -0,0 +1,47 @@
|
||||
package smarttypes
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type MatchableString string
|
||||
|
||||
func (m MatchableString) isExpr() bool {
|
||||
if len(m) < 3 {
|
||||
return false
|
||||
}
|
||||
if m[0] == '/' && m[len(m)-1] == '/' {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m MatchableString) expr() *regexp.Regexp {
|
||||
return regexp.MustCompile(string(m[1 : len(m)-2]))
|
||||
}
|
||||
|
||||
func (m MatchableString) Match(val string) bool {
|
||||
if m.isExpr() {
|
||||
return m.expr().Match([]byte(val))
|
||||
}
|
||||
return val == string(m)
|
||||
}
|
||||
|
||||
func (m MatchableString) String() string {
|
||||
return string(m)
|
||||
}
|
||||
|
||||
func (m MatchableString) MarshalText() ([]byte, error) {
|
||||
return []byte(m), nil
|
||||
}
|
||||
|
||||
func (m *MatchableString) UnmarshalText(src []byte) error {
|
||||
if len(src) > 2 && src[0] == '/' && src[len(src)-1] == '/' {
|
||||
_, err := regexp.Compile(string(src[1 : len(src)-2]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
*m = MatchableString(src)
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user