You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
noxy/cmd/noxy/main.go

229 lines
6.2 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
"strings"
"time"
"git.qcode.ch/nostr/noxy"
)
var noxyVersion = "dev" // overwritten by linker flags in release build
var (
listenAddr = flag.String("addr", "127.0.0.1:8889", "listen address")
cacheDir = flag.String("cachedir", "/tmp", "absolute cache dir")
maxFileSize = flag.Int64("maxfilesize", 1<<23, "refuse to handle files larger than this, in bytes")
minCacheAge = flag.Uint("mincacheage", 3600, "min cache-control max-age value, in seconds")
idleRelayTimeout = flag.Duration("idlerelaytimeout", 10*time.Minute, "remove relay connections after idling this long")
showVersion = flag.Bool("V", false, "print version and exit")
// the -relay flag, populated by parseRelayFlag.
// set to defaultKnownRelays if empty.
knownRelays []string
defaultKnownRelays = []string{
"nostr.x1ddos.ch",
// from https://nostr.info/relays/
"expensive-relay.fiatjaf.com",
"nostr-pub.semisol.dev",
"nostr-pub.wellorder.net",
"nostr-relay-dev.wlvs.space",
"nostr-relay.wlvs.space",
"nostr-verified.wellorder.net",
"nostr.bitcoiner.social",
"nostr.delo.software",
"nostr.ono.re",
"nostr.onsats.org",
"nostr.openchain.fr",
"nostr.oxtr.dev",
"nostr.rdfriedl.com",
"nostr.semisol.dev",
"nostr.zaprite.io",
"relay.cynsar.foundation",
"relay.damus.io",
"relay.farscapian.com",
"relay.minds.com",
"relay.nostr.ch",
"relay.nostr.info",
"relay.oldcity-bitcoiners.info",
"relay.sovereign-stack.org",
}
)
func init() {
flag.Func("relay", "a comma separated nostr relays noxy is allowed to connect to", parseRelayFlag)
sort.Strings(defaultKnownRelays)
}
func parseRelayFlag(v string) error {
for _, s := range strings.FieldsFunc(v, func(r rune) bool { return r == ',' }) {
s = strings.TrimSpace(s)
if s == "" {
continue
}
u, err := url.Parse(s)
if err != nil {
return fmt.Errorf("invalid relay URL %s: %w", s, err)
}
host := u.Hostname()
if host == "" {
return fmt.Errorf("invalid relay URL %s: no hostname", s)
}
knownRelays = append(knownRelays, host)
}
return nil
}
func usage() {
w := flag.CommandLine.Output()
fmt.Fprintf(w, "usage of %s:\n", os.Args[0])
flag.PrintDefaults()
fmt.Fprintln(w, "\nthe -relay flag may be specified multiple times.")
fmt.Fprintf(w, "its default value is the following list:\n\n")
fmt.Fprintln(w, strings.Join(defaultKnownRelays, "\n"))
}
// set up in main and used by handleXxx HTTP server handlers.
var noxer *noxy.Noxer
func main() {
flag.Usage = usage
flag.Parse()
if *showVersion {
fmt.Println(noxyVersion)
return
}
if len(knownRelays) == 0 {
knownRelays = defaultKnownRelays
}
sort.Strings(knownRelays)
if !filepath.IsAbs(*cacheDir) {
log.Fatal("cache dir must be absolute path")
}
noxer = &noxy.Noxer{
MaxFileSize: *maxFileSize,
IdleRelayTimeout: *idleRelayTimeout,
KnownRelays: knownRelays,
Cache: noxy.DirCache{Root: *cacheDir},
HTTPClient: &http.Client{Transport: &http.Transport{
MaxIdleConns: 100,
MaxConnsPerHost: 2,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}},
}
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(handleRoot))
mux.Handle("/meta", http.HandlerFunc(handleMeta))
mux.Handle("/data", http.HandlerFunc(handleData))
log.Printf("listening on %s", *listenAddr)
log.Printf("known relays: %s", strings.Join(knownRelays, ", "))
http.ListenAndServe(*listenAddr, logHandler(cors(mux)))
}
// handles requests to /
func handleRoot(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "this is noxy version %s\nhttps://git.qcode.ch/nostr/noxy\n", noxyVersion)
}
// handles requests to /meta
func handleMeta(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
eventID := r.FormValue("id")
relayURL := r.FormValue("relay")
linkURL := r.FormValue("url")
meta, err := noxer.FetchLinkMeta(r.Context(), eventID, relayURL, linkURL)
if err != nil {
writeError(w, err)
return
}
res := struct {
Type string `json:"type"`
Title string `json:"title"`
Descr string `json:"descr"`
Images []string `json:"images"`
}{
Type: meta.Type,
Title: meta.Title,
Descr: meta.Description,
Images: meta.ImageURLs,
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", *minCacheAge))
json.NewEncoder(w).Encode(res)
}
// handles requests to /data
func handleData(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
eventID := r.FormValue("id")
relayURL := r.FormValue("relay")
linkURL := r.FormValue("url")
ds, err := noxer.StreamLinkData(r.Context(), eventID, relayURL, linkURL)
if err != nil {
writeError(w, err)
return
}
defer ds.Close()
w.Header().Set("Content-Type", ds.ContentType)
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", *minCacheAge))
io.Copy(w, ds)
}
func writeError(w http.ResponseWriter, err error) {
log.Printf("ERROR: %v", err)
w.Header().Set("Content-Type", "text/plain")
switch {
default:
w.WriteHeader(http.StatusBadRequest)
case errors.Is(err, noxy.ErrNotFound):
w.WriteHeader(http.StatusNotFound)
case errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled):
w.WriteHeader(http.StatusServiceUnavailable)
}
fmt.Fprint(w, err.Error())
}
func cors(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "*") // nb: wildcard prevents authentication
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Max-Age", "2592000") // valid for 30 days
w.WriteHeader(http.StatusNoContent)
return
}
w.Header().Set("Access-Control-Expose-Headers", "*")
h.ServeHTTP(w, r)
})
}
func logHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.RequestURI)
h.ServeHTTP(w, r)
})
}