diff --git a/blossom/handlers.go b/blossom/handlers.go index dcbd99d..f747082 100644 --- a/blossom/handlers.go +++ b/blossom/handlers.go @@ -7,6 +7,7 @@ import ( "io" "mime" "net/http" + "regexp" "strconv" "strings" "time" @@ -198,8 +199,13 @@ func (bs BlossomServer) handleGetBlob(w http.ResponseWriter, r *http.Request) { for _, redirect := range bs.RedirectGet { redirectURL, code, err := redirect(r.Context(), hhash, ext) if err == nil && redirectURL != "" { - // Not sure if browsers will cache redirects - // But it doesn't hurt anyway + // check that the redirectURL contains the hash of the file + if ok, _ := regexp.MatchString(`\b`+hhash+`\b`, redirectURL); !ok { + continue + } + + // not sure if browsers will cache redirects + // but it doesn't hurt anyway w.Header().Set("ETag", hhash) w.Header().Set("Cache-Control", "public, max-age=604800, immutable") http.Redirect(w, r, redirectURL, code) diff --git a/blossom/server.go b/blossom/server.go index 5bc7cf7..28ee8c4 100644 --- a/blossom/server.go +++ b/blossom/server.go @@ -29,26 +29,13 @@ type BlossomServer struct { // ServerOption represents a functional option for configuring a BlossomServer type ServerOption func(*BlossomServer) -// WithRedirectURL configures a redirect URL for the RedirectGet function -func WithRedirectURL(urlTemplate string, statusCode int) ServerOption { - return func(bs *BlossomServer) { - redirectFn := redirectGet(urlTemplate, statusCode) - bs.RedirectGet = append(bs.RedirectGet, redirectFn) - } -} - // New creates a new BlossomServer with the given relay and service URL // Optional configuration can be provided via functional options -func New(rl *khatru.Relay, serviceURL string, opts ...ServerOption) *BlossomServer { +func New(rl *khatru.Relay, serviceURL string) *BlossomServer { bs := &BlossomServer{ ServiceURL: serviceURL, } - // Apply any provided options - for _, opt := range opts { - opt(bs) - } - base := rl.Router() mux := http.NewServeMux() @@ -103,40 +90,3 @@ func New(rl *khatru.Relay, serviceURL string, opts ...ServerOption) *BlossomServ return bs } - -// redirectGet returns a function that redirects to a specified URL template with the given status code. -// The URL template can include {sha256} and/or {extension} placeholders that will be replaced -// with the actual values. If neither placeholder is present, {sha256}.{extension} will be -// appended to the URL with proper forward slash handling. -func redirectGet(urlTemplate string, statusCode int) func(context.Context, string, string) (url string, code int, err error) { - return func(ctx context.Context, sha256 string, extension string) (string, int, error) { - finalURL := urlTemplate - - // Replace placeholders if they exist - hasSHA256Placeholder := strings.Contains(finalURL, "{sha256}") - hasExtensionPlaceholder := strings.Contains(finalURL, "{extension}") - - if hasSHA256Placeholder { - finalURL = strings.Replace(finalURL, "{sha256}", sha256, -1) - } - - if hasExtensionPlaceholder { - finalURL = strings.Replace(finalURL, "{extension}", extension, -1) - } - - // If neither placeholder is present, append sha256.extension - if !hasSHA256Placeholder && !hasExtensionPlaceholder { - // Ensure URL ends with a forward slash - if !strings.HasSuffix(finalURL, "/") { - finalURL += "/" - } - - finalURL += sha256 - if extension != "" { - finalURL += "." + extension - } - } - - return finalURL, statusCode, nil - } -} diff --git a/docs/core/blossom.md b/docs/core/blossom.md index a92cf41..c4db949 100644 --- a/docs/core/blossom.md +++ b/docs/core/blossom.md @@ -51,48 +51,40 @@ You can integrate any storage backend by implementing the three core functions: Blossom supports redirection to external storage locations when retrieving blobs. This is useful when you want to serve files from a CDN or cloud storage service while keeping Blossom compatibility. -### Simple Redirect - -You can use the `WithRedirectURL` option when creating your Blossom server to enable this functionality: +You can implement a custom redirect function. This function should return a string with the redirect URL and an HTTP status code. +Here's an example that redirects to a templated URL: ```go -// Create blossom server with redirection enabled -bl := blossom.New( - relay, - "http://localhost:3334", - blossom.WithRedirectURL("https://blossom.exampleserver.com", http.StatusMovedPermanently), -) +import "github.com/fiatjaf/khatru/policies" + +// ... + +bl.RedirectGet = append(bl.RedirectGet, policies.RedirectGet("https://blossom.example.com", http.StatusMovedPermanently)) ``` -By default the `WithRedirectURL` option will append the blob's SHA256 hash and file extension to the redirect URL. -For example, if the blob's SHA256 hash is `b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553` and the file extension is `pdf`, -the redirect URL will be https://blossom.exampleserver.com/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf. +The `RedirectGet` hook will append the blob's SHA256 hash and file extension to the redirect URL. -### Redirect URL placeholders +For example, if the blob's SHA256 hash is `b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553` and the file extension is `pdf`, the redirect URL will be `https://blossom.exampleserver.com/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf`. You can also customize the redirect URL by passing `{sha256}` and `{extension}` placeholders in the URL. For example: ```go -bl := blossom.New( - relay, - "http://localhost:3334", - blossom.WithRedirectURL("https://mybucket.myblobstorage.com/{sha256}.{extension}?ref=xxxx", http.StatusFound), -) +bl.RedirectGet = append(bl.RedirectGet, policies.RedirectGet("https://mybucket.myblobstorage.com/{sha256}.{extension}?ref=xxxx", http.StatusFound)) ``` -### Custom Redirect Function - -If you need more control over the redirect URL, you can implement a custom redirect function. This function should return a string with the redirect URL and an HTTP status code. +If you need more control over the redirect URL, you can implement a custom redirect function from scratch. This function should return a string with the redirect URL and an HTTP status code. ```go bl.RedirectGet = append(bl.RedirectGet, func(ctx context.Context, sha256 string, ext string) (string, int, error) { // generate a custom redirect URL - cid := IpfsCID(sha256) - redirectURL := fmt.Sprintf("https://ipfs.io/ipfs/%s/%s.%s", cid, sha256, ext) + cid := IPFSCID(sha256) + redirectURL := fmt.Sprintf("https://ipfs.io/ipfs/%s/%s.%s", cid, sha256, ext) return redirectURL, http.StatusTemporaryRedirect, nil }) ``` +This URL must include the sha256 hash somewhere. If you return an empty string `""` as the URL, your redirect call will be ignored and the next one in the chain (if any) will be called. + ## Upload Restrictions You can implement upload restrictions using the `RejectUpload` hook. Here's an example that limits file size and restricts uploads to whitelisted users: diff --git a/go.mod b/go.mod index 1f5db35..2835f4b 100644 --- a/go.mod +++ b/go.mod @@ -63,10 +63,10 @@ require ( go.opentelemetry.io/otel v1.32.0 // indirect go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/otel/trace v1.32.0 // indirect - golang.org/x/arch v0.15.0 // indirect + golang.org/x/arch v0.16.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/net v0.37.0 // indirect - golang.org/x/sys v0.31.0 // indirect + golang.org/x/sys v0.32.0 // indirect google.golang.org/protobuf v1.36.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b0de1e8..13385de 100644 --- a/go.sum +++ b/go.sum @@ -14,13 +14,10 @@ github.com/aquasecurity/esquery v0.2.0 h1:9WWXve95TE8hbm3736WB7nS6Owl8UGDeu+0jiy github.com/aquasecurity/esquery v0.2.0/go.mod h1:VU+CIFR6C+H142HHZf9RUkp4Eedpo9UrEKeCQHWf9ao= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= -github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= -github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g= -github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= @@ -34,8 +31,6 @@ github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCy github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= -github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -134,8 +129,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/nbd-wtf/go-nostr v0.51.7 h1:dGjtaaFQ1kA3H+vF8wt9a9WYl54K8C0JmVDf4cp+a4A= -github.com/nbd-wtf/go-nostr v0.51.7/go.mod h1:d6+DfvMWYG5pA3dmNMBJd6WCHVDDhkXbHqvfljf0Gzg= github.com/nbd-wtf/go-nostr v0.51.8 h1:CIoS+YqChcm4e1L1rfMZ3/mIwTz4CwApM2qx7MHNzmE= github.com/nbd-wtf/go-nostr v0.51.8/go.mod h1:d6+DfvMWYG5pA3dmNMBJd6WCHVDDhkXbHqvfljf0Gzg= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -185,8 +178,8 @@ go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZ go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= -golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= -golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= +golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U= +golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -211,8 +204,8 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/policies/blossom.go b/policies/blossom.go new file mode 100644 index 0000000..0eddb89 --- /dev/null +++ b/policies/blossom.go @@ -0,0 +1,43 @@ +package policies + +import ( + "context" + "strings" +) + +// RedirectGet returns a function that redirects to a specified URL template with the given status code. +// The URL template can include {sha256} and/or {extension} placeholders that will be replaced +// with the actual values. If neither placeholder is present, {sha256}.{extension} will be +// appended to the URL with proper forward slash handling. +func RedirectGet(urlTemplate string, statusCode int) func(context.Context, string, string) (url string, code int, err error) { + return func(ctx context.Context, sha256 string, extension string) (string, int, error) { + finalURL := urlTemplate + + // Replace placeholders if they exist + hasSHA256Placeholder := strings.Contains(finalURL, "{sha256}") + hasExtensionPlaceholder := strings.Contains(finalURL, "{extension}") + + if hasSHA256Placeholder { + finalURL = strings.Replace(finalURL, "{sha256}", sha256, -1) + } + + if hasExtensionPlaceholder { + finalURL = strings.Replace(finalURL, "{extension}", extension, -1) + } + + // If neither placeholder is present, append sha256.extension + if !hasSHA256Placeholder && !hasExtensionPlaceholder { + // Ensure URL ends with a forward slash + if !strings.HasSuffix(finalURL, "/") { + finalURL += "/" + } + + finalURL += sha256 + if extension != "" { + finalURL += "." + extension + } + } + + return finalURL, statusCode, nil + } +}