diff --git a/Dockerfile b/Dockerfile index 2051bb8..bf45906 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,8 @@ VOLUME ["/data"] CMD ["/app/scripts/cross-compile.sh"] FROM alpine:latest +VOLUME [ "/data" ] COPY --from=builder-base /app/dist/ezshare /usr/bin/ezshare EXPOSE 50051 EXPOSE 8088 -CMD ["/usr/bin/ezshare", "--config", "/data/ezshare.toml", "serve"] \ No newline at end of file +CMD ["/usr/bin/ezshare", "serve"] \ No newline at end of file diff --git a/README.md b/README.md index b3b9599..5fcbd0d 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,9 @@ Running a server using docker: # First, build the image docker build -t ezshare:latest . -# Then run an interactive container to generate certificates -# This will generate CA, server and client certs in /var/ezshare -docker run --rm -it -v /var/ezshare:/data ezshare:latest sh -# Run this inside the container -ezshare cert gen-all --out-dir /data/certs -exit - -# Then copy ezshare.example.toml to /var/ezshare/ezshare.toml -# Edit the config, making sure to point certificates to /data/server.pem etc - +Copy config file to /var/ezshare/ezshare.toml (or see ezshare.minimal.env to see how to set config-values using envvars) # Run the server -docker run --rm -d -v /var/ezshare:/data ezshare:latest +docker run --rm -d -v /var/ezshare:/data -e EZSHARE_CONFIG=/data/ezshare.toml ezshare:latest ``` ## Client @@ -29,11 +20,12 @@ Compile the client: go build -o ezshare cmd/ezshare.go ``` -Copy the resulting binary somewhere into `$PATH`. Then generate an empty client config using `ezshare client init-config`. -Copy certificates from server (`srv.pem`, `client.pem`, `client.key`) to the same dir as the generated config. -Edit the config-file, and update the `Client` section with correct default-server and path to certificates. - -Client should then be ready for use. +Generate client config, and fetch needed certificates: +``` +./ezshare client login --overwrite "https://ezshare.example.org" +username: admin +password: +``` ```text NAME: diff --git a/actions/client.go b/actions/client.go index a5ed700..a31dfd8 100644 --- a/actions/client.go +++ b/actions/client.go @@ -103,7 +103,7 @@ func ActionClientUpload(c *cli.Context) error { if err != nil { return err } - fmt.Printf("%s uploaded with id %s. Available at %s\n", arg, resp.Id, resp.FileUrl) + fmt.Printf("%s uploaded with id %s. Available at:\n%s\n", arg, resp.Id, resp.FileUrl) } return nil } diff --git a/actions/misc.go b/actions/misc.go index b64da2e..6ca2c34 100644 --- a/actions/misc.go +++ b/actions/misc.go @@ -2,6 +2,7 @@ package actions import ( "fmt" + "os" "gitea.benny.dog/torjus/ezshare/certs" "gitea.benny.dog/torjus/ezshare/config" @@ -25,10 +26,20 @@ func getConfig(c *cli.Context) (*config.Config, error) { cfgPath := c.String("config") return config.FromFile(cfgPath) } + if val, ok := os.LookupEnv("EZSHARE_CONFIG"); ok { + return config.FromFile(val) + } cfg, err := config.FromDefaultLocations() if err == nil { verbosePrint(c, fmt.Sprintf("Config loaded from %s", cfg.Location())) } + if cfg == nil { + cfg = config.FromDefault() + } + cfg.UpdateFromEnv() + if cfg.Client.Valid() == nil || cfg.Server.Valid() == nil { + return cfg, nil + } return cfg, err } diff --git a/actions/serve.go b/actions/serve.go index f7c1ebb..cdb77ae 100644 --- a/actions/serve.go +++ b/actions/serve.go @@ -4,11 +4,15 @@ import ( "context" "crypto/tls" "crypto/x509" + "errors" "fmt" + "io/fs" "net" "net/http" + "net/url" "os" "os/signal" + "strings" "time" "gitea.benny.dog/torjus/ezshare/certs" @@ -37,21 +41,9 @@ func ActionServe(c *cli.Context) error { binsLogger := logger.Named("BINS") // Read certificates - srvCertBytes, err := cfg.Server.GRPC.Certs.GetCertBytes() + certificates, err := getCerts(c, serverLogger) if err != nil { - return err - } - srvKeyBytes, err := cfg.Server.GRPC.Certs.GetKeyBytes() - if err != nil { - return err - } - caCertBytes, err := cfg.Server.GRPC.CACerts.GetCertBytes() - if err != nil { - return err - } - caKeyBytes, err := cfg.Server.GRPC.CACerts.GetKeyBytes() - if err != nil { - return err + return cli.Exit(fmt.Sprintf("Error getting certificates: %s", err), 1) } // Setup file store @@ -74,7 +66,7 @@ func ActionServe(c *cli.Context) error { } // Setup cert-service - certSvc, err := certs.NewCertService(dataStore, caCertBytes, caKeyBytes) + certSvc, err := certs.NewCertService(dataStore, certificates.caCert, certificates.caCertKey) if err != nil { return fmt.Errorf("error initializing certificate service: %w", err) } @@ -122,13 +114,13 @@ func ActionServe(c *cli.Context) error { serverLogger.Errorw("Unable to setup GRPC listener.", "error", err) rootCancel() } - srvCert, err := tls.X509KeyPair(srvCertBytes, srvKeyBytes) + srvCert, err := tls.X509KeyPair(certificates.serverCert, certificates.serverKey) if err != nil { serverLogger.Errorw("Unable to load server certs.", "error", err) rootCancel() } certPool := x509.NewCertPool() - if !certPool.AppendCertsFromPEM(caCertBytes) { + if !certPool.AppendCertsFromPEM(certificates.caCert) { serverLogger.Errorw("Unable to load CA certs.") rootCancel() } @@ -176,7 +168,7 @@ func ActionServe(c *cli.Context) error { if c.IsSet("http-addr") { httpAddr = c.String("http-addr") } - httpServer := server.NewHTTPSever(s, dataStore, srvCertBytes, cfg.Server.GRPCEndpoint) + httpServer := server.NewHTTPSever(s, dataStore, certificates.serverCert, cfg.Server.GRPCEndpoint) httpServer.Logger = httpLogger httpServer.Addr = httpAddr @@ -235,3 +227,140 @@ func initializeUsers(us store.UserStore, logger *zap.SugaredLogger) error { return nil } + +type certBytes struct { + caCert []byte + caCertKey []byte + serverCert []byte + serverKey []byte +} + +func getCerts(c *cli.Context, logger *zap.SugaredLogger) (*certBytes, error) { + cfg, err := getConfig(c) + if err != nil { + return nil, err + } + + cb := &certBytes{} + + caCertBytes, caCertErr := cfg.Server.GRPC.CACerts.GetCertBytes() + caKeyBytes, caKeyErr := cfg.Server.GRPC.CACerts.GetKeyBytes() + if caCertErr != nil || caKeyErr != nil { + if errors.Is(caCertErr, fs.ErrNotExist) && errors.Is(caKeyErr, fs.ErrNotExist) { + // Neither cert or key found, generate + logger.Warn("Certificates not found. Generating.") + priv, pub, err := certs.GenCACert() + if err != nil { + return nil, err + } + cb.caCert = pub + cb.caCertKey = priv + + // Since we remade ca certs, any existing server certs are useless + parsedUrl, err := url.Parse(cfg.Server.Hostname) + if err != nil { + return nil, fmt.Errorf("unable to parse hostname: %w", err) + } + host := parsedUrl.Host + if strings.Contains(host, ":") { + host, _, err = net.SplitHostPort(host) + if err != nil { + return nil, fmt.Errorf("unable to parse hostname: %w", err) + } + } + if err != nil { + return nil, fmt.Errorf("unable to generate certs due to unknown hostname") + } + priv, pub, err = certs.GenCert(host, cb.caCert, cb.caCertKey, []string{host}) + if err != nil { + return nil, fmt.Errorf("error creating server cert: %s", err) + } + pub, err = certs.ToPEM(pub, "CERTIFICATE") + if err != nil { + return nil, fmt.Errorf("error encoding server cert: %s", err) + } + priv, err = certs.ToPEM(priv, "EC PRIVATE KEY") + if err != nil { + return nil, fmt.Errorf("error encoding server cert: %s", err) + } + cb.serverCert = pub + cb.serverKey = priv + cb.caCert, err = certs.ToPEM(cb.caCert, "CERTIFICATE") + if err != nil { + return nil, fmt.Errorf("error encoding server cert: %s", err) + } + cb.caCertKey, err = certs.ToPEM(cb.caCertKey, "EC PRIVATE KEY") + if err != nil { + return nil, fmt.Errorf("error encoding server cert: %s", err) + } + + // Write them to files + if cfg.Server.GRPC.CACerts.CertificatePath != "" { + f, err := os.Create(cfg.Server.GRPC.CACerts.CertificatePath) + if err != nil { + return nil, fmt.Errorf("error writing certificate: %w", err) + } + defer f.Close() + if _, err := f.Write(cb.caCert); err != nil { + return nil, fmt.Errorf("error writing certificate: %w", err) + } + logger.Infow("Wrote CACert.", "path", cfg.Server.GRPC.CACerts.CertificatePath) + } + if cfg.Server.GRPC.CACerts.CertificateKeyPath != "" { + f, err := os.Create(cfg.Server.GRPC.CACerts.CertificateKeyPath) + if err != nil { + return nil, fmt.Errorf("error writing certificate: %w", err) + } + defer f.Close() + if _, err := f.Write(cb.caCertKey); err != nil { + return nil, fmt.Errorf("error writing certificate: %w", err) + } + logger.Infow("Wrote CACert key.", "path", cfg.Server.GRPC.CACerts.CertificateKeyPath) + } + if cfg.Server.GRPC.Certs.CertificateKeyPath != "" { + f, err := os.Create(cfg.Server.GRPC.Certs.CertificateKeyPath) + if err != nil { + return nil, fmt.Errorf("error writing certificate: %w", err) + } + defer f.Close() + if _, err := f.Write(cb.serverKey); err != nil { + return nil, fmt.Errorf("error writing certificate: %w", err) + } + logger.Infow("Wrote server cert key.", "path", cfg.Server.GRPC.Certs.CertificateKeyPath) + } + if cfg.Server.GRPC.Certs.CertificatePath != "" { + f, err := os.Create(cfg.Server.GRPC.Certs.CertificatePath) + if err != nil { + return nil, fmt.Errorf("error writing certificate: %w", err) + } + defer f.Close() + if _, err := f.Write(cb.serverCert); err != nil { + return nil, fmt.Errorf("error writing certificate: %w", err) + } + logger.Infow("Wrote server cert key.", "path", cfg.Server.GRPC.Certs.CertificatePath) + } + + return cb, nil + } else { + if caCertErr != nil { + return nil, caCertErr + } + return nil, caKeyErr + } + } + + srvCertBytes, err := cfg.Server.GRPC.Certs.GetCertBytes() + if err != nil { + return nil, err + } + srvKeyBytes, err := cfg.Server.GRPC.Certs.GetKeyBytes() + if err != nil { + return nil, err + } + + return &certBytes{ + caCert: caCertBytes, + caCertKey: caKeyBytes, + serverCert: srvCertBytes, + serverKey: srvKeyBytes}, nil +} diff --git a/config/config.go b/config/config.go index d21f00c..acb393c 100644 --- a/config/config.go +++ b/config/config.go @@ -149,7 +149,7 @@ func FromDefaultLocations() (*Config, error) { return nil, fmt.Errorf("config not found") } -func (c *Config) UpdateFromEnv() error { +func (c *Config) UpdateFromEnv() { // Server stuff if val, found := os.LookupEnv("EZSHARE_SERVER_LOGLEVEL"); found { c.Server.LogLevel = val @@ -207,8 +207,6 @@ func (c *Config) UpdateFromEnv() error { if val, found := os.LookupEnv("EZSHARE_CLIENT_SERVERCERTPATH"); found { c.Client.ServerCertPath = val } - - return nil } func (sc *ServerConfig) Valid() error { diff --git a/config/config_test.go b/config/config_test.go index b53040e..7f9b0c1 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -219,9 +219,7 @@ func TestConfig(t *testing.T) { t.Errorf("Loglevel is WARN before updating from env.") } os.Setenv("EZSHARE_SERVER_LOGLEVEL", "WARN") - if err := cfg.UpdateFromEnv(); err != nil { - t.Fatalf("Error updating config from environment: %s", err) - } + cfg.UpdateFromEnv() if cfg.Server.LogLevel != "WARN" { t.Errorf("Loglevel is not WARN after updating from env.") } @@ -229,9 +227,7 @@ func TestConfig(t *testing.T) { // Test Server.Hostname hostname := "https://share.example.org" os.Setenv("EZSHARE_SERVER_HOSTNAME", hostname) - if err := cfg.UpdateFromEnv(); err != nil { - t.Fatalf("Error updating config from environment: %s", err) - } + cfg.UpdateFromEnv() if cfg.Server.Hostname != hostname { t.Errorf("Hostname is incorrect after updating from env.") } @@ -239,9 +235,7 @@ func TestConfig(t *testing.T) { // Test Server.Datastore.Bolt.Path boltPath := "/data/bolt.db" os.Setenv("EZSHARE_SERVER_DATASTORE_BOLT_PATH", boltPath) - if err := cfg.UpdateFromEnv(); err != nil { - t.Fatalf("Error updating config from environment: %s", err) - } + cfg.UpdateFromEnv() if cfg.Server.DataStoreConfig.BoltStoreConfig.Path != boltPath { t.Errorf("Bolt path is incorrect after updating from env.") } @@ -249,9 +243,7 @@ func TestConfig(t *testing.T) { // Test Server.Datastore.Bolt.Path caCertPath := "/data/cert.pem" os.Setenv("EZSHARE_SERVER_GRPC_CACERTS_CERTIFICATEKEYPATH", caCertPath) - if err := cfg.UpdateFromEnv(); err != nil { - t.Fatalf("Error updating config from environment: %s", err) - } + cfg.UpdateFromEnv() if cfg.Server.GRPC.CACerts.CertificateKeyPath != caCertPath { t.Errorf("GPRC CA Cert path is incorrect after updating from env.") } diff --git a/ezshare.minimal.env b/ezshare.minimal.env new file mode 100644 index 0000000..22e4b80 --- /dev/null +++ b/ezshare.minimal.env @@ -0,0 +1,10 @@ +EZSHARE_SERVER_HOSTNAME=https://server.example.org +EZSHARE_SERVER_GRPCENDPOINT=server.example.org:50051 +EZSHARE_SERVER_DATASTORE_TYPE=bolt +EZSHARE_SERVER_DATASTORE_BOLT_PATH=/data/ds.db +EZSHARE_SERVER_FILESTORE_TYPE=bolt +EZSHARE_SERVER_FILESTORE_BOLT_PATH=/data/fs.db +EZSHARE_SERVER_GRPC_CACERTS_CERTIFICATEPATH=/data/ca.pem +EZSHARE_SERVER_GRPC_CACERTS_CERTIFICATEKEYPATH=/data/ca.key +EZSHARE_SERVER_GRPC_CERTS_CERTIFICATEPATH=/data/server.pem +EZSHARE_SERVER_GRPC_CERTS_CERTIFICATEKEYPATH=/data/server.key \ No newline at end of file