From 0781a91ff497d591a03dfa10f2bdea7b87faf0be Mon Sep 17 00:00:00 2001 From: Egon Elbre Date: Thu, 19 Mar 2020 16:51:26 +0200 Subject: [PATCH] linksharing: move to storj.io/linksharing Change-Id: If3bca64f80de9c244414e2aff28bb8b3bf9e87a2 --- Makefile | 7 +- cmd/linksharing/.gitignore | 1 - cmd/linksharing/README.md | 57 ----- cmd/linksharing/main.go | 143 ------------- linksharing/handler.go | 218 ------------------- linksharing/handler_test.go | 287 -------------------------- linksharing/httpserver/server.go | 132 ------------ linksharing/httpserver/server_test.go | 180 ---------------- 8 files changed, 2 insertions(+), 1023 deletions(-) delete mode 100644 cmd/linksharing/.gitignore delete mode 100644 cmd/linksharing/README.md delete mode 100644 cmd/linksharing/main.go delete mode 100644 linksharing/handler.go delete mode 100644 linksharing/handler_test.go delete mode 100644 linksharing/httpserver/server.go delete mode 100644 linksharing/httpserver/server_test.go diff --git a/Makefile b/Makefile index 63f44caab..f21d6e430 100644 --- a/Makefile +++ b/Makefile @@ -244,9 +244,6 @@ identity_%: .PHONY: inspector_% inspector_%: $(MAKE) binary-check COMPONENT=inspector GOARCH=$(word 3, $(subst _, ,$@)) GOOS=$(word 2, $(subst _, ,$@)) -.PHONY: linksharing_% -linksharing_%: - $(MAKE) binary-check COMPONENT=linksharing GOARCH=$(word 3, $(subst _, ,$@)) GOOS=$(word 2, $(subst _, ,$@)) .PHONY: satellite_% satellite_%: $(MAKE) binary-check COMPONENT=satellite GOARCH=$(word 3, $(subst _, ,$@)) GOOS=$(word 2, $(subst _, ,$@)) @@ -264,11 +261,11 @@ versioncontrol_%: $(MAKE) binary-check COMPONENT=versioncontrol GOARCH=$(word 3, $(subst _, ,$@)) GOOS=$(word 2, $(subst _, ,$@)) -COMPONENTLIST := certificates identity inspector linksharing satellite storagenode storagenode-updater uplink versioncontrol +COMPONENTLIST := certificates identity inspector satellite storagenode storagenode-updater uplink versioncontrol OSARCHLIST := darwin_amd64 linux_amd64 linux_arm linux_arm64 windows_amd64 freebsd_amd64 BINARIES := $(foreach C,$(COMPONENTLIST),$(foreach O,$(OSARCHLIST),$C_$O)) .PHONY: binaries -binaries: ${BINARIES} ## Build certificates, identity, inspector, linksharing, satellite, storagenode, uplink, and versioncontrol binaries (jenkins) +binaries: ${BINARIES} ## Build certificates, identity, inspector, satellite, storagenode, uplink, and versioncontrol binaries (jenkins) .PHONY: sign-windows-installer sign-windows-installer: diff --git a/cmd/linksharing/.gitignore b/cmd/linksharing/.gitignore deleted file mode 100644 index 1564dc00f..000000000 --- a/cmd/linksharing/.gitignore +++ /dev/null @@ -1 +0,0 @@ -linksharing diff --git a/cmd/linksharing/README.md b/cmd/linksharing/README.md deleted file mode 100644 index 40e81ac73..000000000 --- a/cmd/linksharing/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# Link Sharing Service - -## Building - -``` -$ go install storj.io/storj/cmd/linksharing -``` - -## Configuring - -### Development - -Default development configuration has the link sharing service hosted on -`localhost:8080` serving plain HTTP. - -``` -$ linksharing setup --defaults dev -``` - -### Production - -To configure the link sharing service for production, run the `setup` command -using the `release` defaults. You must also provide the public URL for -the sharing service, which is used to construct URLs returned to -clients. Since there is currently no server affinity for requests, the URL -can point to a pool of servers: - -``` -$ linksharing setup --defaults release --public-url -``` - -Default release configuration has the link sharing service hosted on `:8443` -serving HTTPS using a server certificate (`server.crt.pem`) and -key (`server.key.pem`) residing in the working directory where the linksharing -service is run. - -You can modify the configuration file or use the `--cert-file` and `--key-file` -flags to configure an alternate location for the server keypair. - -In order to run the link sharing service in release mode serving HTTP, you must -clear the certificate and key file configurables: - -``` -$ linksharing setup --defaults release --public-url --cert-file="" --key-file="" --address=":8080" -``` - -**WARNING** HTTP is only recommended if you are doing TLS termination on the -same machine running the link sharing service as the link sharing service -serves unencrypted user data. - -## Running - -After configuration is complete, running the link sharing is as simple as: - -``` -$ linksharing run -``` diff --git a/cmd/linksharing/main.go b/cmd/linksharing/main.go deleted file mode 100644 index f5755bfa2..000000000 --- a/cmd/linksharing/main.go +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (C) 2019 Storj Labs, Inc. -// See LICENSE for copying information. - -package main - -import ( - "crypto/tls" - "fmt" - "os" - "path/filepath" - - "github.com/spf13/cobra" - "github.com/zeebo/errs" - "go.uber.org/zap" - - "storj.io/common/fpath" - "storj.io/storj/lib/uplink" - "storj.io/storj/linksharing" - "storj.io/storj/linksharing/httpserver" - "storj.io/storj/pkg/cfgstruct" - "storj.io/storj/pkg/process" -) - -// LinkSharing defines link sharing configuration -type LinkSharing struct { - Address string `user:"true" help:"public address to listen on" devDefault:"localhost:8080" releaseDefault:":8443"` - CertFile string `user:"true" help:"server certificate file" devDefault:"" releaseDefault:"server.crt.pem"` - KeyFile string `user:"true" help:"server key file" devDefault:"" releaseDefault:"server.key.pem"` - PublicURL string `user:"true" help:"public url for the server" devDefault:"http://localhost:8080" releaseDefault:""` -} - -var ( - rootCmd = &cobra.Command{ - Use: "link sharing service", - Short: "Link Sharing Service", - } - runCmd = &cobra.Command{ - Use: "run", - Short: "Run the link sharing service", - RunE: cmdRun, - } - setupCmd = &cobra.Command{ - Use: "setup", - Short: "Create config files", - RunE: cmdSetup, - Annotations: map[string]string{"type": "setup"}, - } - - runCfg LinkSharing - setupCfg LinkSharing - - confDir string -) - -func init() { - defaultConfDir := fpath.ApplicationDir("storj", "linksharing") - cfgstruct.SetupFlag(zap.L(), rootCmd, &confDir, "config-dir", defaultConfDir, "main directory for link sharing configuration") - defaults := cfgstruct.DefaultsFlag(rootCmd) - rootCmd.AddCommand(runCmd) - rootCmd.AddCommand(setupCmd) - process.Bind(runCmd, &runCfg, defaults, cfgstruct.ConfDir(confDir)) - process.Bind(setupCmd, &setupCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.SetupMode()) -} - -func cmdRun(cmd *cobra.Command, args []string) (err error) { - ctx, _ := process.Ctx(cmd) - log := zap.L() - - uplink, err := uplink.NewUplink(ctx, nil) - if err != nil { - return err - } - - tlsConfig, err := configureTLS(runCfg.CertFile, runCfg.KeyFile) - if err != nil { - return err - } - - handler, err := linksharing.NewHandler(log, linksharing.HandlerConfig{ - Uplink: uplink, - URLBase: runCfg.PublicURL, - }) - if err != nil { - return err - } - - server, err := httpserver.New(log, httpserver.Config{ - Name: "Link Sharing", - Address: runCfg.Address, - Handler: handler, - TLSConfig: tlsConfig, - ShutdownTimeout: -1, - }) - if err != nil { - return err - } - - return server.Run(ctx) -} - -func cmdSetup(cmd *cobra.Command, args []string) (err error) { - setupDir, err := filepath.Abs(confDir) - if err != nil { - return err - } - - valid, _ := fpath.IsValidSetupDir(setupDir) - if !valid { - return fmt.Errorf("link sharing configuration already exists (%v)", setupDir) - } - - err = os.MkdirAll(setupDir, 0700) - if err != nil { - return err - } - - return process.SaveConfig(cmd, filepath.Join(setupDir, "config.yaml")) -} - -func configureTLS(certFile, keyFile string) (*tls.Config, error) { - switch { - case certFile != "" && keyFile != "": - case certFile == "" && keyFile == "": - return nil, nil - case certFile != "" && keyFile == "": - return nil, errs.New("key file must be provided with cert file") - case certFile == "" && keyFile != "": - return nil, errs.New("cert file must be provided with key file") - } - - cert, err := tls.LoadX509KeyPair(certFile, keyFile) - if err != nil { - return nil, errs.New("unable to load server keypair: %v", err) - } - - return &tls.Config{ - Certificates: []tls.Certificate{cert}, - }, nil -} - -func main() { - process.Exec(rootCmd) -} diff --git a/linksharing/handler.go b/linksharing/handler.go deleted file mode 100644 index c38d59295..000000000 --- a/linksharing/handler.go +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright (C) 2019 Storj Labs, Inc. -// See LICENSE for copying information. - -package linksharing - -import ( - "context" - "fmt" - "io" - "net/http" - "net/url" - "path" - "strings" - - "github.com/spacemonkeygo/monkit/v3" - "github.com/zeebo/errs" - "go.uber.org/zap" - - "storj.io/common/ranger" - "storj.io/common/storj" - "storj.io/storj/lib/uplink" -) - -var ( - mon = monkit.Package() -) - -// HandlerConfig specifies the handler configuration -type HandlerConfig struct { - // Uplink is the uplink used to talk to the storage network - Uplink *uplink.Uplink - - // URLBase is the base URL of the link sharing handler. It is used - // to construct URLs returned to clients. It should be a fully formed URL. - URLBase string -} - -// Handler implements the link sharing HTTP handler -type Handler struct { - log *zap.Logger - uplink *uplink.Uplink - urlBase *url.URL -} - -// NewHandler creates a new link sharing HTTP handler -func NewHandler(log *zap.Logger, config HandlerConfig) (*Handler, error) { - if config.Uplink == nil { - return nil, errs.New("uplink is required") - } - - urlBase, err := parseURLBase(config.URLBase) - if err != nil { - return nil, err - } - - return &Handler{ - log: log, - uplink: config.Uplink, - urlBase: urlBase, - }, nil -} - -// ServeHTTP handles link sharing requests -func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // serveHTTP handles the request in full. the error that is returned can - // be ignored since it was only added to facilitate monitoring. - _ = handler.serveHTTP(w, r) -} - -func (handler *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) (err error) { - ctx := r.Context() - defer mon.Task()(&ctx)(&err) - - locationOnly := false - - switch r.Method { - case http.MethodHead: - locationOnly = true - case http.MethodGet: - default: - err = errs.New("method not allowed") - http.Error(w, err.Error(), http.StatusMethodNotAllowed) - return err - } - - access, bucket, unencPath, err := parseRequestPath(r.URL.Path) - if err != nil { - err = fmt.Errorf("invalid request: %v", err) - http.Error(w, err.Error(), http.StatusBadRequest) - return err - } - - p, err := handler.uplink.OpenProject(ctx, access.SatelliteAddr, access.APIKey) - if err != nil { - handler.handleUplinkErr(w, "open project", err) - return err - } - defer func() { - if err := p.Close(); err != nil { - handler.log.With(zap.Error(err)).Warn("unable to close project") - } - }() - - b, err := p.OpenBucket(ctx, bucket, access.EncryptionAccess) - if err != nil { - handler.handleUplinkErr(w, "open bucket", err) - return err - } - defer func() { - if err := b.Close(); err != nil { - handler.log.With(zap.Error(err)).Warn("unable to close bucket") - } - }() - - o, err := b.OpenObject(ctx, unencPath) - if err != nil { - handler.handleUplinkErr(w, "open object", err) - return err - } - defer func() { - if err := o.Close(); err != nil { - handler.log.With(zap.Error(err)).Warn("unable to close object") - } - }() - - if locationOnly { - location := makeLocation(handler.urlBase, r.URL.Path) - http.Redirect(w, r, location, http.StatusFound) - return nil - } - - ranger.ServeContent(ctx, w, r, unencPath, o.Meta.Modified, newObjectRanger(o)) - return nil -} - -func (handler *Handler) handleUplinkErr(w http.ResponseWriter, action string, err error) { - switch { - case storj.ErrBucketNotFound.Has(err): - http.Error(w, "bucket not found", http.StatusNotFound) - case storj.ErrObjectNotFound.Has(err): - http.Error(w, "object not found", http.StatusNotFound) - default: - handler.log.Error("unable to handle request", zap.String("action", action), zap.Error(err)) - http.Error(w, "unable to handle request", http.StatusInternalServerError) - } -} - -func parseRequestPath(p string) (*uplink.Scope, string, string, error) { - // Drop the leading slash, if necessary - p = strings.TrimPrefix(p, "/") - - // Split the request path - segments := strings.SplitN(p, "/", 3) - switch len(segments) { - case 1: - if segments[0] == "" { - return nil, "", "", errs.New("missing access") - } - return nil, "", "", errs.New("missing bucket") - case 2: - return nil, "", "", errs.New("missing bucket path") - } - scopeb58 := segments[0] - bucket := segments[1] - unencPath := segments[2] - - access, err := uplink.ParseScope(scopeb58) - if err != nil { - return nil, "", "", err - } - return access, bucket, unencPath, nil -} - -type objectRanger struct { - o *uplink.Object -} - -func newObjectRanger(o *uplink.Object) ranger.Ranger { - return &objectRanger{ - o: o, - } -} - -func (ranger *objectRanger) Size() int64 { - return ranger.o.Meta.Size -} - -func (ranger *objectRanger) Range(ctx context.Context, offset, length int64) (_ io.ReadCloser, err error) { - defer mon.Task()(&ctx)(&err) - return ranger.o.DownloadRange(ctx, offset, length) -} - -func parseURLBase(s string) (*url.URL, error) { - u, err := url.Parse(s) - if err != nil { - return nil, errs.Wrap(err) - } - - switch { - case u.Scheme != "http" && u.Scheme != "https": - return nil, errs.New("URL base must be http:// or https://") - case u.Host == "": - return nil, errs.New("URL base must contain host") - case u.User != nil: - return nil, errs.New("URL base must not contain user info") - case u.RawQuery != "": - return nil, errs.New("URL base must not contain query values") - case u.Fragment != "": - return nil, errs.New("URL base must not contain a fragment") - } - return u, nil -} - -func makeLocation(base *url.URL, reqPath string) string { - location := *base - location.Path = path.Join(location.Path, reqPath) - return location.String() -} diff --git a/linksharing/handler_test.go b/linksharing/handler_test.go deleted file mode 100644 index 62c985996..000000000 --- a/linksharing/handler_test.go +++ /dev/null @@ -1,287 +0,0 @@ -// Copyright (C) 2019 Storj Labs, Inc. -// See LICENSE for copying information. - -package linksharing - -import ( - "context" - "net/http" - "net/http/httptest" - "path" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/zap/zaptest" - - "storj.io/common/storj" - "storj.io/common/testcontext" - "storj.io/storj/lib/uplink" - "storj.io/storj/private/testplanet" -) - -func TestNewHandler(t *testing.T) { - ctx := testcontext.New(t) - defer ctx.Cleanup() - - uplink := newUplink(ctx, t) - defer ctx.Check(uplink.Close) - - testCases := []struct { - name string - config HandlerConfig - err string - }{ - { - name: "missing uplink", - config: HandlerConfig{ - URLBase: "http://localhost", - }, - err: "uplink is required", - }, - { - name: "URL base must be http or https", - config: HandlerConfig{ - Uplink: uplink, - URLBase: "gopher://chunks", - }, - err: "URL base must be http:// or https://", - }, - { - name: "URL base must contain host", - config: HandlerConfig{ - Uplink: uplink, - URLBase: "http://", - }, - err: "URL base must contain host", - }, - { - name: "URL base can have a port", - config: HandlerConfig{ - Uplink: uplink, - URLBase: "http://host:99", - }, - }, - { - name: "URL base can have a path", - config: HandlerConfig{ - Uplink: uplink, - URLBase: "http://host/gopher", - }, - }, - { - name: "URL base must not contain user info", - config: HandlerConfig{ - Uplink: uplink, - URLBase: "http://joe@host", - }, - err: "URL base must not contain user info", - }, - { - name: "URL base must not contain query values", - config: HandlerConfig{ - Uplink: uplink, - URLBase: "http://host/?gopher=chunks", - }, - err: "URL base must not contain query values", - }, - { - name: "URL base must not contain a fragment", - config: HandlerConfig{ - Uplink: uplink, - URLBase: "http://host/#gopher-chunks", - }, - err: "URL base must not contain a fragment", - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - - handler, err := NewHandler(zaptest.NewLogger(t), testCase.config) - if testCase.err != "" { - require.EqualError(t, err, testCase.err) - return - } - require.NoError(t, err) - require.NotNil(t, handler) - }) - } -} - -func TestHandlerRequests(t *testing.T) { - testplanet.Run(t, testplanet.Config{ - SatelliteCount: 2, - StorageNodeCount: 1, - UplinkCount: 1, - }, testHandlerRequests) -} - -func testHandlerRequests(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { - err := planet.Uplinks[0].Upload(ctx, planet.Satellites[0], "testbucket", "test/foo", []byte("FOO")) - require.NoError(t, err) - - apiKey, err := uplink.ParseAPIKey(planet.Uplinks[0].APIKey[planet.Satellites[0].ID()].Serialize()) - require.NoError(t, err) - - encAccess := uplink.NewEncryptionAccessWithDefaultKey(storj.Key{}) - encAccess.SetDefaultPathCipher(storj.EncAESGCM) - - access, err := (&uplink.Scope{ - SatelliteAddr: planet.Satellites[0].Addr(), - APIKey: apiKey, - EncryptionAccess: encAccess, - }).Serialize() - require.NoError(t, err) - - testCases := []struct { - name string - method string - path string - status int - header http.Header - body string - }{ - { - name: "invalid method", - method: "PUT", - status: http.StatusMethodNotAllowed, - body: "method not allowed\n", - }, - { - name: "GET missing access", - method: "GET", - status: http.StatusBadRequest, - body: "invalid request: missing access\n", - }, - { - name: "GET malformed access", - method: "GET", - path: path.Join("BADACCESS", "testbucket", "test/foo"), - status: http.StatusBadRequest, - body: "invalid request: invalid scope format\n", - }, - { - name: "GET missing bucket", - method: "GET", - path: access, - status: http.StatusBadRequest, - body: "invalid request: missing bucket\n", - }, - { - name: "GET bucket not found", - method: "GET", - path: path.Join(access, "someotherbucket", "test/foo"), - status: http.StatusNotFound, - body: "bucket not found\n", - }, - { - name: "GET missing bucket path", - method: "GET", - path: path.Join(access, "testbucket"), - status: http.StatusBadRequest, - body: "invalid request: missing bucket path\n", - }, - { - name: "GET object not found", - method: "GET", - path: path.Join(access, "testbucket", "test/bar"), - status: http.StatusNotFound, - body: "object not found\n", - }, - { - name: "GET success", - method: "GET", - path: path.Join(access, "testbucket", "test/foo"), - status: http.StatusOK, - body: "FOO", - }, - { - name: "HEAD missing access", - method: "HEAD", - status: http.StatusBadRequest, - body: "invalid request: missing access\n", - }, - { - name: "HEAD malformed access", - method: "HEAD", - path: path.Join("BADACCESS", "testbucket", "test/foo"), - status: http.StatusBadRequest, - body: "invalid request: invalid scope format\n", - }, - { - name: "HEAD missing bucket", - method: "HEAD", - path: access, - status: http.StatusBadRequest, - body: "invalid request: missing bucket\n", - }, - { - name: "HEAD bucket not found", - method: "HEAD", - path: path.Join(access, "someotherbucket", "test/foo"), - status: http.StatusNotFound, - body: "bucket not found\n", - }, - { - name: "HEAD missing bucket path", - method: "HEAD", - path: path.Join(access, "testbucket"), - status: http.StatusBadRequest, - body: "invalid request: missing bucket path\n", - }, - { - name: "HEAD object not found", - method: "HEAD", - path: path.Join(access, "testbucket", "test/bar"), - status: http.StatusNotFound, - body: "object not found\n", - }, - { - name: "HEAD success", - method: "HEAD", - path: path.Join(access, "testbucket", "test/foo"), - status: http.StatusFound, - header: http.Header{ - "Location": []string{"http://localhost/" + path.Join(access, "testbucket", "test/foo")}, - }, - body: "", - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - uplink := newUplink(ctx, t) - defer ctx.Check(uplink.Close) - - handler, err := NewHandler(zaptest.NewLogger(t), HandlerConfig{ - Uplink: uplink, - URLBase: "http://localhost", - }) - require.NoError(t, err) - - url := "http://localhost/" + testCase.path - w := httptest.NewRecorder() - r, err := http.NewRequest(testCase.method, url, nil) - require.NoError(t, err) - handler.ServeHTTP(w, r) - - assert.Equal(t, testCase.status, w.Code, "status code does not match") - for h, v := range testCase.header { - assert.Equal(t, v, w.Header()[h], "%q header does not match", h) - } - assert.Equal(t, testCase.body, w.Body.String(), "body does not match") - }) - } -} - -func newUplink(ctx context.Context, tb testing.TB) *uplink.Uplink { - cfg := new(uplink.Config) - cfg.Volatile.Log = zaptest.NewLogger(tb) - cfg.Volatile.TLS.SkipPeerCAWhitelist = true - up, err := uplink.NewUplink(ctx, cfg) - require.NoError(tb, err) - return up -} diff --git a/linksharing/httpserver/server.go b/linksharing/httpserver/server.go deleted file mode 100644 index 4bc12482a..000000000 --- a/linksharing/httpserver/server.go +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (C) 2019 Storj Labs, Inc. -// See LICENSE for copying information. - -package httpserver - -import ( - "context" - "crypto/tls" - "net" - "net/http" - "time" - - "github.com/zeebo/errs" - "go.uber.org/zap" - "golang.org/x/sync/errgroup" - - "storj.io/common/errs2" -) - -const ( - // DefaultShutdownTimeout is the default ShutdownTimeout (see Config) - DefaultShutdownTimeout = time.Second * 10 -) - -// Config holds the HTTP server configuration -type Config struct { - // Name is the name of the server. It is only used for logging. It can - // be empty. - Name string - - // Address is the address to bind the server to. It must be set. - Address string - - // Handler is the HTTP handler to be served. It must be set. - Handler http.Handler - - // TLSConfig is the TLS configuration for the server. It is optional. - TLSConfig *tls.Config - - // ShutdownTimeout controls how long to wait for requests to finish before - // returning from Run() after the context is canceled. It defaults to - // 10 seconds if unset. If set to a negative value, the server will be - // closed immediately. - ShutdownTimeout time.Duration -} - -// Server is the HTTP server -type Server struct { - log *zap.Logger - name string - listener net.Listener - server *http.Server - shutdownTimeout time.Duration -} - -// New creates a new URL Service Server. -func New(log *zap.Logger, config Config) (*Server, error) { - switch { - case config.Address == "": - return nil, errs.New("server address is required") - case config.Handler == nil: - return nil, errs.New("server handler is required") - } - - listener, err := net.Listen("tcp", config.Address) - if err != nil { - return nil, errs.New("unable to listen on %s: %v", config.Address, err) - } - - server := &http.Server{ - Handler: config.Handler, - TLSConfig: config.TLSConfig, - ErrorLog: zap.NewStdLog(log), - } - - if config.ShutdownTimeout == 0 { - config.ShutdownTimeout = DefaultShutdownTimeout - } - - if config.Name != "" { - log = log.With(zap.String("server", config.Name)) - } - - return &Server{ - log: log, - name: config.Name, - listener: listener, - server: server, - shutdownTimeout: config.ShutdownTimeout, - }, nil -} - -// Run runs the server until it's either closed or it errors. -func (server *Server) Run(ctx context.Context) (err error) { - ctx, cancel := context.WithCancel(ctx) - var group errgroup.Group - - group.Go(func() error { - <-ctx.Done() - server.log.Info("Server shutting down") - return shutdownWithTimeout(server.server, server.shutdownTimeout) - }) - group.Go(func() (err error) { - defer cancel() - server.log.With(zap.String("addr", server.Addr())).Sugar().Info("Server started") - if server.server.TLSConfig == nil { - err = server.server.Serve(server.listener) - } else { - err = server.server.ServeTLS(server.listener, "", "") - } - if err == http.ErrServerClosed { - return nil - } - server.log.With(zap.Error(err)).Error("Server closed unexpectedly") - return err - }) - return group.Wait() -} - -// Addr returns the public address. -func (server *Server) Addr() string { - return server.listener.Addr().String() -} - -func shutdownWithTimeout(server *http.Server, timeout time.Duration) error { - if timeout < 0 { - return server.Close() - } - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - return errs2.IgnoreCanceled(server.Shutdown(ctx)) -} diff --git a/linksharing/httpserver/server_test.go b/linksharing/httpserver/server_test.go deleted file mode 100644 index b82db9e09..000000000 --- a/linksharing/httpserver/server_test.go +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright (C) 2019 Storj Labs, Inc. -// See LICENSE for copying information. - -package httpserver - -import ( - "context" - "crypto" - "crypto/rand" - "crypto/tls" - "crypto/x509" - "fmt" - "io/ioutil" - "math/big" - "net" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/zap/zaptest" - - "storj.io/common/pkcrypto" - "storj.io/common/testcontext" -) - -var ( - testKey = mustSignerFromPEM(`-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgT8yIof+3qG3wQzXf -eAOcuTgWmgqXRnHVwKJl2g1pCb2hRANCAARWxVAPyT1BRs2hqiDuHlPXr1kVDXuw -7/a1USmgsVWiZ0W3JopcTbTMhvMZk+2MKqtWcc3gHF4vRDnHTeQl4lsx ------END PRIVATE KEY----- -`) - testCert = mustCreateLocalhostCert() -) - -func TestServer(t *testing.T) { - address := "localhost:0" - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, "OK") - }) - tlsConfig := &tls.Config{ - Certificates: []tls.Certificate{ - { - Certificate: [][]byte{testCert.Raw}, - PrivateKey: testKey, - }, - }, - } - - testCases := []serverTestCase{ - { - Name: "missing address", - Handler: handler, - NewErr: "server address is required", - }, - { - Name: "bad address", - Address: "this is no good", - Handler: handler, - NewErr: "unable to listen on this is no good: listen tcp: address this is no good: missing port in address", - }, - { - Name: "missing handler", - Address: address, - NewErr: "server handler is required", - }, - { - Name: "success via HTTP", - Address: address, - Handler: handler, - }, - { - Name: "success via HTTPS", - Address: address, - Handler: handler, - TLSConfig: tlsConfig, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.Name, func(t *testing.T) { - ctx := testcontext.NewWithTimeout(t, time.Minute) - defer ctx.Cleanup() - - s, ok := testCase.NewServer(t) - if !ok { - return - } - - runCtx, cancel := context.WithCancel(ctx) - ctx.Go(func() error { - return s.Run(runCtx) - }) - - testCase.DoGet(t, s) - cancel() - }) - } -} - -type serverTestCase struct { - Name string - Address string - Handler http.Handler - TLSConfig *tls.Config - NewErr string -} - -func (testCase *serverTestCase) NewServer(tb testing.TB) (*Server, bool) { - s, err := New(zaptest.NewLogger(tb), Config{ - Name: "test", - Address: testCase.Address, - Handler: testCase.Handler, - TLSConfig: testCase.TLSConfig, - }) - if testCase.NewErr != "" { - require.EqualError(tb, err, testCase.NewErr) - return nil, false - } - require.NoError(tb, err) - return s, true -} - -func (testCase *serverTestCase) DoGet(tb testing.TB, s *Server) { - scheme := "http" - client := &http.Client{} - if testCase.TLSConfig != nil { - scheme = "https" - client.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: certPoolFromCert(testCert), - }, - } - } - - resp, err := client.Get(fmt.Sprintf("%s://%s", scheme, s.Addr())) - require.NoError(tb, err) - defer func() { _ = resp.Body.Close() }() - - assert.Equal(tb, resp.StatusCode, http.StatusOK) - - body, err := ioutil.ReadAll(resp.Body) - assert.NoError(tb, err) - assert.Equal(tb, "OK", string(body)) -} - -func mustSignerFromPEM(keyBytes string) crypto.Signer { - key, err := pkcrypto.PrivateKeyFromPEM([]byte(keyBytes)) - if err != nil { - panic(err) - } - return key.(crypto.Signer) -} - -func mustCreateLocalhostCert() *x509.Certificate { - tmpl := &x509.Certificate{ - SerialNumber: big.NewInt(0), - NotAfter: time.Now().Add(time.Hour), - DNSNames: []string{"localhost"}, - IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)}, - } - certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, testKey.Public(), testKey) - if err != nil { - panic(err) - } - cert, err := x509.ParseCertificate(certDER) - if err != nil { - panic(err) - } - return cert -} - -func certPoolFromCert(cert *x509.Certificate) *x509.CertPool { - pool := x509.NewCertPool() - pool.AddCert(cert) - return pool -}