mirror of
https://github.com/fiatjaf/khatru.git
synced 2026-02-27 18:08:38 +01:00
refactor @aaccioly's blossom redirect thing.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
4
go.mod
4
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
|
||||
)
|
||||
|
||||
15
go.sum
15
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=
|
||||
|
||||
43
policies/blossom.go
Normal file
43
policies/blossom.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user