public pages, rate limiters

This commit is contained in:
Barry Deen
2024-09-25 13:43:21 -04:00
parent b53bb285e2
commit 0d3cc0b232
17 changed files with 423 additions and 4 deletions

View File

@@ -1,12 +1,12 @@
package main package main
import ( import (
"encoding/json"
"io/ioutil"
"log" "log"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"encoding/json"
"io/ioutil"
"github.com/joho/godotenv" "github.com/joho/godotenv"
) )
@@ -146,6 +146,17 @@ func getEnvInt(key string, defaultValue int) int {
return defaultValue 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 = ` var art = `
██╗ ██╗ █████╗ ██╗ ██╗███████╗███╗ ██╗ ██╗ ██╗ █████╗ ██╗ ██╗███████╗███╗ ██╗
██║ ██║██╔══██╗██║ ██║██╔════╝████╗ ██║ ██║ ██║██╔══██╗██║ ██║██╔════╝████╗ ██║

View File

@@ -10,7 +10,7 @@ services:
- "./db:/app/db" - "./db:/app/db"
- "./haven:/app/haven" - "./haven:/app/haven"
ports: ports:
- "3335" - "3355"
user: "${DOCKER_UID:-1000}:${DOCKER_GID:-1000}" user: "${DOCKER_UID:-1000}:${DOCKER_GID:-1000}"
tor: tor:

View File

@@ -10,5 +10,5 @@ services:
- "./db:/app/db" - "./db:/app/db"
- "./haven:/app/haven" - "./haven:/app/haven"
ports: ports:
- "3335:3335" - "3355:3355"
user: "${DOCKER_UID:-1000}:${DOCKER_GID:-1000}" user: "${DOCKER_UID:-1000}:${DOCKER_GID:-1000}"

BIN
haven Executable file

Binary file not shown.

106
init.go
View File

@@ -1,8 +1,11 @@
package main package main
import ( import (
"time"
"github.com/fiatjaf/eventstore/lmdb" "github.com/fiatjaf/eventstore/lmdb"
"github.com/fiatjaf/khatru" "github.com/fiatjaf/khatru"
"github.com/fiatjaf/khatru/policies"
) )
var ( var (
@@ -66,6 +69,8 @@ func initRelays() {
panic(err) panic(err)
} }
initRelayLimits()
privateRelay.Info.Name = config.PrivateRelayName privateRelay.Info.Name = config.PrivateRelayName
privateRelay.Info.PubKey = nPubToPubkey(config.PrivateRelayNpub) privateRelay.Info.PubKey = nPubToPubkey(config.PrivateRelayNpub)
privateRelay.Info.Description = config.PrivateRelayDescription privateRelay.Info.Description = config.PrivateRelayDescription
@@ -74,6 +79,31 @@ func initRelays() {
privateRelay.Info.Software = config.RelaySoftware privateRelay.Info.Software = config.RelaySoftware
privateRelay.ServiceURL = "https://" + config.RelayURL + "/private" 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.Name = config.ChatRelayName
chatRelay.Info.PubKey = nPubToPubkey(config.ChatRelayNpub) chatRelay.Info.PubKey = nPubToPubkey(config.ChatRelayNpub)
chatRelay.Info.Description = config.ChatRelayDescription chatRelay.Info.Description = config.ChatRelayDescription
@@ -82,6 +112,31 @@ func initRelays() {
chatRelay.Info.Software = config.RelaySoftware chatRelay.Info.Software = config.RelaySoftware
chatRelay.ServiceURL = "https://" + config.RelayURL + "/chat" 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.Name = config.OutboxRelayName
outboxRelay.Info.PubKey = nPubToPubkey(config.OutboxRelayNpub) outboxRelay.Info.PubKey = nPubToPubkey(config.OutboxRelayNpub)
outboxRelay.Info.Description = config.OutboxRelayDescription outboxRelay.Info.Description = config.OutboxRelayDescription
@@ -89,6 +144,31 @@ func initRelays() {
outboxRelay.Info.Version = config.RelayVersion outboxRelay.Info.Version = config.RelayVersion
outboxRelay.Info.Software = config.RelaySoftware 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.Name = config.InboxRelayName
inboxRelay.Info.PubKey = nPubToPubkey(config.InboxRelayNpub) inboxRelay.Info.PubKey = nPubToPubkey(config.InboxRelayNpub)
inboxRelay.Info.Description = config.InboxRelayDescription inboxRelay.Info.Description = config.InboxRelayDescription
@@ -96,4 +176,30 @@ func initRelays() {
inboxRelay.Info.Version = config.RelayVersion inboxRelay.Info.Version = config.RelayVersion
inboxRelay.Info.Software = config.RelaySoftware inboxRelay.Info.Software = config.RelaySoftware
inboxRelay.ServiceURL = "https://" + config.RelayURL + "/inbox" 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
View 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
View File

@@ -7,6 +7,7 @@ import (
"io" "io"
"log" "log"
"net/http" "net/http"
"text/template"
"github.com/fiatjaf/khatru" "github.com/fiatjaf/khatru"
"github.com/nbd-wtf/go-nostr" "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" 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 return privateRelay
case "/chat": case "/chat":
@@ -150,6 +176,31 @@ func makeNewRelay(relayType string) *khatru.Relay {
return true, "only direct messages are allowed in this 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 return chatRelay
case "/inbox": 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" 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 return inboxRelay
default: // default to outbox 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" 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 return outboxRelay
} }
} }

64
templates/index.html Normal file
View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View 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