nips icon indicating copy to clipboard operation
nips copied to clipboard

NIP-XX Decentralized Web Hosting on Nostr

Open studiokaiji opened this issue 1 year ago • 39 comments

Decentralized Web Hosting on Nostr

By recording HTML, CSS, and JS on the Nostr relay, it becomes possible to create a decentralized web hosting solution that eliminates the need for centralized servers. Web servers or Nostr clients retrieve these recorded data, transform them into appropriate forms, and deliver them.

Reasons for Hosting on Nostr

  • Tamper-resistant through public key-based integrity
  • Fault tolerance through deployment on multiple relays
  • Resistance to blocking due to the distribution of web servers or clients
  • Faster retrieval speed compared to IPFS's DHT

Proposed Approach

Each HTML, CSS, and JS file is assigned a kind for identification.

  • HTML: kind: 5392
  • CSS: kind: 5393
  • JS: kind: 5394

The "content" field contains the content of the file. However, internal links (href, src, etc.) referenced within should be replaced with event IDs.

Example: <link rel="stylesheet" href="066b7ca0b167f0adad5c6d619ab1177050423e3979e83b8dfa069992533bdcf5">

Implementation on Web Server or Client

Access events using /e/{event_id}. Since event IDs are specified for each internal link, opening an HTML file enables automatic retrieval of data from this endpoint.

Implementation Example (Golang)

r.GET("/e/:idHex", func(ctx *gin.Context) {
	id := ctx.Param("idHex")

	// Fetch data from nostr pool
	ev := pool.QuerySingle(ctx, allRelays, nostr.Filter{
		Kinds: []int{consts.KindWebhostHTML, consts.KindWebhostCSS, consts.KindWebhostJS, consts.KindWebhostPicture},
		IDs:   []string{id},
	})

	if ev != nil {
		// Return data with content-type adapted to kind
		switch ev.Kind {
		case consts.KindWebhostHTML:
			ctx.Data(http.StatusOK, "text/html", []byte(ev.Content))
		case consts.KindWebhostCSS:
			ctx.Data(http.StatusOK, "text/css", []byte(ev.Content))
		case consts.KindWebhostJS:
			ctx.Data(http.StatusOK, "text/javascript", []byte(ev.Content))
		default:
			ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
		}
	} else {
		ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
	}

	return
})

Replaceable Decentralized Web Hosting

Additionally, this proposal can be extended to incorporate the NIP-33 based decentralized web hosting specification. This allows tracking of website data with a single identifier, keeping URL paths immutable.

Following the NIP-33 specification, the "kind" would be as follows.

  • HTML: kind: 35392
  • CSS: kind: 35393
  • JS: kind: 35394

Identifiers must be included within the "d" tag.

Example

{
	...,
	"kind": 35392,
	"tags": [["e", "hostr-lp"]]
}

Moreover, internal links within the "content" should be assigned NIP-33 identifiers instead of event IDs.

Identifier Example: [html_identifier]-[filepath(replace '/' with a character that can be included in the URL path e.g. '_')]

Link Example: <link rel="stylesheet" href="hostr-lp-_assets_index-ab834f60.css">

Implementation on Web Server or Client

Events can be accessed through /p/{npub_or_hex}/d/{d_tag}.

Implementation Example (Golang)

r.GET("/p/:pubKey/d/:dTag", func(ctx *gin.Context) {
	pubKey := ctx.Param("pubKey")
	// decode npub
	if pubKey[0:4] == "npub" {
		_, v, err := nip19.Decode(pubKey)
		if err != nil {
			ctx.String(http.StatusBadRequest, "Invalid npub")
			return
		}
		pubKey = v.(string)
	}
	// Add authors filter
	authors := []string{pubKey}

	// Add #d tag to filter
	dTag := ctx.Param("dTag")
	tags := nostr.TagMap{}
	tags["d"] = []string{dTag}

	// Fetch data from pool
	ev := pool.QuerySingle(ctx, allRelays, nostr.Filter{
		Kinds: []int{
			consts.KindWebhostReplaceableHTML,
			consts.KindWebhostReplaceableCSS,
			consts.KindWebhostReplaceableJS,
		},
		Authors: authors,
		Tags:    tags,
	})
	if ev != nil {
		// Return data with content-type adapted to kind
		switch ev.Kind {
		case consts.KindWebhostReplaceableHTML:
			ctx.Data(http.StatusOK, "text/html", []byte(ev.Content))
		case consts.KindWebhostReplaceableCSS:
			ctx.Data(http.StatusOK, "text/css", []byte(ev.Content))
		case consts.KindWebhostReplaceableJS:
			ctx.Data(http.StatusOK, "text/javascript", []byte(ev.Content))
		default:
			ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
		}
	} else {
		ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
	}

	return
})

Web Server Implementation Vulnerabilities

The current web server implementation allows access to websites within a single domain. While this reduces the implementation complexity on the server side and provides resilience against blocking, it is not suitable for use with domain-based authorization systems (such as NIP-07). For instance, if signing is permitted for Nostr clients on the web hosting relay, it would grant permission for all web pages hosted on that relay, making it vulnerable to spam postings.

Implementation

Repository: https://github.com/studiokaiji/nostr-webhost

Example Implementation: https://h.hostr.cc/p/a5a44e2a531efcc86491c4b9a3fa67daee8f60c9d2757a12eed95d98c5d6fc42/d/hostr-lp

studiokaiji avatar Aug 26 '23 10:08 studiokaiji

Event id is obtained by following structure. So if you change the content, IDs must be changed.

[
  0,
  <pubkey, as a lowercase hex string>,
  <created_at, as a number>,
  <kind, as a number>,
  <tags, as an array of arrays of non-null strings>,
  <content, as a string>
]

If you provide an original ID system (hostr-id) for hostr different from the ID on nostr, and put the hostr-id in note1, you may be able to access /e/hostr-id. If you rewrite content, you will want to rewrite note1 content (hostr-id should be written), so I think you should provide a kind in the range 10000 <= n < 20000.

mattn avatar Aug 26 '23 11:08 mattn

A note with kind: 10000 <= n < 20000 is a unique note for pubkey and kind. When you record the hostr id in note1, does that mean you write the identifiers of multiple websites in one note?

studiokaiji avatar Aug 26 '23 12:08 studiokaiji

My implementation (AsaiToshiya/brostr) uses NIP-21 nostr:nevent1... for links.

<link rel="stylesheet" href="nostr:nevent1qqsqv6mu5zck0u9d44wx6cv6kythq5zz8cuhn6pm3haqdxvj2vaaeagvsfd05">

NIP-19 is also fine, I think this is more unified and has more informative.

AsaiToshiya avatar Aug 26 '23 13:08 AsaiToshiya

Super nice!

Replaceable Decentralized Web Hosting

The replaceable events idea only work for things the pubkey controls. It doesn't make sense for pubkey A's website to reference a pubkey B that has a jQuery in a replaceable event because that event can be independently changed to a scam script and it will be immediately live on pubkey A's website. Replaceable events should be used only to link files within the pubkey and, in that use case, the d-tag can then be a stringified file directory+name (my_site/src/index.css) .

Events can be accessed through /p/{npub_or_hex}/d/{event_id}.

should be something like /a/{hex}/d/{d_tag}. Don't do OR in a spec (unnecessary complications), and event_ID is not replaceable. It will change every new version. The d-tag is stable across versions.

Event id is obtained by following structure...

I couldn't find where this proposal is changing the hash of event.id.

kind: 10000 <= n < 20000

It doesn't make much sense because these types of events only offer 1 event per event type. There is no d-tag to differentiate them.

My implementation (AsaiToshiya/brostr) uses NIP-21 nostr:nevent1... for links.

This makes more sense than hexes because it includes information that helps the webserver find the event in multiple relays.

vitorpamplona avatar Aug 26 '23 13:08 vitorpamplona

My implementation (AsaiToshiya/brostr) uses NIP-21 nostr:nevent1... for links.

I think using NIP-21 is a great idea and should definitely be adopted.

The replaceable events idea only work for things the pubkey controls.

Implementing replaceable events without affecting other sites is required.

How about assigning public keys or similar to subdomains? While this will increase the implementation cost for web server administrators, who would need to specify subdomains using a wildcard, it can minimize the impact on other sites.

should be something like /a/{hex}/d/{d_tag}.

I don't think NIP-19 is a must, but from a usability perspective, it's worth considering thoroughly on the web server side. Therefore, I think it's okay to include it in the specifications. What do you think?

studiokaiji avatar Aug 26 '23 14:08 studiokaiji

The root problem is how to replace HTML content posted at a specific URL with different content that keep the same URL. Thus, you can't use note1 for the URL at least, I think. Right?

mattn avatar Aug 26 '23 14:08 mattn

Here's a cool thing. Deno is similar to Node, except you can import modules by URL. With this, it's possible to import JavaScript files hosted on Nostr directly into Deno code.

IMG_20230826_092600_370

So, you could use this not just for hosting websites, but for hosting libraries.

Now, is it a good idea? Probably not, at least not yet. If web browsers (and Deno) added native support for Nostr URIs it would be more appealing. Even then I'm still not sure. It's experimental, and we're waiting for the gem to be uncovered.

alexgleason avatar Aug 26 '23 14:08 alexgleason

The root problem is how to replace HTML content posted at a specific URL with different content that keep the same URL. Thus, you can use note1 for the URL at least, I think. Right?

You can use a note1 or nevent1 with a link to the replaceable event. It's the same if you use /a/{pub_key_hex}/d/{d_tag}. It's just a different format.

vitorpamplona avatar Aug 26 '23 14:08 vitorpamplona

It's just a different format.

make sense.

mattn avatar Aug 26 '23 14:08 mattn

My implementation (AsaiToshiya/brostr) uses NIP-21 nostr:nevent1... for links.

<link rel="stylesheet" href="nostr:nevent1qqsqv6mu5zck0u9d44wx6cv6kythq5zz8cuhn6pm3haqdxvj2vaaeagvsfd05">

nostr:nevent1... also used to link to other events.

AsaiToshiya avatar Aug 26 '23 18:08 AsaiToshiya

I didn't read it very carefully, so there may have been something I missed.
But why use an event for each file type? Wouldn't it be better to have a single kind with a TAG specifying the data format? Well, that's how it works for the numerous formats that exist and are currently used.

frbitten avatar Aug 26 '23 21:08 frbitten

Mostly because tags are case sensitive and we have had issues in other events (hashtags are a classic problem) when filtering for events of a certain type in all possible character cases. :(

vitorpamplona avatar Aug 26 '23 22:08 vitorpamplona

I agree that inserting the Content-Type into tags can make filtering more difficult, but such cases may be rare. Because most of the time, data is retrieved using the event id or d tag. I apologize if I've overlooked anything🙇‍♀.

studiokaiji avatar Aug 27 '23 00:08 studiokaiji

Something like a search engine using NIP-50 would be one use case.

AsaiToshiya avatar Aug 27 '23 02:08 AsaiToshiya

I'm with @frbitten on using tags to indicate file types instead of assigning kinds for each possible file type, especially if you have a vision for extending this proposal to allow hosting of arbitrary file types.

The inconvenience of filtering is not a strong reason to use separate kinds for different file types, since there is no need for the ability to filter files by content type for web hosting, I think.

We may make use of the m-tag defined in NIP-94 to indicate the content type of the content in MIME type format.

jiftechnify avatar Aug 27 '23 03:08 jiftechnify

I'm with @frbitten on using tags to indicate file types instead of assigning kinds for each possible file type, especially if you have a vision for extending this proposal to allow hosting of arbitrary file types.

I agree with the use of tags.

since there is no need for the ability to filter files by content type for web hosting

Search engine and other may want to search for only hosted HTML.

We may make use of the m-tag defined in NIP-94 to indicate the content type of the content in MIME type format.

I am concerned that MIME types are case sensitive.

AsaiToshiya avatar Aug 27 '23 12:08 AsaiToshiya

I agree that inserting the Content-Type into tags can make filtering more difficult, but such cases may be rare. Because most of the time, data is retrieved using the event id or d tag. I apologize if I've overlooked anything🙇‍♀.

For a web browser application, sure.

But this isn't just for web browsers. I can do a js minifier Data Vending Machine and for that, I need to filter only JS and process them. Or maybe I do a JS to web assembly compiler. In both cases, one can injest the event type and create a new event (on a new type) with the resulting compilation.

Different kinds also help storage systems easily pick what they want to store, which kinds they allow on their relays.

An important distinction is between media and text Mine types. If we merge them as one type, relays will need to say things like "we support nip-xx/kind-Y, but only text ones in the x, y, z mime types". It becomes messy.

Also, there is no shortage of event kinds. We can create as many as we want.

vitorpamplona avatar Aug 27 '23 12:08 vitorpamplona

I see. Being able to specify a 'kind' to differentiate the files that the relay accepts seems good, considering the versatility of this system.

studiokaiji avatar Aug 27 '23 13:08 studiokaiji

How about adding rule to m tag? It can be filtered by #m. m tag MUST be lower case.

The allow-list is essentially the same between kind and m tag unless define separated NIPs as NIP-11 supported_nips.

SnowCait avatar Aug 27 '23 13:08 SnowCait

I agree that inserting the Content-Type into tags can make filtering more difficult, but such cases may be rare. Because most of the time, data is retrieved using the event id or d tag. I apologize if I've overlooked anything🙇‍♀.

For a web browser application, sure.

But this isn't just for web browsers. I can do a js minifier Data Vending Machine and for that, I need to filter only JS and process them. Or maybe I do a JS to web assembly compiler. In both cases, one can injest the event type and create a new event (on a new type) with the resulting compilation.

Different kinds also help storage systems easily pick what they want to store, which kinds they allow on their relays.

An important distinction is between media and text Mine types. If we merge them as one type, relays will need to say things like "we support nip-xx/kind-Y, but only text ones in the x, y, z mime types". It becomes messy.

Also, there is no shortage of event kinds. We can create as many as we want.

Ok. It's a valid reason. I don't know if it's enough, but I understand the option. So I suggest that NIP defines a range of allowed kinds and a list of each type and its respective kind. As we have NIPs that define kinds 10000, 20000, 30000, etc. So to avoid that in the future we have numbers of random kinds and having to make chains of IFs to support. It's a simple way to bypass relays that aren't interested in this feature.

It might be interesting to use the NIP-94 in this idea. So I can make a "site" that uses part hosted on the relay and part hosted in other ways via NIP-94 that contains the external URL.

frbitten avatar Aug 27 '23 13:08 frbitten

How about adding rule to m tag? It can be filtered by #m. m tag MUST be lower case.

The allow-list is essentially the same between kind and m tag unless define separated NIPs as NIP-11 supported_nips.

@vitorpamplona What do you think about this?

But this isn't just for web browsers. I can do a js minifier Data Vending Machine and for that, I need to filter only JS and process them. Or maybe I do a JS to web assembly compiler. In both cases, one can injest the event type and create a new event (on a new type) with the resulting compilation.

We can keep unchanged naddr1... by using m tag.

AsaiToshiya avatar Sep 04 '23 09:09 AsaiToshiya

I see. Being able to specify a 'kind' to differentiate the files that the relay accepts seems good, considering the versatility of this system.

That said, I think it is somewhat clear what MIME types this NIP supports.

AsaiToshiya avatar Sep 04 '23 09:09 AsaiToshiya

Here's a cool thing. Deno is similar to Node, except you can import modules by URL. With this, it's possible to import JavaScript files hosted on Nostr directly into Deno code.

As in this example, if there is a relay that only hosts JavaScript, and if filtering is possible with the "kind" parameter, there is no need for additional feature implementation in the relay. However, defining "kind" for each Content-Type is not practical, so some level of selection or an alternative solution is necessary, I believe.

studiokaiji avatar Sep 05 '23 22:09 studiokaiji

We might not need a new kind for every mime/type. We just new kinds when there is a chance relays and clients can use that information to simplify their work. We can have these 3 types + the NIP-94/95/96/97 types (with the mime tag) for now. Maybe there are other mime types worth considering for their own kind, but we can leave that decision for later when the need arises.

vitorpamplona avatar Sep 05 '23 22:09 vitorpamplona

text files would not need to use NIP-95 or some variation. I think there could be a NIP to define a kind or tag to inform the format of what is in "content", it would be a generic solution for several uses.

frbitten avatar Sep 06 '23 17:09 frbitten

The problem with using m tag has been resolved.

I prefer m tag because it will be one kind for one purpose, but I'll go along with the consensus.

AsaiToshiya avatar Sep 27 '23 08:09 AsaiToshiya

Sorry, the link above to this is wrong.

AsaiToshiya avatar Sep 29 '23 03:09 AsaiToshiya

If we have one or two serious implementers of this idea, we should move it to a Draft PR.

vitorpamplona avatar Sep 29 '23 13:09 vitorpamplona

Based on the discussions we've had here, I'd like to propose the following changes and simultaneously work on their implementation:

  1. Switch to a URL format that does not use NIP-19 (npub).
  2. Modify the <d> tags for internal links in replaceable events.
  3. Utilize references using NIP-21 (nevent) (limited to non-replaceable ones).

Regarding the ongoing discussion about how to identify the MIME type of files, whether to use the m tag or differentiate by kind, both approaches have their merits. However, it seems that the specification of differentiating by kind has no significant drawbacks other than allowing multiple kind for a single purpose. Therefore, I'd like to keep it as it is for now.

Do you think it's okay to proceed with the above changes? If there are any further points to discuss or any aspects that we might have missed, I'd appreciate your input.

studiokaiji avatar Oct 04 '23 22:10 studiokaiji

What's this URL format switch? I am not sure what it is about.

vitorpamplona avatar Oct 04 '23 22:10 vitorpamplona