diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..17beaed --- /dev/null +++ b/Makefile @@ -0,0 +1,80 @@ +NAME=bitwarden-sdk-server + +# Set the build dir, where built cross-compiled binaries will be output +BUILDDIR := bin + +# VERSION defines the project version for the bundle. +VERSION ?= 0.0.1 + +UNAME ?= $(shell uname|tr '[:upper:]' '[:lower:]') + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# This is a requirement for 'setup-envtest.sh' in the test target. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +GOLANGCI_LINT ?= $(LOCALBIN)/golangci-lint + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + + +# List the GOOS and GOARCH to build +GO_LDFLAGS_STATIC="-s -w $(CTIMEVAR) -extldflags -static" +GOLANGCI_LINT_VERSION ?= v1.57.2 + +.DEFAULT_GOAL := help + +##@ Build + +binaries: ## Builds binaries for all supported platforms, linux, darwin + CGO_ENABLED=0 gox \ + -osarch="linux/amd64 linux/arm darwin/amd64" \ + -ldflags=${GO_LDFLAGS_STATIC} \ + -output="$(BUILDDIR)/{{.OS}}/{{.Arch}}/$(NAME)" \ + -tags="netgo" \ + ./ + +bootstrap: ## Installs necessary third party components + go get github.com/mitchellh/gox + +##@ Testing + +.PHONY: lint +lint: golangci-lint ## Run golangci-lint. + $(GOLANGCI_LINT) run + +clean: ## Runs go clean + go clean -i + +##@ Docker + +IMG ?= ghcr.io/external-secrets/bitwarden-sdk-server +TAG ?= v0.1.0 + +docker_image: ## Creates a docker image. Requires `image` and `version` variables on command line + docker build -t $(IMG):$(VERSION) . + +##@ Utilities + +.PHONY: golangci-lint +golangci-lint: $(GOLANGCI_LINT) +$(GOLANGCI_LINT): $(LOCALBIN) + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s $(GOLANGCI_LINT_VERSION) + +help: ## Display this help. Thanks to https://www.thapaliya.com/en/writings/well-documented-makefiles/ +ifeq ($(OS),Windows_NT) + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \n"} /^[a-zA-Z_-]+:.*?##/ { printf " %-40s %s\n", $$1, $$2 } /^##@/ { printf "\n%s\n", substr($$0, 5) } ' $(MAKEFILE_LIST) +else + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-40s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) +endif diff --git a/README.md b/README.md index 78ed735..451fd2a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # bitwarden-sdk-server + This repository contains a simple REST wrapper for the Bitwarden Rust SDK diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..66ccfda --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,80 @@ +package cmd + +import ( + "context" + "log/slog" + "os" + "os/signal" + "syscall" + "time" + + "github.com/external-secrets/bitwarden-sdk-server/pkg/server" + "github.com/spf13/cobra" +) + +var ( + rootCmd = &cobra.Command{ + Use: "serve", + Short: "Serve the Bitwarden API", + RunE: runServeCmd, + } + + rootArgs struct { + server server.Config + } +) + +func init() { + flag := rootCmd.Flags() + // Server Configs + flag.BoolVar(&rootArgs.server.Debug, "debug", false, "--debug") + flag.BoolVar(&rootArgs.server.Insecure, "insecure", false, "--insecure") + flag.StringVar(&rootArgs.server.KeyFile, "key-file", "", "--key-file /home/user/.server/server.key") + flag.StringVar(&rootArgs.server.CertFile, "cert-file", "", "--cert-file /home/user/.server/server.crt") + flag.StringVar(&rootArgs.server.Addr, "hostname", ":9998", "--hostname :9998") +} + +const timeout = 15 * time.Second + +func runServeCmd(_ *cobra.Command, _ []string) error { + svr := server.NewServer(rootArgs.server) + go func() { + if err := svr.Run(context.Background()); err != nil { + slog.Error("failed to start server", "error", err) + os.Exit(1) + } + }() + + interruptChannel := make(chan os.Signal, 2) + signal.Notify(interruptChannel, os.Interrupt, syscall.SIGTERM) + + <-interruptChannel + done := make(chan struct{}) + // start the timer for the shutdown sequence + go func() { + select { + case <-done: + return + case <-time.After(timeout): + slog.Error("graceful shutdown timed out... forcing shutdown") + os.Exit(1) + } + }() + + slog.Info("received shutdown signal... gracefully terminating servers...") + if err := svr.Shutdown(context.Background()); err != nil { + slog.Error("graceful shutdown failed... forcing shutdown", "error", err) + os.Exit(1) + } + + slog.Info("all done. Goodbye.") + + done <- struct{}{} + + return nil +} + +// Execute runs the main serve command. +func Execute() error { + return rootCmd.Execute() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..df6434b --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/external-secrets/bitwarden-sdk-server + +go 1.22 + +require ( + github.com/go-chi/chi v1.5.5 + github.com/spf13/cobra v1.8.0 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4b00704 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= +github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..37d0728 --- /dev/null +++ b/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + "github.com/external-secrets/bitwarden-sdk-server/cmd" +) + +func main() { + if err := cmd.Execute(); err != nil { + log.Fatal(err) + } +} diff --git a/pkg/server/server.go b/pkg/server/server.go new file mode 100644 index 0000000..35f24ba --- /dev/null +++ b/pkg/server/server.go @@ -0,0 +1,53 @@ +package server + +import ( + "context" + "net/http" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +const ( + api = "/rest/api/1" +) + +type Config struct { + Insecure bool + Debug bool + Addr string + KeyFile string + CertFile string +} + +// Server defines a server which runs and accepts requests. +type Server struct { + Config + + server *http.Server +} + +func NewServer(cfg Config) *Server { + return &Server{Config: cfg} +} + +func (s *Server) Run(_ context.Context) error { + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Get(api, func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("welcome")) + }) + + srv := &http.Server{Addr: s.Addr, Handler: r} + s.server = srv + + if s.Insecure { + return srv.ListenAndServe() + } + + return srv.ListenAndServeTLS(s.CertFile, s.KeyFile) +} + +func (s *Server) Shutdown(ctx context.Context) error { + return s.server.Shutdown(ctx) +}