public pages, rate limiters
15
config.go
@@ -1,12 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
@@ -146,6 +146,17 @@ func getEnvInt(key string, defaultValue int) int {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func getEnvBool(key string, defaultValue bool) bool {
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
boolValue, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return boolValue
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
var art = `
|
||||
██╗ ██╗ █████╗ ██╗ ██╗███████╗███╗ ██╗
|
||||
██║ ██║██╔══██╗██║ ██║██╔════╝████╗ ██║
|
||||
|
||||
@@ -10,7 +10,7 @@ services:
|
||||
- "./db:/app/db"
|
||||
- "./haven:/app/haven"
|
||||
ports:
|
||||
- "3335"
|
||||
- "3355"
|
||||
user: "${DOCKER_UID:-1000}:${DOCKER_GID:-1000}"
|
||||
|
||||
tor:
|
||||
|
||||
@@ -10,5 +10,5 @@ services:
|
||||
- "./db:/app/db"
|
||||
- "./haven:/app/haven"
|
||||
ports:
|
||||
- "3335:3335"
|
||||
- "3355:3355"
|
||||
user: "${DOCKER_UID:-1000}:${DOCKER_GID:-1000}"
|
||||
|
||||
106
init.go
@@ -1,8 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/fiatjaf/eventstore/lmdb"
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/fiatjaf/khatru/policies"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -66,6 +69,8 @@ func initRelays() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
initRelayLimits()
|
||||
|
||||
privateRelay.Info.Name = config.PrivateRelayName
|
||||
privateRelay.Info.PubKey = nPubToPubkey(config.PrivateRelayNpub)
|
||||
privateRelay.Info.Description = config.PrivateRelayDescription
|
||||
@@ -74,6 +79,31 @@ func initRelays() {
|
||||
privateRelay.Info.Software = config.RelaySoftware
|
||||
privateRelay.ServiceURL = "https://" + config.RelayURL + "/private"
|
||||
|
||||
if !privateRelayLimits.AllowEmptyFilters {
|
||||
privateRelay.RejectFilter = append(privateRelay.RejectFilter, policies.NoEmptyFilters)
|
||||
}
|
||||
|
||||
if !privateRelayLimits.AllowComplexFilters {
|
||||
privateRelay.RejectFilter = append(privateRelay.RejectFilter, policies.NoComplexFilters)
|
||||
}
|
||||
|
||||
privateRelay.RejectEvent = append(privateRelay.RejectEvent,
|
||||
policies.RejectEventsWithBase64Media,
|
||||
policies.EventIPRateLimiter(
|
||||
privateRelayLimits.EventIPLimiterTokensPerInterval,
|
||||
time.Minute*time.Duration(privateRelayLimits.EventIPLimiterInterval),
|
||||
privateRelayLimits.EventIPLimiterMaxTokens,
|
||||
),
|
||||
)
|
||||
|
||||
privateRelay.RejectConnection = append(privateRelay.RejectConnection,
|
||||
policies.ConnectionRateLimiter(
|
||||
privateRelayLimits.ConnectionRateLimiterTokensPerInterval,
|
||||
time.Minute*time.Duration(privateRelayLimits.ConnectionRateLimiterInterval),
|
||||
privateRelayLimits.ConnectionRateLimiterMaxTokens,
|
||||
),
|
||||
)
|
||||
|
||||
chatRelay.Info.Name = config.ChatRelayName
|
||||
chatRelay.Info.PubKey = nPubToPubkey(config.ChatRelayNpub)
|
||||
chatRelay.Info.Description = config.ChatRelayDescription
|
||||
@@ -82,6 +112,31 @@ func initRelays() {
|
||||
chatRelay.Info.Software = config.RelaySoftware
|
||||
chatRelay.ServiceURL = "https://" + config.RelayURL + "/chat"
|
||||
|
||||
if !chatRelayLimits.AllowEmptyFilters {
|
||||
chatRelay.RejectFilter = append(chatRelay.RejectFilter, policies.NoEmptyFilters)
|
||||
}
|
||||
|
||||
if !chatRelayLimits.AllowComplexFilters {
|
||||
chatRelay.RejectFilter = append(chatRelay.RejectFilter, policies.NoComplexFilters)
|
||||
}
|
||||
|
||||
chatRelay.RejectEvent = append(chatRelay.RejectEvent,
|
||||
policies.RejectEventsWithBase64Media,
|
||||
policies.EventIPRateLimiter(
|
||||
chatRelayLimits.EventIPLimiterTokensPerInterval,
|
||||
time.Minute*time.Duration(chatRelayLimits.EventIPLimiterInterval),
|
||||
chatRelayLimits.EventIPLimiterMaxTokens,
|
||||
),
|
||||
)
|
||||
|
||||
chatRelay.RejectConnection = append(chatRelay.RejectConnection,
|
||||
policies.ConnectionRateLimiter(
|
||||
chatRelayLimits.ConnectionRateLimiterTokensPerInterval,
|
||||
time.Minute*time.Duration(chatRelayLimits.ConnectionRateLimiterInterval),
|
||||
chatRelayLimits.ConnectionRateLimiterMaxTokens,
|
||||
),
|
||||
)
|
||||
|
||||
outboxRelay.Info.Name = config.OutboxRelayName
|
||||
outboxRelay.Info.PubKey = nPubToPubkey(config.OutboxRelayNpub)
|
||||
outboxRelay.Info.Description = config.OutboxRelayDescription
|
||||
@@ -89,6 +144,31 @@ func initRelays() {
|
||||
outboxRelay.Info.Version = config.RelayVersion
|
||||
outboxRelay.Info.Software = config.RelaySoftware
|
||||
|
||||
if !outboxRelayLimits.AllowEmptyFilters {
|
||||
outboxRelay.RejectFilter = append(outboxRelay.RejectFilter, policies.NoEmptyFilters)
|
||||
}
|
||||
|
||||
if !outboxRelayLimits.AllowComplexFilters {
|
||||
outboxRelay.RejectFilter = append(outboxRelay.RejectFilter, policies.NoComplexFilters)
|
||||
}
|
||||
|
||||
outboxRelay.RejectEvent = append(outboxRelay.RejectEvent,
|
||||
policies.RejectEventsWithBase64Media,
|
||||
policies.EventIPRateLimiter(
|
||||
outboxRelayLimits.EventIPLimiterTokensPerInterval,
|
||||
time.Minute*time.Duration(outboxRelayLimits.EventIPLimiterInterval),
|
||||
outboxRelayLimits.EventIPLimiterMaxTokens,
|
||||
),
|
||||
)
|
||||
|
||||
outboxRelay.RejectConnection = append(outboxRelay.RejectConnection,
|
||||
policies.ConnectionRateLimiter(
|
||||
outboxRelayLimits.ConnectionRateLimiterTokensPerInterval,
|
||||
time.Minute*time.Duration(outboxRelayLimits.ConnectionRateLimiterInterval),
|
||||
outboxRelayLimits.ConnectionRateLimiterMaxTokens,
|
||||
),
|
||||
)
|
||||
|
||||
inboxRelay.Info.Name = config.InboxRelayName
|
||||
inboxRelay.Info.PubKey = nPubToPubkey(config.InboxRelayNpub)
|
||||
inboxRelay.Info.Description = config.InboxRelayDescription
|
||||
@@ -96,4 +176,30 @@ func initRelays() {
|
||||
inboxRelay.Info.Version = config.RelayVersion
|
||||
inboxRelay.Info.Software = config.RelaySoftware
|
||||
inboxRelay.ServiceURL = "https://" + config.RelayURL + "/inbox"
|
||||
|
||||
if !inboxRelayLimits.AllowEmptyFilters {
|
||||
inboxRelay.RejectFilter = append(inboxRelay.RejectFilter, policies.NoEmptyFilters)
|
||||
}
|
||||
|
||||
if !inboxRelayLimits.AllowComplexFilters {
|
||||
inboxRelay.RejectFilter = append(inboxRelay.RejectFilter, policies.NoComplexFilters)
|
||||
}
|
||||
|
||||
inboxRelay.RejectEvent = append(inboxRelay.RejectEvent,
|
||||
policies.RejectEventsWithBase64Media,
|
||||
policies.EventIPRateLimiter(
|
||||
inboxRelayLimits.EventIPLimiterTokensPerInterval,
|
||||
time.Minute*time.Duration(inboxRelayLimits.EventIPLimiterInterval),
|
||||
inboxRelayLimits.EventIPLimiterMaxTokens,
|
||||
),
|
||||
)
|
||||
|
||||
inboxRelay.RejectConnection = append(inboxRelay.RejectConnection,
|
||||
policies.ConnectionRateLimiter(
|
||||
inboxRelayLimits.ConnectionRateLimiterTokensPerInterval,
|
||||
time.Minute*time.Duration(inboxRelayLimits.ConnectionRateLimiterInterval),
|
||||
inboxRelayLimits.ConnectionRateLimiterMaxTokens,
|
||||
),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
113
limits.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
)
|
||||
|
||||
var (
|
||||
privateRelayLimits PrivateRelayLimits
|
||||
chatRelayLimits ChatRelayLimits
|
||||
inboxRelayLimits InboxRelayLimits
|
||||
outboxRelayLimits OutboxRelayLimits
|
||||
)
|
||||
|
||||
type PrivateRelayLimits struct {
|
||||
EventIPLimiterTokensPerInterval int
|
||||
EventIPLimiterInterval int
|
||||
EventIPLimiterMaxTokens int
|
||||
AllowEmptyFilters bool
|
||||
AllowComplexFilters bool
|
||||
ConnectionRateLimiterTokensPerInterval int
|
||||
ConnectionRateLimiterInterval int
|
||||
ConnectionRateLimiterMaxTokens int
|
||||
}
|
||||
|
||||
type ChatRelayLimits struct {
|
||||
EventIPLimiterTokensPerInterval int
|
||||
EventIPLimiterInterval int
|
||||
EventIPLimiterMaxTokens int
|
||||
AllowEmptyFilters bool
|
||||
AllowComplexFilters bool
|
||||
ConnectionRateLimiterTokensPerInterval int
|
||||
ConnectionRateLimiterInterval int
|
||||
ConnectionRateLimiterMaxTokens int
|
||||
}
|
||||
|
||||
type InboxRelayLimits struct {
|
||||
EventIPLimiterTokensPerInterval int
|
||||
EventIPLimiterInterval int
|
||||
EventIPLimiterMaxTokens int
|
||||
AllowEmptyFilters bool
|
||||
AllowComplexFilters bool
|
||||
ConnectionRateLimiterTokensPerInterval int
|
||||
ConnectionRateLimiterInterval int
|
||||
ConnectionRateLimiterMaxTokens int
|
||||
}
|
||||
|
||||
type OutboxRelayLimits struct {
|
||||
EventIPLimiterTokensPerInterval int
|
||||
EventIPLimiterInterval int
|
||||
EventIPLimiterMaxTokens int
|
||||
AllowEmptyFilters bool
|
||||
AllowComplexFilters bool
|
||||
ConnectionRateLimiterTokensPerInterval int
|
||||
ConnectionRateLimiterInterval int
|
||||
ConnectionRateLimiterMaxTokens int
|
||||
}
|
||||
|
||||
func initRelayLimits() {
|
||||
privateRelayLimits = PrivateRelayLimits{
|
||||
EventIPLimiterTokensPerInterval: getEnvInt("PRIVATE_RELAY_EVENT_IP_LIMITER_TOKENS_PER_INTERVAL", 50),
|
||||
EventIPLimiterInterval: getEnvInt("PRIVATE_RELAY_EVENT_IP_LIMITER_INTERVAL", 1),
|
||||
EventIPLimiterMaxTokens: getEnvInt("PRIVATE_RELAY_EVENT_IP_LIMITER_MAX_TOKENS", 100),
|
||||
AllowEmptyFilters: getEnvBool("PRIVATE_RELAY_ALLOW_EMPTY_FILTERS", true),
|
||||
AllowComplexFilters: getEnvBool("PRIVATE_RELAY_ALLOW_COMPLEX_FILTERS", true),
|
||||
ConnectionRateLimiterTokensPerInterval: getEnvInt("PRIVATE_RELAY_CONNECTION_RATE_LIMITER_TOKENS_PER_INTERVAL", 3),
|
||||
ConnectionRateLimiterInterval: getEnvInt("PRIVATE_RELAY_CONNECTION_RATE_LIMITER_INTERVAL", 5),
|
||||
ConnectionRateLimiterMaxTokens: getEnvInt("PRIVATE_RELAY_CONNECTION_RATE_LIMITER_MAX_TOKENS", 9),
|
||||
}
|
||||
|
||||
chatRelayLimits = ChatRelayLimits{
|
||||
EventIPLimiterTokensPerInterval: getEnvInt("CHAT_RELAY_EVENT_IP_LIMITER_TOKENS_PER_INTERVAL", 50),
|
||||
EventIPLimiterInterval: getEnvInt("CHAT_RELAY_EVENT_IP_LIMITER_INTERVAL", 1),
|
||||
EventIPLimiterMaxTokens: getEnvInt("CHAT_RELAY_EVENT_IP_LIMITER_MAX_TOKENS", 100),
|
||||
AllowEmptyFilters: getEnvBool("CHAT_RELAY_ALLOW_EMPTY_FILTERS", false),
|
||||
AllowComplexFilters: getEnvBool("CHAT_RELAY_ALLOW_COMPLEX_FILTERS", false),
|
||||
ConnectionRateLimiterTokensPerInterval: getEnvInt("CHAT_RELAY_CONNECTION_RATE_LIMITER_TOKENS_PER_INTERVAL", 3),
|
||||
ConnectionRateLimiterInterval: getEnvInt("CHAT_RELAY_CONNECTION_RATE_LIMITER_INTERVAL", 3),
|
||||
ConnectionRateLimiterMaxTokens: getEnvInt("CHAT_RELAY_CONNECTION_RATE_LIMITER_MAX_TOKENS", 9),
|
||||
}
|
||||
|
||||
inboxRelayLimits = InboxRelayLimits{
|
||||
EventIPLimiterTokensPerInterval: getEnvInt("INBOX_RELAY_EVENT_IP_LIMITER_TOKENS_PER_INTERVAL", 10),
|
||||
EventIPLimiterInterval: getEnvInt("INBOX_RELAY_EVENT_IP_LIMITER_INTERVAL", 1),
|
||||
EventIPLimiterMaxTokens: getEnvInt("INBOX_RELAY_EVENT_IP_LIMITER_MAX_TOKENS", 20),
|
||||
AllowEmptyFilters: getEnvBool("INBOX_RELAY_ALLOW_EMPTY_FILTERS", false),
|
||||
AllowComplexFilters: getEnvBool("INBOX_RELAY_ALLOW_COMPLEX_FILTERS", false),
|
||||
ConnectionRateLimiterTokensPerInterval: getEnvInt("INBOX_RELAY_CONNECTION_RATE_LIMITER_TOKENS_PER_INTERVAL", 3),
|
||||
ConnectionRateLimiterInterval: getEnvInt("INBOX_RELAY_CONNECTION_RATE_LIMITER_INTERVAL", 1),
|
||||
ConnectionRateLimiterMaxTokens: getEnvInt("INBOX_RELAY_CONNECTION_RATE_LIMITER_MAX_TOKENS", 9),
|
||||
}
|
||||
|
||||
outboxRelayLimits = OutboxRelayLimits{
|
||||
EventIPLimiterTokensPerInterval: getEnvInt("OUTBOX_RELAY_EVENT_IP_LIMITER_TOKENS_PER_INTERVAL", 10),
|
||||
EventIPLimiterInterval: getEnvInt("OUTBOX_RELAY_EVENT_IP_LIMITER_INTERVAL", 60),
|
||||
EventIPLimiterMaxTokens: getEnvInt("OUTBOX_RELAY_EVENT_IP_LIMITER_MAX_TOKENS", 100),
|
||||
AllowEmptyFilters: getEnvBool("OUTBOX_RELAY_ALLOW_EMPTY_FILTERS", false),
|
||||
AllowComplexFilters: getEnvBool("OUTBOX_RELAY_ALLOW_COMPLEX_FILTERS", false),
|
||||
ConnectionRateLimiterTokensPerInterval: getEnvInt("OUTBOX_RELAY_CONNECTION_RATE_LIMITER_TOKENS_PER_INTERVAL", 3),
|
||||
ConnectionRateLimiterInterval: getEnvInt("OUTBOX_RELAY_CONNECTION_RATE_LIMITER_INTERVAL", 1),
|
||||
ConnectionRateLimiterMaxTokens: getEnvInt("OUTBOX_RELAY_CONNECTION_RATE_LIMITER_MAX_TOKENS", 9),
|
||||
}
|
||||
|
||||
prettyPrintLimits("Private relay limits", privateRelayLimits)
|
||||
prettyPrintLimits("Chat relay limits", chatRelayLimits)
|
||||
prettyPrintLimits("Inbox relay limits", inboxRelayLimits)
|
||||
prettyPrintLimits("Outbox relay limits", outboxRelayLimits)
|
||||
}
|
||||
|
||||
func prettyPrintLimits(label string, value interface{}) {
|
||||
b, _ := json.MarshalIndent(value, "", " ")
|
||||
log.Printf("🚧 %s:\n%s\n", label, string(b))
|
||||
}
|
||||
101
main.go
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"text/template"
|
||||
|
||||
"github.com/fiatjaf/khatru"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
@@ -96,6 +97,31 @@ func makeNewRelay(relayType string) *khatru.Relay {
|
||||
return true, "auth-required: publishing this event requires authentication"
|
||||
})
|
||||
|
||||
mux := privateRelay.Router()
|
||||
static := http.FileServer(http.Dir("templates/static"))
|
||||
|
||||
mux.Handle("GET /static/", http.StripPrefix("/static/", static))
|
||||
mux.Handle("GET /favicon.ico", http.StripPrefix("/", static))
|
||||
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
tmpl := template.Must(template.ParseFiles("templates/index.html"))
|
||||
data := struct {
|
||||
RelayName string
|
||||
RelayPubkey string
|
||||
RelayDescription string
|
||||
RelayURL string
|
||||
}{
|
||||
RelayName: config.PrivateRelayName,
|
||||
RelayPubkey: nPubToPubkey(config.PrivateRelayNpub),
|
||||
RelayDescription: config.PrivateRelayDescription,
|
||||
RelayURL: "wss://" + config.RelayURL + "/outbox",
|
||||
}
|
||||
err := tmpl.Execute(w, data)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
|
||||
return privateRelay
|
||||
|
||||
case "/chat":
|
||||
@@ -150,6 +176,31 @@ func makeNewRelay(relayType string) *khatru.Relay {
|
||||
return true, "only direct messages are allowed in this relay"
|
||||
})
|
||||
|
||||
mux := chatRelay.Router()
|
||||
static := http.FileServer(http.Dir("templates/static"))
|
||||
|
||||
mux.Handle("GET /static/", http.StripPrefix("/static/", static))
|
||||
mux.Handle("GET /favicon.ico", http.StripPrefix("/", static))
|
||||
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
tmpl := template.Must(template.ParseFiles("templates/index.html"))
|
||||
data := struct {
|
||||
RelayName string
|
||||
RelayPubkey string
|
||||
RelayDescription string
|
||||
RelayURL string
|
||||
}{
|
||||
RelayName: config.ChatRelayName,
|
||||
RelayPubkey: nPubToPubkey(config.ChatRelayNpub),
|
||||
RelayDescription: config.ChatRelayDescription,
|
||||
RelayURL: "wss://" + config.RelayURL + "/chat",
|
||||
}
|
||||
err := tmpl.Execute(w, data)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
|
||||
return chatRelay
|
||||
|
||||
case "/inbox":
|
||||
@@ -170,6 +221,31 @@ func makeNewRelay(relayType string) *khatru.Relay {
|
||||
return true, "you can only post notes if you've tagged the owner of this relay"
|
||||
})
|
||||
|
||||
mux := inboxRelay.Router()
|
||||
static := http.FileServer(http.Dir("templates/static"))
|
||||
|
||||
mux.Handle("GET /static/", http.StripPrefix("/static/", static))
|
||||
mux.Handle("GET /favicon.ico", http.StripPrefix("/", static))
|
||||
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
tmpl := template.Must(template.ParseFiles("templates/index.html"))
|
||||
data := struct {
|
||||
RelayName string
|
||||
RelayPubkey string
|
||||
RelayDescription string
|
||||
RelayURL string
|
||||
}{
|
||||
RelayName: config.InboxRelayName,
|
||||
RelayPubkey: nPubToPubkey(config.InboxRelayNpub),
|
||||
RelayDescription: config.InboxRelayDescription,
|
||||
RelayURL: "wss://" + config.RelayURL + "/inbox",
|
||||
}
|
||||
err := tmpl.Execute(w, data)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
|
||||
return inboxRelay
|
||||
|
||||
default: // default to outbox
|
||||
@@ -187,6 +263,31 @@ func makeNewRelay(relayType string) *khatru.Relay {
|
||||
return true, "only notes signed by the owner of this relay are allowed"
|
||||
})
|
||||
|
||||
mux := outboxRelay.Router()
|
||||
static := http.FileServer(http.Dir("templates/static"))
|
||||
|
||||
mux.Handle("GET /static/", http.StripPrefix("/static/", static))
|
||||
mux.Handle("GET /favicon.ico", http.StripPrefix("/", static))
|
||||
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
tmpl := template.Must(template.ParseFiles("templates/index.html"))
|
||||
data := struct {
|
||||
RelayName string
|
||||
RelayPubkey string
|
||||
RelayDescription string
|
||||
RelayURL string
|
||||
}{
|
||||
RelayName: config.OutboxRelayName,
|
||||
RelayPubkey: nPubToPubkey(config.OutboxRelayNpub),
|
||||
RelayDescription: config.OutboxRelayDescription,
|
||||
RelayURL: "wss://" + config.RelayURL + "/outbox",
|
||||
}
|
||||
err := tmpl.Execute(w, data)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
|
||||
return outboxRelay
|
||||
}
|
||||
}
|
||||
|
||||
64
templates/index.html
Normal file
@@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{.RelayName}}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap");
|
||||
body {
|
||||
font-family: "Inter", sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-900 text-white">
|
||||
<div class="flex-grow flex flex-col justify-center items-center px-4">
|
||||
<!-- Container -->
|
||||
<div class="text-center max-w-2xl">
|
||||
<!-- Heading (Relay Name) -->
|
||||
<h1 class="text-5xl md:text-6xl font-bold text-purple-400 mb-6">
|
||||
{{.RelayName}}
|
||||
</h1>
|
||||
|
||||
<!-- Relay Description -->
|
||||
<p class="text-xl md:text-2xl text-gray-300 mb-8">
|
||||
{{.RelayDescription}}
|
||||
</p>
|
||||
|
||||
<!-- Relay URL -->
|
||||
<a
|
||||
href="#"
|
||||
class="text-lg md:text-xl text-purple-300 hover:text-purple-400 underline"
|
||||
>
|
||||
{{.RelayURL}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="text-center py-6 bg-gray-800 w-full">
|
||||
<p class="text-lg text-gray-400">
|
||||
Proudly powered by
|
||||
<a
|
||||
href="https://khatru.nostr.technology/"
|
||||
target="_blank"
|
||||
class="text-purple-300 hover:text-purple-400 underline"
|
||||
>Khatru</a
|
||||
>
|
||||
|
|
||||
<a
|
||||
href="https://github.com/bitvora/haven"
|
||||
target="_blank"
|
||||
class="text-purple-300 hover:text-purple-400 underline"
|
||||
>Haven Relay on Github</a
|
||||
>
|
||||
</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
BIN
templates/static/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
templates/static/android-chrome-256x256.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
templates/static/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
9
templates/static/browserconfig.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#da532c</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
BIN
templates/static/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 846 B |
BIN
templates/static/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
templates/static/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
templates/static/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
15
templates/static/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="274.000000pt" height="274.000000pt" viewBox="0 0 274.000000 274.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,274.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M0 1365 l0 -1285 1370 0 1370 0 0 1285 0 1285 -1370 0 -1370 0 0
|
||||
-1285z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 603 B |