normalize.go raw

   1  // Package normalize is a set of tools for cleaning up URL s and formatting
   2  // nostr OK and CLOSED messages.
   3  package normalize
   4  
   5  import (
   6  	"bytes"
   7  	"errors"
   8  	"fmt"
   9  	"net/url"
  10  
  11  	"next.orly.dev/pkg/nostr/encoders/ints"
  12  	"next.orly.dev/pkg/nostr/utils/constraints"
  13  	"next.orly.dev/pkg/lol/chk"
  14  	"next.orly.dev/pkg/lol/log"
  15  )
  16  
  17  var (
  18  	hp    = bytes.HasPrefix
  19  	WS    = []byte("ws://")
  20  	WSS   = []byte("wss://")
  21  	HTTP  = []byte("http://")
  22  	HTTPS = []byte("https://")
  23  )
  24  
  25  // URL normalizes the URL
  26  //
  27  // - Adds wss:// to addresses without a port, or with 443 that have no protocol
  28  // prefix
  29  //
  30  // - Adds ws:// to addresses with any other port
  31  //
  32  // - Converts http/s to ws/s
  33  func URL[V constraints.Bytes](v V) (b []byte) {
  34  	u := []byte(v)
  35  	if len(u) == 0 {
  36  		return nil
  37  	}
  38  	u = bytes.TrimSpace(u)
  39  	u = bytes.ToLower(u)
  40  	// if the address has a port number, we can probably assume it is insecure
  41  	// websocket as most public or production relays have a domain name and a
  42  	// well-known port 80 or 443 and thus no port number.
  43  	//
  44  	// if a protocol prefix is present, we assume it is already complete.
  45  	// Converting http/s to websocket-equivalent will be done later anyway.
  46  	if bytes.Contains(u, []byte(":")) &&
  47  		!(hp(u, HTTP) || hp(u, HTTPS) || hp(u, WS) || hp(u, WSS)) {
  48  
  49  		split := bytes.Split(u, []byte(":"))
  50  		if len(split) != 2 {
  51  			log.D.F("Error: more than one ':' in URL: '%s'", u)
  52  			// this is a malformed URL if it has more than one ":", return empty
  53  			// since this function does not return an error explicitly.
  54  			return
  55  		}
  56  		p := ints.New(0)
  57  		_, err := p.Unmarshal(split[1])
  58  		if chk.E(err) {
  59  			log.D.F("Error normalizing URL '%s': %s", u, err)
  60  			// again, without an error, we must return nil
  61  			return
  62  		}
  63  		if p.Uint64() > 65535 {
  64  			log.D.F(
  65  				"Port on address %d: greater than maximum 65535",
  66  				p.Uint64(),
  67  			)
  68  			return
  69  		}
  70  		// if the port is explicitly set to 443 we assume it is wss:// and drop
  71  		// the port.
  72  		if p.Uint16() == 443 {
  73  			u = append(WSS, split[0]...)
  74  		} else {
  75  			// non-443 port likely means insecure websocket (local dev, etc.)
  76  			u = append(WS, u...)
  77  		}
  78  	}
  79  
  80  	// if the prefix isn't specified as http/s or websocket, assume secure
  81  	// websocket and add wss prefix (this is the most common).
  82  	if !(hp(u, HTTP) || hp(u, HTTPS) || hp(u, WS) || hp(u, WSS)) {
  83  		u = append(WSS, u...)
  84  	}
  85  	var err error
  86  	var p *url.URL
  87  	if p, err = url.Parse(string(u)); chk.E(err) {
  88  		return
  89  	}
  90  	// convert http/s to ws/s
  91  	switch p.Scheme {
  92  	case "https":
  93  		p.Scheme = "wss"
  94  	case "http":
  95  		p.Scheme = "ws"
  96  	}
  97  	// remove trailing path slash
  98  	p.Path = string(bytes.TrimRight([]byte(p.Path), "/"))
  99  	return []byte(p.String())
 100  }
 101  
 102  // Msg constructs a properly formatted message with a machine-readable prefix
 103  // for OK and CLOSED envelopes.
 104  func Msg(prefix Reason, format string, params ...any) []byte {
 105  	if len(prefix) < 1 {
 106  		prefix = Error
 107  	}
 108  	return []byte(fmt.Sprintf(prefix.S()+": "+format, params...))
 109  }
 110  
 111  // MsgString constructs a properly formatted message with a machine-readable prefix
 112  // for OK and CLOSED envelopes.
 113  func MsgString(prefix Reason, format string, params ...any) string {
 114  	if len(prefix) < 1 {
 115  		prefix = Error
 116  	}
 117  	return fmt.Sprintf(prefix.S()+": "+format, params...)
 118  }
 119  
 120  // Reason is the machine-readable prefix before the colon in an OK or CLOSED
 121  // envelope message. Below are the most common kinds that are mentioned in
 122  // NIP-01.
 123  type Reason []byte
 124  
 125  var (
 126  	AuthRequired = Reason("auth-required")
 127  	PoW          = Reason("pow")
 128  	Duplicate    = Reason("duplicate")
 129  	Blocked      = Reason("blocked")
 130  	RateLimited  = Reason("rate-limited")
 131  	Invalid      = Reason("invalid")
 132  	Error        = Reason("error")
 133  	Unsupported  = Reason("unsupported")
 134  	Restricted   = Reason("restricted")
 135  )
 136  
 137  // S returns the Reason as a string
 138  func (r Reason) S() string { return string(r) }
 139  
 140  // B returns the Reason as a byte slice.
 141  func (r Reason) B() []byte { return r }
 142  
 143  // IsPrefix returns whether a text contains the same Reason prefix.
 144  func (r Reason) IsPrefix(reason []byte) bool {
 145  	return bytes.HasPrefix(
 146  		reason, r.B(),
 147  	)
 148  }
 149  
 150  // F allows creation of a full Reason text with a printf style format.
 151  func (r Reason) F(format string, params ...any) []byte {
 152  	return Msg(
 153  		r, format, params...,
 154  	)
 155  }
 156  
 157  // Errorf allows creation of a full Reason text with a printf style as an error.
 158  func (r Reason) Errorf(format string, params ...any) (err error) {
 159  	return errors.New(
 160  		MsgString(
 161  			r, format, params...,
 162  		),
 163  	)
 164  }
 165