diff --git a/hostr/cmd/server/server.go b/hostr/cmd/server/server.go index 4dc57ef..61043d6 100644 --- a/hostr/cmd/server/server.go +++ b/hostr/cmd/server/server.go @@ -2,17 +2,18 @@ package server import ( "context" - "encoding/base64" "net/http" + "strings" "github.com/gin-gonic/gin" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" "github.com/studiokaiji/nostr-webhost/hostr/cmd/consts" "github.com/studiokaiji/nostr-webhost/hostr/cmd/relays" + "github.com/studiokaiji/nostr-webhost/hostr/cmd/tools" ) -func Start(port string) { +func Start(port string, mode string) { ctx := context.Background() allRelays, err := relays.GetAllRelays() @@ -27,6 +28,19 @@ func Start(port string) { r.GET("/e/:hex_or_nevent", func(ctx *gin.Context) { hexOrNevent := ctx.Param("hex_or_nevent") + subdomainPubKey := "" + + if mode == "secure" { + // modeがsecureの場合、サブドメインにnpubが含まれていないルーティングは許可しない + host := ctx.Request.Host + subdomain := strings.Split(host, ".")[0] + subdomainPubKey, err = tools.ResolvePubKey(subdomain) + if err != nil { + ctx.String(http.StatusBadRequest, "Routing without npub in the subdomain is not allowed") + return + } + } + ids := []string{} // neventからIDを取得 @@ -49,98 +63,22 @@ func Start(port string) { ids = append(ids, hexOrNevent) } - // Poolからデータを取得する - ev := pool.QuerySingle(ctx, allRelays, nostr.Filter{ + filter := nostr.Filter{ Kinds: []int{consts.KindWebhostHTML, consts.KindWebhostCSS, consts.KindWebhostJS, consts.KindWebhostPicture}, IDs: ids, - }) - if ev != nil { - switch ev.Kind { - case consts.KindWebhostHTML: - ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(ev.Content)) - case consts.KindWebhostCSS: - ctx.Data(http.StatusOK, "text/css; charset=utf-8", []byte(ev.Content)) - case consts.KindWebhostJS: - ctx.Data(http.StatusOK, "text/javascript; charset=utf-8", []byte(ev.Content)) - case consts.KindWebhostPicture: - { - eTag := ev.Tags.GetFirst([]string{"e"}) - mTag := ev.Tags.GetFirst([]string{"m"}) - - if eTag == nil || mTag == nil { - ctx.String(http.StatusBadRequest, http.StatusText((http.StatusBadRequest))) - return - } - - evData := pool.QuerySingle(ctx, allRelays, nostr.Filter{ - IDs: []string{eTag.Value()}, - }) - - if evData == nil { - ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound)) - return - } - - data, err := base64.StdEncoding.DecodeString(evData.Content) - if err != nil { - ctx.String(http.StatusBadRequest, http.StatusText((http.StatusBadRequest))) - return - } - - ctx.Data(http.StatusOK, mTag.Value(), data) - } - default: - ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound)) - } - } else { - ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound)) } - - return - }) - - // Replaceable Event (NIP-33) - r.GET("/p/:pubKey/d/*dTag", func(ctx *gin.Context) { - // pubKeyを取得しFilterに追加 - pubKey := ctx.Param("pubKey") - // npubから始まる場合はデコードする - if pubKey[0:4] == "npub" { - _, v, err := nip19.Decode(pubKey) - if err != nil { - ctx.String(http.StatusBadRequest, "Invalid npub") - return - } - pubKey = v.(string) + if mode == "secure" { + filter.Authors = []string{subdomainPubKey} } - authors := []string{pubKey} - - // dTagを取得しFilterに追加 - // dTagの最初は`/`ではじまるのでそれをslice - dTag := ctx.Param("dTag")[1:] - - tags := nostr.TagMap{} - tags["d"] = []string{dTag} // Poolからデータを取得する - ev := pool.QuerySingle(ctx, allRelays, nostr.Filter{ - Kinds: []int{ - consts.KindWebhostReplaceableHTML, - consts.KindWebhostReplaceableCSS, - consts.KindWebhostReplaceableJS, - }, - Authors: authors, - Tags: tags, - }) + ev := pool.QuerySingle(ctx, allRelays, filter) if ev != nil { - switch ev.Kind { - case consts.KindWebhostReplaceableHTML: - ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(ev.Content)) - case consts.KindWebhostReplaceableCSS: - ctx.Data(http.StatusOK, "text/css; charset=utf-8", []byte(ev.Content)) - case consts.KindWebhostReplaceableJS: - ctx.Data(http.StatusOK, "text/javascript; charset=utf-8", []byte(ev.Content)) - default: + contentType, err := tools.GetContentType(ev.Kind) + if err != nil { ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound)) + } else { + ctx.Data(http.StatusOK, contentType, []byte(ev.Content)) } } else { ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound)) @@ -149,5 +87,94 @@ func Start(port string) { return }) + if mode != "secure" { + r.GET("/p/:pubKey/d/*dTag", func(ctx *gin.Context) { + // pubKeyを取得しFilterに追加 + pubKey := ctx.Param("pubKey") + pubKey, err := tools.ResolvePubKey(pubKey) + if err != nil { + ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound)) + return + } + + authors := []string{pubKey} + + // dTagを取得しFilterに追加 + // dTagの最初は`/`ではじまるのでそれをslice + dTag := ctx.Param("dTag")[1:] + + tags := nostr.TagMap{} + tags["d"] = []string{dTag} + + // Poolからデータを取得する + ev := pool.QuerySingle(ctx, allRelays, nostr.Filter{ + Kinds: []int{ + consts.KindWebhostReplaceableHTML, + consts.KindWebhostReplaceableCSS, + consts.KindWebhostReplaceableJS, + }, + Authors: authors, + Tags: tags, + }) + if ev != nil { + contentType, err := tools.GetContentType(ev.Kind) + if err != nil { + ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound)) + } + ctx.Data(http.StatusOK, contentType, []byte(ev.Content)) + } else { + ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound)) + } + + return + }) + + } + + if mode != "normal" { + r.GET("/d/*dTag", func(ctx *gin.Context) { + host := ctx.Request.Host + subdomain := strings.Split(host, ".")[0] + + // subdomainからpubKeyを取得しFilterに追加 + pubKey, err := tools.ResolvePubKey(subdomain) + if err != nil { + ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound)) + return + } + + authors := []string{pubKey} + + // dTagを取得しFilterに追加 + // dTagの最初は`/`ではじまるのでそれをslice + dTag := ctx.Param("dTag")[1:] + + tags := nostr.TagMap{} + tags["d"] = []string{dTag} + + // Poolからデータを取得する + ev := pool.QuerySingle(ctx, allRelays, nostr.Filter{ + Kinds: []int{ + consts.KindWebhostReplaceableHTML, + consts.KindWebhostReplaceableCSS, + consts.KindWebhostReplaceableJS, + }, + Authors: authors, + Tags: tags, + }) + if ev != nil { + contentType, err := tools.GetContentType(ev.Kind) + if err != nil { + ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound)) + } + ctx.Data(http.StatusOK, contentType, []byte(ev.Content)) + } else { + ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound)) + } + + return + }) + } + r.Run(":" + port) } diff --git a/hostr/cmd/tools/getContentType.go b/hostr/cmd/tools/getContentType.go new file mode 100644 index 0000000..4d25943 --- /dev/null +++ b/hostr/cmd/tools/getContentType.go @@ -0,0 +1,19 @@ +package tools + +import ( + "fmt" + + "github.com/studiokaiji/nostr-webhost/hostr/cmd/consts" +) + +func GetContentType(kind int) (string, error) { + if kind == consts.KindWebhostHTML || kind == consts.KindWebhostReplaceableHTML { + return "text/html; charset=utf-8", nil + } else if kind == consts.KindWebhostCSS || kind == consts.KindWebhostReplaceableCSS { + return "text/css; charset=utf-8", nil + } else if kind == consts.KindWebhostJS || kind == consts.KindWebhostReplaceableJS { + return "text/javascript; charset=utf-8", nil + } else { + return "", fmt.Errorf("Invalid Kind") + } +} diff --git a/hostr/cmd/tools/resolvePubKey.go b/hostr/cmd/tools/resolvePubKey.go new file mode 100644 index 0000000..e3732dd --- /dev/null +++ b/hostr/cmd/tools/resolvePubKey.go @@ -0,0 +1,24 @@ +package tools + +import ( + "fmt" + + "github.com/nbd-wtf/go-nostr/nip19" +) + +func ResolvePubKey(npubOrHex string) (string, error) { + // npubから始まる場合はデコードする + if npubOrHex[0:4] == "npub" { + _, v, err := nip19.Decode(npubOrHex) + if err != nil { + return "", fmt.Errorf("Invalid npub") + } + return v.(string), nil + } else { + _, err := nip19.EncodePublicKey(npubOrHex) + if err != nil { + return "", fmt.Errorf("Invalid pubkey") + } + } + return npubOrHex, nil +} diff --git a/hostr/main.go b/hostr/main.go index 55691d6..24f5c43 100644 --- a/hostr/main.go +++ b/hostr/main.go @@ -17,10 +17,6 @@ import ( var cuteOstrich string func main() { - var ( - port string - ) - app := &cli.App{ Commands: []*cli.Command{ { @@ -143,15 +139,28 @@ func main() { Usage: "🕺 Wake up web server", Flags: []cli.Flag{ &cli.StringFlag{ - Name: "port", - Aliases: []string{"p"}, - Value: "3000", - Usage: "Web server port", - Destination: &port, + Name: "port", + Aliases: []string{"p"}, + Value: "3000", + Usage: "Web server port", + }, + &cli.StringFlag{ + Name: "mode", + Aliases: []string{"m"}, + Value: "normal", + Usage: "🧪 Experimental: Enabled subdomain-based access in replaceable events.", + Action: func(ctx *cli.Context, v string) error { + if v != "normal" && v != "hybrid" && v != "secure" { + return fmt.Errorf("Invalid mode flag. Must be 'normal', 'hybrid', or 'secure'.") + } + return nil + }, }, }, Action: func(ctx *cli.Context) error { - server.Start(port) + port := ctx.String("port") + mode := ctx.String("mode") + server.Start(port, mode) return nil }, },