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:
commit
156ff4e207
340
actions/client.go
Normal file
340
actions/client.go
Normal 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
44
actions/misc.go
Normal 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
222
actions/serve.go
Normal 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
|
||||
}
|
@ -15,6 +15,19 @@ import (
|
||||
"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 {
|
||||
// Convert to PEM
|
||||
certPEM := new(bytes.Buffer)
|
||||
@ -104,7 +117,7 @@ func GenAllCerts(path, domain string) error {
|
||||
|
||||
// Create server certs
|
||||
dnsNames := []string{domain}
|
||||
srvKey, srvCrt, err := GenCert(caPub, caPriv, dnsNames)
|
||||
srvKey, srvCrt, err := GenCert("server", caPub, caPriv, dnsNames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -115,7 +128,7 @@ func GenAllCerts(path, domain string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
clientKey, clientCrt, err := GenCert(caPub, caPriv, []string{})
|
||||
clientKey, clientCrt, err := GenCert("client", caPub, caPriv, []string{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -129,7 +142,7 @@ func GenAllCerts(path, domain string) error {
|
||||
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
|
||||
ca, err := x509.ParseCertificate(caPub)
|
||||
if err != nil {
|
||||
@ -144,6 +157,7 @@ func GenCert(caPub, caPrivKey []byte, dnsNames []string) (priv, pub []byte, err
|
||||
cert := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(time.Now().Unix()),
|
||||
Subject: pkix.Name{
|
||||
CommonName: cn,
|
||||
Organization: []string{"ezshare"},
|
||||
Country: []string{"No"},
|
||||
Locality: []string{"Oslo"},
|
||||
|
452
cmd/ezshare.go
452
cmd/ezshare.go
@ -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)
|
||||
}
|
||||
}
|
@ -17,6 +17,8 @@ import (
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
var ErrNotFound = fmt.Errorf("config not found")
|
||||
|
||||
type Config struct {
|
||||
Server *ServerConfig `toml:"Server"`
|
||||
Client *ClientConfig `toml:"Client"`
|
||||
@ -28,19 +30,26 @@ type CertificatePaths struct {
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
LogLevel string `toml:"LogLevel"`
|
||||
Hostname string `toml:"Hostname"`
|
||||
StoreConfig *ServerStoreConfig `toml:"Store"`
|
||||
GRPC *ServerGRPCConfig `toml:"GRPC"`
|
||||
HTTP *ServerHTTPConfig `toml:"HTTP"`
|
||||
LogLevel string `toml:"LogLevel"`
|
||||
Hostname string `toml:"Hostname"`
|
||||
GRPCEndpoint string `toml:"GRPCEndpoint"`
|
||||
UserStoreConfig *ServerUserStoreConfig `toml:"UserStore"`
|
||||
FileStoreConfig *ServerFileStoreConfig `toml:"FileStore"`
|
||||
GRPC *ServerGRPCConfig `toml:"GRPC"`
|
||||
HTTP *ServerHTTPConfig `toml:"HTTP"`
|
||||
}
|
||||
|
||||
type ServerStoreConfig struct {
|
||||
type ServerFileStoreConfig struct {
|
||||
Type string `toml:"Type"`
|
||||
FSStoreConfig *FSStoreConfig `toml:"Filesystem"`
|
||||
BoltStoreConfig *BoltStoreConfig `toml:"Bolt"`
|
||||
}
|
||||
|
||||
type ServerUserStoreConfig struct {
|
||||
Type string `toml:"Type"`
|
||||
BoltStoreConfig *BoltStoreConfig `toml:"Bolt"`
|
||||
}
|
||||
|
||||
type BoltStoreConfig struct {
|
||||
Path string `toml:"Path"`
|
||||
}
|
||||
@ -78,7 +87,9 @@ func FromDefault() *Config {
|
||||
ListenAddr: ":8089",
|
||||
},
|
||||
},
|
||||
Client: &ClientConfig{},
|
||||
Client: &ClientConfig{
|
||||
Certs: &CertificatePaths{},
|
||||
},
|
||||
}
|
||||
|
||||
return cfg
|
||||
@ -97,6 +108,9 @@ func FromReader(r io.Reader) (*Config, error) {
|
||||
func FromFile(path string) (*Config, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("unable to open config-file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
@ -188,7 +202,7 @@ func (cc *ClientConfig) Creds() (credentials.TransportCredentials, error) {
|
||||
return credentials.NewTLS(config), nil
|
||||
}
|
||||
|
||||
func (c *Config) ToDefaultFile() error {
|
||||
func CreateDefaultConfigDir() error {
|
||||
userConfigDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -207,8 +221,25 @@ func (c *Config) ToDefaultFile() error {
|
||||
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)
|
||||
if err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
@ -226,7 +257,7 @@ func (c *Config) ToDefaultFile() error {
|
||||
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 }
|
||||
if strings.EqualFold(sc.Type, "bolt") {
|
||||
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") {
|
||||
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")
|
||||
}
|
||||
|
@ -12,23 +12,38 @@ LogLevel = "INFO"
|
||||
# Required
|
||||
Hostname = "localhost"
|
||||
|
||||
# Storage configuration
|
||||
[Server.Store]
|
||||
# Endpoint reachable by clients
|
||||
# Fetched by clients for automatic setup
|
||||
# Required
|
||||
GRPCEndpoint = "localhost:50051"
|
||||
|
||||
# File store configuration
|
||||
[Server.FileStore]
|
||||
# How server stores file
|
||||
# Must be one of: filesystem, memory, bolt
|
||||
# Required
|
||||
Type = "filesystem"
|
||||
Type = "bolt"
|
||||
|
||||
[Server.Store.Bolt]
|
||||
[Server.FileStore.Bolt]
|
||||
# Where the bolt-db is stored
|
||||
# Required if store-type is bolt
|
||||
Path = ""
|
||||
Path = "/data/files.db"
|
||||
|
||||
[Server.Store.Filesystem]
|
||||
[Server.FileStore.Filesystem]
|
||||
# Where files are stored
|
||||
# Required if store-type is filesystem
|
||||
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
|
||||
[Server.GRPC]
|
||||
# Address to listen to
|
||||
|
6
go.mod
6
go.mod
@ -4,10 +4,13 @@ go 1.17
|
||||
|
||||
require (
|
||||
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/pelletier/go-toml v1.9.4
|
||||
github.com/urfave/cli/v2 v2.3.0
|
||||
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/protobuf v1.27.1
|
||||
)
|
||||
@ -17,7 +20,8 @@ require (
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // 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/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9 // indirect
|
||||
)
|
||||
|
13
go.sum
13
go.sum
@ -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.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.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.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.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
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=
|
||||
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-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/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=
|
||||
@ -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-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-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/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
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-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-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc=
|
||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
142
main.go
Normal file
142
main.go
Normal 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)
|
||||
}
|
||||
|
||||
}
|
1033
pb/ezshare.pb.go
1033
pb/ezshare.pb.go
File diff suppressed because it is too large
Load Diff
@ -207,3 +207,197 @@ var FileService_ServiceDesc = grpc.ServiceDesc{
|
||||
Streams: []grpc.StreamDesc{},
|
||||
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",
|
||||
}
|
||||
|
@ -1,11 +1,18 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "gitea.benny.dog/torjus/ezshare/pb";
|
||||
package ezshare;
|
||||
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
option go_package = "gitea.benny.dog/torjus/ezshare/pb";
|
||||
|
||||
/////////////////////
|
||||
// Common messages //
|
||||
/////////////////////
|
||||
message Empty {}
|
||||
|
||||
////////////////////////
|
||||
// FILE RELATED STUFF //
|
||||
////////////////////////
|
||||
message File {
|
||||
string file_id = 1;
|
||||
bytes data = 2;
|
||||
@ -63,3 +70,60 @@ service FileService {
|
||||
rpc DeleteFile(DeleteFileRequest) returns (DeleteFileResponse) {}
|
||||
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) {}
|
||||
}
|
||||
|
@ -10,28 +10,49 @@ import (
|
||||
)
|
||||
|
||||
type HTTPServer struct {
|
||||
store store.FileStore
|
||||
store store.FileStore
|
||||
serverGRPCCert []byte
|
||||
grpcEndpoint string
|
||||
|
||||
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{
|
||||
store: store,
|
||||
store: store,
|
||||
serverGRPCCert: certBytes,
|
||||
grpcEndpoint: grpcEndpoint,
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Get("/server.pem", srv.ServerCertHandler)
|
||||
r.Get("/metadata", srv.MetadataHandler)
|
||||
r.Get("/files/{id}", srv.FileHandler)
|
||||
|
||||
srv.Handler = r
|
||||
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) {
|
||||
id := chi.URLParam(r, "id")
|
||||
f, err := s.store.GetFile(id)
|
||||
if err != nil {
|
||||
if err == store.ErrNoSuchFile {
|
||||
if err == store.ErrNoSuchItem {
|
||||
WriteErrorResponse(w, http.StatusNotFound, "file not found")
|
||||
return
|
||||
}
|
||||
|
55
server/interceptors/auth.go
Normal file
55
server/interceptors/auth.go
Normal 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
86
server/userservice.go
Normal 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)
|
||||
}
|
@ -20,6 +20,7 @@ type BoltStore struct {
|
||||
var bktKey = []byte("files")
|
||||
var bktKeyCerts = []byte("certs")
|
||||
var bktKeyKeys = []byte("keys")
|
||||
var bktKeyUsers = []byte("users")
|
||||
|
||||
func NewBoltStore(path string) (*BoltStore, error) {
|
||||
s := &BoltStore{}
|
||||
@ -38,7 +39,9 @@ func NewBoltStore(path string) (*BoltStore, error) {
|
||||
if _, err := t.CreateBucketIfNotExists(bktKeyKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := t.CreateBucketIfNotExists(bktKeyUsers); err != nil {
|
||||
return err
|
||||
}
|
||||
return 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)
|
||||
}
|
||||
if data == nil {
|
||||
return nil, ErrNoSuchFile
|
||||
return nil, ErrNoSuchItem
|
||||
}
|
||||
|
||||
err := proto.Unmarshal(data, &file)
|
||||
@ -95,7 +98,7 @@ func (s *BoltStore) DeleteFile(id string) error {
|
||||
bkt := t.Bucket(bktKey)
|
||||
data := bkt.Get([]byte(id))
|
||||
if data == nil {
|
||||
return ErrNoSuchFile
|
||||
return ErrNoSuchItem
|
||||
}
|
||||
return bkt.Delete([]byte(id))
|
||||
})
|
||||
@ -137,7 +140,7 @@ func (s *BoltStore) GetCertificate(id string) (*x509.Certificate, error) {
|
||||
return nil, err
|
||||
}
|
||||
if raw == nil {
|
||||
return nil, ErrNoSuchFile
|
||||
return nil, ErrNoSuchItem
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(raw)
|
||||
@ -188,14 +191,83 @@ func (s *BoltStore) ListCertificates() ([]string, error) {
|
||||
var ids []string
|
||||
err := s.db.View(func(tx *bolt.Tx) error {
|
||||
bkt := tx.Bucket(bktKeyCerts)
|
||||
bkt.ForEach(func(k, v []byte) error {
|
||||
return bkt.ForEach(func(k, v []byte) error {
|
||||
ids = append(ids, string(k))
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
//nolint:staticcheck
|
||||
package store_test
|
||||
|
||||
import (
|
||||
@ -10,19 +11,29 @@ import (
|
||||
func TestBoltStore(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)
|
||||
}
|
||||
doFileStoreTest(s, t)
|
||||
_ = s.Close()
|
||||
}
|
||||
|
||||
func TestBoltCertificateStore(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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ func (s *FileSystemStore) GetFile(id string) (*pb.File, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(os.ErrNotExist) {
|
||||
return nil, ErrNoSuchFile
|
||||
return nil, ErrNoSuchItem
|
||||
}
|
||||
return nil, fmt.Errorf("unable to open file: %w", err)
|
||||
}
|
||||
|
@ -9,9 +9,6 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var _ FileStore = &MemoryStore{}
|
||||
var _ CertificateStore = &MemoryStore{}
|
||||
|
||||
type MemoryStore struct {
|
||||
filesLock sync.RWMutex
|
||||
files map[string]*pb.File
|
||||
@ -19,6 +16,8 @@ type MemoryStore struct {
|
||||
certs map[string][]byte
|
||||
keyLock sync.RWMutex
|
||||
keys map[string][]byte
|
||||
usersLock sync.RWMutex
|
||||
users map[string]*pb.User
|
||||
}
|
||||
|
||||
func NewMemoryStore() *MemoryStore {
|
||||
@ -26,9 +25,16 @@ func NewMemoryStore() *MemoryStore {
|
||||
files: make(map[string]*pb.File),
|
||||
certs: 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) {
|
||||
s.filesLock.RLock()
|
||||
defer s.filesLock.RUnlock()
|
||||
@ -37,7 +43,7 @@ func (s *MemoryStore) GetFile(id string) (*pb.File, error) {
|
||||
return file, nil
|
||||
}
|
||||
|
||||
return nil, ErrNoSuchFile
|
||||
return nil, ErrNoSuchItem
|
||||
}
|
||||
|
||||
func (s *MemoryStore) StoreFile(file *pb.File) (string, error) {
|
||||
@ -56,7 +62,7 @@ func (s *MemoryStore) DeleteFile(id string) error {
|
||||
s.filesLock.Lock()
|
||||
defer s.filesLock.Unlock()
|
||||
if _, ok := s.files[id]; !ok {
|
||||
return ErrNoSuchFile
|
||||
return ErrNoSuchItem
|
||||
}
|
||||
|
||||
delete(s.files, id)
|
||||
@ -76,6 +82,12 @@ func (s *MemoryStore) ListFiles() ([]*pb.ListFilesResponse_ListFileInfo, error)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// CertificateStore //
|
||||
//////////////////////
|
||||
|
||||
var _ CertificateStore = &MemoryStore{}
|
||||
|
||||
func (s *MemoryStore) GetCertificate(id string) (*x509.Certificate, error) {
|
||||
s.certLock.Lock()
|
||||
defer s.certLock.Unlock()
|
||||
@ -83,7 +95,7 @@ func (s *MemoryStore) GetCertificate(id string) (*x509.Certificate, error) {
|
||||
data, ok := s.certs[id]
|
||||
if !ok {
|
||||
// TODO: Make separate error, or rename error
|
||||
return nil, ErrNoSuchFile
|
||||
return nil, ErrNoSuchItem
|
||||
}
|
||||
|
||||
return x509.ParseCertificate(data)
|
||||
@ -106,7 +118,7 @@ func (s *MemoryStore) GetKey(id string) (*ecdsa.PrivateKey, error) {
|
||||
defer s.keyLock.RUnlock()
|
||||
data, ok := s.keys[id]
|
||||
if !ok {
|
||||
return nil, ErrNoSuchFile
|
||||
return nil, ErrNoSuchItem
|
||||
}
|
||||
|
||||
return x509.ParseECPrivateKey(data)
|
||||
@ -134,3 +146,50 @@ func (s *MemoryStore) ListCertificates() ([]string, error) {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -14,3 +14,8 @@ func TestMemoryCertificateStore(t *testing.T) {
|
||||
s := store.NewMemoryStore()
|
||||
doCertificateStoreTest(s, t)
|
||||
}
|
||||
|
||||
func TestMemoryUserStore(t *testing.T) {
|
||||
s := store.NewMemoryStore()
|
||||
doUserStoreTests(s, t)
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
"gitea.benny.dog/torjus/ezshare/pb"
|
||||
)
|
||||
|
||||
var ErrNoSuchFile = fmt.Errorf("no such file")
|
||||
var ErrNoSuchItem = fmt.Errorf("no such item")
|
||||
|
||||
type FileStore interface {
|
||||
GetFile(id string) (*pb.File, error)
|
||||
@ -24,3 +24,10 @@ type CertificateStore interface {
|
||||
StoreKey(id string, key *ecdsa.PrivateKey) 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)
|
||||
}
|
||||
|
@ -10,6 +10,10 @@ import (
|
||||
"testing"
|
||||
"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/store"
|
||||
"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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
package ezshare
|
||||
|
||||
const Version = "v0.1.1"
|
Loading…
Reference in New Issue
Block a user