openrport-openrport/client/config.go
2023-09-27 15:43:23 +02:00

440 lines
11 KiB
Go

package chclient
import (
"errors"
"fmt"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/openrport/openrport/share/files"
"github.com/openrport/openrport/client/system"
chshare "github.com/openrport/openrport/share"
"github.com/openrport/openrport/share/clientconfig"
"github.com/openrport/openrport/share/logger"
"github.com/openrport/openrport/share/models"
)
const DefaultMonitoringInterval = 60 * time.Second
var (
allowDenyOrder = [2]string{"allow", "deny"}
denyAllowOrder = [2]string{"deny", "allow"}
)
type ClientConfigHolder struct {
*clientconfig.Config
}
func (c *ClientConfigHolder) ParseAndValidate(skipScriptsDirValidation bool) error {
if err := c.parseHeaders(); err != nil {
return err
}
if err := c.parseServerURL(); err != nil {
return err
}
if err := c.parseFallbackServers(); err != nil {
return err
}
if err := c.parseProxyURL(); err != nil {
return err
}
if err := c.parseRemotes(); err != nil {
return err
}
if err := c.parseInterpreterAliases(); err != nil {
return err
}
if c.Connection.MaxRetryInterval < time.Second {
c.Connection.MaxRetryInterval = 5 * time.Minute
}
if c.Client.DataDir == "" {
return errors.New("'data directory path' cannot be empty")
}
if err := c.parseRemoteCommands(); err != nil {
return fmt.Errorf("remote commands: %v", err)
}
c.Client.AuthUser, c.Client.AuthPass = chshare.ParseAuth(c.Client.Auth)
if err := c.parseRemoteScripts(skipScriptsDirValidation); err != nil {
return err
}
if err := c.ParseAndValidateMonitoring(); err != nil {
return err
}
if err := c.ParseAndValidateFilePushConfig(); err != nil {
return err
}
if err := c.ParseAndValidateConnection(); err != nil {
return err
}
if err := c.parseAndValidateIPAPIURL(); err != nil {
return err
}
return nil
}
func (c *ClientConfigHolder) ParseAndValidateMonitoring() error {
if c.Monitoring.Interval < DefaultMonitoringInterval {
c.Monitoring.Interval = DefaultMonitoringInterval
}
if len(c.Monitoring.NetLan) > 0 {
lanCard, err := models.DecodeCard(c.Monitoring.NetLan)
if err != nil {
return err
}
c.Monitoring.LanCard = lanCard
}
if len(c.Monitoring.NetWan) > 0 {
wanCard, err := models.DecodeCard(c.Monitoring.NetWan)
if err != nil {
return err
}
c.Monitoring.WanCard = wanCard
}
return nil
}
func (c *ClientConfigHolder) ParseAndValidateConnection() error {
if !c.Connection.WatchdogIntegration {
return nil
}
if c.Connection.KeepAlive == 0 {
return errors.New("watchdog integration requires 'keep_alive > 0'")
}
if c.Connection.MaxRetryCount > 0 {
return errors.New("watchdog integration requires 'max_retry_count = -1' (=disabled)")
}
return nil
}
func (c *ClientConfigHolder) ParseAndValidateFilePushConfig() error {
for _, globPattern := range c.FileReceptionConfig.Protected {
_, err := filepath.Match(globPattern, "/test")
if err != nil {
return fmt.Errorf("invalid glob pattern %s: %v", globPattern, err)
}
}
return nil
}
func (c *ClientConfigHolder) parseHeaders() error {
c.Connection.HTTPHeaders = http.Header{}
for _, h := range c.Connection.HeadersRaw {
name, val, err := parseHeader(h)
if err != nil {
return err
}
c.Connection.HTTPHeaders.Set(name, val)
}
if c.Connection.Hostname != "" {
c.Connection.HTTPHeaders.Set("Host", c.Connection.Hostname)
}
if len(c.Connection.HTTPHeaders.Values("User-Agent")) == 0 {
c.Connection.HTTPHeaders.Set("User-Agent", fmt.Sprintf("rport %s", chshare.BuildVersion))
}
return nil
}
func (c *ClientConfigHolder) parseServerURL() error {
if c.Client.Server == "" {
return errors.New("server address is required")
}
url, err := c.parseURL(c.Client.Server)
if err != nil {
return fmt.Errorf("invalid server address: %v", err)
}
c.Client.Server = url
return nil
}
func (c *ClientConfigHolder) parseFallbackServers() error {
for i := range c.Client.FallbackServers {
url, err := c.parseURL(c.Client.FallbackServers[i])
if err != nil {
return fmt.Errorf("invalid fallback server address: %v", err)
}
c.Client.FallbackServers[i] = url
}
return nil
}
func (ClientConfigHolder) parseURL(urlStr string) (string, error) {
//apply default scheme
if !strings.Contains(urlStr, "://") {
urlStr = "http://" + urlStr
}
u, err := url.Parse(urlStr)
if err != nil {
return "", err
}
//apply default port
if !regexp.MustCompile(`:\d+$`).MatchString(u.Host) {
if u.Scheme == "https" || u.Scheme == "wss" {
u.Host = u.Host + ":443"
} else {
u.Host = u.Host + ":80"
}
}
//swap to websockets scheme
u.Scheme = strings.Replace(u.Scheme, "http", "ws", 1)
return u.String(), nil
}
func (c *ClientConfigHolder) parseProxyURL() error {
if p := c.Client.Proxy; p != "" {
proxyURL, err := url.Parse(p)
if err != nil {
return fmt.Errorf("invalid proxy URL: %v", err)
}
if proxyURL.Scheme == "https" {
return fmt.Errorf("https proxies not (yet) supported")
}
c.Client.ProxyURL = proxyURL
}
return nil
}
func (c *ClientConfigHolder) parseRemotes() error {
if err := c.parseTunnelsConfig(); err != nil {
return fmt.Errorf("invalid tunnels config: %w", err)
}
for _, ta := range c.Client.TunnelAllowed {
_, _, err := ParseTunnelAllowed(ta)
if err != nil {
return fmt.Errorf(`invalid "tunnel_allowed" config: %v`, err)
}
}
for _, s := range c.Client.Remotes {
r, err := models.NewRemote(s)
if err != nil {
return fmt.Errorf("failed to decode remote %q: %v", s, err)
}
allowed, err := TunnelIsAllowed(c.Client.TunnelAllowed, r.Remote())
if err != nil {
return fmt.Errorf("failed to check if remote %q is allowed: %v", s, err)
}
if !allowed {
return fmt.Errorf(`remote %q is not allowed by "tunnel_allowed" config`, s)
}
r = c.applyTunnelsConfig(r)
c.Client.Tunnels = append(c.Client.Tunnels, r)
}
return nil
}
func (c *ClientConfigHolder) parseTunnelsConfig() error {
if c.Tunnels.HostHeader != "" && !c.Tunnels.ReverseProxy {
return errors.New("host-header requires enabled reverse-proxy")
}
return nil
}
func (c *ClientConfigHolder) applyTunnelsConfig(r *models.Remote) *models.Remote {
if c.Tunnels.Scheme != "" {
r.Scheme = &c.Tunnels.Scheme
}
r.HTTPProxy = c.Tunnels.ReverseProxy
r.HostHeader = c.Tunnels.HostHeader
return r
}
func parseHeader(h string) (string, string, error) {
index := strings.Index(h, ":")
if index < 0 {
return "", "", fmt.Errorf(`invalid header %q. Should be in the format "HeaderName: HeaderContent"`, h)
}
return h[0:index], strings.TrimSpace(h[index+1:]), nil
}
func (c *ClientConfigHolder) parseRemoteCommands() error {
if c.RemoteCommands.SendBackLimit < 0 {
return fmt.Errorf("send back limit can not be negative: %d", c.RemoteCommands.SendBackLimit)
}
allow, err := parseRegexpList(c.RemoteCommands.Allow)
if err != nil {
return fmt.Errorf("allow regexp: %v", err)
}
c.RemoteCommands.AllowRegexp = allow
deny, err := parseRegexpList(c.RemoteCommands.Deny)
if err != nil {
return fmt.Errorf("deny regexp: %v", err)
}
c.RemoteCommands.DenyRegexp = deny
if c.RemoteCommands.Order != allowDenyOrder && c.RemoteCommands.Order != denyAllowOrder {
return fmt.Errorf("invalid order: %v", c.RemoteCommands.Order)
}
return nil
}
func (c *ClientConfigHolder) GetScriptsDir() string {
return filepath.Join(c.Client.DataDir, "scripts")
}
func (c *ClientConfigHolder) GetUploadDir() string {
return filepath.Join(c.Client.DataDir, files.DefaultUploadTempFolder)
}
func (c *ClientConfigHolder) GetProtectedUploadDirs() []string {
return c.FileReceptionConfig.Protected
}
func (c *ClientConfigHolder) IsFileReceptionEnabled() bool {
return c.FileReceptionConfig.Enabled
}
func (c *ClientConfigHolder) parseRemoteScripts(skipScriptsDirValidation bool) error {
if skipScriptsDirValidation {
return nil
}
if c.RemoteScripts.Enabled && !c.RemoteCommands.Enabled {
return errors.New("remote scripts execution requires remote commands to be enabled")
}
if !c.RemoteScripts.Enabled {
return nil
}
err := system.ValidateScriptDir(c.GetScriptsDir())
// we allow to start a client if the script dir is not good because clients might never run scripts
if err != nil {
log.Printf("ERROR: %v\n", err)
}
return nil
}
func parseRegexpList(regexpList []string) ([]*regexp.Regexp, error) {
res := make([]*regexp.Regexp, 0, len(regexpList))
for _, cur := range regexpList {
r, err := regexp.Compile(cur)
if err != nil {
return nil, fmt.Errorf("invalid regular expression %q: %v", cur, err)
}
res = append(res, r)
}
return res, nil
}
func PrepareDirs(c *ClientConfigHolder) error {
logger := logger.NewLogger("client", c.Logging.LogOutput, c.Logging.LogLevel)
if err := os.MkdirAll(c.Client.DataDir, os.ModePerm); err != nil {
return fmt.Errorf("failed to create dir %q: %s", c.Client.DataDir, err)
}
logger.Debugf("Data directory path: %q", c.Client.DataDir)
scriptDir := c.GetScriptsDir()
if _, err := os.Stat(scriptDir); os.IsNotExist(err) {
err := os.Mkdir(scriptDir, system.DefaultDirMode)
if err != nil {
return err
}
}
return nil
}
func (c *ClientConfigHolder) parseInterpreterAliases() error {
c.InterpreterAliases = make(map[string]string, len(c.InterpreterAliasesConfig))
c.InterpreterAliasesEncodings = make(map[string]clientconfig.InterpreterAliasEncoding)
for alias, value := range c.InterpreterAliasesConfig {
if str, ok := value.(string); ok {
c.InterpreterAliases[alias] = str
continue
}
if values, ok := value.([]any); ok && len(values) > 0 {
str, ok := values[0].(string)
if !ok {
return fmt.Errorf("interpreter alias %q shell should be a string, got: %v", alias, values[0])
}
c.InterpreterAliases[alias] = str
if len(values) > 1 {
enc, ok := values[1].(string)
if !ok {
return fmt.Errorf("interpreter alias %q encoding should be a string, got: %v", alias, values[1])
}
// set both encodings in case only 1 value is provided, otherwise output encoding is overrided with 2nd value
encoding := clientconfig.InterpreterAliasEncoding{
InputEncoding: enc,
OutputEncoding: enc,
}
if len(values) > 2 {
outputEncoding, ok := values[2].(string)
if !ok {
return fmt.Errorf("interpreter alias %q output encoding should be a string, got: %v", alias, values[2])
}
encoding.OutputEncoding = outputEncoding
}
_, err := system.EncodingFromConfig(encoding)
if err != nil {
return fmt.Errorf("interpreter alias %q: %w", alias, err)
}
c.InterpreterAliasesEncodings[alias] = encoding
}
continue
}
return fmt.Errorf("invalid interpreter alias %q: %v", alias, value)
}
return nil
}
func (c *ClientConfigHolder) parseAndValidateIPAPIURL() error {
if c.Client.IPAPIURL == "" {
return nil
}
u, err := url.Parse(c.Client.IPAPIURL)
if err != nil {
return fmt.Errorf("invalid ip_api_url: %s", err)
}
if !strings.HasPrefix(u.Scheme, "http") {
return fmt.Errorf("invalid ip_api_url. only http(s):// supported, '%s' given", u.Scheme)
}
return nil
}