config.go raw

   1  // Package config loads torly's settings from the environment.
   2  //
   3  // Every variable has a TORLY_ name and an ORLY_ fallback, so an existing orly
   4  // or morly deployment can be pointed at torly by changing only the binary.
   5  package config
   6  
   7  import (
   8  	"fmt"
   9  	"os"
  10  	"path/filepath"
  11  	"strconv"
  12  	"strings"
  13  )
  14  
  15  // Version is the relay version, set by the build.
  16  var Version = "0.1.0"
  17  
  18  // Config is the complete runtime configuration.
  19  type Config struct {
  20  	AppName     string
  21  	Description string
  22  	Contact     string
  23  	Icon        string
  24  
  25  	ListenHost string
  26  	Port       int
  27  	DataDir    string
  28  	StaticDir  string
  29  	RelayURL   string
  30  	SecretKey  string
  31  
  32  	// RelayPubKey is derived at startup from SecretKey or the persisted relay
  33  	// key file. It is not read from the environment directly.
  34  	RelayPubKey string
  35  
  36  	AuthRequired   bool
  37  	MaxConnections int
  38  	ConnWorkers    int
  39  	VerifyWorkers  int
  40  
  41  	BlossomEnabled bool
  42  	BlossomMaxBlob int64
  43  
  44  	MaxEventBytes  int
  45  	MaxSubsPerConn int
  46  	MaxFilters     int
  47  	MaxLimit       int
  48  	QueryLimit     int
  49  	MaxFutureSecs  int64
  50  
  51  	ExpiryInterval int64 // seconds between NIP-40 purges
  52  
  53  	LogLevel string
  54  }
  55  
  56  // FromEnv builds a Config, applying defaults for absent variables.
  57  func FromEnv() (*Config, error) {
  58  	home, _ := os.UserHomeDir()
  59  	if home == "" {
  60  		home = "."
  61  	}
  62  	c := &Config{
  63  		AppName:        env("APP_NAME", "torly"),
  64  		Description:    env("DESCRIPTION", "torly - a TinyGo Nostr relay"),
  65  		Contact:        env("CONTACT", ""),
  66  		Icon:           env("ICON", ""),
  67  		ListenHost:     env("LISTEN", "127.0.0.1"),
  68  		DataDir:        env("DATA_DIR", filepath.Join(home, ".local", "share", "torly")),
  69  		StaticDir:      env("STATIC_DIR", ""),
  70  		SecretKey:      env("SECRET_KEY", ""),
  71  		AuthRequired:   envBool("AUTH_REQUIRED", false),
  72  		MaxConnections: envInt("MAX_CONNECTIONS", 0),
  73  		ConnWorkers:    envInt("CONN_WORKERS", 64),
  74  		VerifyWorkers:  envInt("VERIFY_WORKERS", 2),
  75  		BlossomEnabled: envBool("BLOSSOM_ENABLED", true),
  76  		BlossomMaxBlob: int64(envInt("BLOSSOM_MAX_BLOB_MB", 50)) << 20,
  77  		MaxEventBytes:  envInt("MAX_EVENT_BYTES", 128<<10),
  78  		MaxSubsPerConn: envInt("MAX_SUBS_PER_CONN", 64),
  79  		MaxFilters:     envInt("MAX_FILTERS", 16),
  80  		MaxLimit:       envInt("MAX_LIMIT", 5000),
  81  		QueryLimit:     envInt("QUERY_LIMIT", 5000),
  82  		MaxFutureSecs:  int64(envInt("MAX_FUTURE_SECS", 900)),
  83  		ExpiryInterval: int64(envInt("EXPIRY_INTERVAL", 60)),
  84  		LogLevel:       strings.ToLower(env("LOG_LEVEL", "info")),
  85  	}
  86  	port, err := strconv.Atoi(env("PORT", "3334"))
  87  	if err != nil || port < 0 || port > 65535 {
  88  		return nil, fmt.Errorf("torly: invalid TORLY_PORT")
  89  	}
  90  	c.Port = port
  91  	if c.MaxEventBytes < 1024 {
  92  		return nil, fmt.Errorf("torly: TORLY_MAX_EVENT_BYTES must be at least 1024")
  93  	}
  94  	if c.BlossomMaxBlob < 1<<20 {
  95  		c.BlossomMaxBlob = 1 << 20
  96  	}
  97  	if c.MaxFilters < 1 {
  98  		c.MaxFilters = 1
  99  	}
 100  	if c.MaxSubsPerConn < 1 {
 101  		c.MaxSubsPerConn = 1
 102  	}
 103  	if c.ConnWorkers < 1 {
 104  		c.ConnWorkers = 1
 105  	}
 106  	if c.ConnWorkers > 1024 {
 107  		c.ConnWorkers = 1024
 108  	}
 109  	if c.VerifyWorkers < 1 {
 110  		c.VerifyWorkers = 1
 111  	}
 112  	if c.VerifyWorkers > 64 {
 113  		c.VerifyWorkers = 64
 114  	}
 115  	if c.MaxLimit < 1 {
 116  		c.MaxLimit = 1
 117  	}
 118  	if c.RelayURL = env("RELAY_URL", ""); c.RelayURL == "" {
 119  		host := c.ListenHost
 120  		if host == "" || host == "0.0.0.0" || host == "::" {
 121  			host = "localhost"
 122  		}
 123  		c.RelayURL = fmt.Sprintf("ws://%s:%d/", host, c.Port)
 124  	}
 125  	return c, nil
 126  }
 127  
 128  // Addr returns the listen address.
 129  func (c *Config) Addr() string {
 130  	return fmt.Sprintf("%s:%d", c.ListenHost, c.Port)
 131  }
 132  
 133  // NIP11 is the relay information document served for NIP-11.
 134  type NIP11 struct {
 135  	Name          string   `json:"name"`
 136  	Description   string   `json:"description"`
 137  	PubKey        string   `json:"pubkey,omitempty"`
 138  	Contact       string   `json:"contact,omitempty"`
 139  	SupportedNIPs []int    `json:"supported_nips"`
 140  	Software      string   `json:"software"`
 141  	Version       string   `json:"version"`
 142  	Icon          string   `json:"icon,omitempty"`
 143  	Limitation    LimitDoc `json:"limitation"`
 144  }
 145  
 146  // LimitDoc is the NIP-11 limitation object.
 147  type LimitDoc struct {
 148  	MaxMessageLength int  `json:"max_message_length"`
 149  	MaxSubscriptions int  `json:"max_subscriptions"`
 150  	MaxFilters       int  `json:"max_filters"`
 151  	MaxLimit         int  `json:"max_limit"`
 152  	MaxSubIDLength   int  `json:"max_subid_length"`
 153  	MinPrefix        int  `json:"min_prefix"`
 154  	MaxEventTags     int  `json:"max_event_tags"`
 155  	MaxContentLength int  `json:"max_content_length"`
 156  	AuthRequired     bool `json:"auth_required"`
 157  	PaymentRequired  bool `json:"payment_required"`
 158  }
 159  
 160  // SupportedNIPs lists the NIPs torly implements.
 161  var SupportedNIPs = []int{1, 9, 11, 33, 40, 42, 45, 50, 70}
 162  
 163  // EnvHelp documents the environment.
 164  func EnvHelp() string {
 165  	return `torly - a TinyGo Nostr relay
 166  
 167  Usage:
 168    torly [serve]        run the relay (default)
 169    torly import [opts]  import newline-delimited NIP-01 events
 170    torly reindex-graph  rebuild derived graph indexes from the store
 171    torly blossom-backup --data-dir DIR --out FILE
 172    torly blossom-restore --data-dir DIR --file FILE [--offline]
 173    torly version        print the version
 174    torly help           print this help
 175  
 176  Configuration is by environment variable. TORLY_* is authoritative; the same
 177  name with ORLY_* is accepted as a fallback.
 178  
 179    TORLY_APP_NAME            NIP-11 name (default torly)
 180    TORLY_DESCRIPTION         NIP-11 description
 181    TORLY_CONTACT             NIP-11 contact
 182    TORLY_ICON                NIP-11 icon URL
 183    TORLY_LISTEN              bind address (default 127.0.0.1)
 184    TORLY_PORT                bind port (default 3334)
 185    TORLY_DATA_DIR            database and blob directory
 186    TORLY_STATIC_DIR          optional static web root
 187    TORLY_RELAY_URL           public URL used in NIP-42 checks
 188    TORLY_SECRET_KEY          relay secret key (64 hex); default is DATA_DIR/relay.key
 189    TORLY_AUTH_REQUIRED       require NIP-42 AUTH for writes (default false)
 190    TORLY_MAX_CONNECTIONS     accepted connections cap, 0 = unlimited
 191    TORLY_CONN_WORKERS        fixed reader/writer worker pairs (default 64)
 192    TORLY_VERIFY_WORKERS      signature verification domains (default 2)
 193    TORLY_BLOSSOM_ENABLED     serve Blossom blobs (default true)
 194    TORLY_BLOSSOM_MAX_BLOB_MB largest accepted blob in MiB (default 50)
 195    TORLY_MAX_EVENT_BYTES     largest event JSON in bytes (default 131072)
 196    TORLY_MAX_SUBS_PER_CONN   subscriptions per connection (default 64)
 197    TORLY_MAX_FILTERS         filters per REQ (default 16)
 198    TORLY_MAX_LIMIT           largest accepted filter limit (default 5000)
 199    TORLY_QUERY_LIMIT         result cap for unbounded filters (default 5000)
 200    TORLY_MAX_FUTURE_SECS     allowed created_at skew into the future (default 900)
 201    TORLY_EXPIRY_INTERVAL     seconds between NIP-40 purges (default 60)
 202    TORLY_LOG_LEVEL           error, warn, info, debug (default info)
 203  `
 204  }
 205  
 206  func env(name, def string) string {
 207  	if v := os.Getenv("TORLY_" + name); v != "" {
 208  		return v
 209  	}
 210  	if v := os.Getenv("ORLY_" + name); v != "" {
 211  		return v
 212  	}
 213  	return def
 214  }
 215  
 216  func envInt(name string, def int) int {
 217  	v := env(name, "")
 218  	if v == "" {
 219  		return def
 220  	}
 221  	n, err := strconv.Atoi(v)
 222  	if err != nil {
 223  		return def
 224  	}
 225  	return n
 226  }
 227  
 228  func envBool(name string, def bool) bool {
 229  	v := strings.ToLower(env(name, ""))
 230  	switch v {
 231  	case "1", "true", "yes", "on":
 232  		return true
 233  	case "0", "false", "no", "off":
 234  		return false
 235  	}
 236  	return def
 237  }
 238