416fa80e85
Link sharing service. See `docs/design/link-sharing-service.md` for the design and `cmd/linksharing/README.md` for operational instructions.
226 lines
5.4 KiB
Go
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()
|
|
}
|