main.go raw

   1  package main
   2  
   3  import (
   4  	"bytes"
   5  	"encoding/base64"
   6  	"encoding/hex"
   7  	"encoding/json"
   8  	"flag"
   9  	"fmt"
  10  	"io"
  11  	"mime"
  12  	"net/http"
  13  	"os"
  14  	"path/filepath"
  15  	"strings"
  16  	"sync"
  17  	"time"
  18  
  19  	"github.com/minio/sha256-simd"
  20  	"git.smesh.lol/orly/pkg/nostr/crypto/ec/secp256k1"
  21  	"git.smesh.lol/orly/pkg/nostr/encoders/bech32encoding"
  22  	"git.smesh.lol/orly/pkg/nostr/interfaces/signer/p8k"
  23  )
  24  
  25  var (
  26  	nsec    = flag.String("nsec", "", "nostr private key (nsec or hex)")
  27  	servers = flag.String("servers", "", "comma-separated server URLs (overrides defaults)")
  28  	verbose = flag.Bool("v", false, "verbose output")
  29  )
  30  
  31  var defaultServers = []string{
  32  	"https://blossom.primal.net",
  33  	"https://nostr.download",
  34  	"https://blossom.oxtr.dev",
  35  	"https://blossom.nostr.build",
  36  	"https://blossom.band",
  37  	"https://blossom.nogood.studio",
  38  	"https://blossom.hzrd149.com",
  39  	"https://cdn.hzrd149.com",
  40  	"https://blossom.f7z.io",
  41  	"https://blossom.azzamo.net",
  42  	"https://files.sovbit.host",
  43  	"https://nosto.re",
  44  	"https://nostrmedia.com",
  45  	"https://cdn.nostrcheck.me",
  46  	"https://media-server.slidestr.net",
  47  	"https://cdn.satellite.earth",
  48  	"https://files.v0l.io",
  49  }
  50  
  51  func main() {
  52  	flag.Parse()
  53  	if *nsec == "" {
  54  		fmt.Fprintln(os.Stderr, "usage: blossom-upload -nsec <key> file1 [file2 ...]")
  55  		os.Exit(1)
  56  	}
  57  	files := flag.Args()
  58  	if len(files) == 0 {
  59  		fmt.Fprintln(os.Stderr, "no files specified")
  60  		os.Exit(1)
  61  	}
  62  
  63  	sec, pub, err := parseKey(*nsec)
  64  	if err != nil {
  65  		fmt.Fprintf(os.Stderr, "key error: %v\n", err)
  66  		os.Exit(1)
  67  	}
  68  
  69  	srvs := defaultServers
  70  	if *servers != "" {
  71  		srvs = strings.Split(*servers, ",")
  72  	}
  73  
  74  	for _, path := range files {
  75  		data, err := os.ReadFile(path)
  76  		if err != nil {
  77  			fmt.Fprintf(os.Stderr, "read %s: %v\n", path, err)
  78  			continue
  79  		}
  80  		hash := sha256.Sum256(data)
  81  		hashHex := hex.EncodeToString(hash[:])
  82  		ct := mimeType(path)
  83  		fmt.Printf("%s (%d bytes, %s, sha256:%s)\n", filepath.Base(path), len(data), ct, hashHex[:16]+"...")
  84  
  85  		var wg sync.WaitGroup
  86  		for _, srv := range srvs {
  87  			wg.Add(1)
  88  			go func(srv string) {
  89  				defer wg.Done()
  90  				url, err := upload(sec, pub, srv, data, ct)
  91  				if err != nil {
  92  					fmt.Printf("  %s: FAIL %v\n", srv, err)
  93  				} else {
  94  					fmt.Printf("  %s: OK %s\n", srv, url)
  95  				}
  96  			}(strings.TrimSpace(srv))
  97  		}
  98  		wg.Wait()
  99  		fmt.Println()
 100  	}
 101  }
 102  
 103  func parseKey(key string) (sec, pub []byte, err error) {
 104  	if strings.HasPrefix(key, "nsec1") {
 105  		var sk *secp256k1.SecretKey
 106  		sk, err = bech32encoding.NsecToSecretKey(key)
 107  		if err != nil {
 108  			return
 109  		}
 110  		sec = sk.Serialize()
 111  	} else {
 112  		sec, err = hex.DecodeString(key)
 113  		if err != nil {
 114  			return
 115  		}
 116  	}
 117  	var s *p8k.Signer
 118  	s, err = p8k.New()
 119  	if err != nil {
 120  		return
 121  	}
 122  	if err = s.InitSec(sec); err != nil {
 123  		return
 124  	}
 125  	pub = s.Pub()
 126  	return
 127  }
 128  
 129  func authHeader(sec, pub []byte, action, hashHex string) (string, error) {
 130  	now := time.Now().Unix()
 131  	tags := [][]string{
 132  		{"t", action},
 133  		{"expiration", fmt.Sprintf("%d", now+300)},
 134  	}
 135  	if hashHex != "" {
 136  		tags = append(tags, []string{"x", hashHex})
 137  	}
 138  	pubHex := hex.EncodeToString(pub)
 139  
 140  	// event ID = sha256 of serialized [0, pubkey, created_at, kind, tags, content]
 141  	ser, err := json.Marshal([]interface{}{0, pubHex, now, 24242, tags, ""})
 142  	if err != nil {
 143  		return "", err
 144  	}
 145  	idHash := sha256.Sum256(ser)
 146  
 147  	signer, err := p8k.New()
 148  	if err != nil {
 149  		return "", err
 150  	}
 151  	if err = signer.InitSec(sec); err != nil {
 152  		return "", err
 153  	}
 154  	sig, err := signer.Sign(idHash[:])
 155  	if err != nil {
 156  		return "", err
 157  	}
 158  
 159  	ev := map[string]interface{}{
 160  		"id":         hex.EncodeToString(idHash[:]),
 161  		"pubkey":     pubHex,
 162  		"created_at": now,
 163  		"kind":       24242,
 164  		"tags":       tags,
 165  		"content":    "",
 166  		"sig":        hex.EncodeToString(sig),
 167  	}
 168  	j, err := json.Marshal(ev)
 169  	if err != nil {
 170  		return "", err
 171  	}
 172  	if *verbose {
 173  		fmt.Printf("    auth: %s\n", j)
 174  	}
 175  	return "Nostr " + base64.StdEncoding.EncodeToString(j), nil
 176  }
 177  
 178  func upload(sec, pub []byte, server string, data []byte, contentType string) (string, error) {
 179  	hash := sha256.Sum256(data)
 180  	hashHex := hex.EncodeToString(hash[:])
 181  
 182  	auth, err := authHeader(sec, pub, "upload", hashHex)
 183  	if err != nil {
 184  		return "", err
 185  	}
 186  
 187  	url := strings.TrimSuffix(server, "/") + "/upload"
 188  	req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(data))
 189  	if err != nil {
 190  		return "", err
 191  	}
 192  	req.Header.Set("Content-Type", contentType)
 193  	req.Header.Set("Authorization", auth)
 194  
 195  	client := &http.Client{Timeout: 60 * time.Second}
 196  	resp, err := client.Do(req)
 197  	if err != nil {
 198  		return "", err
 199  	}
 200  	defer resp.Body.Close()
 201  
 202  	body, _ := io.ReadAll(resp.Body)
 203  	if resp.StatusCode != 200 && resp.StatusCode != 201 {
 204  		reason := resp.Header.Get("X-Reason")
 205  		if reason == "" {
 206  			reason = string(body)
 207  			if len(reason) > 200 {
 208  				reason = reason[:200]
 209  			}
 210  		}
 211  		return "", fmt.Errorf("%d: %s", resp.StatusCode, reason)
 212  	}
 213  
 214  	var desc struct {
 215  		URL string `json:"url"`
 216  	}
 217  	if err := json.Unmarshal(body, &desc); err != nil {
 218  		return string(body), nil
 219  	}
 220  	return desc.URL, nil
 221  }
 222  
 223  func mimeType(path string) string {
 224  	ext := filepath.Ext(path)
 225  	ct := mime.TypeByExtension(ext)
 226  	if ct == "" {
 227  		ct = "application/octet-stream"
 228  	}
 229  	return ct
 230  }
 231