storj/pkg/linksharing/handler.go
Andrew Harding 416fa80e85
Link Sharing Service (#2431)
Link sharing service. See `docs/design/link-sharing-service.md` for the design and `cmd/linksharing/README.md` for operational instructions.
2019-07-18 06:26:09 -06:00

226 lines
5.4 KiB
Go

// 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/zeebo/errs"
"go.uber.org/zap"
"gopkg.in/spacemonkeygo/monkit.v2"
"storj.io/storj/lib/uplink"
"storj.io/storj/pkg/ranger"
"storj.io/storj/pkg/storj"
)
var (
mon = monkit.Package()
)
// HandlerConfig specifies the handler configuration
type HandlerConfig struct {
// Log is a logger used for logging
Log *zap.Logger
// 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(config HandlerConfig) (*Handler, error) {
if config.Log == nil {
config.Log = zap.L()
}
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: config.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
}
scope, 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, scope.SatelliteAddr, scope.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, scope.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 scope")
}
return nil, "", "", errs.New("missing bucket")
case 2:
return nil, "", "", errs.New("missing bucket path")
}
scopeb58 := segments[0]
bucket := segments[1]
unencPath := segments[2]
scope, err := uplink.ParseScope(scopeb58)
if err != nil {
return nil, "", "", err
}
return scope, 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()
}