Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MS Windows fixes #310

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ jobs:
strategy:
matrix:
go-version: [1.13.x, 1.14.x, 1.15.x]
os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
env:
GO111MODULE: "on"
Expand Down
7 changes: 3 additions & 4 deletions beehive.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ package main
import (
"encoding/json"
"fmt"
"net/url"
"os"
"os/signal"
"syscall"
Expand Down Expand Up @@ -106,12 +105,12 @@ func main() {
log.Fatalf("Error creating the configuration %s", err)
}

if config.URL().String() != cfg.DefaultPath() { // the user specified a custom config path or URI
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String() does some encoding when using Windows paths which is not what we want here.

if config.URL().Raw != cfg.DefaultPath() { // the user specified a custom config path or URI
err = config.Load()
if err != nil {
log.Fatalf("Error loading configuration file from %s. err: %v", config.URL(), err)
}
log.Infof("Loading configuration from %s", config.URL())
log.Infof("Loading configuration from %s", config.URL().Raw)
} else { // try to load default config from user paths
path := cfg.Lookup()
if path == "" {
Expand Down Expand Up @@ -180,7 +179,7 @@ func main() {
func decryptConfig(u string) {
b := cfg.AESBackend{}

pu, err := url.Parse(u)
pu, err := cfg.ParseURL(u)
if err != nil {
log.Fatal("Invalid configuration URL. err: ", err)
}
Expand Down
9 changes: 4 additions & 5 deletions cfg/aesbackend.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import (
"encoding/json"
"errors"
"io/ioutil"
"net/url"
"os"
"path/filepath"

Expand All @@ -50,7 +49,7 @@ type AESBackend struct{}
// Given the password is required to encrypt/decrypt the configuration, if the
// URL passed doesn't have a password or PasswordEnvVar is not defined,
// it'll return an error.
func NewAESBackend(u *url.URL) (*AESBackend, error) {
func NewAESBackend(u *URL) (*AESBackend, error) {
if _, err := getPassword(u); err != nil {
return nil, err
}
Expand All @@ -62,7 +61,7 @@ func NewAESBackend(u *url.URL) (*AESBackend, error) {
//
// If the error returned is not nil, an error was returned while opening or
// reading the file.
func IsEncrypted(u *url.URL) (bool, error) {
func IsEncrypted(u *URL) (bool, error) {
f, err := os.Open(u.Path)
if err != nil {
return false, err
Expand All @@ -83,7 +82,7 @@ func IsEncrypted(u *url.URL) (bool, error) {
}

// Load configuration file from the given URL and decrypt it
func (b *AESBackend) Load(u *url.URL) (*Config, error) {
func (b *AESBackend) Load(u *URL) (*Config, error) {
config := &Config{url: u}

if !exist(u.Path) {
Expand Down Expand Up @@ -222,7 +221,7 @@ func deriveKey(password, salt []byte) ([]byte, []byte, error) {
return key, salt, nil
}

func getPassword(u *url.URL) (string, error) {
func getPassword(u *URL) (string, error) {
p := os.Getenv(PasswordEnvVar)
if p != "" {
return p, nil
Expand Down
52 changes: 20 additions & 32 deletions cfg/aesbackend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,14 @@
package cfg

import (
"io/ioutil"
"net/url"
"os"
"path/filepath"
"testing"
)

const testPassword = "foo"

func TestAESBackendLoad(t *testing.T) {
u, _ := url.Parse("crypt://foo@foobar")
u, _ := ParseURL("crypt://foo@foobar")
backend, err := NewAESBackend(u)
if err != nil {
t.Error("The backend should return an error if no password was specified")
Expand All @@ -43,31 +40,28 @@ func TestAESBackendLoad(t *testing.T) {
}

// try to load the config from an absolute path using a URI
cwd, _ := os.Getwd()
u, err = url.Parse("crypto://" + testPassword + "@" + filepath.Join(cwd, "testdata", "beehive-crypto.conf"))
u, err = ParseURL("crypto://" + testPassword + "@" + encryptedConfPath())
if err != nil {
t.Fatalf("Can't parse crypto URL: %v", err)
t.Errorf("Can't parse crypto URL: %v", err)
}

backend, err = NewAESBackend(u)
if err != nil {
t.Fatalf("Can't create AES backend: %v", err)
t.Errorf("Error loading AES backend. %v", err)
}
conf, err := backend.Load(u)
if err != nil {
t.Errorf("Error loading config file fixture from absolute path %s. %v", u, err)
t.Errorf("Error loading config file fixture from absolute path %s. %v", u.Raw, err)
}
if conf.Bees[0].Name != "echo" {
t.Error("The first bee should be an exec bee named echo")
}

// try to load the config using the password from the environment
os.Setenv(PasswordEnvVar, testPassword)
u, err = url.Parse("crypto://" + filepath.Join(cwd, "testdata", "beehive-crypto.conf"))
u, err = ParseURL("crypto://" + encryptedConfPath())
if err != nil {
t.Fatalf("Can't parse crypto URL: %v", err)
}

backend, err = NewAESBackend(u)
if err != nil {
t.Fatalf("Can't create AES backend: %v", err)
Expand All @@ -79,11 +73,10 @@ func TestAESBackendLoad(t *testing.T) {

// try to load the config with an invalid password
os.Setenv(PasswordEnvVar, "")
u, err = url.Parse("crypto://bar@" + filepath.Join(cwd, "testdata", "beehive-crypto.conf"))
u, err = ParseURL("crypto://bar@" + encryptedConfPath())
if err != nil {
t.Fatalf("Can't parse crypto URL: %v", err)
}

backend, err = NewAESBackend(u)
if err != nil {
t.Fatalf("Can't create AES backend: %v", err)
Expand All @@ -95,7 +88,7 @@ func TestAESBackendLoad(t *testing.T) {

// environment password takes prececence
os.Setenv(PasswordEnvVar, testPassword)
u, err = url.Parse("crypto://bar@" + filepath.Join(cwd, "testdata", "beehive-crypto.conf"))
u, err = ParseURL("crypto://bar@" + encryptedConfPath())
if err != nil {
t.Fatalf("Can't parse crypto URL: %v", err)
}
Expand All @@ -109,47 +102,42 @@ func TestAESBackendLoad(t *testing.T) {
t.Errorf("the password defined in %s should take precedence. %v", PasswordEnvVar, err)
}

u, err = url.Parse("crypto://" + testPassword + "@" + filepath.Join(cwd, "testdata", "beehive.conf"))
u, err = ParseURL("crypto://" + testPassword + "@" + confPath())
if err != nil {
t.Fatalf("Can't parse crypto URL: %v", err)
}
_, err = backend.Load(u)
if err == nil || err.Error() != "encrypted configuration header not valid" {
t.Errorf("the password defined in %s should take precedence. %v", PasswordEnvVar, err)
t.Errorf("Loading a non-encrypted config should error")
}
}

func TestAESBackendSave(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "beehivetest")
if err != nil {
t.Error("Could not create temp directory")
}

cwd, _ := os.Getwd()
u, err := url.Parse(filepath.Join("crypto://"+testPassword+"@", cwd, "testdata", "beehive-crypto.conf"))
p := encryptedConfPath()
u, err := ParseURL("crypto://" + testPassword + "@" + p)
backend, _ := NewAESBackend(u)
if err != nil {
t.Error("cannot parse config url")
t.Fatalf("Can't parse crypto URL: %v", err)
}
backend, _ := NewAESBackend(u)
c, err := backend.Load(u)
if err != nil {
t.Errorf("Failed to load config fixture from relative path %s: %v", u, err)
t.Errorf("Failed to load config fixture from relative path %s. %v", p, err)
}

// Save the config file to a new absolute path using a URL
p := filepath.Join(tmpdir, "beehive-crypto.conf")
u, err = url.Parse("crypto://" + testPassword + "@" + p)
p = encryptedTempConf()
u, err = ParseURL("crypto://" + testPassword + "@" + p)
if err != nil {
t.Error("cannot parse config url")
}
err = c.SetURL(u.String())
c.SetURL(u.String())
if err != nil {
t.Error("cannot set url")
t.Error("cannont set config url")
}
backend, _ = NewAESBackend(u)
err = backend.Save(c)
if err != nil {
t.Errorf("cailed to save the config to %s", u)
t.Errorf("failed to save the config to %s. %v", u, err)
}
if !exist(p) {
t.Errorf("configuration file wasn't saved to %s", p)
Expand Down
59 changes: 43 additions & 16 deletions cfg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package cfg

import (
"fmt"
"net/url"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"

"github.com/muesli/beehive/bees"
gap "github.com/muesli/go-app-paths"
Expand All @@ -21,15 +23,15 @@ type Config struct {
Actions []bees.Action
Chains []bees.Chain
backend ConfigBackend
url *url.URL
url *URL
}

// ConfigBackend is the interface implemented by the configuration backends.
//
// Backends are responsible for loading and saving the Config struct to
// memory, the local filesystem, the network, etc.
type ConfigBackend interface {
Load(*url.URL) (*Config, error)
Load(*URL) (*Config, error)
Save(*Config) error
}

Expand Down Expand Up @@ -66,7 +68,7 @@ func (c *Config) Backend() ConfigBackend {
// Next time the config is loaded or saved
// the new URL will be used.
func (c *Config) SetURL(u string) error {
url, err := url.Parse(u)
url, err := ParseURL(u)
if err != nil {
return err
}
Expand All @@ -77,7 +79,7 @@ func (c *Config) SetURL(u string) error {
}

// URL currently being used.
func (c *Config) URL() *url.URL {
func (c *Config) URL() *URL {
return c.url
}

Expand All @@ -97,22 +99,20 @@ func New(url string) (*Config, error) {
return nil, fmt.Errorf("Empty URL provided but not supported")
}

err := config.SetURL(url)
winURL := regexp.MustCompile(`^[a-zA-Z]:`)
var err error
if runtime.GOOS == "windows" && winURL.MatchString(url) {
err = config.SetURL("file://" + url)
} else {
err = config.SetURL(url)
}
if err != nil {
return nil, err
}

switch config.url.Scheme {
case "", "file":
if ok, _ := IsEncrypted(config.url); ok {
log.Debugf("Loading encrypted configuration file")
backend, err = NewAESBackend(config.url)
if err != nil {
log.Fatalf("error loading the AES configuration backend. err: %v", err)
}
} else {
backend = NewFileBackend()
}
backend = loadLocalFileBackend(config)
case "mem":
backend = NewMemBackend()
case "crypto":
Expand All @@ -129,6 +129,23 @@ func New(url string) (*Config, error) {
return config, nil
}

func loadLocalFileBackend(config *Config) ConfigBackend {
var backend ConfigBackend
var err error

if ok, _ := IsEncrypted(config.url); ok {
log.Debugf("Loading encrypted configuration file")
backend, err = NewAESBackend(config.url)
if err != nil {
log.Fatalf("error loading the AES configuration backend. err: %v", err)
}
} else {
backend = NewFileBackend()
}

return backend
}

// DefaultPath returns Beehive's default config path.
//
// The path returned is OS dependant. If there's an error
Expand All @@ -141,7 +158,7 @@ func DefaultPath() string {
return cfgFileName
}

return path
return fixWindowsPath(path)
}

// Lookup tries to find the config file.
Expand Down Expand Up @@ -185,3 +202,13 @@ func exist(file string) bool {
_, err := os.Stat(file)
return err == nil
}

// Replace backward slashes in Windows paths with /, to make them suitable
// for Go URL parsing.
func fixWindowsPath(path string) string {
if runtime.GOOS == "windows" {
return strings.Replace(path, `\`, "/", -1)
}

return path
}
11 changes: 3 additions & 8 deletions cfg/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
func TestNew(t *testing.T) {
conf, err := New("/foobar")
if err != nil {
t.Fatal("cannot create config from path")
t.Fatalf("Error in New. %v", err)
}
if _, ok := conf.Backend().(*FileBackend); !ok {
t.Error("Backend for '/foobar' should be a FileBackend")
Expand All @@ -24,10 +24,10 @@ func TestNew(t *testing.T) {
}

cwd, _ := os.Getwd()
p := filepath.Join(cwd, "testdata/beehive-crypto.conf")
p := fixWindowsPath(filepath.Join(cwd, "testdata", "beehive-crypto.conf"))
conf, err = New(p)
if err != nil {
t.Fatal(err)
t.Fatalf("Error in New: %v", err)
}
if _, ok := conf.Backend().(*AESBackend); !ok {
t.Errorf("Backend for '%s' should be an AESBackend", p)
Expand All @@ -41,11 +41,6 @@ func TestNew(t *testing.T) {
t.Error("Backend for 'mem:' should be a MemoryBackend")
}

_, err = New("c:\\foobar")
if err == nil {
t.Error("Not a valid URL, should return an error")
}

_, err = New("")
if err == nil {
t.Error("Not a valid URL, should return an error")
Expand Down
Loading