feat: add independent platform modules and multi-provider storage
This commit is contained in:
93
storage/signed.go
Normal file
93
storage/signed.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Signer struct {
|
||||
store Store
|
||||
base *url.URL
|
||||
secret []byte
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewSigner(store Store, baseURL string, secret []byte) (*Signer, error) {
|
||||
base, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if store == nil || len(secret) < 32 || base.Host == "" || (base.Scheme != "https" && base.Scheme != "http") || base.RawQuery != "" || base.Fragment != "" || base.User != nil {
|
||||
return nil, errors.New("storage: store, HTTP base URL and >=32-byte secret required")
|
||||
}
|
||||
return &Signer{store: store, base: base, secret: append([]byte(nil), secret...), now: time.Now}, nil
|
||||
}
|
||||
func (s *Signer) signature(key, expiry string) string {
|
||||
mac := hmac.New(sha256.New, s.secret)
|
||||
io.WriteString(mac, "GET\n"+key+"\n"+expiry)
|
||||
return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
func (s *Signer) PresignGet(ctx context.Context, key string, ttl time.Duration) (*url.URL, error) {
|
||||
if err := Check(ctx, key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ttl < time.Second || ttl > time.Hour {
|
||||
return nil, errors.New("storage: TTL must be 1s..1h")
|
||||
}
|
||||
expiry := strconv.FormatInt(s.now().Add(ttl).Unix(), 10)
|
||||
u := *s.base
|
||||
u.RawQuery = url.Values{"key": {key}, "expires": {expiry}, "signature": {s.signature(key, expiry)}}.Encode()
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
// ServeHTTP serves signed downloads as attachments. Issue links only after
|
||||
// authorization; possession of an unexpired URL grants access to its object.
|
||||
func (s *Signer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" && r.Method != "HEAD" {
|
||||
w.Header().Set("Allow", "GET, HEAD")
|
||||
http.Error(w, "method not allowed", 405)
|
||||
return
|
||||
}
|
||||
q, err := url.ParseQuery(r.URL.RawQuery)
|
||||
if err != nil || len(q) != 3 || len(q["key"]) != 1 || len(q["expires"]) != 1 || len(q["signature"]) != 1 {
|
||||
http.Error(w, "invalid link", 403)
|
||||
return
|
||||
}
|
||||
key, expiry := q.Get("key"), q.Get("expires")
|
||||
expires, err := strconv.ParseInt(expiry, 10, 64)
|
||||
now := s.now().Unix()
|
||||
if err != nil || expires <= now || expires > now+3600 || Check(r.Context(), key) != nil || !hmac.Equal([]byte(q.Get("signature")), []byte(s.signature(key, expiry))) {
|
||||
http.Error(w, "invalid or expired link", 403)
|
||||
return
|
||||
}
|
||||
object, err := s.store.Get(r.Context(), key)
|
||||
if err != nil {
|
||||
status := 500
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
status = 404
|
||||
}
|
||||
http.Error(w, "object unavailable", status)
|
||||
return
|
||||
}
|
||||
defer object.Body.Close()
|
||||
contentType := object.ContentType
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set("Content-Disposition", "attachment")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("Cache-Control", "private, no-store")
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(object.Size, 10))
|
||||
if r.Method == "GET" {
|
||||
_, _ = io.Copy(w, object.Body)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user