├── pkg
├── nostr
│ ├── .gitignore
│ ├── log_normal.go
│ ├── timestamp.go
│ ├── log.go
│ ├── nip19
│ │ ├── utils.go
│ │ ├── nip19.go
│ │ └── nip19_test.go
│ ├── pointers.go
│ ├── normalize.go
│ ├── normalize_test.go
│ ├── log_debug.go
│ ├── metadata.go
│ ├── subscription_test.go
│ ├── keys.go
│ ├── LICENSE.md
│ ├── nip06
│ │ └── nip06.go
│ ├── nip04
│ │ ├── nip04_test.go
│ │ └── nip04.go
│ ├── nip10
│ │ └── nip10.go
│ ├── nip11
│ │ ├── fetch.go
│ │ └── types.go
│ ├── tag_test.go
│ ├── nip26
│ │ └── nip26_test.go
│ ├── event_extra.go
│ ├── sdk
│ │ ├── input.go
│ │ ├── references.go
│ │ └── references_test.go
│ ├── utils.go
│ ├── nip42
│ │ └── nip42.go
│ ├── nip05
│ │ └── nip05.go
│ ├── nip13
│ │ ├── nip13.go
│ │ └── nip13_test.go
│ ├── filter.go
│ ├── subscription.go
│ ├── pool.go
│ ├── filter_test.go
│ ├── tags.go
│ ├── README.md
│ ├── event.go
│ ├── envelopes_test.go
│ ├── event_easyjson.go
│ ├── connection.go
│ ├── event_test.go
│ ├── envelopes.go
│ ├── filter_easyjson.go
│ └── relay_test.go
├── ishell
│ ├── .gitignore
│ ├── .travis.yml
│ ├── utils_windows.go
│ ├── utils_unix.go
│ ├── functions.go
│ ├── LICENSE
│ ├── CHANGES.md
│ ├── completer.go
│ ├── context.go
│ ├── reader.go
│ ├── command.go
│ ├── example
│ │ └── main.go
│ ├── actions.go
│ ├── README.md
│ └── progress.go
└── unostr
│ └── unostr.go
├── cmd
├── control
│ ├── internal
│ │ ├── storage
│ │ │ ├── default.json
│ │ │ └── storage.go
│ │ └── control
│ │ │ └── control.go
│ └── main.go
└── agent
│ ├── internal
│ ├── storage
│ │ ├── empty_gen.py
│ │ └── storage.go
│ └── agent
│ │ ├── handler.go
│ │ └── agent.go
│ └── main.go
├── .gitignore
├── model
├── unostr.go
├── event.go
└── storage.go
├── LICENSE
├── go.mod
├── README.md
└── utils
└── utils.go
/pkg/nostr/.gitignore:
--------------------------------------------------------------------------------
1 | go-nostr
2 |
--------------------------------------------------------------------------------
/pkg/ishell/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | .DS_Store
3 | example/example
4 | .idea/
5 | *.iml
6 |
--------------------------------------------------------------------------------
/pkg/nostr/log_normal.go:
--------------------------------------------------------------------------------
1 | //go:build !debug
2 |
3 | package nostr
4 |
5 | func debugLog(str string, args ...any) {
6 | }
7 |
--------------------------------------------------------------------------------
/pkg/ishell/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | script:
3 | - go vet
4 | - test -z $(gofmt -l .)
5 | - go test -v ./...
6 | - go build
7 | go:
8 | - 1.10.x
9 |
--------------------------------------------------------------------------------
/pkg/ishell/utils_windows.go:
--------------------------------------------------------------------------------
1 | // +build windows
2 |
3 | package ishell
4 |
5 | import (
6 | "github.com/abiosoft/readline"
7 | )
8 |
9 | func clearScreen(s *Shell) error {
10 | return readline.ClearScreen(s.writer)
11 | }
12 |
--------------------------------------------------------------------------------
/cmd/control/internal/storage/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "relay": "ws://127.0.0.1:7447",
3 | "proxy": "",
4 | "connect_timeout": "5s",
5 | "ping_interval": "10s",
6 | "private_key": "",
7 | "agent_public_key_list": null
8 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.exe
2 | *.exe~
3 | *.dll
4 | *.so
5 | *.dylib
6 |
7 | *.test
8 | *.out
9 |
10 | .settings/
11 | .vscode/
12 | log/
13 | bin/
14 | test/
15 | */.DS_Store
16 | main
17 | __debug_bin
18 |
19 | history.txt
20 | .control.json
21 |
22 | /agent*
--------------------------------------------------------------------------------
/pkg/nostr/timestamp.go:
--------------------------------------------------------------------------------
1 | package nostr
2 |
3 | import "time"
4 |
5 | type Timestamp int64
6 |
7 | func Now() Timestamp {
8 | return Timestamp(time.Now().Unix())
9 | }
10 |
11 | func (t Timestamp) Time() time.Time {
12 | return time.Unix(int64(t), 0)
13 | }
14 |
--------------------------------------------------------------------------------
/model/unostr.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "nrat/pkg/nostr"
7 | )
8 |
9 | type Unostr interface {
10 | Connect() error
11 | SetConnectEvent(f func())
12 | Relay() *nostr.Relay
13 | Close() error
14 | ConnectTimeout() time.Duration
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/ishell/utils_unix.go:
--------------------------------------------------------------------------------
1 | // +build darwin dragonfly freebsd linux,!appengine netbsd openbsd solaris
2 |
3 | package ishell
4 |
5 | import (
6 | "github.com/abiosoft/readline"
7 | )
8 |
9 | func clearScreen(s *Shell) error {
10 | _, err := readline.ClearScreen(s.writer)
11 | return err
12 | }
13 |
--------------------------------------------------------------------------------
/pkg/nostr/log.go:
--------------------------------------------------------------------------------
1 | package nostr
2 |
3 | import (
4 | "log"
5 | "os"
6 | )
7 |
8 | var (
9 | // call SetOutput on InfoLogger to enable info logging
10 | InfoLogger = log.New(os.Stderr, "[go-nostr][info] ", log.LstdFlags)
11 |
12 | // call SetOutput on DebugLogger to enable debug logging
13 | DebugLogger = log.New(os.Stderr, "[go-nostr][debug] ", log.LstdFlags)
14 | )
15 |
--------------------------------------------------------------------------------
/cmd/control/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "uw/uboot"
5 |
6 | "nrat/cmd/control/internal/control"
7 | "nrat/cmd/control/internal/storage"
8 | "nrat/pkg/unostr"
9 | )
10 |
11 | func main() {
12 | uboot.NewBoot().Register(
13 | uboot.Uint("storage", uboot.UintNormal, storage.StorageUint),
14 | uboot.Uint("unostr", uboot.UintNormal, unostr.UnostrUint),
15 | uboot.Uint("control", uboot.UintAfter, control.ControlUint),
16 | ).BootTimeout(0).Start()
17 | }
18 |
--------------------------------------------------------------------------------
/cmd/agent/internal/storage/empty_gen.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import sys
3 |
4 |
5 | parser = argparse.ArgumentParser()
6 | parser.add_argument("size", help="empty file size", type=int)
7 | args = parser.parse_args()
8 | start = "%%%#"
9 | end = start[::-1]
10 |
11 | print(f"generating empty file of size {args.size} bytes")
12 | print(f"start: {start}, end: {end}")
13 |
14 | with open(f"empty.bin", "w") as f:
15 | f.write(start)
16 | f.write("\0" * (args.size * 1024))
17 | f.write(end)
18 |
--------------------------------------------------------------------------------
/pkg/nostr/nip19/utils.go:
--------------------------------------------------------------------------------
1 | package nip19
2 |
3 | import (
4 | "bytes"
5 | )
6 |
7 | const (
8 | TLVDefault uint8 = 0
9 | TLVRelay uint8 = 1
10 | TLVAuthor uint8 = 2
11 | TLVKind uint8 = 3
12 | )
13 |
14 | func readTLVEntry(data []byte) (typ uint8, value []byte) {
15 | if len(data) < 2 {
16 | return 0, nil
17 | }
18 |
19 | typ = data[0]
20 | length := int(data[1])
21 | value = data[2 : 2+length]
22 | return
23 | }
24 |
25 | func writeTLVEntry(buf *bytes.Buffer, typ uint8, value []byte) {
26 | length := len(value)
27 | buf.WriteByte(typ)
28 | buf.WriteByte(uint8(length))
29 | buf.Write(value)
30 | }
31 |
--------------------------------------------------------------------------------
/pkg/nostr/pointers.go:
--------------------------------------------------------------------------------
1 | package nostr
2 |
3 | type ProfilePointer struct {
4 | PublicKey string `json:"pubkey"`
5 | Relays []string `json:"relays,omitempty"`
6 | }
7 |
8 | type EventPointer struct {
9 | ID string `json:"id"`
10 | Relays []string `json:"relays,omitempty"`
11 | Author string `json:"author,omitempty"`
12 | Kind int `json:"kind,omitempty"`
13 | }
14 |
15 | type EntityPointer struct {
16 | PublicKey string `json:"pubkey"`
17 | Kind int `json:"kind,omitempty"`
18 | Identifier string `json:"identifier,omitempty"`
19 | Relays []string `json:"relays,omitempty"`
20 | }
21 |
--------------------------------------------------------------------------------
/model/event.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | const (
9 | EventSeparator = "\x1e"
10 | DataSeparator = "\x1f"
11 | )
12 |
13 | type Event struct {
14 | Id string // 编号
15 | Type string // 事件类型
16 | Error string // 错误消息
17 | Content string // 事件内容
18 | }
19 |
20 | func (evt *Event) Encode() string {
21 | return evt.Type + EventSeparator + evt.Error + EventSeparator + evt.Content
22 | }
23 |
24 | func (evt *Event) Decode(t string) error {
25 | n := strings.SplitN(t, EventSeparator, 3)
26 | if len(n) < 3 {
27 | return fmt.Errorf("invalid event: %s", t)
28 | }
29 |
30 | evt.Type, evt.Error, evt.Content = n[0], n[1], n[2]
31 | return nil
32 | }
33 |
--------------------------------------------------------------------------------
/cmd/agent/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "uw/uboot"
5 | "uw/ulog"
6 |
7 | "nrat/cmd/agent/internal/agent"
8 | "nrat/cmd/agent/internal/storage"
9 | "nrat/pkg/unostr"
10 |
11 | "github.com/abiosoft/readline"
12 | )
13 |
14 | func main() {
15 | ulog.GlobalFormat().SetWriter(func(s string) {
16 | _, _ = readline.Stdout.Write([]byte(s))
17 | })
18 |
19 | uboot.NewBoot().Register(
20 | uboot.Uint("storage", uboot.UintNormal, storage.StorageUint),
21 | uboot.Uint("unostr", uboot.UintNormal, unostr.UnostrUint),
22 | uboot.Uint("agent", uboot.UintNormal, agent.AgentUint),
23 | uboot.Uint("loop", uboot.UintNormal, func(c *uboot.Context) error {
24 | select {}
25 | }),
26 | ).BootTimeout(0).Start()
27 | }
28 |
--------------------------------------------------------------------------------
/pkg/nostr/normalize.go:
--------------------------------------------------------------------------------
1 | package nostr
2 |
3 | import (
4 | "net/url"
5 | "strings"
6 | )
7 |
8 | // NormalizeURL normalizes the url and replaces http://, https:// schemes by ws://, wss://.
9 | func NormalizeURL(u string) string {
10 | if u == "" {
11 | return ""
12 | }
13 |
14 | u = strings.TrimSpace(u)
15 | u = strings.ToLower(u)
16 |
17 | if !strings.HasPrefix(u, "http") && !strings.HasPrefix(u, "ws") {
18 | u = "wss://" + u
19 | }
20 | p, err := url.Parse(u)
21 | if err != nil {
22 | return ""
23 | }
24 |
25 | if p.Scheme == "http" {
26 | p.Scheme = "ws"
27 | } else if p.Scheme == "https" {
28 | p.Scheme = "wss"
29 | }
30 |
31 | p.Path = strings.TrimRight(p.Path, "/")
32 |
33 | return p.String()
34 | }
35 |
--------------------------------------------------------------------------------
/pkg/ishell/functions.go:
--------------------------------------------------------------------------------
1 | package ishell
2 |
3 | import (
4 | "os"
5 | )
6 |
7 | func exitFunc(c *Context) {
8 | c.Stop()
9 | }
10 |
11 | func helpFunc(c *Context) {
12 | c.Println(c.HelpText())
13 | }
14 |
15 | func clearFunc(c *Context) {
16 | err := c.ClearScreen()
17 | if err != nil {
18 | c.Err(err)
19 | }
20 | }
21 |
22 | func addDefaultFuncs(s *Shell) {
23 | s.AddCmd(&Cmd{
24 | Name: "exit",
25 | Help: "exit the program",
26 | Func: exitFunc,
27 | })
28 | s.AddCmd(&Cmd{
29 | Name: "help",
30 | Help: "display help",
31 | Func: helpFunc,
32 | })
33 | s.AddCmd(&Cmd{
34 | Name: "clear",
35 | Help: "clear the screen",
36 | Func: clearFunc,
37 | })
38 | s.Interrupt(interruptFunc)
39 | }
40 |
41 | func interruptFunc(c *Context, count int, line string) {
42 | if count >= 2 {
43 | c.Println("Interrupted")
44 | os.Exit(1)
45 | }
46 | c.Println("Input Ctrl-c once more to exit")
47 | }
48 |
--------------------------------------------------------------------------------
/pkg/nostr/normalize_test.go:
--------------------------------------------------------------------------------
1 | package nostr
2 |
3 | import "fmt"
4 |
5 | func ExampleNormalizeURL() {
6 | fmt.Println(NormalizeURL(""))
7 | fmt.Println(NormalizeURL("wss://x.com/y"))
8 | fmt.Println(NormalizeURL("wss://x.com/y/"))
9 | fmt.Println(NormalizeURL("http://x.com/y"))
10 | fmt.Println(NormalizeURL(NormalizeURL("http://x.com/y")))
11 | fmt.Println(NormalizeURL("wss://x.com"))
12 | fmt.Println(NormalizeURL("wss://x.com/"))
13 | fmt.Println(NormalizeURL(NormalizeURL(NormalizeURL("wss://x.com/"))))
14 | fmt.Println(NormalizeURL("x.com"))
15 | fmt.Println(NormalizeURL("x.com/"))
16 | fmt.Println(NormalizeURL("x.com////"))
17 | fmt.Println(NormalizeURL("x.com/?x=23"))
18 |
19 | // Output:
20 | //
21 | // wss://x.com/y
22 | // wss://x.com/y
23 | // ws://x.com/y
24 | // ws://x.com/y
25 | // wss://x.com
26 | // wss://x.com
27 | // wss://x.com
28 | // wss://x.com
29 | // wss://x.com
30 | // wss://x.com
31 | // wss://x.com?x=23
32 | }
33 |
--------------------------------------------------------------------------------
/pkg/nostr/log_debug.go:
--------------------------------------------------------------------------------
1 | //go:build debug
2 |
3 | package nostr
4 |
5 | import (
6 | "encoding/json"
7 | "fmt"
8 | )
9 |
10 | func debugLog(str string, args ...any) {
11 | // this is such that we don't modify the actual args that may be used outside of this function
12 | printableArgs := make([]any, len(args))
13 |
14 | for i, v := range args {
15 | printableArgs[i] = stringify(v)
16 | }
17 |
18 | DebugLogger.Printf(str, printableArgs...)
19 | }
20 |
21 | func stringify(anything any) any {
22 | switch v := anything.(type) {
23 | case []any:
24 | // this is such that we don't modify the actual values that may be used outside of this function
25 | printableValues := make([]any, len(v))
26 | for i, subv := range v {
27 | printableValues[i] = stringify(subv)
28 | }
29 | return printableValues
30 | case []json.RawMessage:
31 | j, _ := json.Marshal(v)
32 | return string(j)
33 | case []byte:
34 | return string(v)
35 | case fmt.Stringer:
36 | return v.String()
37 | default:
38 | return v
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/nostr/metadata.go:
--------------------------------------------------------------------------------
1 | package nostr
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | )
7 |
8 | type ProfileMetadata struct {
9 | Name string `json:"name,omitempty"`
10 | DisplayName string `json:"display_name,omitempty"`
11 | About string `json:"about,omitempty"`
12 | Website string `json:"website,omitempty"`
13 | Picture string `json:"picture,omitempty"`
14 | Banner string `json:"banner,omitempty"`
15 | NIP05 string `json:"nip05,omitempty"`
16 | LUD16 string `json:"lud16,omitempty"`
17 | }
18 |
19 | func ParseMetadata(event Event) (*ProfileMetadata, error) {
20 | if event.Kind != 0 {
21 | return nil, fmt.Errorf("event %s is kind %d, not 0", event.ID, event.Kind)
22 | }
23 |
24 | var meta ProfileMetadata
25 | err := json.Unmarshal([]byte(event.Content), &meta)
26 | if err != nil {
27 | cont := event.Content
28 | if len(cont) > 100 {
29 | cont = cont[0:99]
30 | }
31 | return nil, fmt.Errorf("failed to parse metadata (%s) from event %s: %w", cont, event.ID, err)
32 | }
33 |
34 | return &meta, nil
35 | }
36 |
--------------------------------------------------------------------------------
/pkg/nostr/subscription_test.go:
--------------------------------------------------------------------------------
1 | package nostr
2 |
3 | import (
4 | "context"
5 | "testing"
6 | "time"
7 | )
8 |
9 | // test if we can connect to wss://relay.damus.io and fetch a couple of random events
10 | func TestSubscribe(t *testing.T) {
11 | rl := mustRelayConnect("wss://relay.damus.io")
12 | defer rl.Close()
13 |
14 | sub, err := rl.Subscribe(context.Background(), Filters{{Kinds: []int{1}, Limit: 2}})
15 | if err != nil {
16 | t.Errorf("subscription failed: %v", err)
17 | return
18 | }
19 |
20 | timeout := time.After(5 * time.Second)
21 | events := 0
22 |
23 | for {
24 | select {
25 | case event := <-sub.Events:
26 | if event == nil {
27 | t.Errorf("event is nil: %v", event)
28 | }
29 | events++
30 | case <-sub.EndOfStoredEvents:
31 | goto end
32 | case <-rl.Context().Done():
33 | t.Errorf("connection closed: %v", rl.Context().Err())
34 | goto end
35 | case <-timeout:
36 | t.Errorf("timeout")
37 | goto end
38 | }
39 | }
40 |
41 | end:
42 | if events != 2 {
43 | t.Errorf("expected 2 events, got %d", events)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/pkg/nostr/keys.go:
--------------------------------------------------------------------------------
1 | package nostr
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/hex"
6 | "io"
7 | "math/big"
8 | "strings"
9 |
10 | "github.com/btcsuite/btcd/btcec/v2"
11 | "github.com/btcsuite/btcd/btcec/v2/schnorr"
12 | )
13 |
14 | func GeneratePrivateKey() string {
15 | params := btcec.S256().Params()
16 | one := new(big.Int).SetInt64(1)
17 |
18 | b := make([]byte, params.BitSize/8+8)
19 | _, err := io.ReadFull(rand.Reader, b)
20 | if err != nil {
21 | return ""
22 | }
23 |
24 | k := new(big.Int).SetBytes(b)
25 | n := new(big.Int).Sub(params.N, one)
26 | k.Mod(k, n)
27 | k.Add(k, one)
28 |
29 | return hex.EncodeToString(k.Bytes())
30 | }
31 |
32 | func GetPublicKey(sk string) (string, error) {
33 | b, err := hex.DecodeString(sk)
34 | if err != nil {
35 | return "", err
36 | }
37 |
38 | _, pk := btcec.PrivKeyFromBytes(b)
39 | return hex.EncodeToString(schnorr.SerializePubKey(pk)), nil
40 | }
41 |
42 | func IsValidPublicKeyHex(pk string) bool {
43 | if strings.ToLower(pk) != pk {
44 | return false
45 | }
46 | dec, _ := hex.DecodeString(pk)
47 | return len(dec) == 32
48 | }
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 ClarkQAQ
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/pkg/nostr/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 nbd
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/pkg/ishell/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Abiola Ibrahim
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/pkg/nostr/nip06/nip06.go:
--------------------------------------------------------------------------------
1 | package nip06
2 |
3 | import (
4 | "encoding/hex"
5 |
6 | "github.com/tyler-smith/go-bip32"
7 | "github.com/tyler-smith/go-bip39"
8 | )
9 |
10 | func GenerateSeedWords() (string, error) {
11 | entropy, err := bip39.NewEntropy(256)
12 | if err != nil {
13 | return "", err
14 | }
15 |
16 | words, err := bip39.NewMnemonic(entropy)
17 | if err != nil {
18 | return "", err
19 | }
20 |
21 | return words, nil
22 | }
23 |
24 | func SeedFromWords(words string) []byte {
25 | return bip39.NewSeed(words, "")
26 | }
27 |
28 | func PrivateKeyFromSeed(seed []byte) (string, error) {
29 | key, err := bip32.NewMasterKey(seed)
30 | if err != nil {
31 | return "", err
32 | }
33 |
34 | derivationPath := []uint32{
35 | bip32.FirstHardenedChild + 44,
36 | bip32.FirstHardenedChild + 1237,
37 | bip32.FirstHardenedChild + 0,
38 | 0,
39 | 0,
40 | }
41 |
42 | next := key
43 | for _, idx := range derivationPath {
44 | var err error
45 | next, err = next.NewChildKey(idx)
46 | if err != nil {
47 | return "", err
48 | }
49 | }
50 |
51 | return hex.EncodeToString(next.Key), nil
52 | }
53 |
54 | func ValidateWords(words string) bool {
55 | return bip39.IsMnemonicValid(words)
56 | }
57 |
--------------------------------------------------------------------------------
/pkg/nostr/nip04/nip04_test.go:
--------------------------------------------------------------------------------
1 | package nip04
2 |
3 | import (
4 | "strings"
5 | "testing"
6 | )
7 |
8 | func TestEncryptionAndDecryption(t *testing.T) {
9 | sharedSecret := make([]byte, 32)
10 | message := "hello hellow"
11 |
12 | ciphertext, err := Encrypt(message, sharedSecret)
13 | if err != nil {
14 | t.Errorf("failed to encrypt: %s", err.Error())
15 | }
16 |
17 | plaintext, err := Decrypt(ciphertext, sharedSecret)
18 | if err != nil {
19 | t.Errorf("failed to decrypt: %s", err.Error())
20 | }
21 |
22 | if message != plaintext {
23 | t.Errorf("original '%s' and decrypted '%s' messages differ", message, plaintext)
24 | }
25 | }
26 |
27 | func TestEncryptionAndDecryptionWithMultipleLengths(t *testing.T) {
28 | sharedSecret := make([]byte, 32)
29 |
30 | for i := 0; i < 150; i++ {
31 | message := strings.Repeat("a", i)
32 |
33 | ciphertext, err := Encrypt(message, sharedSecret)
34 | if err != nil {
35 | t.Errorf("failed to encrypt: %s", err.Error())
36 | }
37 |
38 | plaintext, err := Decrypt(ciphertext, sharedSecret)
39 | if err != nil {
40 | t.Errorf("failed to decrypt: %s", err.Error())
41 | }
42 |
43 | if message != plaintext {
44 | t.Errorf("original '%s' and decrypted '%s' messages differ", message, plaintext)
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/model/storage.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | type UnostrStorageData struct {
4 | Relay string `json:"relay"` // 中继器
5 | Proxy string `json:"proxy"` // 代理
6 | ConnectTimeout string `json:"connect_timeout"` // 连接超时
7 | PingInterval string `json:"ping_interval"` // ping间隔
8 | }
9 |
10 | type UnostrStorage interface {
11 | Unostr() *UnostrStorageData
12 | Write() error
13 | Read() error
14 | }
15 |
16 | type AgentStorageData struct {
17 | *UnostrStorageData
18 | PrivateKey string `json:"private_key"` // 私钥
19 | BroadcastInterval string `json:"broadcast_interval"` // 广播间隔
20 | PublicKey string `json:"-"` // 公钥
21 | }
22 |
23 | type ControlStorageData struct {
24 | *UnostrStorageData
25 | PrivateKey string `json:"private_key"` // 私钥
26 | AgentPrivateKeyList []string `json:"agent_private_key_list"` // 客户端私钥列表
27 | PublicKey string `json:"-"` // 公钥
28 | CmdTimeout string `json:"cmd_timeout"` // 命令等待超时
29 | HistoryFile string `json:"history_file"` // 历史文件
30 | ExecTimeout string `json:"exec_timeout"` // 远程命令执行超时
31 | }
32 |
33 | type Storage[T any] interface {
34 | Storage() T
35 | Write() error
36 | Read() error
37 | }
38 |
--------------------------------------------------------------------------------
/pkg/ishell/CHANGES.md:
--------------------------------------------------------------------------------
1 | For now, dates (DD/MM/YYYY) are used until ishell gets stable enough to warrant tags.
2 | Attempts will be made to ensure non breaking updates as much as possible.
3 | #### 28/05/2017
4 | * Added `shell.Process(os.Args[1:]...)` for non-interactive execution
5 | *
6 |
7 |
8 | #### 07/02/2016
9 | Added multiline support to shell mode.
10 |
11 | #### 23/01/2016
12 | * Added history support.
13 | * Added tab completion support.
14 | * Added `SetHistoryPath`, `SetMultiPrompt`
15 | * Removed password masks.
16 | * **Breaking Change**: changed definition of `ReadPassword` from `(string)` to `()`
17 | * **Breaking Change**: changed name of `Shell` constructor from `NewShell` to `New`
18 |
19 | #### 13/07/2015
20 | * Added `ClearScreen` method.
21 | * Added `clear` to default commands.
22 |
23 | #### 12/07/2015:
24 | * Added `PrintCommands`, `Commands` and `ShowPrompt` methods.
25 | * Added default `exit` and `help` commands.
26 | * **Breaking Change**: changed return values of `ReadLine` from `(string, error)` to `string.`
27 | * **Breaking Change**: changed definition of `CmdFunc` from `(cmd string, args []string)` to `(args ...String)` to remove redundant command being passed.
28 | * Added multiline input support.
29 | * Added case insensitive command support.
30 |
31 | #### 11/07/2015:
32 | * Initial version.
33 |
--------------------------------------------------------------------------------
/pkg/nostr/nip10/nip10.go:
--------------------------------------------------------------------------------
1 | package nip10
2 |
3 | import "nrat/pkg/nostr"
4 |
5 | func GetThreadRoot(tags nostr.Tags) *nostr.Tag {
6 | for _, tag := range tags {
7 | if len(tag) >= 4 && tag[0] == "e" && tag[3] == "root" {
8 | return &tag
9 | }
10 | }
11 |
12 | return tags.GetFirst([]string{"e", ""})
13 | }
14 |
15 | func GetImmediateReply(tags nostr.Tags) *nostr.Tag {
16 | var root *nostr.Tag
17 | var lastE *nostr.Tag
18 |
19 | for i := len(tags) - 1; i >= 0; i-- {
20 | tag := tags[i]
21 |
22 | if len(tag) < 2 {
23 | continue
24 | }
25 | if tag[0] != "e" {
26 | continue
27 | }
28 |
29 | if len(tag) >= 4 {
30 | if tag[3] == "reply" {
31 | return &tag
32 | }
33 | if tag[3] == "root" {
34 | // will be used as our first fallback
35 | root = &tag
36 | continue
37 | }
38 | if tag[3] == "mention" {
39 | // this invalidates this tag as a second fallback mechanism (clients that don't add markers)
40 | continue
41 | }
42 | }
43 |
44 | lastE = &tag // will be used as our second fallback (clients that don't add markers)
45 | }
46 |
47 | // if we reached this point we don't have a "reply", but if we have a "root"
48 | // that means this event is a direct reply to the root
49 | if root != nil {
50 | return root
51 | }
52 |
53 | // if we reached this point and we have at least one "e" we'll use that (the last)
54 | return lastE
55 | }
56 |
--------------------------------------------------------------------------------
/pkg/nostr/nip11/fetch.go:
--------------------------------------------------------------------------------
1 | package nip11
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 |
8 | "net/http"
9 | "net/url"
10 | "strings"
11 |
12 | "time"
13 | )
14 |
15 |
16 | // Fetch fetches the NIP-11 RelayInformationDocument.
17 | func Fetch(ctx context.Context, u string) (info *RelayInformationDocument, err error) {
18 | if _, ok := ctx.Deadline(); !ok {
19 | // if no timeout is set, force it to 7 seconds
20 | var cancel context.CancelFunc
21 | ctx, cancel = context.WithTimeout(ctx, 7*time.Second)
22 | defer cancel()
23 | }
24 |
25 | // normalize URL to start with http:// or https://
26 | if !strings.HasPrefix(u, "http") && !strings.HasPrefix(u, "ws") {
27 | u = "wss://" + u
28 | }
29 | p, err := url.Parse(u)
30 | if err != nil {
31 | return nil, fmt.Errorf("Cannot parse url: %s", u)
32 | }
33 | if p.Scheme == "ws" {
34 | p.Scheme = "http"
35 | } else if p.Scheme == "wss" {
36 | p.Scheme = "https"
37 | }
38 | p.Path = strings.TrimRight(p.Path, "/")
39 |
40 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.String(), nil)
41 |
42 | // add the NIP-11 header
43 | req.Header.Add("Accept", "application/nostr+json")
44 |
45 | // send the request
46 | resp, err := http.DefaultClient.Do(req)
47 | if err != nil {
48 | return nil, err
49 | }
50 | info = &RelayInformationDocument{}
51 | dec := json.NewDecoder(resp.Body)
52 | err = dec.Decode(info)
53 | return info, err
54 | }
55 |
--------------------------------------------------------------------------------
/pkg/nostr/tag_test.go:
--------------------------------------------------------------------------------
1 | package nostr
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestTagHelpers(t *testing.T) {
8 | tags := Tags{
9 | Tag{"x"},
10 | Tag{"p", "abcdef", "wss://x.com"},
11 | Tag{"p", "123456", "wss://y.com"},
12 | Tag{"e", "eeeeee"},
13 | Tag{"e", "ffffff"},
14 | }
15 |
16 | if tags.GetFirst([]string{"x"}) == nil {
17 | t.Error("failed to get existing prefix")
18 | }
19 | if tags.GetFirst([]string{"x", ""}) != nil {
20 | t.Error("got with wrong prefix")
21 | }
22 | if tags.GetFirst([]string{"p", "abcdef", "wss://"}) == nil {
23 | t.Error("failed to get with existing prefix")
24 | }
25 | if tags.GetFirst([]string{"p", "abcdef", ""}) == nil {
26 | t.Error("failed to get with existing prefix (blank last string)")
27 | }
28 | if (*(tags.GetLast([]string{"e"})))[1] != "ffffff" {
29 | t.Error("failed to get last")
30 | }
31 |
32 | if len(tags.GetAll([]string{"e", ""})) != 2 {
33 | t.Error("failed to get all")
34 | }
35 |
36 | if len(tags.AppendUnique(Tag{"e", "ffffff"})) != 5 {
37 | t.Error("append unique changed the array size when existed")
38 | }
39 | if len(tags.AppendUnique(Tag{"e", "bbbbbb"})) != 6 {
40 | t.Error("append unique failed to append when didn't exist")
41 | }
42 | if tags.AppendUnique(Tag{"e", "eeeeee"})[4][1] != "ffffff" {
43 | t.Error("append unique changed the order")
44 | }
45 | if tags.AppendUnique(Tag{"e", "eeeeee"})[3][1] != "eeeeee" {
46 | t.Error("append unique changed the order")
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/pkg/ishell/completer.go:
--------------------------------------------------------------------------------
1 | package ishell
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/flynn-archive/go-shlex"
7 | )
8 |
9 | type iCompleter struct {
10 | cmd *Cmd
11 | disabled func() bool
12 | }
13 |
14 | func (ic iCompleter) Do(line []rune, pos int) (newLine [][]rune, length int) {
15 | if ic.disabled != nil && ic.disabled() {
16 | return nil, len(line)
17 | }
18 | var words []string
19 | if w, err := shlex.Split(string(line)); err == nil {
20 | words = w
21 | } else {
22 | // fall back
23 | words = strings.Fields(string(line))
24 | }
25 |
26 | var cWords []string
27 | prefix := ""
28 | if len(words) > 0 && pos > 0 && line[pos-1] != ' ' {
29 | prefix = words[len(words)-1]
30 | cWords = ic.getWords(prefix, words[:len(words)-1])
31 | } else {
32 | cWords = ic.getWords(prefix, words)
33 | }
34 |
35 | var suggestions [][]rune
36 | for _, w := range cWords {
37 | if strings.HasPrefix(w, prefix) {
38 | suggestions = append(suggestions, []rune(strings.TrimPrefix(w, prefix)))
39 | }
40 | }
41 | if len(suggestions) == 1 && prefix != "" && string(suggestions[0]) == "" {
42 | suggestions = [][]rune{[]rune(" ")}
43 | }
44 | return suggestions, len(prefix)
45 | }
46 |
47 | func (ic iCompleter) getWords(prefix string, w []string) (s []string) {
48 | cmd, args := ic.cmd.FindCmd(w)
49 | if cmd == nil {
50 | cmd, args = ic.cmd, w
51 | }
52 | if cmd.CompleterWithPrefix != nil {
53 | return cmd.CompleterWithPrefix(prefix, args)
54 | }
55 | if cmd.Completer != nil {
56 | return cmd.Completer(args)
57 | }
58 | for k := range cmd.children {
59 | s = append(s, k)
60 | }
61 | return
62 | }
63 |
--------------------------------------------------------------------------------
/pkg/ishell/context.go:
--------------------------------------------------------------------------------
1 | package ishell
2 |
3 | // Context is an ishell context. It embeds ishell.Actions.
4 | type Context struct {
5 | contextValues
6 | progressBar ProgressBar
7 | err error
8 |
9 | // Args is command arguments.
10 | Args []string
11 |
12 | // RawArgs is unprocessed command arguments.
13 | RawArgs []string
14 |
15 | // Cmd is the currently executing command. This is empty for NotFound and Interrupt.
16 | Cmd Cmd
17 |
18 | Actions
19 | }
20 |
21 | // Err informs ishell that an error occurred in the current
22 | // function.
23 | func (c *Context) Err(err error) {
24 | c.err = err
25 | }
26 |
27 | // ProgressBar returns the progress bar for the current shell context.
28 | func (c *Context) ProgressBar() ProgressBar {
29 | return c.progressBar
30 | }
31 |
32 | // contextValues is the map for values in the context.
33 | type contextValues map[string]interface{}
34 |
35 | // Get returns the value associated with this context for key, or nil
36 | // if no value is associated with key. Successive calls to Get with
37 | // the same key returns the same result.
38 | func (c contextValues) Get(key string) interface{} {
39 | return c[key]
40 | }
41 |
42 | // Set sets the key in this context to value.
43 | func (c *contextValues) Set(key string, value interface{}) {
44 | if *c == nil {
45 | *c = make(map[string]interface{})
46 | }
47 | (*c)[key] = value
48 | }
49 |
50 | // Del deletes key and its value in this context.
51 | func (c contextValues) Del(key string) {
52 | delete(c, key)
53 | }
54 |
55 | // Keys returns all keys in the context.
56 | func (c contextValues) Keys() (keys []string) {
57 | for key := range c {
58 | keys = append(keys, key)
59 | }
60 | return
61 | }
62 |
--------------------------------------------------------------------------------
/pkg/nostr/nip26/nip26_test.go:
--------------------------------------------------------------------------------
1 | package nip26
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "nrat/pkg/nostr"
8 | )
9 |
10 | func TestDelegateSign(t *testing.T) {
11 | since := time.Unix(1600000000, 0)
12 | until := time.Unix(1600000100, 0)
13 | delegator_secret_key, delegatee_secret_key := "3f0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459da", "e9142f724955c5854de36324dab0434f97b15ec6b33464d56ebe491e3f559d1b"
14 | delegatee_pubkey, _ := nostr.GetPublicKey(delegatee_secret_key)
15 | d1, err := CreateToken(delegator_secret_key, delegatee_pubkey, []int{1, 2, 3}, &since, &until)
16 | if err != nil {
17 | t.Error(err)
18 | }
19 | ev := &nostr.Event{}
20 | ev.CreatedAt = nostr.Timestamp(1600000050)
21 | ev.Content = "hello world"
22 | ev.Kind = 1
23 | if err != nil {
24 | t.Error(err)
25 | }
26 | d2, err := Import(d1.Tag(), delegatee_pubkey)
27 | if err != nil {
28 | t.Error(err)
29 | }
30 |
31 | if err = DelegatedSign(ev, d2, delegatee_secret_key); err != nil {
32 | t.Error(err)
33 | }
34 | if ok, err := CheckDelegation(ev); err != nil || ok == false {
35 | t.Error(err)
36 | }
37 |
38 | tag := []string{"delegation", "9ea72be3fcfe38103195a41b67b6f96c14ed92d2091d6d9eb8166a5c27b0c35d", "kind=1&kind=2&kind=3&created_at>1600000000", "8432b8c86f789c2783ef3becb0fabf4def6031c6a615fa7a622f31329d80ed1b2a79ab753c0462f1440503c94e1829158a3a854a1d418ad256ae2cf8aa19fa9a"}
39 | d3, err := Import(tag, delegatee_pubkey)
40 | if err != nil {
41 | t.Error(err)
42 | }
43 |
44 | ev.Tags = nil
45 | if err = DelegatedSign(ev, d3, delegatee_secret_key); err != nil {
46 | t.Error(err)
47 | }
48 | if ok, err := d3.Parse(ev); err != nil || ok == false {
49 | t.Error(err)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/pkg/nostr/event_extra.go:
--------------------------------------------------------------------------------
1 | package nostr
2 |
3 | // SetExtra sets an out-of-the-spec value under the given key into the event object.
4 | func (evt *Event) SetExtra(key string, value any) {
5 | if evt.extra == nil {
6 | evt.extra = make(map[string]any)
7 | }
8 | evt.extra[key] = value
9 | }
10 |
11 | // GetExtra tries to get a value under the given key that may be present in the event object
12 | // but is hidden in the basic type since it is out of the spec.
13 | func (evt Event) GetExtra(key string) any {
14 | return evt.extra[key]
15 | }
16 |
17 | // GetExtraString is like [Event.GetExtra], but only works if the value is a string,
18 | // otherwise returns the zero-value.
19 | func (evt Event) GetExtraString(key string) string {
20 | ival, ok := evt.extra[key]
21 | if !ok {
22 | return ""
23 | }
24 | val, ok := ival.(string)
25 | if !ok {
26 | return ""
27 | }
28 | return val
29 | }
30 |
31 | // GetExtraNumber is like [Event.GetExtra], but only works if the value is a float64,
32 | // otherwise returns the zero-value.
33 | func (evt Event) GetExtraNumber(key string) float64 {
34 | ival, ok := evt.extra[key]
35 | if !ok {
36 | return 0
37 | }
38 |
39 | switch val := ival.(type) {
40 | case float64:
41 | return val
42 | case int:
43 | return float64(val)
44 | case int64:
45 | return float64(val)
46 | }
47 |
48 | return 0
49 | }
50 |
51 | // GetExtraBoolean is like [Event.GetExtra], but only works if the value is a boolean,
52 | // otherwise returns the zero-value.
53 | func (evt Event) GetExtraBoolean(key string) bool {
54 | ival, ok := evt.extra[key]
55 | if !ok {
56 | return false
57 | }
58 | val, ok := ival.(bool)
59 | if !ok {
60 | return false
61 | }
62 | return val
63 | }
64 |
--------------------------------------------------------------------------------
/pkg/nostr/sdk/input.go:
--------------------------------------------------------------------------------
1 | package sdk
2 |
3 | import (
4 | "context"
5 | "encoding/hex"
6 |
7 | "nrat/pkg/nostr"
8 |
9 | "nrat/pkg/nostr/nip05"
10 | "nrat/pkg/nostr/nip19"
11 | )
12 |
13 | // InputToProfile turns any npub/nprofile/hex/nip05 input into a ProfilePointer (or nil)
14 | func InputToProfile(ctx context.Context, input string) *nostr.ProfilePointer {
15 | // handle if it is a hex string
16 | if len(input) == 64 {
17 | if _, err := hex.DecodeString(input); err == nil {
18 | return &nostr.ProfilePointer{PublicKey: input}
19 | }
20 | }
21 |
22 | // handle nip19 codes, if that's the case
23 | prefix, data, _ := nip19.Decode(input)
24 | switch prefix {
25 | case "npub":
26 | input = data.(string)
27 | return &nostr.ProfilePointer{PublicKey: input}
28 | case "nprofile":
29 | pp := data.(nostr.ProfilePointer)
30 | return &pp
31 | }
32 |
33 | // handle nip05 ids, if that's the case
34 | pp, _ := nip05.QueryIdentifier(ctx, input)
35 | if pp != nil {
36 | return pp
37 | }
38 |
39 | return nil
40 | }
41 |
42 | // InputToEventPointer turns any note/nevent/hex input into a EventPointer (or nil)
43 | func InputToEventPointer(input string) *nostr.EventPointer {
44 | // handle if it is a hex string
45 | if len(input) == 64 {
46 | if _, err := hex.DecodeString(input); err == nil {
47 | return &nostr.EventPointer{ID: input}
48 | }
49 | }
50 |
51 | // handle nip19 codes, if that's the case
52 | prefix, data, _ := nip19.Decode(input)
53 | switch prefix {
54 | case "note":
55 | input = data.(string)
56 | return &nostr.EventPointer{ID: input}
57 | case "nevent":
58 | ep := data.(nostr.EventPointer)
59 | return &ep
60 | }
61 |
62 | // handle nip05 ids, if that's the case
63 | return nil
64 | }
65 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module nrat
2 |
3 | go 1.20
4 |
5 | replace uw => github.com/ClarkQAQ/uw v0.0.0-20230911090314-9617b4352e12
6 |
7 | require (
8 | github.com/SaveTheRbtz/generic-sync-map-go v0.0.0-20230201052002-6c5833b989be
9 | github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db
10 | github.com/atotto/clipboard v0.1.4
11 | github.com/btcsuite/btcd/btcec/v2 v2.3.2
12 | github.com/btcsuite/btcd/btcutil v1.1.3
13 | github.com/fatih/color v1.15.0
14 | github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568
15 | github.com/gobwas/httphead v0.1.0
16 | github.com/gobwas/ws v1.2.1
17 | github.com/mailru/easyjson v0.7.7
18 | github.com/nbd-wtf/go-nostr v0.18.7
19 | github.com/tidwall/gjson v1.14.4
20 | github.com/tyler-smith/go-bip32 v1.0.0
21 | github.com/tyler-smith/go-bip39 v1.1.0
22 | golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
23 | golang.org/x/net v0.36.0
24 | uw v0.0.0-00010101000000-000000000000
25 | )
26 |
27 | require (
28 | github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect
29 | github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect
30 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect
31 | github.com/chzyer/test v1.0.0 // indirect
32 | github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
33 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
34 | github.com/gobwas/pool v0.2.1 // indirect
35 | github.com/josharian/intern v1.0.0 // indirect
36 | github.com/mattn/go-colorable v0.1.13 // indirect
37 | github.com/mattn/go-isatty v0.0.17 // indirect
38 | github.com/puzpuzpuz/xsync v1.5.2 // indirect
39 | github.com/tidwall/match v1.1.1 // indirect
40 | github.com/tidwall/pretty v1.2.0 // indirect
41 | golang.org/x/crypto v0.35.0 // indirect
42 | golang.org/x/sys v0.30.0 // indirect
43 | )
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NRAT
2 |
3 | > 一个基于 Nostr 去中心的匿名远程控制工具
4 |
5 | > A decentralized anonymous remote control tool based on Nostr
6 |
7 | > Децентрализованный анонимный инструмент удаленного управления на основе Nostr
8 |
9 | ## 介绍
10 |
11 | Nrat 是一个基于 Nostr 去中心的匿名远程控制工具, 使用 Nostr 的匿名通信特性, 使得 Nrat 可以在不暴露服务器 IP 的情况下进行远程控制.
12 |
13 | 并且由于愈发健壮的 Nostr 网络, 在全球范围内的节点都可以进行通信, 相较于传统的 IRC 中继网络, Nrat 的通信更加稳定, 延迟更低, 并且支持非对称加密, 使得通信更加安全.
14 |
15 | Nrat 由两个部分组成, 一个是控制端, 一个是被控端.
16 |
17 | control: 控制端用于控制被控端, 并且在没有 Golang 语言环境的情况下修补被控端二进制嵌入配置文件数据.
18 | agent: 被控端用于接收控制端的指令, 并定期通过 meta 广播自身的信息, 以便控制端发现.
19 |
20 |
21 | #### 已知问题
22 |
23 | 1. 在 Windows 平台 cd 命令不能正常处理路径
24 | 2. 文件上传下载没有切片和断点续传, 在某些节点上面可能会严格限制报文大小, 导致不能传输大文件...
25 |
26 | ## 功能
27 |
28 | 1. 文件管理 (文件增删改查)
29 | 2. 远程执行命令
30 | 3. 剪切板
31 | 4. 截图 (WIP)
32 |
33 | ## 使用
34 |
35 | ### 控制端
36 |
37 | 编译或者在 [Release](https://github.com/ClarkQAQ/nrat/releases) 中下载控制端二进制文件, 然后运行即可.
38 |
39 | ### 被控端
40 |
41 | 编译或者在 [Release](https://github.com/ClarkQAQ/nrat/releases) 中下载被控端二进制文件, 然后使用控制端的 `fix