Merge pull request 'feature/auth' (#6) from feature/auth into master

Reviewed-on: https://gitea.benny.dog/torjus/ezshare/pulls/6
This commit is contained in:
Torjus Håkestad 2021-12-06 05:10:13 +00:00
commit 156ff4e207
25 changed files with 2404 additions and 657 deletions

340
actions/client.go Normal file
View File

@ -0,0 +1,340 @@
package actions
import (
"bufio"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"syscall"
"time"
"gitea.benny.dog/torjus/ezshare/certs"
"gitea.benny.dog/torjus/ezshare/config"
"gitea.benny.dog/torjus/ezshare/pb"
"gitea.benny.dog/torjus/ezshare/server"
"github.com/urfave/cli/v2"
"golang.org/x/term"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
func ActionClientGet(c *cli.Context) error {
addr := c.String("addr")
conn, err := grpc.DialContext(c.Context, addr, grpc.WithInsecure())
if err != nil {
return err
}
defer conn.Close()
client := pb.NewFileServiceClient(conn)
for _, arg := range c.Args().Slice() {
req := &pb.GetFileRequest{Id: arg}
resp, err := client.GetFile(c.Context, req)
if err != nil {
return err
}
filename := resp.File.FileId
if resp.File.Metadata.OriginalFilename != "" {
filename = resp.File.Metadata.OriginalFilename
}
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
if _, err := f.Write(resp.File.Data); err != nil {
return err
}
fmt.Printf("Wrote file '%s'\n", filename)
}
return nil
}
func ActionClientUpload(c *cli.Context) error {
cfg, err := getConfig(c)
if err != nil {
return err
}
addr := cfg.Client.DefaultServer
if c.IsSet("addr") {
addr = c.String("addr")
}
clientCreds, err := cfg.Client.Creds()
if err != nil {
return err
}
conn, err := grpc.DialContext(c.Context, addr, grpc.WithTransportCredentials(clientCreds))
if err != nil {
return err
}
defer conn.Close()
client := pb.NewFileServiceClient(conn)
for _, arg := range c.Args().Slice() {
f, err := os.Open(arg)
if err != nil {
return err
}
data, err := io.ReadAll(f)
if err != nil {
return err
}
req := &pb.UploadFileRequest{Data: data, OriginalFilename: filepath.Base(arg)}
resp, err := client.UploadFile(c.Context, req)
if err != nil {
return err
}
fmt.Printf("%s uploaded with id %s. Available at %s\n", arg, resp.Id, resp.FileUrl)
}
return nil
}
func ActionClientList(c *cli.Context) error {
cfg, err := getConfig(c)
if err != nil {
return err
}
addr := cfg.Client.DefaultServer
if c.IsSet("addr") {
addr = c.String("addr")
}
clientCreds, err := cfg.Client.Creds()
if err != nil {
return err
}
conn, err := grpc.DialContext(c.Context, addr, grpc.WithTransportCredentials(clientCreds))
if err != nil {
return err
}
defer conn.Close()
client := pb.NewFileServiceClient(conn)
resp, err := client.ListFiles(c.Context, &pb.ListFilesRequest{})
if err != nil {
return err
}
for _, elem := range resp.Files {
fmt.Println(elem.FileId)
}
return nil
}
func ActionClientDelete(c *cli.Context) error {
cfg, err := getConfig(c)
if err != nil {
return err
}
addr := cfg.Client.DefaultServer
if c.IsSet("addr") {
addr = c.String("addr")
}
clientCreds, err := cfg.Client.Creds()
if err != nil {
return err
}
conn, err := grpc.DialContext(c.Context, addr, grpc.WithTransportCredentials(clientCreds))
if err != nil {
return err
}
defer conn.Close()
client := pb.NewFileServiceClient(conn)
for _, arg := range c.Args().Slice() {
_, err := client.DeleteFile(c.Context, &pb.DeleteFileRequest{Id: arg})
if err != nil {
return fmt.Errorf("error deleting file: %w", err)
}
fmt.Printf("Deleted file %s\n", arg)
}
return nil
}
func ActionClientLogin(c *cli.Context) error {
if err := config.CreateDefaultConfigDir(); err != nil {
return err
}
configFilePath, err := config.DefaultConfigFilePath()
if err != nil {
return err
}
if _, err := os.Stat(configFilePath); !errors.Is(err, os.ErrNotExist) {
if err == nil {
if !c.Bool("overwrite") {
return cli.Exit("Config already exists. To overwrite run with --overwrite\n", 1)
}
} else {
return err
}
}
configDirPath := filepath.Dir(configFilePath)
clientCertPath := filepath.Join(configDirPath, "client.pem")
clientKeyPath := filepath.Join(configDirPath, "client.key")
serverCertPath := filepath.Join(configDirPath, "server.pem")
if c.Args().Len() != 1 {
return cli.Exit("Need 1 argument", 1)
}
// Fetch server certificate
ctx, cancel := context.WithTimeout(c.Context, 10*time.Second)
defer cancel()
certEndpoint := fmt.Sprintf("%s/%s", c.Args().First(), "server.pem")
certReq, err := http.NewRequestWithContext(ctx, http.MethodGet, certEndpoint, nil)
if err != nil {
return cli.Exit(fmt.Sprintf("unable to create http request: %s", err), 1)
}
verbosePrint(c, fmt.Sprintf("fetching cert from %s", certEndpoint))
certResp, err := http.DefaultClient.Do(certReq)
if err != nil {
return cli.Exit(fmt.Sprintf("error fetching server cert: %s", err), 1)
}
defer certResp.Body.Close()
serverCert, err := io.ReadAll(certResp.Body)
if err != nil {
return cli.Exit(fmt.Sprintf("error reading certificate from server: %s", err), 1)
}
// Fetch metadata
mdEndpoint := fmt.Sprintf("%s/%s", c.Args().First(), "metadata")
mdReq, err := http.NewRequestWithContext(ctx, http.MethodGet, mdEndpoint, nil)
if err != nil {
return cli.Exit(fmt.Sprintf("unable to create http request: %s", err), 1)
}
verbosePrint(c, fmt.Sprintf("fetching metadata from %s", mdEndpoint))
mdResp, err := http.DefaultClient.Do(mdReq)
if err != nil {
return cli.Exit(fmt.Sprintf("error fetching server cert: %s", err), 1)
}
defer mdResp.Body.Close()
decoder := json.NewDecoder(mdResp.Body)
var md server.MetadataResponse
if err := decoder.Decode(&md); err != nil {
return cli.Exit(fmt.Sprintf("unable to decode metadata response: %s", err), 1)
}
scanner := bufio.NewScanner(os.Stdin)
fmt.Printf("username: ")
scanner.Scan()
username := scanner.Text()
fmt.Printf("password: ")
passwordBytes, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return cli.Exit(fmt.Sprintf("unable to read password: %s", err), 1)
}
password := string(passwordBytes)
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(serverCert) {
return cli.Exit(fmt.Sprintf("unable to use server certificate: %s", err), 1)
}
// Generate temporary self-signed cert
keyBytes, certBytes, err := certs.GenCACert()
if err != nil {
return cli.Exit(fmt.Sprintf("unable to generate self-signed certificate: %s", err), 1)
}
keyPem, err := certs.ToPEM(keyBytes, "EC PRIVATE KEY")
if err != nil {
return cli.Exit(fmt.Sprintf("unable to pem-encode key: %s", err), 1)
}
certPem, err := certs.ToPEM(certBytes, "CERTIFICATE")
if err != nil {
return cli.Exit(fmt.Sprintf("unable to pem-encode key: %s", err), 1)
}
clientCert, err := tls.X509KeyPair(certPem, keyPem)
if err != nil {
return cli.Exit(fmt.Sprintf("unable to use self-signed certificate: %s", err), 1)
}
creds := credentials.NewTLS(&tls.Config{RootCAs: certPool, Certificates: []tls.Certificate{clientCert}})
verbosePrint(c, fmt.Sprintf("dialing grpc at %s", md.GRPCEndpoint))
conn, err := grpc.DialContext(c.Context, md.GRPCEndpoint, grpc.WithTransportCredentials(creds))
if err != nil {
return err
}
defer conn.Close()
client := pb.NewUserServiceClient(conn)
resp, err := client.Login(c.Context, &pb.LoginUserRequest{Username: username, Password: password})
if err != nil {
return err
}
// Write key to file
verbosePrint(c, fmt.Sprintf("Writing client certificate key to %s", clientKeyPath))
keyFile, err := os.Create(clientKeyPath)
if err != nil {
return cli.Exit(fmt.Sprintf("unable create file for client key: %s", err), 1)
}
defer keyFile.Close()
if _, err := keyFile.Write(resp.ClientKey); err != nil {
return cli.Exit(fmt.Sprintf("unable write client key to file: %s", err), 1)
}
// Write client cert to file
verbosePrint(c, fmt.Sprintf("Writing client certificate to %s", clientKeyPath))
clientCertFile, err := os.Create(clientCertPath)
if err != nil {
return cli.Exit(fmt.Sprintf("unable create file for client cert: %s", err), 1)
}
defer clientCertFile.Close()
if _, err := clientCertFile.Write(resp.ClientCert); err != nil {
return cli.Exit(fmt.Sprintf("unable write client cert to file: %s", err), 1)
}
// Write server cer to file
verbosePrint(c, fmt.Sprintf("Writing server certificate to %s", serverCertPath))
serverCertFile, err := os.Create(serverCertPath)
if err != nil {
return cli.Exit(fmt.Sprintf("unable create file for client key: %s", err), 1)
}
defer serverCertFile.Close()
if _, err := serverCertFile.Write(serverCert); err != nil {
return cli.Exit(fmt.Sprintf("unable write client cert to file: %s", err), 1)
}
// Write config
cfg := config.FromDefault()
cfg.Client.Certs.CertificateKeyPath = clientKeyPath
cfg.Client.Certs.CertificatePath = clientCertPath
cfg.Client.DefaultServer = md.GRPCEndpoint
cfg.Client.ServerCertPath = serverCertPath
if err := cfg.ToDefaultFile(); err != nil {
return cli.Exit(fmt.Sprintf("unable write config to file: %s", err), 1)
}
return nil
}

44
actions/misc.go Normal file
View File

@ -0,0 +1,44 @@
package actions
import (
"fmt"
"gitea.benny.dog/torjus/ezshare/certs"
"gitea.benny.dog/torjus/ezshare/config"
"github.com/urfave/cli/v2"
)
func ActionGencerts(c *cli.Context) error {
outDir := "."
if c.IsSet("out-dir") {
outDir = c.String("out-dir")
}
if !c.IsSet("hostname") {
return fmt.Errorf("--hostname required")
}
hostname := c.String("hostname")
return certs.GenAllCerts(outDir, hostname)
}
func ActionInitConfig(c *cli.Context) error {
defaultCfg := config.FromDefault()
return defaultCfg.ToDefaultFile()
}
func getConfig(c *cli.Context) (*config.Config, error) {
if c.IsSet("config") {
cfgPath := c.String("config")
return config.FromFile(cfgPath)
}
cfg, err := config.FromDefaultLocations()
if err == nil {
verbosePrint(c, fmt.Sprintf("Config loaded from %s", cfg.Location()))
}
return cfg, err
}
func verbosePrint(c *cli.Context, message string) {
if c.Bool("verbose") {
fmt.Println(message)
}
}

222
actions/serve.go Normal file
View File

@ -0,0 +1,222 @@
package actions
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"time"
"gitea.benny.dog/torjus/ezshare/certs"
"gitea.benny.dog/torjus/ezshare/pb"
"gitea.benny.dog/torjus/ezshare/server"
"gitea.benny.dog/torjus/ezshare/server/interceptors"
"gitea.benny.dog/torjus/ezshare/store"
"github.com/google/uuid"
"github.com/urfave/cli/v2"
"golang.org/x/crypto/bcrypt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
func ActionServe(c *cli.Context) error {
cfg, err := getConfig(c)
if err != nil {
return err
}
// Read certificates
srvCertBytes, err := cfg.Server.GRPC.Certs.GetCertBytes()
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
}
// Setup file store
s, fileCloseFunc, err := cfg.Server.FileStoreConfig.GetStore()
if err != nil {
return fmt.Errorf("unable to initialize file store: %w", err)
}
defer fileCloseFunc()
// Setup user store
userStore, userCloseFunc, err := cfg.Server.UserStoreConfig.GetStore()
if err != nil {
return fmt.Errorf("unable to initialize user store: %w", err)
}
defer userCloseFunc()
// Create initial admin-user if neccessary
if err := initializeUsers(userStore); err != nil {
return fmt.Errorf("error initializing store: %w", err)
}
// Setup cert store
// TODO: User proper store
certStore := store.NewMemoryStore()
// Setup cert-service
certSvc, err := certs.NewCertService(certStore, caCertBytes, caKeyBytes)
if err != nil {
return fmt.Errorf("error initializing certificate service: %w", err)
}
// Setup shutdown-handling
rootCtx, rootCancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer rootCancel()
// Used to initiate grpc shutdown
grpcCtx, grpcCancel := context.WithCancel(rootCtx)
defer grpcCancel()
// Cancelled once grpc is successfully shut down
grpcShutdownCtx, grpcShutdownCancel := context.WithCancel(context.Background())
defer grpcShutdownCancel()
// Start grpc server
go func() {
grpcAddr := cfg.Server.GRPC.ListenAddr
if c.IsSet("grpc-addr") {
grpcAddr = c.String("grpc-addr")
}
// Setup file-service
grpcFileServer := server.NewGRPCFileServiceServer(s)
grpcFileServer.Hostname = cfg.Server.Hostname
if c.IsSet("hostname") {
grpcFileServer.Hostname = c.String("hostname")
}
// Setup user-service
grpcUserServer := server.NewGRPCUserServiceServer(userStore, certSvc)
lis, err := net.Listen("tcp", grpcAddr)
if err != nil {
log.Printf("Unable to setup grpc listener: %s\n", err)
rootCancel()
}
srvCert, err := tls.X509KeyPair(srvCertBytes, srvKeyBytes)
if err != nil {
log.Printf("Unable load server certs: %s\n", err)
rootCancel()
}
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(caCertBytes) {
log.Println("Unable to load CA cert")
rootCancel()
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{srvCert},
ClientAuth: tls.RequireAnyClientCert,
ClientCAs: certPool,
}
creds := credentials.NewTLS(tlsConfig)
grpcServer := grpc.NewServer(
grpc.Creds(creds),
grpc.ChainUnaryInterceptor(interceptors.NewAuthInterceptor(&store.MemoryStore{})),
)
pb.RegisterFileServiceServer(grpcServer, grpcFileServer)
pb.RegisterUserServiceServer(grpcServer, grpcUserServer)
// wait for cancel
go func() {
<-grpcCtx.Done()
grpcServer.GracefulStop()
}()
log.Printf("Starting grpc server")
if err = grpcServer.Serve(lis); err != nil {
log.Printf("GRPC Shutdown with error: %s\n", err)
rootCancel()
}
log.Println("GRPC Shutdown")
grpcShutdownCancel()
}()
httpCtx, httpCancel := context.WithCancel(rootCtx)
defer httpCancel()
httpShutdownCtx, httpShutdownCancel := context.WithCancel(context.Background())
defer httpShutdownCancel()
// Start http server
go func() {
httpAddr := ":8088"
if c.IsSet("http-addr") {
httpAddr = c.String("http-addr")
}
httpServer := server.NewHTTPSever(s, srvCertBytes, cfg.Server.GRPCEndpoint)
httpServer.Addr = httpAddr
// wait for cancel
go func() {
<-httpCtx.Done()
timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
httpServer.Shutdown(timeoutCtx)
}()
log.Printf("Starting http server")
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("HTTP Server shutdown with error: %s\n", err)
rootCancel()
}
log.Println("HTTP Shutdown")
httpShutdownCancel()
}()
<-grpcShutdownCtx.Done()
<-httpShutdownCtx.Done()
return nil
}
func initializeUsers(us store.UserStore) error {
// TODO: Logging
userIDs, err := us.ListUsers()
if err != nil {
return err
}
if len(userIDs) != 0 {
return nil
}
// no users, create initial admin-user
log.Printf("No users in store. Creating admin-user.")
password := uuid.Must(uuid.NewRandom()).String()
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
admin := &pb.User{
Id: uuid.Must(uuid.NewRandom()).String(),
HashedPassword: hashedPassword,
Username: "admin",
UserRole: pb.User_ADMIN,
Active: true,
}
if err := us.StoreUser(admin); err != nil {
return err
}
log.Printf("user created %s:%s", admin.Username, password)
return nil
}

View File

@ -15,6 +15,19 @@ import (
"time" "time"
) )
func ToPEM(data []byte, pemType string) ([]byte, error) {
pemData := new(bytes.Buffer)
err := pem.Encode(pemData, &pem.Block{
Type: pemType,
Bytes: data,
})
if err != nil {
return nil, err
}
return pemData.Bytes(), nil
}
func WriteCert(data []byte, filename string) error { func WriteCert(data []byte, filename string) error {
// Convert to PEM // Convert to PEM
certPEM := new(bytes.Buffer) certPEM := new(bytes.Buffer)
@ -104,7 +117,7 @@ func GenAllCerts(path, domain string) error {
// Create server certs // Create server certs
dnsNames := []string{domain} dnsNames := []string{domain}
srvKey, srvCrt, err := GenCert(caPub, caPriv, dnsNames) srvKey, srvCrt, err := GenCert("server", caPub, caPriv, dnsNames)
if err != nil { if err != nil {
return err return err
} }
@ -115,7 +128,7 @@ func GenAllCerts(path, domain string) error {
return err return err
} }
clientKey, clientCrt, err := GenCert(caPub, caPriv, []string{}) clientKey, clientCrt, err := GenCert("client", caPub, caPriv, []string{})
if err != nil { if err != nil {
return err return err
} }
@ -129,7 +142,7 @@ func GenAllCerts(path, domain string) error {
return nil return nil
} }
func GenCert(caPub, caPrivKey []byte, dnsNames []string) (priv, pub []byte, err error) { func GenCert(cn string, caPub, caPrivKey []byte, dnsNames []string) (priv, pub []byte, err error) {
// Parse ca // Parse ca
ca, err := x509.ParseCertificate(caPub) ca, err := x509.ParseCertificate(caPub)
if err != nil { if err != nil {
@ -144,6 +157,7 @@ func GenCert(caPub, caPrivKey []byte, dnsNames []string) (priv, pub []byte, err
cert := &x509.Certificate{ cert := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().Unix()), SerialNumber: big.NewInt(time.Now().Unix()),
Subject: pkix.Name{ Subject: pkix.Name{
CommonName: cn,
Organization: []string{"ezshare"}, Organization: []string{"ezshare"},
Country: []string{"No"}, Country: []string{"No"},
Locality: []string{"Oslo"}, Locality: []string{"Oslo"},

View File

@ -1,452 +0,0 @@
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"time"
"gitea.benny.dog/torjus/ezshare/certs"
"gitea.benny.dog/torjus/ezshare/config"
"gitea.benny.dog/torjus/ezshare/pb"
"gitea.benny.dog/torjus/ezshare/server"
"github.com/urfave/cli/v2"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
func main() {
app := cli.App{
Name: "ezshare",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
Usage: "Path to config-file.",
},
&cli.BoolFlag{
Name: "verbose",
Aliases: []string{"v"},
Usage: "Be more verbose",
},
},
Commands: []*cli.Command{
{
Name: "serve",
Usage: "Start ezshare server",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "no-grpc",
Usage: "Do not enable grpc.",
},
&cli.BoolFlag{
Name: "no-http",
Usage: "Do not enable http.",
},
&cli.StringFlag{
Name: "grpc-addr",
Usage: "Address to listen for grpc.",
},
&cli.StringFlag{
Name: "http-addr",
Usage: "Address to listen for http.",
},
&cli.StringFlag{
Name: "hostname",
Usage: "Hostname used in links",
},
},
Action: ActionServe,
},
{
Name: "client",
Usage: "Client commands",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "addr",
Usage: "Address of server.",
},
},
Subcommands: []*cli.Command{
{
Name: "get",
Usage: "Get file with id",
ArgsUsage: "ID [ID]..",
Action: ActionClientGet,
},
{
Name: "upload",
Usage: "Upload file(s)",
ArgsUsage: "PATH [PATH]..",
Action: ActionClientUpload,
},
{
Name: "delete",
Usage: "Delete file with id",
ArgsUsage: "ID [ID]..",
Action: ActionClientDelete,
},
{
Name: "list",
Usage: "List files",
Action: ActionClientList,
},
{
Name: "config-init",
Usage: "Initialize default config",
Action: ActionInitConfig,
},
},
},
{
Name: "cert",
Usage: "Certificate commands",
Subcommands: []*cli.Command{
{
Name: "gen-all",
Usage: "Generate CA, Server and Client certificates",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "out-dir",
Usage: "Directory where certificates will be stored.",
},
&cli.StringFlag{
Name: "hostname",
Usage: "Hostname used for server certificate.",
},
},
Action: ActionGencerts,
},
},
},
},
}
err := app.Run(os.Args)
if err != nil {
log.Printf("Error: %s\n", err)
}
}
func ActionServe(c *cli.Context) error {
cfg, err := getConfig(c)
if err != nil {
return err
}
// Read certificates
srvCertBytes, err := cfg.Server.GRPC.Certs.GetCertBytes()
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
}
// Setup store
s, closeFunc, err := cfg.Server.StoreConfig.GetStore()
if err != nil {
return fmt.Errorf("unable to initialize store: %w", err)
}
defer closeFunc()
// Setup shutdown-handling
rootCtx, rootCancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer rootCancel()
// Used to initiate grpc shutdown
grpcCtx, grpcCancel := context.WithCancel(rootCtx)
defer grpcCancel()
// Cancelled once grpc is successfully shut down
grpcShutdownCtx, grpcShutdownCancel := context.WithCancel(context.Background())
defer grpcShutdownCancel()
// Start grpc server
go func() {
grpcAddr := cfg.Server.GRPC.ListenAddr
if c.IsSet("grpc-addr") {
grpcAddr = c.String("grpc-addr")
}
grpcFileServer := server.NewGRPCFileServiceServer(s)
grpcFileServer.Hostname = cfg.Server.Hostname
if c.IsSet("hostname") {
grpcFileServer.Hostname = c.String("hostname")
}
lis, err := net.Listen("tcp", grpcAddr)
if err != nil {
log.Printf("Unable to setup grpc listener: %s\n", err)
rootCancel()
}
srvCert, err := tls.X509KeyPair(srvCertBytes, srvKeyBytes)
if err != nil {
log.Printf("Unable load server certs: %s\n", err)
rootCancel()
}
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(caCertBytes) {
log.Println("Unable to load CA cert")
rootCancel()
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{srvCert},
ClientAuth: tls.RequestClientCert,
ClientCAs: certPool,
}
creds := credentials.NewTLS(tlsConfig)
grpcServer := grpc.NewServer(
grpc.Creds(creds),
)
pb.RegisterFileServiceServer(grpcServer, grpcFileServer)
// wait for cancel
go func() {
<-grpcCtx.Done()
grpcServer.GracefulStop()
}()
log.Printf("Starting grpc server")
if err = grpcServer.Serve(lis); err != nil {
log.Printf("GRPC Shutdown with error: %s\n", err)
rootCancel()
}
log.Println("GRPC Shutdown")
grpcShutdownCancel()
}()
httpCtx, httpCancel := context.WithCancel(rootCtx)
defer httpCancel()
httpShutdownCtx, httpShutdownCancel := context.WithCancel(context.Background())
defer httpShutdownCancel()
// Start http server
go func() {
httpAddr := ":8088"
if c.IsSet("http-addr") {
httpAddr = c.String("http-addr")
}
httpServer := server.NewHTTPSever(s)
httpServer.Addr = httpAddr
// wait for cancel
go func() {
<-httpCtx.Done()
timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
httpServer.Shutdown(timeoutCtx)
}()
log.Printf("Starting http server")
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("HTTP Server shutdown with error: %s\n", err)
rootCancel()
}
log.Println("HTTP Shutdown")
httpShutdownCancel()
}()
<-grpcShutdownCtx.Done()
<-httpShutdownCtx.Done()
return nil
}
func ActionClientGet(c *cli.Context) error {
addr := c.String("addr")
conn, err := grpc.DialContext(c.Context, addr, grpc.WithInsecure())
if err != nil {
return err
}
defer conn.Close()
client := pb.NewFileServiceClient(conn)
for _, arg := range c.Args().Slice() {
req := &pb.GetFileRequest{Id: arg}
resp, err := client.GetFile(c.Context, req)
if err != nil {
return err
}
filename := resp.File.FileId
if resp.File.Metadata.OriginalFilename != "" {
filename = resp.File.Metadata.OriginalFilename
}
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
if _, err := f.Write(resp.File.Data); err != nil {
return err
}
fmt.Printf("Wrote file '%s'\n", filename)
}
return nil
}
func ActionClientUpload(c *cli.Context) error {
cfg, err := getConfig(c)
if err != nil {
return err
}
addr := cfg.Client.DefaultServer
if c.IsSet("addr") {
addr = c.String("addr")
}
clientCreds, err := cfg.Client.Creds()
if err != nil {
return err
}
conn, err := grpc.DialContext(c.Context, addr, grpc.WithTransportCredentials(clientCreds))
if err != nil {
return err
}
defer conn.Close()
client := pb.NewFileServiceClient(conn)
for _, arg := range c.Args().Slice() {
f, err := os.Open(arg)
if err != nil {
return err
}
data, err := io.ReadAll(f)
if err != nil {
return err
}
req := &pb.UploadFileRequest{Data: data, OriginalFilename: filepath.Base(arg)}
resp, err := client.UploadFile(c.Context, req)
if err != nil {
return err
}
fmt.Printf("%s uploaded with id %s. Available at %s\n", arg, resp.Id, resp.FileUrl)
}
return nil
}
func ActionClientList(c *cli.Context) error {
cfg, err := getConfig(c)
if err != nil {
return err
}
addr := cfg.Client.DefaultServer
if c.IsSet("addr") {
addr = c.String("addr")
}
clientCreds, err := cfg.Client.Creds()
if err != nil {
return err
}
conn, err := grpc.DialContext(c.Context, addr, grpc.WithTransportCredentials(clientCreds))
if err != nil {
return err
}
defer conn.Close()
client := pb.NewFileServiceClient(conn)
resp, err := client.ListFiles(c.Context, &pb.ListFilesRequest{})
if err != nil {
return err
}
for _, elem := range resp.Files {
fmt.Println(elem.FileId)
}
return nil
}
func ActionClientDelete(c *cli.Context) error {
cfg, err := getConfig(c)
if err != nil {
return err
}
addr := cfg.Client.DefaultServer
if c.IsSet("addr") {
addr = c.String("addr")
}
clientCreds, err := cfg.Client.Creds()
if err != nil {
return err
}
conn, err := grpc.DialContext(c.Context, addr, grpc.WithTransportCredentials(clientCreds))
if err != nil {
return err
}
defer conn.Close()
client := pb.NewFileServiceClient(conn)
for _, arg := range c.Args().Slice() {
_, err := client.DeleteFile(c.Context, &pb.DeleteFileRequest{Id: arg})
if err != nil {
return fmt.Errorf("error deleting file: %w", err)
}
fmt.Printf("Deleted file %s\n", arg)
}
return nil
}
func ActionGencerts(c *cli.Context) error {
outDir := "."
if c.IsSet("out-dir") {
outDir = c.String("out-dir")
}
if !c.IsSet("hostname") {
return fmt.Errorf("--hostname required")
}
hostname := c.String("hostname")
return certs.GenAllCerts(outDir, hostname)
}
func ActionInitConfig(c *cli.Context) error {
defaultCfg := config.FromDefault()
return defaultCfg.ToDefaultFile()
}
func getConfig(c *cli.Context) (*config.Config, error) {
if c.IsSet("config") {
cfgPath := c.String("config")
return config.FromFile(cfgPath)
}
cfg, err := config.FromDefaultLocations()
if err == nil {
verbosePrint(c, fmt.Sprintf("Config loaded from %s", cfg.Location()))
}
return cfg, err
}
func verbosePrint(c *cli.Context, message string) {
if c.Bool("verbose") {
fmt.Println(message)
}
}

View File

@ -17,6 +17,8 @@ import (
"google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials"
) )
var ErrNotFound = fmt.Errorf("config not found")
type Config struct { type Config struct {
Server *ServerConfig `toml:"Server"` Server *ServerConfig `toml:"Server"`
Client *ClientConfig `toml:"Client"` Client *ClientConfig `toml:"Client"`
@ -28,19 +30,26 @@ type CertificatePaths struct {
} }
type ServerConfig struct { type ServerConfig struct {
LogLevel string `toml:"LogLevel"` LogLevel string `toml:"LogLevel"`
Hostname string `toml:"Hostname"` Hostname string `toml:"Hostname"`
StoreConfig *ServerStoreConfig `toml:"Store"` GRPCEndpoint string `toml:"GRPCEndpoint"`
GRPC *ServerGRPCConfig `toml:"GRPC"` UserStoreConfig *ServerUserStoreConfig `toml:"UserStore"`
HTTP *ServerHTTPConfig `toml:"HTTP"` FileStoreConfig *ServerFileStoreConfig `toml:"FileStore"`
GRPC *ServerGRPCConfig `toml:"GRPC"`
HTTP *ServerHTTPConfig `toml:"HTTP"`
} }
type ServerStoreConfig struct { type ServerFileStoreConfig struct {
Type string `toml:"Type"` Type string `toml:"Type"`
FSStoreConfig *FSStoreConfig `toml:"Filesystem"` FSStoreConfig *FSStoreConfig `toml:"Filesystem"`
BoltStoreConfig *BoltStoreConfig `toml:"Bolt"` BoltStoreConfig *BoltStoreConfig `toml:"Bolt"`
} }
type ServerUserStoreConfig struct {
Type string `toml:"Type"`
BoltStoreConfig *BoltStoreConfig `toml:"Bolt"`
}
type BoltStoreConfig struct { type BoltStoreConfig struct {
Path string `toml:"Path"` Path string `toml:"Path"`
} }
@ -78,7 +87,9 @@ func FromDefault() *Config {
ListenAddr: ":8089", ListenAddr: ":8089",
}, },
}, },
Client: &ClientConfig{}, Client: &ClientConfig{
Certs: &CertificatePaths{},
},
} }
return cfg return cfg
@ -97,6 +108,9 @@ func FromReader(r io.Reader) (*Config, error) {
func FromFile(path string) (*Config, error) { func FromFile(path string) (*Config, error) {
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("unable to open config-file: %w", err) return nil, fmt.Errorf("unable to open config-file: %w", err)
} }
defer f.Close() defer f.Close()
@ -188,7 +202,7 @@ func (cc *ClientConfig) Creds() (credentials.TransportCredentials, error) {
return credentials.NewTLS(config), nil return credentials.NewTLS(config), nil
} }
func (c *Config) ToDefaultFile() error { func CreateDefaultConfigDir() error {
userConfigDir, err := os.UserConfigDir() userConfigDir, err := os.UserConfigDir()
if err != nil { if err != nil {
return err return err
@ -207,8 +221,25 @@ func (c *Config) ToDefaultFile() error {
return fmt.Errorf("config-directory is not a directory") return fmt.Errorf("config-directory is not a directory")
} }
} }
return nil
}
func DefaultConfigFilePath() (string, error) {
userConfigDir, err := os.UserConfigDir()
if err != nil {
return "", err
}
return filepath.Join(userConfigDir, "ezshare", "ezshare.toml"), nil
configFilePath := filepath.Join(configDirPath, "ezshare.toml") }
func (c *Config) ToDefaultFile() error {
if err := CreateDefaultConfigDir(); err != nil {
return err
}
configFilePath, err := DefaultConfigFilePath()
if err != nil {
return err
}
_, err = os.Stat(configFilePath) _, err = os.Stat(configFilePath)
if err != nil { if err != nil {
if !errors.Is(err, fs.ErrNotExist) { if !errors.Is(err, fs.ErrNotExist) {
@ -226,7 +257,7 @@ func (c *Config) ToDefaultFile() error {
return fmt.Errorf("config-file already exists") return fmt.Errorf("config-file already exists")
} }
func (sc *ServerStoreConfig) GetStore() (store.FileStore, func() error, error) { func (sc *ServerFileStoreConfig) GetStore() (store.FileStore, func() error, error) {
nopCloseFunc := func() error { return nil } nopCloseFunc := func() error { return nil }
if strings.EqualFold(sc.Type, "bolt") { if strings.EqualFold(sc.Type, "bolt") {
s, err := store.NewBoltStore(sc.BoltStoreConfig.Path) s, err := store.NewBoltStore(sc.BoltStoreConfig.Path)
@ -243,6 +274,39 @@ func (sc *ServerStoreConfig) GetStore() (store.FileStore, func() error, error) {
if strings.EqualFold(sc.Type, "memory") { if strings.EqualFold(sc.Type, "memory") {
return store.NewMemoryStore(), nopCloseFunc, nil return store.NewMemoryStore(), nopCloseFunc, nil
} }
if strings.EqualFold(sc.Type, "bolt") {
s, err := store.NewBoltStore(sc.BoltStoreConfig.Path)
if err != nil {
return nil, nil, err
}
closeFunc := func() error { return s.Close() }
return s, closeFunc, nil
}
return nil, nil, fmt.Errorf("invalid store config")
}
func (sc *ServerUserStoreConfig) GetStore() (store.UserStore, func() error, error) {
nopCloseFunc := func() error { return nil }
if strings.EqualFold(sc.Type, "bolt") {
s, err := store.NewBoltStore(sc.BoltStoreConfig.Path)
if err != nil {
return nil, nil, err
}
return s, s.Close, err
}
if strings.EqualFold(sc.Type, "memory") {
return store.NewMemoryStore(), nopCloseFunc, nil
}
if strings.EqualFold(sc.Type, "bolt") {
s, err := store.NewBoltStore(sc.BoltStoreConfig.Path)
if err != nil {
return nil, nil, err
}
closeFunc := func() error { return s.Close() }
return s, closeFunc, nil
}
return nil, nil, fmt.Errorf("invalid store config") return nil, nil, fmt.Errorf("invalid store config")
} }

View File

@ -12,23 +12,38 @@ LogLevel = "INFO"
# Required # Required
Hostname = "localhost" Hostname = "localhost"
# Storage configuration # Endpoint reachable by clients
[Server.Store] # Fetched by clients for automatic setup
# Required
GRPCEndpoint = "localhost:50051"
# File store configuration
[Server.FileStore]
# How server stores file # How server stores file
# Must be one of: filesystem, memory, bolt # Must be one of: filesystem, memory, bolt
# Required # Required
Type = "filesystem" Type = "bolt"
[Server.Store.Bolt] [Server.FileStore.Bolt]
# Where the bolt-db is stored # Where the bolt-db is stored
# Required if store-type is bolt # Required if store-type is bolt
Path = "" Path = "/data/files.db"
[Server.Store.Filesystem] [Server.FileStore.Filesystem]
# Where files are stored # Where files are stored
# Required if store-type is filesystem # Required if store-type is filesystem
Dir = "/data" Dir = "/data"
[Server.UserStore]
# What store to use for users
# Must be one of: memory, bolt
# Required
Type = "bolt"
[Server.UserStore.Bolt]
# Path to bolt database-file
Path = "/data/users.db"
# GRPC Configuration # GRPC Configuration
[Server.GRPC] [Server.GRPC]
# Address to listen to # Address to listen to

6
go.mod
View File

@ -4,10 +4,13 @@ go 1.17
require ( require (
github.com/go-chi/chi/v5 v5.0.7 github.com/go-chi/chi/v5 v5.0.7
github.com/google/go-cmp v0.5.6
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/pelletier/go-toml v1.9.4 github.com/pelletier/go-toml v1.9.4
github.com/urfave/cli/v2 v2.3.0 github.com/urfave/cli/v2 v2.3.0
go.etcd.io/bbolt v1.3.6 go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
google.golang.org/grpc v1.42.0 google.golang.org/grpc v1.42.0
google.golang.org/protobuf v1.27.1 google.golang.org/protobuf v1.27.1
) )
@ -17,7 +20,8 @@ require (
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
golang.org/x/net v0.0.0-20211203184738-4852103109b8 // indirect golang.org/x/net v0.0.0-20211203184738-4852103109b8 // indirect
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect golang.org/x/sys v0.0.0-20211205182925-97ca703d548d // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9 // indirect google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9 // indirect
) )

13
go.sum
View File

@ -49,8 +49,9 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -75,6 +76,8 @@ go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e h1:MUP6MR3rJ7Gk9LEia0LP2ytiH6MuCfs7qYz+47jGdD8=
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@ -87,6 +90,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211203184738-4852103109b8 h1:PFkPt/jI9Del3hmFplBtRp8tDhSRpFu7CyRs7VmEC0M= golang.org/x/net v0.0.0-20211203184738-4852103109b8 h1:PFkPt/jI9Del3hmFplBtRp8tDhSRpFu7CyRs7VmEC0M=
golang.org/x/net v0.0.0-20211203184738-4852103109b8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211203184738-4852103109b8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -104,9 +108,12 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

142
main.go Normal file
View File

@ -0,0 +1,142 @@
package main
import (
"log"
"os"
"gitea.benny.dog/torjus/ezshare/actions"
"github.com/urfave/cli/v2"
)
const Version = "v0.1.1"
func main() {
cli.VersionFlag = &cli.BoolFlag{Name: "version"}
app := cli.App{
Name: "ezshare",
Version: Version,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
Usage: "Path to config-file.",
},
&cli.BoolFlag{
Name: "verbose",
Aliases: []string{"v"},
Usage: "Be more verbose",
},
},
Commands: []*cli.Command{
{
Name: "serve",
Usage: "Start ezshare server",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "no-grpc",
Usage: "Do not enable grpc.",
},
&cli.BoolFlag{
Name: "no-http",
Usage: "Do not enable http.",
},
&cli.StringFlag{
Name: "grpc-addr",
Usage: "Address to listen for grpc.",
},
&cli.StringFlag{
Name: "http-addr",
Usage: "Address to listen for http.",
},
&cli.StringFlag{
Name: "hostname",
Usage: "Hostname used in links",
},
},
Action: actions.ActionServe,
},
{
Name: "client",
Usage: "Client commands",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "addr",
Usage: "Address of server.",
},
},
Subcommands: []*cli.Command{
{
Name: "get",
Usage: "Get file with id",
ArgsUsage: "ID [ID]..",
Action: actions.ActionClientGet,
},
{
Name: "upload",
Usage: "Upload file(s)",
ArgsUsage: "PATH [PATH]..",
Action: actions.ActionClientUpload,
},
{
Name: "delete",
Usage: "Delete file with id",
ArgsUsage: "ID [ID]..",
Action: actions.ActionClientDelete,
},
{
Name: "list",
Usage: "List files",
Action: actions.ActionClientList,
},
{
Name: "login",
Usage: "Login and retrieve client cert",
ArgsUsage: "[PROTO://]HOSTNAME[:PORT]",
Action: actions.ActionClientLogin,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Usage: "Username used for login.",
},
&cli.BoolFlag{
Name: "overwrite",
Usage: "Overwrite existing config",
},
},
},
{
Name: "config-init",
Usage: "Initialize default config",
Action: actions.ActionInitConfig,
},
},
},
{
Name: "cert",
Usage: "Certificate commands",
Subcommands: []*cli.Command{
{
Name: "gen-all",
Usage: "Generate CA, Server and Client certificates",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "out-dir",
Usage: "Directory where certificates will be stored.",
},
&cli.StringFlag{
Name: "hostname",
Usage: "Hostname used for server certificate.",
},
},
Action: actions.ActionGencerts,
},
},
},
},
}
err := app.Run(os.Args)
if err != nil {
log.Printf("Error: %s\n", err)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -207,3 +207,197 @@ var FileService_ServiceDesc = grpc.ServiceDesc{
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "protos/ezshare.proto", Metadata: "protos/ezshare.proto",
} }
// UserServiceClient is the client API for UserService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type UserServiceClient interface {
Register(ctx context.Context, in *RegisterUserRequest, opts ...grpc.CallOption) (*RegisterUserResponse, error)
Login(ctx context.Context, in *LoginUserRequest, opts ...grpc.CallOption) (*LoginUserResponse, error)
List(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error)
Approve(ctx context.Context, in *ApproveUserRequest, opts ...grpc.CallOption) (*Empty, error)
}
type userServiceClient struct {
cc grpc.ClientConnInterface
}
func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient {
return &userServiceClient{cc}
}
func (c *userServiceClient) Register(ctx context.Context, in *RegisterUserRequest, opts ...grpc.CallOption) (*RegisterUserResponse, error) {
out := new(RegisterUserResponse)
err := c.cc.Invoke(ctx, "/ezshare.UserService/Register", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) Login(ctx context.Context, in *LoginUserRequest, opts ...grpc.CallOption) (*LoginUserResponse, error) {
out := new(LoginUserResponse)
err := c.cc.Invoke(ctx, "/ezshare.UserService/Login", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) List(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) {
out := new(ListUsersResponse)
err := c.cc.Invoke(ctx, "/ezshare.UserService/List", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) Approve(ctx context.Context, in *ApproveUserRequest, opts ...grpc.CallOption) (*Empty, error) {
out := new(Empty)
err := c.cc.Invoke(ctx, "/ezshare.UserService/Approve", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// UserServiceServer is the server API for UserService service.
// All implementations must embed UnimplementedUserServiceServer
// for forward compatibility
type UserServiceServer interface {
Register(context.Context, *RegisterUserRequest) (*RegisterUserResponse, error)
Login(context.Context, *LoginUserRequest) (*LoginUserResponse, error)
List(context.Context, *ListUsersRequest) (*ListUsersResponse, error)
Approve(context.Context, *ApproveUserRequest) (*Empty, error)
mustEmbedUnimplementedUserServiceServer()
}
// UnimplementedUserServiceServer must be embedded to have forward compatible implementations.
type UnimplementedUserServiceServer struct {
}
func (UnimplementedUserServiceServer) Register(context.Context, *RegisterUserRequest) (*RegisterUserResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Register not implemented")
}
func (UnimplementedUserServiceServer) Login(context.Context, *LoginUserRequest) (*LoginUserResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Login not implemented")
}
func (UnimplementedUserServiceServer) List(context.Context, *ListUsersRequest) (*ListUsersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
}
func (UnimplementedUserServiceServer) Approve(context.Context, *ApproveUserRequest) (*Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Approve not implemented")
}
func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {}
// UnsafeUserServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to UserServiceServer will
// result in compilation errors.
type UnsafeUserServiceServer interface {
mustEmbedUnimplementedUserServiceServer()
}
func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) {
s.RegisterService(&UserService_ServiceDesc, srv)
}
func _UserService_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RegisterUserRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).Register(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/ezshare.UserService/Register",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).Register(ctx, req.(*RegisterUserRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(LoginUserRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).Login(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/ezshare.UserService/Login",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).Login(ctx, req.(*LoginUserRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListUsersRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).List(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/ezshare.UserService/List",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).List(ctx, req.(*ListUsersRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_Approve_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ApproveUserRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).Approve(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/ezshare.UserService/Approve",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).Approve(ctx, req.(*ApproveUserRequest))
}
return interceptor(ctx, in, info, handler)
}
// UserService_ServiceDesc is the grpc.ServiceDesc for UserService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var UserService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "ezshare.UserService",
HandlerType: (*UserServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Register",
Handler: _UserService_Register_Handler,
},
{
MethodName: "Login",
Handler: _UserService_Login_Handler,
},
{
MethodName: "List",
Handler: _UserService_List_Handler,
},
{
MethodName: "Approve",
Handler: _UserService_Approve_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "protos/ezshare.proto",
}

View File

@ -1,11 +1,18 @@
syntax = "proto3"; syntax = "proto3";
option go_package = "gitea.benny.dog/torjus/ezshare/pb";
package ezshare; package ezshare;
import "google/protobuf/timestamp.proto"; import "google/protobuf/timestamp.proto";
option go_package = "gitea.benny.dog/torjus/ezshare/pb";
/////////////////////
// Common messages //
/////////////////////
message Empty {}
////////////////////////
// FILE RELATED STUFF //
////////////////////////
message File { message File {
string file_id = 1; string file_id = 1;
bytes data = 2; bytes data = 2;
@ -62,4 +69,61 @@ service FileService {
rpc GetFile(GetFileRequest) returns (GetFileResponse) {} rpc GetFile(GetFileRequest) returns (GetFileResponse) {}
rpc DeleteFile(DeleteFileRequest) returns (DeleteFileResponse) {} rpc DeleteFile(DeleteFileRequest) returns (DeleteFileResponse) {}
rpc ListFiles(ListFilesRequest) returns (ListFilesResponse) {} rpc ListFiles(ListFilesRequest) returns (ListFilesResponse) {}
} }
////////////////////////
// USER RELATED STUFF //
////////////////////////
message User {
string id = 1;
string username = 2;
bytes hashed_password = 3;
enum Role {
UNKNOWN = 0;
VIEWONLY = 1;
USER = 2;
ADMIN = 3;
UNAPPROVED = 4;
}
Role user_role = 4;
bool active = 5;
}
// Register
message RegisterUserRequest {
string username = 1;
string password = 2;
}
message RegisterUserResponse {
string id = 1;
string token = 2;
}
// Login
message LoginUserRequest {
string username = 1;
string password = 2;
}
message LoginUserResponse {
bytes client_cert = 1;
bytes client_key = 2;
}
// List
message ListUsersRequest {
}
message ListUsersResponse {
repeated User users = 1;
}
// Approve
message ApproveUserRequest {
string user_id = 1;
}
service UserService {
rpc Register(RegisterUserRequest) returns (RegisterUserResponse) {}
rpc Login(LoginUserRequest) returns (LoginUserResponse) {}
rpc List(ListUsersRequest) returns (ListUsersResponse) {}
rpc Approve(ApproveUserRequest) returns (Empty) {}
}

View File

@ -10,28 +10,49 @@ import (
) )
type HTTPServer struct { type HTTPServer struct {
store store.FileStore store store.FileStore
serverGRPCCert []byte
grpcEndpoint string
http.Server http.Server
} }
func NewHTTPSever(store store.FileStore) *HTTPServer { type MetadataResponse struct {
GRPCEndpoint string `json:"grpc_endpoint"`
}
func NewHTTPSever(store store.FileStore, certBytes []byte, grpcEndpoint string) *HTTPServer {
srv := &HTTPServer{ srv := &HTTPServer{
store: store, store: store,
serverGRPCCert: certBytes,
grpcEndpoint: grpcEndpoint,
} }
r := chi.NewRouter() r := chi.NewRouter()
r.Get("/server.pem", srv.ServerCertHandler)
r.Get("/metadata", srv.MetadataHandler)
r.Get("/files/{id}", srv.FileHandler) r.Get("/files/{id}", srv.FileHandler)
srv.Handler = r srv.Handler = r
return srv return srv
} }
func (s *HTTPServer) ServerCertHandler(w http.ResponseWriter, r *http.Request) {
w.Write(s.serverGRPCCert)
}
func (s *HTTPServer) MetadataHandler(w http.ResponseWriter, r *http.Request) {
md := &MetadataResponse{
GRPCEndpoint: s.grpcEndpoint,
}
encoder := json.NewEncoder(w)
encoder.Encode(md)
}
func (s *HTTPServer) FileHandler(w http.ResponseWriter, r *http.Request) { func (s *HTTPServer) FileHandler(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id") id := chi.URLParam(r, "id")
f, err := s.store.GetFile(id) f, err := s.store.GetFile(id)
if err != nil { if err != nil {
if err == store.ErrNoSuchFile { if err == store.ErrNoSuchItem {
WriteErrorResponse(w, http.StatusNotFound, "file not found") WriteErrorResponse(w, http.StatusNotFound, "file not found")
return return
} }

View File

@ -0,0 +1,55 @@
package interceptors
import (
"context"
"fmt"
"gitea.benny.dog/torjus/ezshare/pb"
"gitea.benny.dog/torjus/ezshare/store"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/peer"
)
type ContextKey string
var ContextKeyRole ContextKey = "role"
func NewAuthInterceptor(s store.UserStore) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
p, ok := peer.FromContext(ctx)
if ok {
tlsInfo, ok := p.AuthInfo.(credentials.TLSInfo)
if ok {
fmt.Printf("%+v\n", tlsInfo.State.PeerCertificates[0].Subject.CommonName)
if len(tlsInfo.State.PeerCertificates) == 1 {
cert := tlsInfo.State.PeerCertificates[0]
id := cert.Subject.CommonName
user, err := s.GetUser(id)
if err == nil {
newCtx := context.WithValue(ctx, ContextKeyRole, user.UserRole)
return handler(newCtx, req)
}
}
}
}
newCtx := context.WithValue(ctx, ContextKeyRole, pb.User_UNKNOWN)
return handler(newCtx, req)
}
}
func RoleFromContext(ctx context.Context) pb.User_Role {
value := ctx.Value(ContextKeyRole)
if value == nil {
return pb.User_UNKNOWN
}
role, ok := value.(pb.User_Role)
if ok {
return role
}
return pb.User_UNKNOWN
}

86
server/userservice.go Normal file
View File

@ -0,0 +1,86 @@
package server
import (
"context"
"fmt"
"gitea.benny.dog/torjus/ezshare/certs"
"gitea.benny.dog/torjus/ezshare/pb"
"gitea.benny.dog/torjus/ezshare/store"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type GRPCUserServiceServer struct {
store store.UserStore
certService *certs.CertService
pb.UnimplementedUserServiceServer
}
func NewGRPCUserServiceServer(store store.UserStore, certSvc *certs.CertService) *GRPCUserServiceServer {
return &GRPCUserServiceServer{store: store, certService: certSvc}
}
func (s *GRPCUserServiceServer) Register(ctx context.Context, req *pb.RegisterUserRequest) (*pb.RegisterUserResponse, error) {
// Check if user already exists
if _, err := s.store.GetUserByUsername(req.Username); err != store.ErrNoSuchItem {
return nil, status.Error(codes.AlreadyExists, "user already exists")
}
pw, err := hashPassword(req.Password)
if err != nil {
return nil, fmt.Errorf("unable to hash password: %w", err)
}
user := &pb.User{
Id: uuid.Must(uuid.NewRandom()).String(),
Username: req.Username,
HashedPassword: pw,
UserRole: pb.User_USER,
Active: true,
}
if err := s.store.StoreUser(user); err != nil {
return nil, status.Error(codes.Internal, fmt.Sprintf("unable to store user: %s", err))
}
return &pb.RegisterUserResponse{Id: user.Id, Token: ""}, nil
}
func (s *GRPCUserServiceServer) Login(_ context.Context, req *pb.LoginUserRequest) (*pb.LoginUserResponse, error) {
user, err := s.store.GetUserByUsername(req.Username)
if err != nil {
if err == store.ErrNoSuchItem {
return nil, status.Error(codes.NotFound, "no such user")
}
return nil, status.Error(codes.Internal, "error getting user from store")
}
if err := bcrypt.CompareHashAndPassword(user.HashedPassword, []byte(req.Password)); err != nil {
return nil, status.Error(codes.Unauthenticated, "wrong username and or password")
}
cert, key, err := s.certService.NewClient(user.Id)
if err != nil {
return nil, status.Error(codes.Internal, "unable to generate client certificate")
}
resp := &pb.LoginUserResponse{
ClientCert: cert,
ClientKey: key,
}
return resp, nil
}
func (s *GRPCUserServiceServer) List(_ context.Context, _ *pb.ListUsersRequest) (*pb.ListUsersResponse, error) {
return nil, status.Error(codes.Unimplemented, "not yet implemented")
}
func (s *GRPCUserServiceServer) Approve(_ context.Context, _ *pb.ApproveUserRequest) (*pb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "not yet implemented")
}
func hashPassword(password string) ([]byte, error) {
return bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
}

View File

@ -20,6 +20,7 @@ type BoltStore struct {
var bktKey = []byte("files") var bktKey = []byte("files")
var bktKeyCerts = []byte("certs") var bktKeyCerts = []byte("certs")
var bktKeyKeys = []byte("keys") var bktKeyKeys = []byte("keys")
var bktKeyUsers = []byte("users")
func NewBoltStore(path string) (*BoltStore, error) { func NewBoltStore(path string) (*BoltStore, error) {
s := &BoltStore{} s := &BoltStore{}
@ -38,7 +39,9 @@ func NewBoltStore(path string) (*BoltStore, error) {
if _, err := t.CreateBucketIfNotExists(bktKeyKeys); err != nil { if _, err := t.CreateBucketIfNotExists(bktKeyKeys); err != nil {
return err return err
} }
if _, err := t.CreateBucketIfNotExists(bktKeyUsers); err != nil {
return err
}
return nil return nil
}) })
if err != nil { if err != nil {
@ -62,7 +65,7 @@ func (s *BoltStore) GetFile(id string) (*pb.File, error) {
return nil, fmt.Errorf("error getting file: %w", err) return nil, fmt.Errorf("error getting file: %w", err)
} }
if data == nil { if data == nil {
return nil, ErrNoSuchFile return nil, ErrNoSuchItem
} }
err := proto.Unmarshal(data, &file) err := proto.Unmarshal(data, &file)
@ -95,7 +98,7 @@ func (s *BoltStore) DeleteFile(id string) error {
bkt := t.Bucket(bktKey) bkt := t.Bucket(bktKey)
data := bkt.Get([]byte(id)) data := bkt.Get([]byte(id))
if data == nil { if data == nil {
return ErrNoSuchFile return ErrNoSuchItem
} }
return bkt.Delete([]byte(id)) return bkt.Delete([]byte(id))
}) })
@ -137,7 +140,7 @@ func (s *BoltStore) GetCertificate(id string) (*x509.Certificate, error) {
return nil, err return nil, err
} }
if raw == nil { if raw == nil {
return nil, ErrNoSuchFile return nil, ErrNoSuchItem
} }
cert, err := x509.ParseCertificate(raw) cert, err := x509.ParseCertificate(raw)
@ -188,14 +191,83 @@ func (s *BoltStore) ListCertificates() ([]string, error) {
var ids []string var ids []string
err := s.db.View(func(tx *bolt.Tx) error { err := s.db.View(func(tx *bolt.Tx) error {
bkt := tx.Bucket(bktKeyCerts) bkt := tx.Bucket(bktKeyCerts)
bkt.ForEach(func(k, v []byte) error { return bkt.ForEach(func(k, v []byte) error {
ids = append(ids, string(k)) ids = append(ids, string(k))
return nil return nil
}) })
return nil
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
return ids, nil return ids, nil
} }
var _ UserStore = &BoltStore{}
func (s *BoltStore) StoreUser(user *pb.User) error {
return s.db.Update(func(tx *bolt.Tx) error {
bkt := tx.Bucket(bktKeyUsers)
data, err := proto.Marshal(user)
if err != nil {
return err
}
return bkt.Put([]byte(user.Id), data)
})
}
func (s *BoltStore) GetUser(id string) (*pb.User, error) {
var data []byte
err := s.db.View(func(tx *bolt.Tx) error {
bkt := tx.Bucket(bktKeyUsers)
data = bkt.Get([]byte(id))
return nil
})
if err != nil {
return nil, err
}
var user pb.User
err = proto.Unmarshal(data, &user)
return &user, err
}
func (s *BoltStore) ListUsers() ([]string, error) {
var ids []string
err := s.db.View(func(tx *bolt.Tx) error {
bkt := tx.Bucket(bktKeyUsers)
return bkt.ForEach(func(k, _ []byte) error {
ids = append(ids, string(k))
return nil
})
})
return ids, err
}
func (s *BoltStore) GetUserByUsername(username string) (*pb.User, error) {
var user pb.User
err := s.db.View(func(tx *bolt.Tx) error {
bkt := tx.Bucket(bktKeyUsers)
c := bkt.Cursor()
for k, v := c.First(); k != nil; c.Next() {
err := proto.Unmarshal(v, &user)
if err != nil {
// TODO: Log that db has invalid user
continue
}
if user.Username == username {
return nil
}
}
return nil
})
if err != nil {
return nil, err
}
if user.Username == username {
return &user, nil
}
return nil, ErrNoSuchItem
}

View File

@ -1,3 +1,4 @@
//nolint:staticcheck
package store_test package store_test
import ( import (
@ -10,19 +11,29 @@ import (
func TestBoltStore(t *testing.T) { func TestBoltStore(t *testing.T) {
path := filepath.Join(t.TempDir(), "boltstore.db") path := filepath.Join(t.TempDir(), "boltstore.db")
s, err := store.NewBoltStore(path) s, err := store.NewBoltStore(path)
defer s.Close()
if err != nil { if err != nil {
t.Fatalf("Error opening store: %s", err) t.Fatalf("Error opening store: %s", err)
} }
doFileStoreTest(s, t) doFileStoreTest(s, t)
_ = s.Close()
} }
func TestBoltCertificateStore(t *testing.T) { func TestBoltCertificateStore(t *testing.T) {
path := filepath.Join(t.TempDir(), "boltstore.db") path := filepath.Join(t.TempDir(), "boltstore.db")
s, err := store.NewBoltStore(path) s, err := store.NewBoltStore(path)
defer s.Close()
if err != nil { if err != nil {
t.Fatalf("Error opening store: %s", err) t.Fatalf("Error opening store: %s", err)
} }
doCertificateStoreTest(s, t) doCertificateStoreTest(s, t)
_ = s.Close() }
func TestBoltUserStore(t *testing.T) {
path := filepath.Join(t.TempDir(), "boltstore.db")
s, err := store.NewBoltStore(path)
defer s.Close()
if err != nil {
t.Fatalf("Error opening store: %s", err)
}
doUserStoreTests(s, t)
} }

View File

@ -27,7 +27,7 @@ func (s *FileSystemStore) GetFile(id string) (*pb.File, error) {
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {
if os.IsNotExist(os.ErrNotExist) { if os.IsNotExist(os.ErrNotExist) {
return nil, ErrNoSuchFile return nil, ErrNoSuchItem
} }
return nil, fmt.Errorf("unable to open file: %w", err) return nil, fmt.Errorf("unable to open file: %w", err)
} }

View File

@ -9,9 +9,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
var _ FileStore = &MemoryStore{}
var _ CertificateStore = &MemoryStore{}
type MemoryStore struct { type MemoryStore struct {
filesLock sync.RWMutex filesLock sync.RWMutex
files map[string]*pb.File files map[string]*pb.File
@ -19,6 +16,8 @@ type MemoryStore struct {
certs map[string][]byte certs map[string][]byte
keyLock sync.RWMutex keyLock sync.RWMutex
keys map[string][]byte keys map[string][]byte
usersLock sync.RWMutex
users map[string]*pb.User
} }
func NewMemoryStore() *MemoryStore { func NewMemoryStore() *MemoryStore {
@ -26,9 +25,16 @@ func NewMemoryStore() *MemoryStore {
files: make(map[string]*pb.File), files: make(map[string]*pb.File),
certs: make(map[string][]byte), certs: make(map[string][]byte),
keys: make(map[string][]byte), keys: make(map[string][]byte),
users: make(map[string]*pb.User),
} }
} }
///////////////
// FileStore //
///////////////
var _ FileStore = &MemoryStore{}
func (s *MemoryStore) GetFile(id string) (*pb.File, error) { func (s *MemoryStore) GetFile(id string) (*pb.File, error) {
s.filesLock.RLock() s.filesLock.RLock()
defer s.filesLock.RUnlock() defer s.filesLock.RUnlock()
@ -37,7 +43,7 @@ func (s *MemoryStore) GetFile(id string) (*pb.File, error) {
return file, nil return file, nil
} }
return nil, ErrNoSuchFile return nil, ErrNoSuchItem
} }
func (s *MemoryStore) StoreFile(file *pb.File) (string, error) { func (s *MemoryStore) StoreFile(file *pb.File) (string, error) {
@ -56,7 +62,7 @@ func (s *MemoryStore) DeleteFile(id string) error {
s.filesLock.Lock() s.filesLock.Lock()
defer s.filesLock.Unlock() defer s.filesLock.Unlock()
if _, ok := s.files[id]; !ok { if _, ok := s.files[id]; !ok {
return ErrNoSuchFile return ErrNoSuchItem
} }
delete(s.files, id) delete(s.files, id)
@ -76,6 +82,12 @@ func (s *MemoryStore) ListFiles() ([]*pb.ListFilesResponse_ListFileInfo, error)
return response, nil return response, nil
} }
//////////////////////
// CertificateStore //
//////////////////////
var _ CertificateStore = &MemoryStore{}
func (s *MemoryStore) GetCertificate(id string) (*x509.Certificate, error) { func (s *MemoryStore) GetCertificate(id string) (*x509.Certificate, error) {
s.certLock.Lock() s.certLock.Lock()
defer s.certLock.Unlock() defer s.certLock.Unlock()
@ -83,7 +95,7 @@ func (s *MemoryStore) GetCertificate(id string) (*x509.Certificate, error) {
data, ok := s.certs[id] data, ok := s.certs[id]
if !ok { if !ok {
// TODO: Make separate error, or rename error // TODO: Make separate error, or rename error
return nil, ErrNoSuchFile return nil, ErrNoSuchItem
} }
return x509.ParseCertificate(data) return x509.ParseCertificate(data)
@ -106,7 +118,7 @@ func (s *MemoryStore) GetKey(id string) (*ecdsa.PrivateKey, error) {
defer s.keyLock.RUnlock() defer s.keyLock.RUnlock()
data, ok := s.keys[id] data, ok := s.keys[id]
if !ok { if !ok {
return nil, ErrNoSuchFile return nil, ErrNoSuchItem
} }
return x509.ParseECPrivateKey(data) return x509.ParseECPrivateKey(data)
@ -134,3 +146,50 @@ func (s *MemoryStore) ListCertificates() ([]string, error) {
} }
return certIDs, nil return certIDs, nil
} }
///////////////
// UserStore //
///////////////
var _ UserStore = &MemoryStore{}
func (s *MemoryStore) StoreUser(user *pb.User) error {
s.usersLock.Lock()
defer s.usersLock.Unlock()
s.users[user.Id] = user
return nil
}
func (s *MemoryStore) GetUser(id string) (*pb.User, error) {
s.usersLock.RLock()
defer s.usersLock.RUnlock()
user, ok := s.users[id]
if !ok {
// TODO: Update error
return nil, ErrNoSuchItem
}
return user, nil
}
func (s *MemoryStore) ListUsers() ([]string, error) {
s.usersLock.RLock()
defer s.usersLock.RUnlock()
var ids []string
for id := range s.users {
ids = append(ids, id)
}
return ids, nil
}
func (s *MemoryStore) GetUserByUsername(username string) (*pb.User, error) {
s.usersLock.RLock()
defer s.usersLock.RUnlock()
for _, user := range s.users {
if user.Username == username {
return user, nil
}
}
return nil, ErrNoSuchItem
}

View File

@ -14,3 +14,8 @@ func TestMemoryCertificateStore(t *testing.T) {
s := store.NewMemoryStore() s := store.NewMemoryStore()
doCertificateStoreTest(s, t) doCertificateStoreTest(s, t)
} }
func TestMemoryUserStore(t *testing.T) {
s := store.NewMemoryStore()
doUserStoreTests(s, t)
}

View File

@ -8,7 +8,7 @@ import (
"gitea.benny.dog/torjus/ezshare/pb" "gitea.benny.dog/torjus/ezshare/pb"
) )
var ErrNoSuchFile = fmt.Errorf("no such file") var ErrNoSuchItem = fmt.Errorf("no such item")
type FileStore interface { type FileStore interface {
GetFile(id string) (*pb.File, error) GetFile(id string) (*pb.File, error)
@ -24,3 +24,10 @@ type CertificateStore interface {
StoreKey(id string, key *ecdsa.PrivateKey) error StoreKey(id string, key *ecdsa.PrivateKey) error
ListCertificates() ([]string, error) ListCertificates() ([]string, error)
} }
type UserStore interface {
StoreUser(user *pb.User) error
GetUser(id string) (*pb.User, error)
GetUserByUsername(username string) (*pb.User, error)
ListUsers() ([]string, error)
}

View File

@ -10,6 +10,10 @@ import (
"testing" "testing"
"time" "time"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"google.golang.org/protobuf/testing/protocmp"
"gitea.benny.dog/torjus/ezshare/pb" "gitea.benny.dog/torjus/ezshare/pb"
"gitea.benny.dog/torjus/ezshare/store" "gitea.benny.dog/torjus/ezshare/store"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
@ -64,7 +68,7 @@ func doFileStoreTest(s store.FileStore, t *testing.T) {
t.Fatalf("Unable to delete file: %s", err) t.Fatalf("Unable to delete file: %s", err)
} }
if _, err := s.GetFile(id); err != store.ErrNoSuchFile { if _, err := s.GetFile(id); err != store.ErrNoSuchItem {
t.Fatalf("Getting deleted file returned wrong error: %s", err) t.Fatalf("Getting deleted file returned wrong error: %s", err)
} }
}) })
@ -140,3 +144,48 @@ func doCertificateStoreTest(s store.CertificateStore, t *testing.T) {
} }
}) })
} }
func doUserStoreTests(s store.UserStore, t *testing.T) {
t.Run("Basics", func(t *testing.T) {
// Store user
user := &pb.User{
Id: uuid.Must(uuid.NewRandom()).String(),
Username: "testuser",
UserRole: pb.User_USER,
Active: true,
}
if err := s.StoreUser(user); err != nil {
t.Fatalf("Error storing user: %s", err)
}
retrieved, err := s.GetUser(user.Id)
if err != nil {
t.Fatalf("Retriving user returned error: %s", err)
}
if diff := cmp.Diff(user, retrieved, protocmp.Transform()); diff != "" {
t.Errorf("User retrieved by name difference:\n%v", diff)
}
named, err := s.GetUserByUsername(user.Username)
if err != nil {
t.Fatalf("Retrieving user by username returned error: %s", err)
}
if diff := cmp.Diff(user, named, protocmp.Transform()); diff != "" {
t.Errorf("User retrieved by name difference:\n%v", diff)
}
list, err := s.ListUsers()
if err != nil {
t.Fatalf("Error listing users")
}
if len(list) != 1 {
t.Fatalf("User list wrong length")
}
if list[0] != user.Id {
t.Fatalf("User list has wrong id")
}
})
}

View File

@ -1,3 +0,0 @@
package ezshare
const Version = "v0.1.1"