final version functioning
This commit is contained in:
commit
858d8877ff
82
README.md
Normal file
82
README.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# whspbrd
|
||||||
|
|
||||||
|
Simple end-to-end encrypted terminal chat with a relay server.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o whspbrd
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./whspbrd --mode server --listen :9090
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client
|
||||||
|
|
||||||
|
First run once to print your identity (share with others):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./whspbrd --mode client --show-identity
|
||||||
|
```
|
||||||
|
|
||||||
|
Then connect (you need the peer fingerprint and base64 public key):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./whspbrd --mode client --connect 127.0.0.1:9090 --peer <peer-fingerprint> --peer-key <base64-pubkey>
|
||||||
|
|
||||||
|
## TUI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./whspbrd --mode client --tui --connect 127.0.0.1:9090
|
||||||
|
```
|
||||||
|
|
||||||
|
TUI commands:
|
||||||
|
|
||||||
|
- `/add <name> <pubkey>` add a contact.
|
||||||
|
- `/rename <name|fingerprint> <new-name>` rename contact.
|
||||||
|
- `/remove <name|fingerprint>` remove contact.
|
||||||
|
- `/trust <pubkey>` trust a peer key.
|
||||||
|
- `/sendfile <path>` send a file.
|
||||||
|
- `/whoami` show your fingerprint.
|
||||||
|
- `/pubkey` show your public key.
|
||||||
|
- `/help` list commands.
|
||||||
|
|
||||||
|
TUI navigation:
|
||||||
|
|
||||||
|
- `Tab` switch focus between Users and Input.
|
||||||
|
- `Enter` send message.
|
||||||
|
- `Up/Down` select contact in Users.
|
||||||
|
- `PgUp/PgDn` scroll chat history.
|
||||||
|
- `Ctrl+U` / `Ctrl+D` page up/down.
|
||||||
|
- `Home` / `End` jump to top/bottom of chat.
|
||||||
|
|
||||||
|
## Self-test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./whspbrd --selftest
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
During chat, commands:
|
||||||
|
|
||||||
|
- `/whoami` shows your fingerprint.
|
||||||
|
- `/pubkey` shows your base64 public key.
|
||||||
|
- `/trust <base64-pubkey>` stores a peer key (TOFU).
|
||||||
|
- `/sendfile <path>` sends a file (images supported).
|
||||||
|
- `/quit` exits.
|
||||||
|
- `/list` shows last 50 messages (stored locally).
|
||||||
|
|
||||||
|
## Data locations
|
||||||
|
|
||||||
|
- Config: `$XDG_CONFIG_HOME/whspbrd` (fallback `~/.config/whspbrd`)
|
||||||
|
- Data: `$XDG_DATA_HOME/whspbrd` (fallback `~/.local/share/whspbrd`)
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- `identity.key` long-term private key.
|
||||||
|
- `peers.json` trusted peers for TOFU.
|
||||||
|
- received files are written into the data directory.
|
||||||
|
- `messages.db` stores chat history (SQLite).
|
||||||
108
colors.go
Normal file
108
colors.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jroimartin/gocui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Color holds the numeric part of an ANSI sequence (e.g. 31, 32, 313, ...)
|
||||||
|
type Color struct{ code int }
|
||||||
|
|
||||||
|
func NewColor(code int) Color { return Color{code} }
|
||||||
|
|
||||||
|
type tuiColors struct {
|
||||||
|
UserFg gocui.Attribute
|
||||||
|
ChatFg gocui.Attribute
|
||||||
|
StatusFg gocui.Attribute
|
||||||
|
InputFg gocui.Attribute
|
||||||
|
SelFg gocui.Attribute
|
||||||
|
SelBg gocui.Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
var TuiColors = tuiColors{
|
||||||
|
UserFg: gocui.ColorWhite,
|
||||||
|
ChatFg: gocui.ColorWhite,
|
||||||
|
StatusFg: gocui.ColorYellow,
|
||||||
|
InputFg: gocui.ColorGreen,
|
||||||
|
SelFg: gocui.ColorBlack,
|
||||||
|
SelBg: gocui.ColorCyan,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text returns the basic foreground sequence, e.g. "\033[31m"
|
||||||
|
func (c Color) Text() string { return fmt.Sprintf("\033[%dm", c.code) }
|
||||||
|
|
||||||
|
// Underline returns the sequence with underline: "\033[31;4m"
|
||||||
|
func (c Color) Underline() string { return fmt.Sprintf("\033[%d;4m", c.code) }
|
||||||
|
|
||||||
|
// Blink returns the sequence with blink: "\033[31;5m" (kept for completeness)
|
||||||
|
func (c Color) Blink() string { return fmt.Sprintf("\033[%d;5m", c.code) }
|
||||||
|
|
||||||
|
// Inverse returns the sequence with inverse/swap: "\033[31;7m"
|
||||||
|
// (this matches your original usage of ;7m as "background"/inverse)
|
||||||
|
func (c Color) Inverse() string { return fmt.Sprintf("\033[%d;7m", c.code) }
|
||||||
|
|
||||||
|
// Bg returns a real background color sequence (code + 10 -> 40..47 etc).
|
||||||
|
// e.g. if code==31 -> Bg() -> "\033[41m"
|
||||||
|
func (c Color) Bg() string { return fmt.Sprintf("\033[%dm", c.code+10) }
|
||||||
|
|
||||||
|
// Attr lets you compose any extra attribute numbers (e.g. 1 for bold)
|
||||||
|
func (c Color) Attr(attrs ...int) string {
|
||||||
|
if len(attrs) == 0 {
|
||||||
|
return c.Text()
|
||||||
|
}
|
||||||
|
s := fmt.Sprintf("\033[%d", c.code)
|
||||||
|
for _, a := range attrs {
|
||||||
|
s += fmt.Sprintf(";%d", a)
|
||||||
|
}
|
||||||
|
s += "m"
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer (defaults to Text())
|
||||||
|
func (c Color) String() string { return c.Text() }
|
||||||
|
|
||||||
|
type Palette struct {
|
||||||
|
Base00 Color
|
||||||
|
Base01 Color
|
||||||
|
Base02 Color
|
||||||
|
Base03 Color
|
||||||
|
Base04 Color
|
||||||
|
Base05 Color
|
||||||
|
Base06 Color
|
||||||
|
Base07 Color
|
||||||
|
Base08 Color
|
||||||
|
Base09 Color
|
||||||
|
Base10 Color
|
||||||
|
Base11 Color
|
||||||
|
Base12 Color
|
||||||
|
Base13 Color
|
||||||
|
Base14 Color
|
||||||
|
Base15 Color
|
||||||
|
Reset string // usually reset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods on the palette for convenience:
|
||||||
|
func (p Palette) Background(c Color) string { return c.Inverse() } // keeps your current ;7m semantics
|
||||||
|
func (p Palette) Underlined(c Color) string { return c.Underline() } // convenience
|
||||||
|
func (p Palette) Text(c Color) string { return c.Text() }
|
||||||
|
|
||||||
|
var Colors = Palette{
|
||||||
|
Base00: NewColor(31),
|
||||||
|
Base01: NewColor(32),
|
||||||
|
Base02: NewColor(33),
|
||||||
|
Base03: NewColor(34),
|
||||||
|
Base04: NewColor(35),
|
||||||
|
Base05: NewColor(36),
|
||||||
|
Base06: NewColor(37),
|
||||||
|
Base07: NewColor(38),
|
||||||
|
Base08: NewColor(39),
|
||||||
|
Base09: NewColor(310),
|
||||||
|
Base10: NewColor(311),
|
||||||
|
Base11: NewColor(312),
|
||||||
|
Base12: NewColor(313),
|
||||||
|
Base13: NewColor(314),
|
||||||
|
Base14: NewColor(315),
|
||||||
|
Base15: NewColor(316),
|
||||||
|
Reset: "\033[0m", // reset
|
||||||
|
}
|
||||||
82
flake.lock
Normal file
82
flake.lock
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gomod2nix": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1770585520,
|
||||||
|
"narHash": "sha256-yBz9Ozd5Wb56i3e3cHZ8WcbzCQ9RlVaiW18qDYA/AzA=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "gomod2nix",
|
||||||
|
"rev": "1201ddd1279c35497754f016ef33d5e060f3da8d",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "gomod2nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1735563628,
|
||||||
|
"narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-24.05",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"gomod2nix": "gomod2nix",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
37
go.mod
Normal file
37
go.mod
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
module whspbrd
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/integrii/flaggy v1.6.0
|
||||||
|
github.com/jroimartin/gocui v0.5.0
|
||||||
|
golang.org/x/crypto v0.22.0
|
||||||
|
modernc.org/sqlite v1.29.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
|
||||||
|
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
|
||||||
|
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
|
||||||
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
|
||||||
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
|
||||||
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
|
||||||
|
github.com/getlantern/systray v1.2.1 // indirect
|
||||||
|
github.com/go-stack/stack v1.8.0 // indirect
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/nsf/termbox-go v1.1.1 // indirect
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
golang.org/x/sys v0.19.0 // indirect
|
||||||
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||||
|
modernc.org/libc v1.41.0 // indirect
|
||||||
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
modernc.org/memory v1.7.2 // indirect
|
||||||
|
modernc.org/strutil v1.2.0 // indirect
|
||||||
|
modernc.org/token v1.1.0 // indirect
|
||||||
|
)
|
||||||
71
go.sum
Normal file
71
go.sum
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
||||||
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||||
|
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
||||||
|
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||||
|
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
||||||
|
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||||
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
||||||
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||||
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
||||||
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||||
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
||||||
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||||
|
github.com/getlantern/systray v1.2.1 h1:udsC2k98v2hN359VTFShuQW6GGprRprw6kD6539JikI=
|
||||||
|
github.com/getlantern/systray v1.2.1/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM=
|
||||||
|
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/integrii/flaggy v1.6.0 h1:uqCN1mDnbux18JENxqi/VNaAwStTssEa/DjPVh6vE4g=
|
||||||
|
github.com/integrii/flaggy v1.6.0/go.mod h1:ir+1vTHHG4iOyQ4u9ovPA1TPQ+l3pU7Z8c/fd07G4Lg=
|
||||||
|
github.com/jroimartin/gocui v0.5.0 h1:DCZc97zY9dMnHXJSJLLmx9VqiEnAj0yh0eTNpuEtG/4=
|
||||||
|
github.com/jroimartin/gocui v0.5.0/go.mod h1:l7Hz8DoYoL6NoYnlnaX6XCNR62G7J5FfSW5jEogzaxE=
|
||||||
|
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||||
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY=
|
||||||
|
github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||||
|
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||||
|
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||||
|
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
|
||||||
|
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||||
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
||||||
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||||
|
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
|
||||||
|
modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
|
||||||
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
|
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||||
|
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||||
|
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||||
|
modernc.org/sqlite v1.29.0 h1:lQVw+ZsFM3aRG5m4myG70tbXpr3S/J1ej0KHIP4EvjM=
|
||||||
|
modernc.org/sqlite v1.29.0/go.mod h1:hG41jCYxOAOoO6BRK66AdRlmOcDzXf7qnwlwjUIOqa0=
|
||||||
|
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||||
|
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
123
menc/menc.go
Normal file
123
menc/menc.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package menc
|
||||||
|
|
||||||
|
// 15% AI generated code
|
||||||
|
// mostly human made; AI comments and details
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrCipherTextTooShort = errors.New("ciphertext too short")
|
||||||
|
)
|
||||||
|
|
||||||
|
type AESGCM_AutoNonce struct {
|
||||||
|
gcm cipher.AEAD
|
||||||
|
nonceSeq uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAESGCM_AutoNonce(key []byte) (*AESGCM_AutoNonce, error) {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &AESGCM_AutoNonce{
|
||||||
|
gcm: gcm,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Encrypt encrypts plaintext with optional AAD.
|
||||||
|
|
||||||
|
Nonce is sequencialy produced and auto managed.
|
||||||
|
|
||||||
|
Output format: nonce || ciphertext
|
||||||
|
*/
|
||||||
|
func (a *AESGCM_AutoNonce) Encrypt(plaintext, aad []byte) ([]byte, error) {
|
||||||
|
ns := a.gcm.NonceSize()
|
||||||
|
out := make([]byte, ns, ns+len(plaintext)+a.gcm.Overhead())
|
||||||
|
nonce := out[:ns]
|
||||||
|
|
||||||
|
binary.BigEndian.PutUint32(nonce[ns-4:], a.nonceSeq)
|
||||||
|
a.nonceSeq++
|
||||||
|
|
||||||
|
return a.gcm.Seal(out, nonce, plaintext, aad), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Decrypt decrypts data produced by some AESGCM encrypt.
|
||||||
|
|
||||||
|
Expects input format: nonce || ciphertext
|
||||||
|
*/
|
||||||
|
func (a *AESGCM_AutoNonce) Decrypt(ciphertext, aad []byte) ([]byte, error) {
|
||||||
|
ns := a.gcm.NonceSize()
|
||||||
|
if len(ciphertext) < ns {
|
||||||
|
return nil, ErrCipherTextTooShort
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := ciphertext[:ns]
|
||||||
|
data := ciphertext[ns:]
|
||||||
|
|
||||||
|
return a.gcm.Open(nil, nonce, data, aad)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Encrypt encrypts plaintext with optional AAD.
|
||||||
|
|
||||||
|
A random nonce is geneated automaticaly.
|
||||||
|
|
||||||
|
Output format: nonce || ciphertext
|
||||||
|
*/
|
||||||
|
func AESGCM_Quick_Encrypt(key, plaintext, aad []byte) ([]byte, error) {
|
||||||
|
gcm, err := helper_AESGCM_Quick(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ns := gcm.NonceSize()
|
||||||
|
out := make([]byte, ns, ns+len(plaintext)+gcm.Overhead())
|
||||||
|
nonce := out[:ns]
|
||||||
|
|
||||||
|
if _, err := rand.Read(nonce); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = gcm.Seal(out, nonce, plaintext, aad)
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Decrypt decrypts data produced by some AESGCM encrypt.
|
||||||
|
|
||||||
|
Expects input format: nonce || ciphertext
|
||||||
|
*/
|
||||||
|
func AESGCM_Quick_Decrypt(key, ciphertext, aad []byte) ([]byte, error) {
|
||||||
|
gcm, err := helper_AESGCM_Quick(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ns := gcm.NonceSize()
|
||||||
|
if len(ciphertext) < ns {
|
||||||
|
return nil, ErrCipherTextTooShort
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := ciphertext[:ns]
|
||||||
|
data := ciphertext[ns:]
|
||||||
|
|
||||||
|
return gcm.Open(nil, nonce, data, aad)
|
||||||
|
}
|
||||||
|
|
||||||
|
func helper_AESGCM_Quick(key []byte) (cipher.AEAD, error) {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cipher.NewGCM(block)
|
||||||
|
}
|
||||||
214
owner/owner.go
Normal file
214
owner/owner.go
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
package owner
|
||||||
|
|
||||||
|
// 5% of AI generated code
|
||||||
|
// human made; AI details
|
||||||
|
// FINAL-FINAL
|
||||||
|
|
||||||
|
import (
|
||||||
|
"WhspBrd/typio/bit"
|
||||||
|
"WhspBrd/typio/yum"
|
||||||
|
"bytes"
|
||||||
|
"crypto/mlkem"
|
||||||
|
"crypto/sha256"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/AeonDave/cryptonite-go/sig"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mldsa87 = sig.NewDeterministicMLDSA87()
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidSignature = errors.New("invalid signature")
|
||||||
|
ErrDestroyedSecret = errors.New("secret has been destroyed")
|
||||||
|
)
|
||||||
|
|
||||||
|
const requiredSeedSizeForMLDSA87 = 32
|
||||||
|
|
||||||
|
const signaturePubPartSize = sig.MLDSA87PublicKeySize
|
||||||
|
const signatureSignSize = sig.MLDSA87SignatureSize
|
||||||
|
const SignatureSize = signaturePubPartSize + signatureSignSize
|
||||||
|
|
||||||
|
const IdentitySize = bit.Size256b_B
|
||||||
|
|
||||||
|
type Identity = bit.Sha256
|
||||||
|
|
||||||
|
func IdentityEq(id, other Identity) bool {
|
||||||
|
return bytes.Equal(id[:], other[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func pubHash(pubKey []byte) Identity {
|
||||||
|
return sha256.Sum256(pubKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Secret interface {
|
||||||
|
Identity() Identity
|
||||||
|
PulicKey() []byte
|
||||||
|
EncapKey() []byte
|
||||||
|
Decapsulate(ciphertext []byte) ([]byte, error)
|
||||||
|
Sign(data []byte) (signedData []byte, err error)
|
||||||
|
Save(path string, password []byte) error
|
||||||
|
Destroy()
|
||||||
|
SaveAndDestroy(path string, password []byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type secret struct {
|
||||||
|
seed []byte
|
||||||
|
pubKey []byte
|
||||||
|
encap []byte
|
||||||
|
identity Identity
|
||||||
|
decap *mlkem.DecapsulationKey1024
|
||||||
|
privKey []byte
|
||||||
|
destroyed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func Encapsulate(secret Secret, encapKey []byte) (sharedkey []byte, ciphertext []byte, err error) {
|
||||||
|
encap, err := mlkem.NewEncapsulationKey1024(encapKey)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sharedkey, ciphertext = encap.Encapsulate()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSecret() (Secret, error) {
|
||||||
|
seed := make([]byte, requiredSeedSizeForMLDSA87)
|
||||||
|
if err := yum.YumSeed(seed); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pubKey, privKey, err := sig.GenerateDeterministicKeyMLDSA87(seed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
decap, err := mlkem.NewDecapsulationKey1024(append(seed, seed...))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
encap := decap.EncapsulationKey().Bytes()
|
||||||
|
return &secret{
|
||||||
|
seed: seed,
|
||||||
|
encap: encap,
|
||||||
|
decap: decap,
|
||||||
|
pubKey: pubKey,
|
||||||
|
identity: pubHash(pubKey),
|
||||||
|
privKey: privKey,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadSecret(path string, password []byte) (Secret, error) {
|
||||||
|
seed, err := yum.YumLoad(path, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pubKey, privKey, err := sig.GenerateDeterministicKeyMLDSA87(seed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
decap, err := mlkem.NewDecapsulationKey1024(append(seed, seed...))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
encap := decap.EncapsulationKey().Bytes()
|
||||||
|
return &secret{
|
||||||
|
seed: seed,
|
||||||
|
encap: encap,
|
||||||
|
decap: decap,
|
||||||
|
pubKey: pubKey,
|
||||||
|
identity: pubHash(pubKey),
|
||||||
|
privKey: privKey,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *secret) Identity() Identity {
|
||||||
|
return s.identity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *secret) PulicKey() []byte {
|
||||||
|
pubCopy := make([]byte, len(s.pubKey))
|
||||||
|
copy(pubCopy, s.pubKey)
|
||||||
|
return pubCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *secret) EncapKey() []byte {
|
||||||
|
encapCopy := make([]byte, len(s.encap))
|
||||||
|
copy(encapCopy, s.encap)
|
||||||
|
return encapCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *secret) Decapsulate(ciphertext []byte) ([]byte, error) {
|
||||||
|
if s.destroyed {
|
||||||
|
return nil, ErrDestroyedSecret
|
||||||
|
}
|
||||||
|
return s.decap.Decapsulate(ciphertext)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *secret) Sign(data []byte) (sign []byte, err error) {
|
||||||
|
if s.destroyed {
|
||||||
|
err = ErrDestroyedSecret
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
signPart, err := mldsa87.Sign(s.privKey, data)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sign = append(s.pubKey, signPart...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *secret) Save(path string, password []byte) error {
|
||||||
|
if s.destroyed {
|
||||||
|
return ErrDestroyedSecret
|
||||||
|
}
|
||||||
|
return yum.YumSave(path, s.seed, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Verify(data []byte, sign []byte) (Identity, error) {
|
||||||
|
if len(sign) < SignatureSize {
|
||||||
|
return Identity{}, ErrInvalidSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
pubPart := sign[:signaturePubPartSize]
|
||||||
|
signPart := sign[signaturePubPartSize:]
|
||||||
|
|
||||||
|
ok := mldsa87.Verify(pubPart, data, signPart)
|
||||||
|
if !ok {
|
||||||
|
return Identity{}, ErrInvalidSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
return pubHash(pubPart), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy zeroes out the secret's sensitive data and marks it as destroyed.
|
||||||
|
func (s *secret) Destroy() {
|
||||||
|
if s.destroyed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := range s.seed {
|
||||||
|
s.seed[i] = 0
|
||||||
|
}
|
||||||
|
for i := range s.privKey {
|
||||||
|
s.privKey[i] = 0
|
||||||
|
}
|
||||||
|
for i := range s.encap {
|
||||||
|
s.encap[i] = 0
|
||||||
|
}
|
||||||
|
s.destroyed = true
|
||||||
|
/*for i := range s.pubKey {
|
||||||
|
s.pubKey[i] = 0
|
||||||
|
}
|
||||||
|
for i := range s.identity {
|
||||||
|
s.identity[i] = 0
|
||||||
|
}*/ // not needed for added security, but could be done if desired
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *secret) SaveAndDestroy(path string, password []byte) error {
|
||||||
|
if s.destroyed {
|
||||||
|
return ErrDestroyedSecret
|
||||||
|
}
|
||||||
|
if err := s.Save(path, password); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.Destroy()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
48
pkg/cell_size/cell_size_unix.go
Normal file
48
pkg/cell_size/cell_size_unix.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package cell_size
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type winsize struct {
|
||||||
|
Rows uint16
|
||||||
|
Cols uint16
|
||||||
|
Xpixels uint16
|
||||||
|
Ypixels uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTerminalCellSizePixels() (widthPx int, heightPx int, err error) {
|
||||||
|
ws := &winsize{}
|
||||||
|
_, _, errno := syscall.Syscall(
|
||||||
|
syscall.SYS_IOCTL,
|
||||||
|
os.Stdout.Fd(),
|
||||||
|
uintptr(syscall.TIOCGWINSZ),
|
||||||
|
uintptr(unsafe.Pointer(ws)),
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
return 0, 0, errno
|
||||||
|
}
|
||||||
|
if ws.Cols == 0 || ws.Rows == 0 {
|
||||||
|
return 0, 0, fmt.Errorf("terminal rows or columns is zero")
|
||||||
|
}
|
||||||
|
widthPx = int(ws.Xpixels) / int(ws.Cols)
|
||||||
|
heightPx = int(ws.Ypixels) / int(ws.Rows)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetConsoleSize() (int, int) {
|
||||||
|
var sz struct {
|
||||||
|
rows uint16
|
||||||
|
cols uint16
|
||||||
|
xpixels uint16
|
||||||
|
ypixels uint16
|
||||||
|
}
|
||||||
|
_, _, _ = syscall.Syscall(syscall.SYS_IOCTL,
|
||||||
|
uintptr(syscall.Stdout), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&sz)))
|
||||||
|
return int(sz.cols), int(sz.rows)
|
||||||
|
}
|
||||||
107
pkg/cell_size/cell_size_win.go
Normal file
107
pkg/cell_size/cell_size_win.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package cell_size
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
type coord struct {
|
||||||
|
X int16
|
||||||
|
Y int16
|
||||||
|
}
|
||||||
|
|
||||||
|
type consoleFontInfoEx struct {
|
||||||
|
cbSize uint32
|
||||||
|
nFont uint32
|
||||||
|
dwFontSize coord
|
||||||
|
fontFamily uint32
|
||||||
|
fontWeight uint32
|
||||||
|
faceName [32]uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||||
|
procGetCurrentConsoleFontEx = kernel32.NewProc("GetCurrentConsoleFontEx")
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetTerminalCellSizePixels() (widthPx int, heightPx int, err error) {
|
||||||
|
var fontInfo consoleFontInfoEx
|
||||||
|
fontInfo.cbSize = uint32(unsafe.Sizeof(fontInfo))
|
||||||
|
|
||||||
|
stdOutHandle := windows.Handle(syscall.Stdout)
|
||||||
|
ret, _, err := procGetCurrentConsoleFontEx.Call(
|
||||||
|
uintptr(stdOutHandle),
|
||||||
|
uintptr(0), // bMaximumWindow = false
|
||||||
|
uintptr(unsafe.Pointer(&fontInfo)),
|
||||||
|
)
|
||||||
|
if ret == 0 {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return int(fontInfo.dwFontSize.X), int(fontInfo.dwFontSize.Y), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
short int16
|
||||||
|
word uint16
|
||||||
|
smallRect struct {
|
||||||
|
Left short
|
||||||
|
Top short
|
||||||
|
Right short
|
||||||
|
Bottom short
|
||||||
|
}
|
||||||
|
consoleScreenBufferInfo struct {
|
||||||
|
Size coord
|
||||||
|
CursorPosition coord
|
||||||
|
Attributes word
|
||||||
|
Window smallRect
|
||||||
|
MaximumWindowSize coord
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var kernel32DLL = syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
var getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo")
|
||||||
|
|
||||||
|
// GetConsoleSize returns the current number of columns and rows in the active console window.
|
||||||
|
// The return value of this function is in the order of cols, rows.
|
||||||
|
func GetConsoleSize() (int, int) {
|
||||||
|
stdoutHandle := getStdHandle(syscall.STD_OUTPUT_HANDLE)
|
||||||
|
var info, err = getConsoleScreenBufferInfo(stdoutHandle)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(info.Window.Right - info.Window.Left + 1), int(info.Window.Bottom - info.Window.Top + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getError(r1, r2 uintptr, lastErr error) error {
|
||||||
|
// If the function fails, the return value is zero.
|
||||||
|
if r1 == 0 {
|
||||||
|
if lastErr != nil {
|
||||||
|
return lastErr
|
||||||
|
}
|
||||||
|
return syscall.EINVAL
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStdHandle(stdhandle int) uintptr {
|
||||||
|
handle, err := syscall.GetStdHandle(stdhandle)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("could not get standard io handle %d", stdhandle))
|
||||||
|
}
|
||||||
|
return uintptr(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConsoleScreenBufferInfo(handle uintptr) (*consoleScreenBufferInfo, error) {
|
||||||
|
var info consoleScreenBufferInfo
|
||||||
|
if err := getError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &info, nil
|
||||||
|
}
|
||||||
257
pkg/clean_image/clean_image.go
Normal file
257
pkg/clean_image/clean_image.go
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
package cleanimage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KittyImageCleaner provides methods to generate Kitty graphics protocol
|
||||||
|
// commands for deleting images.
|
||||||
|
type KittyImageCleaner struct{}
|
||||||
|
|
||||||
|
// NewKittyImageCleaner creates a new instance of KittyImageCleaner.
|
||||||
|
func NewKittyImageCleaner() *KittyImageCleaner {
|
||||||
|
return &KittyImageCleaner{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildCommand constructs the base Kitty graphics protocol command.
|
||||||
|
func (kic *KittyImageCleaner) buildCommand(params map[string]string) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("\033_Ga=d") // Start with the delete action
|
||||||
|
|
||||||
|
if len(params) > 0 {
|
||||||
|
var paramStrings []string
|
||||||
|
for key, value := range params {
|
||||||
|
paramStrings = append(paramStrings, fmt.Sprintf("%s=%s", key, value))
|
||||||
|
}
|
||||||
|
sb.WriteString(",")
|
||||||
|
sb.WriteString(strings.Join(paramStrings, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString("\033\\") // End the command
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAllVisiblePlacements deletes all images visible on screen.
|
||||||
|
// 'freeData' determines if the underlying image data should also be freed.
|
||||||
|
func (kic *KittyImageCleaner) DeleteAllVisiblePlacements(freeData bool) string {
|
||||||
|
if freeData {
|
||||||
|
return kic.buildCommand(map[string]string{"d": "A"})
|
||||||
|
}
|
||||||
|
return kic.buildCommand(map[string]string{"d": "a"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByID deletes images with a specific ID.
|
||||||
|
// 'imageID' is the ID of the image to delete.
|
||||||
|
// 'placementID' is an optional placement ID. If 0, all placements with the imageID are deleted.
|
||||||
|
// 'freeData' determines if the underlying image data should also be freed.
|
||||||
|
func (kic *KittyImageCleaner) DeleteByID(imageID int, placementID int, freeData bool) string {
|
||||||
|
params := make(map[string]string)
|
||||||
|
if freeData {
|
||||||
|
params["d"] = "I"
|
||||||
|
} else {
|
||||||
|
params["d"] = "i"
|
||||||
|
}
|
||||||
|
params["i"] = fmt.Sprintf("%d", imageID)
|
||||||
|
if placementID != 0 {
|
||||||
|
params["p"] = fmt.Sprintf("%d", placementID)
|
||||||
|
}
|
||||||
|
return kic.buildCommand(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteNewestByID deletes the newest image with a specified number (ID).
|
||||||
|
// 'imageNumber' is the number (ID) of the newest image to delete.
|
||||||
|
// 'placementID' is an optional placement ID. If 0, all placements with the imageNumber are deleted.
|
||||||
|
// 'freeData' determines if the underlying image data should also be freed.
|
||||||
|
func (kic *KittyImageCleaner) DeleteNewestByID(imageNumber int, placementID int, freeData bool) string {
|
||||||
|
params := make(map[string]string)
|
||||||
|
if freeData {
|
||||||
|
params["d"] = "N"
|
||||||
|
} else {
|
||||||
|
params["d"] = "n"
|
||||||
|
}
|
||||||
|
params["I"] = fmt.Sprintf("%d", imageNumber) // Note: Kitty uses 'I' for number here
|
||||||
|
if placementID != 0 {
|
||||||
|
params["p"] = fmt.Sprintf("%d", placementID)
|
||||||
|
}
|
||||||
|
return kic.buildCommand(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByCursorPosition deletes all placements that intersect with the current cursor position.
|
||||||
|
// 'freeData' determines if the underlying image data should also be freed.
|
||||||
|
func (kic *KittyImageCleaner) DeleteByCursorPosition(freeData bool) string {
|
||||||
|
if freeData {
|
||||||
|
return kic.buildCommand(map[string]string{"d": "C"})
|
||||||
|
}
|
||||||
|
return kic.buildCommand(map[string]string{"d": "c"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAnimationFrames deletes animation frames.
|
||||||
|
// 'freeData' determines if the underlying image data should also be freed.
|
||||||
|
func (kic *KittyImageCleaner) DeleteAnimationFrames(freeData bool) string {
|
||||||
|
if freeData {
|
||||||
|
return kic.buildCommand(map[string]string{"d": "F"})
|
||||||
|
}
|
||||||
|
return kic.buildCommand(map[string]string{"d": "f"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByCellPosition deletes all placements that intersect a specific cell.
|
||||||
|
// 'x', 'y' are the coordinates of the cell (1-indexed).
|
||||||
|
// 'freeData' determines if the underlying image data should also be freed.
|
||||||
|
func (kic *KittyImageCleaner) DeleteByCellPosition(x, y int, freeData bool) string {
|
||||||
|
params := map[string]string{
|
||||||
|
"x": fmt.Sprintf("%d", x),
|
||||||
|
"y": fmt.Sprintf("%d", y),
|
||||||
|
}
|
||||||
|
if freeData {
|
||||||
|
params["d"] = "P"
|
||||||
|
} else {
|
||||||
|
params["d"] = "p"
|
||||||
|
}
|
||||||
|
return kic.buildCommand(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByCellAndZIndex deletes all placements that intersect a specific cell
|
||||||
|
// and have a specific z-index.
|
||||||
|
// 'x', 'y' are the coordinates of the cell (1-indexed).
|
||||||
|
// 'zIndex' is the z-index of the placements to delete.
|
||||||
|
// 'freeData' determines if the underlying image data should also be freed.
|
||||||
|
func (kic *KittyImageCleaner) DeleteByCellAndZIndex(x, y, zIndex int, freeData bool) string {
|
||||||
|
params := map[string]string{
|
||||||
|
"x": fmt.Sprintf("%d", x),
|
||||||
|
"y": fmt.Sprintf("%d", y),
|
||||||
|
"z": fmt.Sprintf("%d", zIndex),
|
||||||
|
}
|
||||||
|
if freeData {
|
||||||
|
params["d"] = "Q"
|
||||||
|
} else {
|
||||||
|
params["d"] = "q"
|
||||||
|
}
|
||||||
|
return kic.buildCommand(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByIDRange deletes all images whose ID is within a specified range.
|
||||||
|
// 'minID' is the minimum ID (inclusive).
|
||||||
|
// 'maxID' is the maximum ID (inclusive).
|
||||||
|
// 'freeData' determines if the underlying image data should also be freed.
|
||||||
|
// (Requires Kitty version 0.33.0 or later)
|
||||||
|
func (kic *KittyImageCleaner) DeleteByIDRange(minID, maxID int, freeData bool) string {
|
||||||
|
params := map[string]string{
|
||||||
|
"x": fmt.Sprintf("%d", minID),
|
||||||
|
"y": fmt.Sprintf("%d", maxID),
|
||||||
|
}
|
||||||
|
if freeData {
|
||||||
|
params["d"] = "R"
|
||||||
|
} else {
|
||||||
|
params["d"] = "r"
|
||||||
|
}
|
||||||
|
return kic.buildCommand(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByColumn deletes all placements that intersect the specified column.
|
||||||
|
// 'column' is the column number (1-indexed).
|
||||||
|
// 'freeData' determines if the underlying image data should also be freed.
|
||||||
|
func (kic *KittyImageCleaner) DeleteByColumn(column int, freeData bool) string {
|
||||||
|
params := map[string]string{
|
||||||
|
"x": fmt.Sprintf("%d", column),
|
||||||
|
}
|
||||||
|
if freeData {
|
||||||
|
params["d"] = "X"
|
||||||
|
} else {
|
||||||
|
params["d"] = "x"
|
||||||
|
}
|
||||||
|
return kic.buildCommand(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByRow deletes all placements that intersect the specified row.
|
||||||
|
// 'row' is the row number (1-indexed).
|
||||||
|
// 'freeData' determines if the underlying image data should also be freed.
|
||||||
|
func (kic *KittyImageCleaner) DeleteByRow(row int, freeData bool) string {
|
||||||
|
params := map[string]string{
|
||||||
|
"y": fmt.Sprintf("%d", row),
|
||||||
|
}
|
||||||
|
if freeData {
|
||||||
|
params["d"] = "Y"
|
||||||
|
} else {
|
||||||
|
params["d"] = "y"
|
||||||
|
}
|
||||||
|
return kic.buildCommand(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByZIndex deletes all placements that have the specified z-index.
|
||||||
|
// 'zIndex' is the z-index of the placements to delete.
|
||||||
|
// 'freeData' determines if the underlying image data should also be freed.
|
||||||
|
func (kic *KittyImageCleaner) DeleteByZIndex(zIndex int, freeData bool) string {
|
||||||
|
params := map[string]string{
|
||||||
|
"z": fmt.Sprintf("%d", zIndex),
|
||||||
|
}
|
||||||
|
if freeData {
|
||||||
|
params["d"] = "Z"
|
||||||
|
} else {
|
||||||
|
params["d"] = "z"
|
||||||
|
}
|
||||||
|
return kic.buildCommand(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cleaner := NewKittyImageCleaner()
|
||||||
|
|
||||||
|
// Example usage:
|
||||||
|
fmt.Println("Kitty Image Cleaning Commands:")
|
||||||
|
fmt.Println("-------------------------------")
|
||||||
|
|
||||||
|
// <ESC>_Ga=d<ESC>\ # delete all visible placements
|
||||||
|
fmt.Println("Delete all visible placements (no data free):",
|
||||||
|
cleaner.DeleteAllVisiblePlacements(false))
|
||||||
|
|
||||||
|
// <ESC>_Ga=d,d=A<ESC>\ # delete all visible placements, freeing data
|
||||||
|
fmt.Println("Delete all visible placements (with data free):",
|
||||||
|
cleaner.DeleteAllVisiblePlacements(true))
|
||||||
|
|
||||||
|
// <ESC>_Ga=d,d=i,i=10<ESC>\ # delete the image with id=10, without freeing data
|
||||||
|
fmt.Println("Delete image with ID 10 (no data free):",
|
||||||
|
cleaner.DeleteByID(10, 0, false))
|
||||||
|
|
||||||
|
// <ESC>_Ga=d,d=I,i=10<ESC>\ # delete the image with id=10, freeing data
|
||||||
|
fmt.Println("Delete image with ID 10 (with data free):",
|
||||||
|
cleaner.DeleteByID(10, 0, true))
|
||||||
|
|
||||||
|
// <ESC>_Ga=d,d=i,i=10,p=7<ESC>\ # delete the image with id=10 and placement id=7, without freeing data
|
||||||
|
fmt.Println("Delete placement 7 of image ID 10 (no data free):",
|
||||||
|
cleaner.DeleteByID(10, 7, false))
|
||||||
|
|
||||||
|
// <ESC>_Ga=d,d=I,i=10,p=7<ESC>\ # delete the image with id=10 and placement id=7, freeing data
|
||||||
|
fmt.Println("Delete placement 7 of image ID 10 (with data free):",
|
||||||
|
cleaner.DeleteByID(10, 7, true))
|
||||||
|
|
||||||
|
// <ESC>_Ga=d,d=Z,z=-1<ESC>\ # delete the placements with z-index -1, also freeing up image data
|
||||||
|
fmt.Println("Delete placements with z-index -1 (with data free):",
|
||||||
|
cleaner.DeleteByZIndex(-1, true))
|
||||||
|
|
||||||
|
// <ESC>_Ga=d,d=z,z=0<ESC>\ # delete the placements with z-index 0, without freeing data
|
||||||
|
fmt.Println("Delete placements with z-index 0 (no data free):",
|
||||||
|
cleaner.DeleteByZIndex(0, false))
|
||||||
|
|
||||||
|
// <ESC>_Ga=d,d=p,x=3,y=4<ESC>\ # delete all placements that intersect the cell at (3, 4), without freeing data
|
||||||
|
fmt.Println("Delete placements at cell (3,4) (no data free):",
|
||||||
|
cleaner.DeleteByCellPosition(3, 4, false))
|
||||||
|
|
||||||
|
// <ESC>_Ga=d,d=P,x=5,y=6<ESC>\ # delete all placements that intersect the cell at (5, 6), freeing data
|
||||||
|
fmt.Println("Delete placements at cell (5,6) (with data free):",
|
||||||
|
cleaner.DeleteByCellPosition(5, 6, true))
|
||||||
|
|
||||||
|
fmt.Println("Delete placements intersecting cursor (no data free):",
|
||||||
|
cleaner.DeleteByCursorPosition(false))
|
||||||
|
|
||||||
|
fmt.Println("Delete placements intersecting column 10 (with data free):",
|
||||||
|
cleaner.DeleteByColumn(10, true))
|
||||||
|
|
||||||
|
fmt.Println("Delete placements intersecting row 5 (no data free):",
|
||||||
|
cleaner.DeleteByRow(5, false))
|
||||||
|
|
||||||
|
fmt.Println("Delete images with ID range 100-200 (with data free):",
|
||||||
|
cleaner.DeleteByIDRange(100, 200, true))
|
||||||
|
|
||||||
|
fmt.Println("Delete placements at cell (1,1) with z-index 10 (no data free):",
|
||||||
|
cleaner.DeleteByCellAndZIndex(1, 1, 10, false))
|
||||||
|
}
|
||||||
196
pkg/icons/icon_unix.go
Normal file
196
pkg/icons/icon_unix.go
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
//go:build linux || darwin
|
||||||
|
// +build linux darwin
|
||||||
|
|
||||||
|
// File generated by 2goarray (http://github.com/cratonica/2goarray)
|
||||||
|
|
||||||
|
package icon
|
||||||
|
|
||||||
|
var Data []byte = []byte{
|
||||||
|
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
|
||||||
|
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20,
|
||||||
|
0x08, 0x06, 0x00, 0x00, 0x00, 0x73, 0x7a, 0x7a, 0xf4, 0x00, 0x00, 0x00,
|
||||||
|
0x19, 0x74, 0x45, 0x58, 0x74, 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72,
|
||||||
|
0x65, 0x00, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x20, 0x49, 0x6d, 0x61, 0x67,
|
||||||
|
0x65, 0x52, 0x65, 0x61, 0x64, 0x79, 0x71, 0xc9, 0x65, 0x3c, 0x00, 0x00,
|
||||||
|
0x03, 0x66, 0x69, 0x54, 0x58, 0x74, 0x58, 0x4d, 0x4c, 0x3a, 0x63, 0x6f,
|
||||||
|
0x6d, 0x2e, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x2e, 0x78, 0x6d, 0x70, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x3c, 0x3f, 0x78, 0x70, 0x61, 0x63, 0x6b, 0x65,
|
||||||
|
0x74, 0x20, 0x62, 0x65, 0x67, 0x69, 0x6e, 0x3d, 0x22, 0xef, 0xbb, 0xbf,
|
||||||
|
0x22, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x57, 0x35, 0x4d, 0x30, 0x4d, 0x70,
|
||||||
|
0x43, 0x65, 0x68, 0x69, 0x48, 0x7a, 0x72, 0x65, 0x53, 0x7a, 0x4e, 0x54,
|
||||||
|
0x63, 0x7a, 0x6b, 0x63, 0x39, 0x64, 0x22, 0x3f, 0x3e, 0x20, 0x3c, 0x78,
|
||||||
|
0x3a, 0x78, 0x6d, 0x70, 0x6d, 0x65, 0x74, 0x61, 0x20, 0x78, 0x6d, 0x6c,
|
||||||
|
0x6e, 0x73, 0x3a, 0x78, 0x3d, 0x22, 0x61, 0x64, 0x6f, 0x62, 0x65, 0x3a,
|
||||||
|
0x6e, 0x73, 0x3a, 0x6d, 0x65, 0x74, 0x61, 0x2f, 0x22, 0x20, 0x78, 0x3a,
|
||||||
|
0x78, 0x6d, 0x70, 0x74, 0x6b, 0x3d, 0x22, 0x41, 0x64, 0x6f, 0x62, 0x65,
|
||||||
|
0x20, 0x58, 0x4d, 0x50, 0x20, 0x43, 0x6f, 0x72, 0x65, 0x20, 0x35, 0x2e,
|
||||||
|
0x30, 0x2d, 0x63, 0x30, 0x36, 0x30, 0x20, 0x36, 0x31, 0x2e, 0x31, 0x33,
|
||||||
|
0x34, 0x37, 0x37, 0x37, 0x2c, 0x20, 0x32, 0x30, 0x31, 0x30, 0x2f, 0x30,
|
||||||
|
0x32, 0x2f, 0x31, 0x32, 0x2d, 0x31, 0x37, 0x3a, 0x33, 0x32, 0x3a, 0x30,
|
||||||
|
0x30, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x3e, 0x20,
|
||||||
|
0x3c, 0x72, 0x64, 0x66, 0x3a, 0x52, 0x44, 0x46, 0x20, 0x78, 0x6d, 0x6c,
|
||||||
|
0x6e, 0x73, 0x3a, 0x72, 0x64, 0x66, 0x3d, 0x22, 0x68, 0x74, 0x74, 0x70,
|
||||||
|
0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x77, 0x33, 0x2e, 0x6f, 0x72,
|
||||||
|
0x67, 0x2f, 0x31, 0x39, 0x39, 0x39, 0x2f, 0x30, 0x32, 0x2f, 0x32, 0x32,
|
||||||
|
0x2d, 0x72, 0x64, 0x66, 0x2d, 0x73, 0x79, 0x6e, 0x74, 0x61, 0x78, 0x2d,
|
||||||
|
0x6e, 0x73, 0x23, 0x22, 0x3e, 0x20, 0x3c, 0x72, 0x64, 0x66, 0x3a, 0x44,
|
||||||
|
0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x72,
|
||||||
|
0x64, 0x66, 0x3a, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x3d, 0x22, 0x22, 0x20,
|
||||||
|
0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x78, 0x6d, 0x70, 0x4d, 0x4d, 0x3d,
|
||||||
|
0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61,
|
||||||
|
0x64, 0x6f, 0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x61, 0x70,
|
||||||
|
0x2f, 0x31, 0x2e, 0x30, 0x2f, 0x6d, 0x6d, 0x2f, 0x22, 0x20, 0x78, 0x6d,
|
||||||
|
0x6c, 0x6e, 0x73, 0x3a, 0x73, 0x74, 0x52, 0x65, 0x66, 0x3d, 0x22, 0x68,
|
||||||
|
0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f,
|
||||||
|
0x62, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x61, 0x70, 0x2f, 0x31,
|
||||||
|
0x2e, 0x30, 0x2f, 0x73, 0x54, 0x79, 0x70, 0x65, 0x2f, 0x52, 0x65, 0x73,
|
||||||
|
0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x66, 0x23, 0x22, 0x20, 0x78,
|
||||||
|
0x6d, 0x6c, 0x6e, 0x73, 0x3a, 0x78, 0x6d, 0x70, 0x3d, 0x22, 0x68, 0x74,
|
||||||
|
0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x6e, 0x73, 0x2e, 0x61, 0x64, 0x6f, 0x62,
|
||||||
|
0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x61, 0x70, 0x2f, 0x31, 0x2e,
|
||||||
|
0x30, 0x2f, 0x22, 0x20, 0x78, 0x6d, 0x70, 0x4d, 0x4d, 0x3a, 0x4f, 0x72,
|
||||||
|
0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65,
|
||||||
|
0x6e, 0x74, 0x49, 0x44, 0x3d, 0x22, 0x78, 0x6d, 0x70, 0x2e, 0x64, 0x69,
|
||||||
|
0x64, 0x3a, 0x36, 0x37, 0x32, 0x34, 0x42, 0x45, 0x31, 0x35, 0x45, 0x44,
|
||||||
|
0x32, 0x30, 0x36, 0x38, 0x31, 0x31, 0x38, 0x38, 0x43, 0x36, 0x46, 0x32,
|
||||||
|
0x38, 0x31, 0x35, 0x44, 0x41, 0x33, 0x43, 0x35, 0x35, 0x35, 0x22, 0x20,
|
||||||
|
0x78, 0x6d, 0x70, 0x4d, 0x4d, 0x3a, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65,
|
||||||
|
0x6e, 0x74, 0x49, 0x44, 0x3d, 0x22, 0x78, 0x6d, 0x70, 0x2e, 0x64, 0x69,
|
||||||
|
0x64, 0x3a, 0x41, 0x33, 0x42, 0x34, 0x46, 0x42, 0x36, 0x36, 0x33, 0x41,
|
||||||
|
0x41, 0x38, 0x31, 0x31, 0x45, 0x32, 0x42, 0x32, 0x43, 0x41, 0x39, 0x37,
|
||||||
|
0x42, 0x44, 0x33, 0x34, 0x34, 0x31, 0x45, 0x46, 0x33, 0x32, 0x22, 0x20,
|
||||||
|
0x78, 0x6d, 0x70, 0x4d, 0x4d, 0x3a, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e,
|
||||||
|
0x63, 0x65, 0x49, 0x44, 0x3d, 0x22, 0x78, 0x6d, 0x70, 0x2e, 0x69, 0x69,
|
||||||
|
0x64, 0x3a, 0x41, 0x33, 0x42, 0x34, 0x46, 0x42, 0x36, 0x35, 0x33, 0x41,
|
||||||
|
0x41, 0x38, 0x31, 0x31, 0x45, 0x32, 0x42, 0x32, 0x43, 0x41, 0x39, 0x37,
|
||||||
|
0x42, 0x44, 0x33, 0x34, 0x34, 0x31, 0x45, 0x46, 0x33, 0x32, 0x22, 0x20,
|
||||||
|
0x78, 0x6d, 0x70, 0x3a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x54,
|
||||||
|
0x6f, 0x6f, 0x6c, 0x3d, 0x22, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x20, 0x50,
|
||||||
|
0x68, 0x6f, 0x74, 0x6f, 0x73, 0x68, 0x6f, 0x70, 0x20, 0x43, 0x53, 0x35,
|
||||||
|
0x20, 0x4d, 0x61, 0x63, 0x69, 0x6e, 0x74, 0x6f, 0x73, 0x68, 0x22, 0x3e,
|
||||||
|
0x20, 0x3c, 0x78, 0x6d, 0x70, 0x4d, 0x4d, 0x3a, 0x44, 0x65, 0x72, 0x69,
|
||||||
|
0x76, 0x65, 0x64, 0x46, 0x72, 0x6f, 0x6d, 0x20, 0x73, 0x74, 0x52, 0x65,
|
||||||
|
0x66, 0x3a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x44,
|
||||||
|
0x3d, 0x22, 0x78, 0x6d, 0x70, 0x2e, 0x69, 0x69, 0x64, 0x3a, 0x45, 0x36,
|
||||||
|
0x38, 0x31, 0x34, 0x43, 0x36, 0x41, 0x45, 0x45, 0x32, 0x30, 0x36, 0x38,
|
||||||
|
0x31, 0x31, 0x38, 0x38, 0x43, 0x36, 0x46, 0x32, 0x38, 0x31, 0x35, 0x44,
|
||||||
|
0x41, 0x33, 0x43, 0x35, 0x35, 0x35, 0x22, 0x20, 0x73, 0x74, 0x52, 0x65,
|
||||||
|
0x66, 0x3a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x44,
|
||||||
|
0x3d, 0x22, 0x78, 0x6d, 0x70, 0x2e, 0x64, 0x69, 0x64, 0x3a, 0x36, 0x37,
|
||||||
|
0x32, 0x34, 0x42, 0x45, 0x31, 0x35, 0x45, 0x44, 0x32, 0x30, 0x36, 0x38,
|
||||||
|
0x31, 0x31, 0x38, 0x38, 0x43, 0x36, 0x46, 0x32, 0x38, 0x31, 0x35, 0x44,
|
||||||
|
0x41, 0x33, 0x43, 0x35, 0x35, 0x35, 0x22, 0x2f, 0x3e, 0x20, 0x3c, 0x2f,
|
||||||
|
0x72, 0x64, 0x66, 0x3a, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
|
||||||
|
0x69, 0x6f, 0x6e, 0x3e, 0x20, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3a, 0x52,
|
||||||
|
0x44, 0x46, 0x3e, 0x20, 0x3c, 0x2f, 0x78, 0x3a, 0x78, 0x6d, 0x70, 0x6d,
|
||||||
|
0x65, 0x74, 0x61, 0x3e, 0x20, 0x3c, 0x3f, 0x78, 0x70, 0x61, 0x63, 0x6b,
|
||||||
|
0x65, 0x74, 0x20, 0x65, 0x6e, 0x64, 0x3d, 0x22, 0x72, 0x22, 0x3f, 0x3e,
|
||||||
|
0x5d, 0xed, 0x35, 0xe2, 0x00, 0x00, 0x04, 0xee, 0x49, 0x44, 0x41, 0x54,
|
||||||
|
0x78, 0xda, 0xc4, 0x57, 0xcf, 0x6f, 0x55, 0x45, 0x18, 0x3d, 0xf3, 0xe3,
|
||||||
|
0xfe, 0xea, 0x7b, 0xaf, 0xa5, 0x6d, 0x0a, 0xd8, 0x34, 0xbe, 0x16, 0x83,
|
||||||
|
0x69, 0x8c, 0x2e, 0x04, 0xe2, 0x86, 0xb8, 0x70, 0xe1, 0x06, 0x35, 0x18,
|
||||||
|
0x13, 0x5d, 0x60, 0x8c, 0xd1, 0x68, 0xe2, 0xca, 0xb8, 0x33, 0x31, 0xf1,
|
||||||
|
0x6f, 0x70, 0x67, 0x5c, 0xb1, 0x62, 0xe1, 0x46, 0x42, 0x8c, 0x0b, 0xe3,
|
||||||
|
0x46, 0x34, 0x25, 0x11, 0x41, 0x14, 0xa4, 0x24, 0xa4, 0x08, 0x58, 0x0a,
|
||||||
|
0x29, 0x14, 0x0a, 0x6d, 0xe9, 0xeb, 0xbb, 0xef, 0xce, 0x9d, 0xf1, 0xcc,
|
||||||
|
0xbd, 0xaf, 0xa5, 0x44, 0x63, 0x49, 0xee, 0x4b, 0x78, 0xc9, 0xf7, 0xee,
|
||||||
|
0x9d, 0x3b, 0x33, 0x77, 0xce, 0x77, 0xbe, 0xf3, 0x7d, 0x33, 0x57, 0x38,
|
||||||
|
0xe7, 0xf0, 0x38, 0x7f, 0x7a, 0xab, 0x01, 0xe2, 0xd9, 0x37, 0xff, 0xeb,
|
||||||
|
0xb1, 0xa2, 0x7d, 0x46, 0xdb, 0xeb, 0x87, 0xd0, 0x8e, 0xd3, 0xbe, 0xf8,
|
||||||
|
0xd7, 0x28, 0x6b, 0xe1, 0x2e, 0x7c, 0xf3, 0xbf, 0xef, 0x97, 0x5b, 0x42,
|
||||||
|
0x34, 0x06, 0xf0, 0x2c, 0x6d, 0x36, 0xe0, 0x43, 0xda, 0x88, 0x90, 0xf2,
|
||||||
|
0x90, 0xea, 0x6f, 0xbc, 0x0b, 0x21, 0x9e, 0x67, 0xfb, 0x8d, 0xa2, 0xcf,
|
||||||
|
0x76, 0xc7, 0x70, 0x5e, 0xff, 0x40, 0x7f, 0x75, 0x06, 0xc6, 0x27, 0x9a,
|
||||||
|
0xb8, 0x76, 0x63, 0xbe, 0x70, 0x53, 0xf0, 0x2f, 0xe7, 0x02, 0xd6, 0xda,
|
||||||
|
0x71, 0x36, 0xaf, 0xd1, 0x0e, 0xcb, 0x38, 0x16, 0x5c, 0xf4, 0x7a, 0xbe,
|
||||||
|
0xbc, 0xd2, 0x14, 0x71, 0x04, 0x19, 0x68, 0xf8, 0xb0, 0x4a, 0xda, 0x2e,
|
||||||
|
0xce, 0xad, 0x0c, 0x60, 0xf7, 0x53, 0xbb, 0x90, 0x87, 0x11, 0x76, 0x8f,
|
||||||
|
0xee, 0x40, 0xa8, 0x35, 0xfe, 0x9a, 0xbf, 0x35, 0x36, 0x73, 0xf9, 0xea,
|
||||||
|
0x24, 0xd2, 0xf4, 0x20, 0x5d, 0x2d, 0xbc, 0x15, 0x49, 0x0c, 0x1d, 0x86,
|
||||||
|
0xdf, 0x09, 0x25, 0x7f, 0x80, 0x14, 0xd3, 0x8e, 0x20, 0x93, 0x30, 0x44,
|
||||||
|
0xa3, 0xd1, 0xd8, 0x12, 0x80, 0xdc, 0x3a, 0x02, 0x06, 0xa4, 0xba, 0xb0,
|
||||||
|
0x5a, 0x12, 0x2b, 0x2e, 0xf4, 0x35, 0xb4, 0x3e, 0x58, 0x84, 0xde, 0xb3,
|
||||||
|
0x91, 0xa6, 0x64, 0x86, 0xf7, 0x4a, 0xbe, 0x4a, 0xcf, 0x8f, 0xe4, 0x26,
|
||||||
|
0x0f, 0x42, 0xa5, 0xf0, 0xcc, 0xe8, 0x13, 0x50, 0xfe, 0x79, 0x65, 0x11,
|
||||||
|
0xf2, 0x1d, 0x86, 0x62, 0x9a, 0x9a, 0xbe, 0x88, 0xa7, 0x77, 0x8e, 0x04,
|
||||||
|
0x9d, 0x34, 0x1b, 0x45, 0xda, 0x81, 0xf7, 0xde, 0x53, 0x9d, 0x77, 0x32,
|
||||||
|
0x04, 0x49, 0x82, 0x88, 0x1e, 0x67, 0xb9, 0xa9, 0x37, 0xe2, 0x44, 0x3c,
|
||||||
|
0x39, 0x3c, 0x84, 0xa8, 0x1b, 0x8a, 0xca, 0x00, 0xba, 0xbf, 0x28, 0x35,
|
||||||
|
0xe6, 0xc0, 0x9f, 0x17, 0x2f, 0x7d, 0x20, 0xad, 0x6d, 0xc2, 0xe4, 0xd8,
|
||||||
|
0x10, 0x45, 0x10, 0x20, 0xd0, 0x01, 0xfa, 0x09, 0xa2, 0x16, 0x85, 0x13,
|
||||||
|
0xf5, 0x28, 0x3a, 0x1a, 0x28, 0x75, 0x98, 0x4b, 0x7f, 0xcf, 0xde, 0x56,
|
||||||
|
0xe5, 0x10, 0xf0, 0xf7, 0x36, 0x6d, 0x4a, 0x0a, 0x71, 0x14, 0x4a, 0x1d,
|
||||||
|
0xb0, 0x7e, 0xce, 0x3a, 0xb5, 0xbc, 0x2a, 0xea, 0x22, 0x50, 0x92, 0x11,
|
||||||
|
0x90, 0xd0, 0x8a, 0xdc, 0x0b, 0xbc, 0xc2, 0x1e, 0x9f, 0x7b, 0xbf, 0xd0,
|
||||||
|
0x3e, 0xea, 0x05, 0x80, 0x8f, 0x69, 0xfb, 0x7c, 0x76, 0x81, 0x8e, 0x23,
|
||||||
|
0x75, 0xa5, 0x75, 0x31, 0x68, 0x0f, 0x80, 0x66, 0xac, 0x44, 0x9a, 0x09,
|
||||||
|
0x38, 0x5b, 0xbe, 0x92, 0xdd, 0xcf, 0xd1, 0xde, 0xab, 0x1c, 0x82, 0x95,
|
||||||
|
0x54, 0xdf, 0x58, 0x5a, 0xe3, 0xab, 0x3a, 0x0e, 0xf5, 0xc8, 0x61, 0x72,
|
||||||
|
0x4c, 0x53, 0x5c, 0x0e, 0x27, 0x67, 0xb2, 0x62, 0x76, 0x28, 0x55, 0x51,
|
||||||
|
0x97, 0xf6, 0x8c, 0x4b, 0xf4, 0x45, 0xc0, 0xad, 0x7b, 0xe4, 0xbd, 0x23,
|
||||||
|
0xb0, 0xda, 0x21, 0x51, 0x10, 0x57, 0x2a, 0x03, 0x38, 0xb4, 0xf7, 0xef,
|
||||||
|
0x99, 0xe0, 0x85, 0x35, 0x34, 0x87, 0x15, 0x26, 0x77, 0x0e, 0xa3, 0x39,
|
||||||
|
0x5e, 0x73, 0xc7, 0x7e, 0x5a, 0xc5, 0x5b, 0x9f, 0xcf, 0x09, 0xd4, 0x15,
|
||||||
|
0x19, 0x50, 0x94, 0x84, 0xc4, 0xfb, 0x2f, 0x25, 0x78, 0x6d, 0x9f, 0xc4,
|
||||||
|
0xed, 0x85, 0x0c, 0x73, 0xf7, 0x52, 0xcc, 0x2e, 0x38, 0x5c, 0x5f, 0x74,
|
||||||
|
0xe7, 0x2a, 0x03, 0xf8, 0xe4, 0xe5, 0x9b, 0xe7, 0xa0, 0xb7, 0xd1, 0xc9,
|
||||||
|
0x41, 0x0a, 0xbf, 0x8f, 0x2e, 0xf7, 0x09, 0xa9, 0x38, 0x4d, 0xcc, 0x16,
|
||||||
|
0x9e, 0xfb, 0xb0, 0x07, 0x5a, 0x50, 0x8b, 0x75, 0x48, 0x1d, 0x61, 0x64,
|
||||||
|
0xb0, 0x8d, 0xed, 0x43, 0x6d, 0xec, 0x99, 0xa0, 0xfe, 0x4c, 0x6b, 0xba,
|
||||||
|
0x32, 0x80, 0xd4, 0xec, 0xb8, 0x2c, 0x5c, 0x9d, 0x45, 0xbd, 0x21, 0x21,
|
||||||
|
0x6b, 0x2c, 0x46, 0x35, 0xa6, 0x9d, 0x7d, 0xa0, 0x01, 0x0a, 0x30, 0x24,
|
||||||
|
0x0b, 0x51, 0x5c, 0x67, 0x23, 0x41, 0x26, 0x42, 0x6a, 0xc5, 0x9b, 0x36,
|
||||||
|
0x70, 0xe1, 0xb5, 0xb0, 0x72, 0x1d, 0x08, 0x86, 0x66, 0xa1, 0xe2, 0x25,
|
||||||
|
0xe8, 0x81, 0x41, 0xa1, 0x6a, 0x64, 0xa0, 0x1f, 0x41, 0xdc, 0x59, 0xef,
|
||||||
|
0xa5, 0xfa, 0x15, 0x42, 0x0f, 0xa2, 0x00, 0xe0, 0x59, 0x08, 0xe0, 0xf8,
|
||||||
|
0xcc, 0x09, 0x71, 0x9b, 0x20, 0x66, 0xab, 0xd7, 0x01, 0x99, 0xdc, 0xa4,
|
||||||
|
0xe7, 0x57, 0x84, 0x6e, 0x0c, 0xd2, 0x98, 0xf7, 0x83, 0xf4, 0x76, 0x6d,
|
||||||
|
0x23, 0x7f, 0x7c, 0x0a, 0xfa, 0x10, 0xc4, 0x31, 0xfb, 0x14, 0x37, 0x1f,
|
||||||
|
0x4d, 0xf1, 0x51, 0x13, 0xac, 0x42, 0x97, 0x9d, 0x50, 0x8b, 0xd5, 0x2b,
|
||||||
|
0x61, 0x30, 0x90, 0x41, 0x86, 0x97, 0xe8, 0xfd, 0x9e, 0x02, 0x80, 0xda,
|
||||||
|
0x46, 0x6f, 0x57, 0x8b, 0x52, 0xe0, 0x33, 0xd3, 0xe7, 0x3f, 0x0b, 0x0f,
|
||||||
|
0xa2, 0x64, 0x80, 0x8d, 0x3a, 0x84, 0x66, 0x85, 0x2c, 0xc2, 0x93, 0xcf,
|
||||||
|
0x08, 0x17, 0xd9, 0xea, 0x75, 0x40, 0x51, 0x78, 0x32, 0xfa, 0x83, 0x61,
|
||||||
|
0x80, 0xf0, 0xf7, 0x5c, 0x24, 0x8c, 0x06, 0x20, 0x65, 0x39, 0xd5, 0xd7,
|
||||||
|
0xfb, 0x52, 0x03, 0x04, 0xa7, 0xfd, 0xd8, 0x84, 0xe0, 0x22, 0xee, 0x1d,
|
||||||
|
0xd1, 0x19, 0x7f, 0xad, 0xce, 0x80, 0xf2, 0x2f, 0x91, 0x67, 0x79, 0xe3,
|
||||||
|
0xe9, 0xf0, 0x60, 0x10, 0x84, 0x0d, 0x36, 0x65, 0xb1, 0x1f, 0x48, 0xd1,
|
||||||
|
0x65, 0x20, 0x68, 0xa0, 0x18, 0x23, 0x03, 0x7f, 0x65, 0x47, 0x78, 0x1e,
|
||||||
|
0xc2, 0x55, 0xdf, 0x0d, 0xfd, 0x82, 0x7c, 0xe9, 0x79, 0xde, 0x2d, 0x95,
|
||||||
|
0xdb, 0xaf, 0x45, 0x18, 0xf8, 0xf4, 0x2b, 0xa7, 0x7a, 0x22, 0x22, 0xb6,
|
||||||
|
0x83, 0x90, 0x0b, 0x93, 0xfb, 0x32, 0x39, 0xe4, 0x02, 0x41, 0x9c, 0x2f,
|
||||||
|
0xc0, 0xf4, 0xa0, 0x14, 0x7b, 0x4f, 0xe7, 0xe0, 0xb2, 0x0b, 0xb0, 0x54,
|
||||||
|
0x7f, 0xbe, 0x46, 0x00, 0xe9, 0x03, 0x00, 0x3e, 0x04, 0xdc, 0xf9, 0x22,
|
||||||
|
0xc5, 0xca, 0xc8, 0x7e, 0xe7, 0x7c, 0xbd, 0xce, 0x09, 0x58, 0x2c, 0x6c,
|
||||||
|
0xe4, 0x6a, 0x25, 0x00, 0x8e, 0x2f, 0x76, 0xc6, 0x72, 0xe3, 0x3f, 0xed,
|
||||||
|
0xf2, 0x55, 0x96, 0xe4, 0x65, 0xc4, 0xaa, 0xc5, 0xb8, 0xcb, 0x82, 0x10,
|
||||||
|
0x81, 0x32, 0x0b, 0x22, 0xc1, 0xcc, 0x30, 0x34, 0xdb, 0x26, 0x88, 0xec,
|
||||||
|
0xd7, 0x62, 0x2f, 0x76, 0xb6, 0x3a, 0x00, 0x97, 0x67, 0xa5, 0x99, 0xd6,
|
||||||
|
0x94, 0x33, 0xcb, 0x04, 0x70, 0x17, 0x7d, 0x62, 0x85, 0x27, 0x9e, 0x2e,
|
||||||
|
0x80, 0x42, 0x84, 0xac, 0x07, 0xee, 0x3e, 0x01, 0xac, 0x70, 0x6c, 0xcb,
|
||||||
|
0x33, 0x71, 0x82, 0x13, 0x50, 0x58, 0xe5, 0x3a, 0x60, 0x56, 0xd7, 0xa1,
|
||||||
|
0x9c, 0x76, 0xb6, 0x73, 0x1f, 0xc2, 0xd4, 0x7d, 0x75, 0xeb, 0x5b, 0x07,
|
||||||
|
0x00, 0x0f, 0x80, 0xd9, 0x60, 0xb9, 0xb8, 0x3f, 0xc0, 0x9a, 0xb5, 0x3b,
|
||||||
|
0x44, 0x71, 0xa6, 0x67, 0xc7, 0x72, 0x97, 0xaf, 0xac, 0xdf, 0x5e, 0x45,
|
||||||
|
0xee, 0xce, 0x5a, 0xb4, 0xf7, 0x2b, 0xab, 0x91, 0x6c, 0xe8, 0x8b, 0x00,
|
||||||
|
0x28, 0x7a, 0x91, 0x2f, 0x77, 0x99, 0x4a, 0x7f, 0x23, 0xb1, 0x37, 0x7a,
|
||||||
|
0x06, 0x00, 0xd9, 0xbd, 0x8d, 0x53, 0xbe, 0x3f, 0x98, 0x38, 0xdb, 0xda,
|
||||||
|
0xaf, 0x84, 0x46, 0xac, 0x5d, 0xa9, 0x7a, 0x86, 0x40, 0x4b, 0x02, 0x30,
|
||||||
|
0x4b, 0x3c, 0x2d, 0xb3, 0xfc, 0x5b, 0x73, 0xbc, 0xa7, 0x1f, 0x26, 0x96,
|
||||||
|
0x9e, 0x6d, 0xa2, 0xe3, 0x47, 0x61, 0xe5, 0xa7, 0x42, 0x05, 0x3c, 0x03,
|
||||||
|
0x5a, 0x94, 0x25, 0xcf, 0x33, 0xc0, 0xfb, 0x9c, 0x59, 0x9a, 0x53, 0xac,
|
||||||
|
0xce, 0xfd, 0xfc, 0x28, 0xea, 0x7f, 0xf4, 0x10, 0x64, 0xf3, 0x9b, 0x9b,
|
||||||
|
0xa7, 0xb8, 0xc9, 0xcc, 0x91, 0x81, 0xb1, 0x7a, 0x68, 0x4a, 0x00, 0xc2,
|
||||||
|
0xef, 0x86, 0x06, 0x8a, 0xa1, 0xe2, 0xfa, 0x97, 0xa8, 0x86, 0xdf, 0x7b,
|
||||||
|
0xca, 0x80, 0xcb, 0x16, 0x36, 0x37, 0x17, 0x73, 0x67, 0x4e, 0xf2, 0x14,
|
||||||
|
0x34, 0x56, 0x8f, 0x92, 0xf2, 0x7c, 0x40, 0x6f, 0x7d, 0x0d, 0x10, 0xf9,
|
||||||
|
0x5d, 0x8e, 0x75, 0x27, 0x18, 0x8c, 0x56, 0x6f, 0x01, 0x98, 0xf9, 0x87,
|
||||||
|
0xdb, 0xce, 0x1c, 0x81, 0x69, 0xbf, 0x58, 0x8b, 0x9b, 0xdb, 0x81, 0x7a,
|
||||||
|
0x91, 0xc9, 0xa1, 0x6c, 0x51, 0x03, 0x77, 0xce, 0xb8, 0x5c, 0x7f, 0xe5,
|
||||||
|
0x8a, 0xcf, 0xc6, 0x1e, 0x02, 0x78, 0x38, 0x9e, 0xfe, 0xde, 0x1c, 0x83,
|
||||||
|
0x5d, 0x38, 0x55, 0x0b, 0x87, 0x5f, 0x67, 0xfb, 0x1d, 0x3e, 0x68, 0x2b,
|
||||||
|
0x61, 0xbe, 0x84, 0x6b, 0x7f, 0xeb, 0x50, 0x6b, 0x97, 0x63, 0x1e, 0xfd,
|
||||||
|
0x8b, 0x5b, 0x3c, 0xee, 0xcf, 0xf3, 0x7f, 0x04, 0x18, 0x00, 0xe0, 0x6e,
|
||||||
|
0xdd, 0x63, 0x24, 0x57, 0x80, 0x6f, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45,
|
||||||
|
0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
|
||||||
|
}
|
||||||
367
pkg/icons/icon_win.go
Normal file
367
pkg/icons/icon_win.go
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
// File generated by 2goarray v0.1.0 (http://github.com/cratonica/2goarray)
|
||||||
|
|
||||||
|
package icon
|
||||||
|
|
||||||
|
var Data []byte = []byte{
|
||||||
|
0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x20, 0x00, 0xa8, 0x10, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x28, 0x00,
|
||||||
|
0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x01, 0x00,
|
||||||
|
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x09, 0xc6, 0xf5, 0x1b, 0x34, 0x69, 0x66, 0x6b, 0x5a, 0x24,
|
||||||
|
0x00, 0xc8, 0x8a, 0x5a, 0x30, 0xc0, 0x7a, 0x88, 0x7d, 0x56, 0x40, 0xbf,
|
||||||
|
0xdf, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0xc6, 0xf2, 0x50, 0x22, 0xcf,
|
||||||
|
0xfe, 0xe9, 0x42, 0x64, 0x5e, 0xff, 0x5b, 0x25, 0x00, 0xff, 0x8b, 0x5b,
|
||||||
|
0x30, 0xff, 0x7f, 0x8a, 0x7b, 0xff, 0x5d, 0xd6, 0xfd, 0xcd, 0x5e, 0xd0,
|
||||||
|
0xf5, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x43, 0xc9, 0xf4, 0xac, 0x46, 0xd4, 0xff, 0xff, 0x4f, 0x5c,
|
||||||
|
0x53, 0xff, 0x59, 0x25, 0x00, 0xff, 0x89, 0x5b, 0x30, 0xff, 0x86, 0x88,
|
||||||
|
0x75, 0xff, 0x71, 0xdb, 0xfe, 0xff, 0x6f, 0xd3, 0xf7, 0x9e, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xc6, 0xf1, 0x12, 0x5c, 0xd1,
|
||||||
|
0xf7, 0xe3, 0x61, 0xd8, 0xfc, 0xff, 0x57, 0x55, 0x47, 0xff, 0x58, 0x26,
|
||||||
|
0x00, 0xff, 0x88, 0x5b, 0x30, 0xff, 0x8b, 0x85, 0x6e, 0xff, 0x82, 0xde,
|
||||||
|
0xfd, 0xff, 0x7f, 0xd8, 0xf7, 0xd9, 0x7f, 0xcc, 0xe5, 0x0a, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x6f, 0xd4, 0xf4, 0x47, 0x73, 0xd7, 0xfa, 0xff, 0x78, 0xda,
|
||||||
|
0xf9, 0xff, 0x5b, 0x4f, 0x3b, 0xff, 0x57, 0x27, 0x00, 0xff, 0x86, 0x5a,
|
||||||
|
0x30, 0xff, 0x8f, 0x81, 0x68, 0xff, 0x93, 0xe1, 0xfa, 0xff, 0x8e, 0xdd,
|
||||||
|
0xfa, 0xfd, 0x8a, 0xd8, 0xf6, 0x3b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, 0xd8,
|
||||||
|
0xf7, 0x88, 0x87, 0xdc, 0xfc, 0xff, 0x8b, 0xdd, 0xf8, 0xff, 0x5d, 0x49,
|
||||||
|
0x34, 0xff, 0x56, 0x28, 0x00, 0xff, 0x85, 0x5a, 0x30, 0xff, 0x91, 0x7e,
|
||||||
|
0x62, 0xff, 0xa2, 0xe4, 0xfa, 0xff, 0x9e, 0xe2, 0xfb, 0xff, 0x98, 0xdd,
|
||||||
|
0xf6, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x7f, 0x7f, 0x7f, 0x02, 0x90, 0xdc, 0xf7, 0xc4, 0x9a, 0xe3,
|
||||||
|
0xfd, 0xff, 0x9e, 0xe0, 0xf5, 0xff, 0x5e, 0x45, 0x2c, 0xff, 0x55, 0x29,
|
||||||
|
0x00, 0xff, 0x84, 0x59, 0x2f, 0xff, 0x92, 0x7c, 0x5e, 0xff, 0xb1, 0xe8,
|
||||||
|
0xf9, 0xff, 0xac, 0xe8, 0xfd, 0xff, 0xa4, 0xe1, 0xf7, 0xb9, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x96, 0xd9,
|
||||||
|
0xf7, 0x22, 0xa0, 0xe1, 0xf8, 0xf3, 0xad, 0xe9, 0xff, 0xff, 0xad, 0xe2,
|
||||||
|
0xf2, 0xff, 0x5e, 0x40, 0x23, 0xff, 0x55, 0x29, 0x00, 0xff, 0x83, 0x59,
|
||||||
|
0x2f, 0xff, 0x93, 0x79, 0x58, 0xff, 0xbf, 0xeb, 0xf8, 0xff, 0xbb, 0xed,
|
||||||
|
0xfe, 0xff, 0xb1, 0xe6, 0xf9, 0xec, 0xa7, 0xe2, 0xf5, 0x1a, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa6, 0xe1, 0xf6, 0x56, 0xb1, 0xe8,
|
||||||
|
0xfb, 0xff, 0xbf, 0xef, 0xff, 0xff, 0xbc, 0xe2, 0xec, 0xff, 0x5c, 0x3a,
|
||||||
|
0x19, 0xff, 0x55, 0x2a, 0x00, 0xff, 0x82, 0x59, 0x2e, 0xff, 0x93, 0x75,
|
||||||
|
0x52, 0xff, 0xcc, 0xed, 0xf5, 0xff, 0xc9, 0xf1, 0xff, 0xff, 0xbe, 0xec,
|
||||||
|
0xfb, 0xff, 0xb5, 0xe7, 0xf8, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0xb0, 0xe6, 0xfa, 0x92, 0xbf, 0xec, 0xfc, 0xff, 0xd0, 0xf5,
|
||||||
|
0xff, 0xff, 0xc9, 0xe1, 0xe2, 0xff, 0x5a, 0x34, 0x10, 0xff, 0x55, 0x2b,
|
||||||
|
0x00, 0xff, 0x81, 0x58, 0x2d, 0xff, 0x92, 0x71, 0x4c, 0xff, 0xd8, 0xee,
|
||||||
|
0xf2, 0xff, 0xd6, 0xf5, 0xff, 0xff, 0xc8, 0xef, 0xfc, 0xff, 0xbb, 0xea,
|
||||||
|
0xf9, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x7f, 0x7f, 0x02, 0xb8, 0xe9,
|
||||||
|
0xf9, 0xc6, 0xca, 0xef, 0xfd, 0xff, 0xde, 0xfb, 0xff, 0xff, 0xd3, 0xde,
|
||||||
|
0xda, 0xff, 0x58, 0x30, 0x0a, 0xff, 0x55, 0x2c, 0x00, 0xff, 0x7f, 0x58,
|
||||||
|
0x2c, 0xff, 0x91, 0x6d, 0x47, 0xff, 0xe1, 0xed, 0xeb, 0xff, 0xdf, 0xf9,
|
||||||
|
0xff, 0xff, 0xd0, 0xf1, 0xfc, 0xff, 0xc1, 0xea, 0xfb, 0xbd, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0xb2, 0xe5, 0xf6, 0x1e, 0xbd, 0xeb, 0xfa, 0xf0, 0xd0, 0xf1,
|
||||||
|
0xfd, 0xff, 0xe7, 0xfe, 0xff, 0xff, 0xda, 0xd9, 0xd4, 0xff, 0x56, 0x2d,
|
||||||
|
0x07, 0xff, 0x55, 0x2c, 0x00, 0xff, 0x7e, 0x57, 0x2a, 0xff, 0x8d, 0x68,
|
||||||
|
0x41, 0xff, 0xe6, 0xea, 0xe4, 0xff, 0xe5, 0xfb, 0xff, 0xff, 0xd3, 0xf2,
|
||||||
|
0xfd, 0xff, 0xc4, 0xeb, 0xfa, 0xe9, 0xb9, 0xe8, 0xf3, 0x16, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xac, 0xe3,
|
||||||
|
0xf8, 0x4a, 0xbe, 0xec, 0xfb, 0xff, 0xd1, 0xf1, 0xfd, 0xff, 0xe9, 0xff,
|
||||||
|
0xff, 0xff, 0xd7, 0xd4, 0xce, 0xff, 0x54, 0x2a, 0x04, 0xff, 0x55, 0x2c,
|
||||||
|
0x00, 0xff, 0x7e, 0x55, 0x28, 0xff, 0x8a, 0x63, 0x3a, 0xff, 0xe1, 0xe4,
|
||||||
|
0xdf, 0xff, 0xe4, 0xfc, 0xff, 0xff, 0xd3, 0xf2, 0xfc, 0xff, 0xc2, 0xec,
|
||||||
|
0xfb, 0xfe, 0xb6, 0xe8, 0xf7, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa6, 0xe3, 0xf8, 0x79, 0xbb, 0xeb,
|
||||||
|
0xfb, 0xff, 0xcc, 0xf0, 0xfd, 0xff, 0xe2, 0xfd, 0xff, 0xff, 0xca, 0xce,
|
||||||
|
0xc9, 0xff, 0x52, 0x27, 0x03, 0xff, 0x56, 0x2c, 0x00, 0xff, 0x7c, 0x54,
|
||||||
|
0x26, 0xff, 0x87, 0x60, 0x35, 0xff, 0xd4, 0xdf, 0xd8, 0xff, 0xdd, 0xfb,
|
||||||
|
0xff, 0xff, 0xce, 0xf0, 0xfc, 0xff, 0xc0, 0xec, 0xfc, 0xff, 0xb0, 0xe8,
|
||||||
|
0xf8, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0xa0, 0xe1, 0xf9, 0xa9, 0xb3, 0xe8, 0xfb, 0xff, 0xc2, 0xed,
|
||||||
|
0xfc, 0xff, 0xd6, 0xfa, 0xff, 0xff, 0xbb, 0xc7, 0xc3, 0xff, 0x50, 0x25,
|
||||||
|
0x01, 0xff, 0x56, 0x2c, 0x00, 0xff, 0x7b, 0x53, 0x23, 0xff, 0x84, 0x5c,
|
||||||
|
0x2f, 0xff, 0xc7, 0xd7, 0xd1, 0xff, 0xd3, 0xf7, 0xff, 0xff, 0xc5, 0xee,
|
||||||
|
0xfc, 0xff, 0xb9, 0xeb, 0xfb, 0xff, 0xab, 0xe5, 0xf9, 0xa0, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0xcc, 0xcc, 0x05, 0x96, 0xdd,
|
||||||
|
0xf7, 0xcd, 0xa8, 0xe5, 0xfa, 0xff, 0xb5, 0xe9, 0xfb, 0xff, 0xc6, 0xf7,
|
||||||
|
0xff, 0xff, 0xab, 0xbf, 0xbd, 0xff, 0x50, 0x24, 0x00, 0xff, 0x56, 0x2c,
|
||||||
|
0x00, 0xff, 0x79, 0x51, 0x20, 0xff, 0x81, 0x59, 0x2a, 0xff, 0xb8, 0xce,
|
||||||
|
0xca, 0xff, 0xc6, 0xf4, 0xff, 0xff, 0xb9, 0xea, 0xfb, 0xff, 0xaf, 0xe7,
|
||||||
|
0xfb, 0xff, 0xa0, 0xe1, 0xf8, 0xc5, 0x7f, 0x7f, 0x7f, 0x02, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x7f, 0xd1, 0xf6, 0x1c, 0x8b, 0xdb, 0xf7, 0xee, 0x9b, 0xe1,
|
||||||
|
0xf9, 0xff, 0xa6, 0xe4, 0xfa, 0xff, 0xb4, 0xf2, 0xff, 0xff, 0x9b, 0xb7,
|
||||||
|
0xb6, 0xff, 0x50, 0x22, 0x00, 0xff, 0x56, 0x2c, 0x00, 0xff, 0x77, 0x4e,
|
||||||
|
0x1b, 0xff, 0x80, 0x55, 0x25, 0xff, 0xab, 0xc6, 0xc2, 0xff, 0xb7, 0xf1,
|
||||||
|
0xff, 0xff, 0xad, 0xe6, 0xfb, 0xff, 0xa3, 0xe4, 0xfa, 0xff, 0x98, 0xdf,
|
||||||
|
0xf7, 0xe7, 0x8c, 0xd9, 0xf2, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6e, 0xd3,
|
||||||
|
0xf6, 0x3a, 0x7d, 0xd7, 0xf8, 0xfd, 0x8b, 0xdc, 0xf9, 0xff, 0x96, 0xde,
|
||||||
|
0xf9, 0xff, 0xa1, 0xec, 0xff, 0xff, 0x8c, 0xaf, 0xb1, 0xff, 0x50, 0x22,
|
||||||
|
0x00, 0xff, 0x55, 0x2c, 0x00, 0xff, 0x74, 0x4c, 0x18, 0xff, 0x7e, 0x52,
|
||||||
|
0x1f, 0xff, 0x9f, 0xbe, 0xbc, 0xff, 0xa8, 0xed, 0xff, 0xff, 0xa0, 0xe2,
|
||||||
|
0xf9, 0xff, 0x98, 0xdf, 0xf9, 0xff, 0x8d, 0xda, 0xf8, 0xfb, 0x85, 0xd6,
|
||||||
|
0xf5, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x59, 0xd5, 0xfc, 0x5c, 0x6b, 0xdd,
|
||||||
|
0xff, 0xff, 0x7a, 0xe0, 0xff, 0xff, 0x84, 0xdf, 0xff, 0xff, 0x8e, 0xe8,
|
||||||
|
0xff, 0xff, 0x7d, 0xa6, 0xac, 0xff, 0x50, 0x21, 0x00, 0xff, 0x55, 0x2c,
|
||||||
|
0x00, 0xff, 0x72, 0x49, 0x14, 0xff, 0x7b, 0x4e, 0x19, 0xff, 0x93, 0xb6,
|
||||||
|
0xb5, 0xff, 0x9a, 0xe9, 0xff, 0xff, 0x92, 0xe1, 0xfe, 0xff, 0x8a, 0xe2,
|
||||||
|
0xff, 0xff, 0x80, 0xe1, 0xff, 0xff, 0x74, 0xdb, 0xfc, 0x4f, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x71, 0x92, 0x8e, 0x8a, 0x69, 0x97, 0x94, 0xff, 0x67, 0xaa,
|
||||||
|
0xb3, 0xff, 0x6e, 0xc2, 0xdb, 0xff, 0x78, 0xe1, 0xf9, 0xfe, 0x6d, 0xa2,
|
||||||
|
0xa9, 0xfe, 0x52, 0x20, 0x00, 0xff, 0x55, 0x2c, 0x00, 0xff, 0x6f, 0x46,
|
||||||
|
0x11, 0xff, 0x7a, 0x4a, 0x13, 0xff, 0x87, 0xb1, 0xb0, 0xff, 0x89, 0xe5,
|
||||||
|
0xfd, 0xff, 0x84, 0xd0, 0xe9, 0xff, 0x80, 0xc0, 0xcd, 0xff, 0x7f, 0xab,
|
||||||
|
0xaf, 0xff, 0x7d, 0x96, 0x92, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x60,
|
||||||
|
0x34, 0xaf, 0x77, 0x45, 0x0b, 0xff, 0x64, 0x35, 0x00, 0xff, 0x5b, 0x37,
|
||||||
|
0x0c, 0xff, 0x5a, 0x4c, 0x34, 0xff, 0x59, 0x4d, 0x37, 0xff, 0x54, 0x28,
|
||||||
|
0x00, 0xff, 0x55, 0x2c, 0x00, 0xff, 0x6c, 0x42, 0x0d, 0xff, 0x78, 0x4d,
|
||||||
|
0x17, 0xff, 0x7e, 0x70, 0x4e, 0xff, 0x82, 0x77, 0x58, 0xff, 0x87, 0x67,
|
||||||
|
0x42, 0xff, 0x8a, 0x61, 0x38, 0xff, 0x8d, 0x5f, 0x33, 0xff, 0x8d, 0x5e,
|
||||||
|
0x34, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8b, 0x67, 0x41, 0xc9, 0x76, 0x4e,
|
||||||
|
0x1a, 0xff, 0x65, 0x3b, 0x00, 0xff, 0x5c, 0x30, 0x00, 0xff, 0x56, 0x27,
|
||||||
|
0x00, 0xff, 0x54, 0x24, 0x00, 0xff, 0x54, 0x2c, 0x00, 0xff, 0x55, 0x2c,
|
||||||
|
0x00, 0xff, 0x69, 0x40, 0x07, 0xff, 0x75, 0x4d, 0x17, 0xff, 0x7c, 0x50,
|
||||||
|
0x1c, 0xff, 0x82, 0x55, 0x25, 0xff, 0x87, 0x5d, 0x33, 0xff, 0x89, 0x62,
|
||||||
|
0x3a, 0xff, 0x8b, 0x65, 0x3e, 0xff, 0x8a, 0x65, 0x3f, 0xc9, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x8c, 0x65, 0x40, 0x5b, 0x75, 0x4c, 0x19, 0xcb, 0x65, 0x3c,
|
||||||
|
0x02, 0xff, 0x5c, 0x33, 0x00, 0xff, 0x57, 0x2e, 0x00, 0xff, 0x55, 0x2b,
|
||||||
|
0x00, 0xff, 0x54, 0x2b, 0x00, 0xff, 0x55, 0x2c, 0x00, 0xff, 0x65, 0x3c,
|
||||||
|
0x03, 0xff, 0x71, 0x49, 0x12, 0xff, 0x7a, 0x52, 0x20, 0xff, 0x81, 0x5a,
|
||||||
|
0x2c, 0xff, 0x85, 0x5f, 0x34, 0xff, 0x89, 0x62, 0x3a, 0xff, 0x8a, 0x63,
|
||||||
|
0x3d, 0xd2, 0x89, 0x64, 0x3d, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x6d, 0x49, 0x00, 0x07, 0x61, 0x39, 0x00, 0x51, 0x57, 0x2e,
|
||||||
|
0x00, 0xb5, 0x56, 0x2d, 0x01, 0xfd, 0x54, 0x2a, 0x00, 0xff, 0x54, 0x2b,
|
||||||
|
0x00, 0xff, 0x54, 0x2b, 0x00, 0xff, 0x61, 0x38, 0x00, 0xff, 0x6f, 0x46,
|
||||||
|
0x0d, 0xff, 0x78, 0x50, 0x1d, 0xff, 0x7d, 0x56, 0x27, 0xfc, 0x84, 0x5d,
|
||||||
|
0x33, 0xb4, 0x86, 0x60, 0x38, 0x52, 0x7f, 0x4c, 0x33, 0x0a, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x40, 0x20, 0x08, 0x77, 0x4f,
|
||||||
|
0x22, 0xde, 0x59, 0x2f, 0x00, 0xff, 0x54, 0x2b, 0x00, 0xff, 0x53, 0x2a,
|
||||||
|
0x00, 0xff, 0x5c, 0x33, 0x00, 0xff, 0x6a, 0x42, 0x08, 0xff, 0x6f, 0x47,
|
||||||
|
0x0f, 0xff, 0x6a, 0x44, 0x09, 0xdb, 0x55, 0x2a, 0x00, 0x06, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x55, 0x55, 0x00, 0x03, 0x7f, 0x58, 0x28, 0xc4, 0x5f, 0x36,
|
||||||
|
0x00, 0xff, 0x55, 0x2b, 0x00, 0xff, 0x54, 0x2b, 0x01, 0xff, 0x61, 0x39,
|
||||||
|
0x03, 0xff, 0x69, 0x41, 0x05, 0xff, 0x68, 0x40, 0x04, 0xff, 0x66, 0x3e,
|
||||||
|
0x04, 0xcf, 0x33, 0x33, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x76, 0x52, 0x24, 0x1c, 0x5a, 0x31, 0x00, 0xb0, 0x54, 0x2b,
|
||||||
|
0x00, 0xff, 0x55, 0x2d, 0x01, 0xff, 0x68, 0x40, 0x05, 0xff, 0x68, 0x40,
|
||||||
|
0x04, 0xff, 0x65, 0x3c, 0x03, 0xb3, 0x65, 0x3b, 0x00, 0x2b, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x52, 0x29, 0x00, 0x88, 0x53, 0x29, 0x00, 0x75, 0x55, 0x2c,
|
||||||
|
0x00, 0xa8, 0x68, 0x3f, 0x05, 0xa9, 0x5f, 0x36, 0x02, 0x8e, 0x51, 0x29,
|
||||||
|
0x00, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x2a,
|
||||||
|
0x00, 0x68, 0x52, 0x2a, 0x00, 0x92, 0x4c, 0x19, 0x00, 0x0a, 0x60, 0x30,
|
||||||
|
0x00, 0x10, 0x53, 0x2a, 0x00, 0xa2, 0x51, 0x28, 0x00, 0x52, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x54, 0x29,
|
||||||
|
0x00, 0x7d, 0x52, 0x2a, 0x00, 0xb0, 0x53, 0x2a, 0x00, 0xb1, 0x53, 0x29,
|
||||||
|
0x00, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
|
||||||
|
0x7f, 0xff, 0xff, 0xf8, 0x1f, 0xff, 0xff, 0xf0, 0x0f, 0xff, 0xff, 0xf0,
|
||||||
|
0x0f, 0xff, 0xff, 0xf0, 0x0f, 0xff, 0xff, 0xe0, 0x0f, 0xff, 0xff, 0xe0,
|
||||||
|
0x07, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xc0,
|
||||||
|
0x03, 0xff, 0xff, 0xc0, 0x03, 0xff, 0xff, 0xc0, 0x03, 0xff, 0xff, 0xc0,
|
||||||
|
0x03, 0xff, 0xff, 0xc0, 0x03, 0xff, 0xff, 0x80, 0x01, 0xff, 0xff, 0x80,
|
||||||
|
0x01, 0xff, 0xff, 0x80, 0x01, 0xff, 0xff, 0x80, 0x01, 0xff, 0xff, 0x80,
|
||||||
|
0x01, 0xff, 0xff, 0x00, 0x01, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00,
|
||||||
|
0x00, 0xff, 0xff, 0x80, 0x01, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xf0,
|
||||||
|
0x0f, 0xff, 0xff, 0xf0, 0x0f, 0xff, 0xff, 0xf8, 0x1f, 0xff, 0xff, 0xfa,
|
||||||
|
0x3f, 0xff, 0xff, 0xfd, 0xbf, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff,
|
||||||
|
0xff, 0xff,
|
||||||
|
}
|
||||||
95
pkg/mpris/mpris.go
Normal file
95
pkg/mpris/mpris.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package mpris
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Endg4meZer0/go-mpris"
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlayerInfo struct {
|
||||||
|
Name string
|
||||||
|
Title string
|
||||||
|
Artist []string
|
||||||
|
Album string
|
||||||
|
ArtURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Mpris() ([]PlayerInfo, error) {
|
||||||
|
conn, err := dbus.SessionBus()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
names, err := mpris.List(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(names) == 0 {
|
||||||
|
return nil, fmt.Errorf("no MPRIS players found")
|
||||||
|
}
|
||||||
|
|
||||||
|
var players []PlayerInfo
|
||||||
|
for _, name := range names {
|
||||||
|
player := mpris.New(conn, name)
|
||||||
|
|
||||||
|
metadata, err := player.GetMetadata()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
title, _ := metadata["xesam:title"].Value().(string)
|
||||||
|
artist, _ := metadata["xesam:artist"].Value().([]string)
|
||||||
|
album, _ := metadata["xesam:album"].Value().(string)
|
||||||
|
|
||||||
|
var artURL string
|
||||||
|
if val, ok := metadata["mpris:artUrl"]; ok {
|
||||||
|
if url, ok := val.Value().(string); ok {
|
||||||
|
artURL = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
players = append(players, PlayerInfo{
|
||||||
|
Name: name,
|
||||||
|
Title: title,
|
||||||
|
Artist: artist,
|
||||||
|
Album: album,
|
||||||
|
ArtURL: artURL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return players, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsRemoteArt(uri string) bool {
|
||||||
|
return strings.HasPrefix(uri, "http://") || strings.HasPrefix(uri, "https://")
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadArt(url, dest string) error {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch art: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("bad status: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := os.Create(dest)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create file: %w", err)
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(out, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to save art: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
75
pkg/render_image/render_image.go
Normal file
75
pkg/render_image/render_image.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package render_image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/draw"
|
||||||
|
_ "image/jpeg"
|
||||||
|
_ "image/png"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"whspbrd/pkg/resize_image"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadImage(filePath string) (image.Image, error) {
|
||||||
|
f, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
img, _, err := image.Decode(f)
|
||||||
|
return img, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertToRGBA(img image.Image) *image.RGBA {
|
||||||
|
rgba := image.NewRGBA(img.Bounds())
|
||||||
|
draw.Draw(rgba, rgba.Bounds(), img, image.Point{0, 0}, draw.Src)
|
||||||
|
return rgba
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeImageToBase64RGBA(rgba *image.RGBA) string {
|
||||||
|
return base64.StdEncoding.EncodeToString(rgba.Pix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderImage(filepath string, row int, col int, width int, height int, units bool) {
|
||||||
|
img, err := LoadImage(filepath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error loading image: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rgba := ConvertToRGBA(img)
|
||||||
|
if units {
|
||||||
|
rgba, _ = resize_image.ResizeInTerminal(*rgba, width, height)
|
||||||
|
} else {
|
||||||
|
rgba, _ = resize_image.Resize(*rgba, width, height)
|
||||||
|
}
|
||||||
|
encoded := EncodeImageToBase64RGBA(rgba)
|
||||||
|
|
||||||
|
imgWidth := rgba.Rect.Dx()
|
||||||
|
imgHeight := rgba.Rect.Dy()
|
||||||
|
|
||||||
|
fmt.Printf("\033[s\033[%d;%dH", row, col)
|
||||||
|
|
||||||
|
chunkSize := 4096
|
||||||
|
pos := 0
|
||||||
|
first := true
|
||||||
|
for pos < len(encoded) {
|
||||||
|
fmt.Print("\033_G")
|
||||||
|
if first {
|
||||||
|
fmt.Printf("q=2,a=T,f=32,s=%d,v=%d,", imgWidth, imgHeight)
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
chunkLen := len(encoded) - pos
|
||||||
|
if chunkLen > chunkSize {
|
||||||
|
chunkLen = chunkSize
|
||||||
|
}
|
||||||
|
if pos+chunkLen < len(encoded) {
|
||||||
|
fmt.Print("m=1")
|
||||||
|
}
|
||||||
|
fmt.Printf(";%s\033\\", encoded[pos:pos+chunkLen])
|
||||||
|
pos += chunkLen
|
||||||
|
}
|
||||||
|
fmt.Print("\033[u")
|
||||||
|
}
|
||||||
68
pkg/resize_image/resize_image.go
Normal file
68
pkg/resize_image/resize_image.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package resize_image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"whspbrd/pkg/cell_size"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Resize(img image.RGBA, width int, height int) (*image.RGBA, error) {
|
||||||
|
originalWidth := img.Bounds().Dx()
|
||||||
|
originalHeight := img.Bounds().Dy()
|
||||||
|
|
||||||
|
if width <= 0 && height <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
} else if width <= 0 {
|
||||||
|
width = int(float64(height) * float64(originalWidth) / float64(originalHeight))
|
||||||
|
} else if height <= 0 {
|
||||||
|
height = int(float64(width) * float64(originalHeight) / float64(originalWidth))
|
||||||
|
}
|
||||||
|
|
||||||
|
newImg := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||||
|
scaleX := float64(originalWidth) / float64(width)
|
||||||
|
scaleY := float64(originalHeight) / float64(height)
|
||||||
|
|
||||||
|
for y := 0; y < height; y++ {
|
||||||
|
for x := 0; x < width; x++ {
|
||||||
|
srcX := int(float64(x) * scaleX)
|
||||||
|
srcY := int(float64(y) * scaleY)
|
||||||
|
|
||||||
|
r, g, b, a := getPixel(img, srcX, srcY)
|
||||||
|
newImg.Set(x, y, color.RGBA{r, g, b, a})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newImg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResizeInTerminal(img image.RGBA, columns int, rows int) (*image.RGBA, error) {
|
||||||
|
if columns <= 0 && rows <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
width := convertColumnsToPixels(columns)
|
||||||
|
height := convertRowsToPixels(rows)
|
||||||
|
|
||||||
|
return Resize(img, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPixel(img image.RGBA, x int, y int) (uint8, uint8, uint8, uint8) {
|
||||||
|
index := img.PixOffset(x, y)
|
||||||
|
return img.Pix[index], img.Pix[index+1], img.Pix[index+2], img.Pix[index+3]
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertColumnsToPixels(columns int) int {
|
||||||
|
w, _, err := cell_size.GetTerminalCellSizePixels()
|
||||||
|
if err != nil {
|
||||||
|
w = 8
|
||||||
|
}
|
||||||
|
return columns * w
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertRowsToPixels(rows int) int {
|
||||||
|
_, h, err := cell_size.GetTerminalCellSizePixels()
|
||||||
|
if err != nil {
|
||||||
|
h = 16
|
||||||
|
}
|
||||||
|
return rows * h
|
||||||
|
}
|
||||||
66
pkg/term_image/term_image.go
Normal file
66
pkg/term_image/term_image.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package term_image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/draw"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadImage(filePath string) (image.Image, error) {
|
||||||
|
f, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
img, _, err := image.Decode(f)
|
||||||
|
return img, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertToRGBA(img image.Image) *image.RGBA {
|
||||||
|
rgba := image.NewRGBA(img.Bounds())
|
||||||
|
draw.Draw(rgba, rgba.Bounds(), img, image.Point{0, 0}, draw.Src)
|
||||||
|
return rgba
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeImageToBase64RGBA(rgba *image.RGBA) string {
|
||||||
|
return base64.StdEncoding.EncodeToString(rgba.Pix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderImage(filepath string, row int, col int) {
|
||||||
|
img, err := LoadImage(filepath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error loading image: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rgba := convertToRGBA(img)
|
||||||
|
encoded := encodeImageToBase64RGBA(rgba)
|
||||||
|
|
||||||
|
width := rgba.Rect.Dx()
|
||||||
|
height := rgba.Rect.Dy()
|
||||||
|
|
||||||
|
fmt.Printf("\033[s\033[%d;%dH", row, col)
|
||||||
|
|
||||||
|
chunkSize := 4096
|
||||||
|
pos := 0
|
||||||
|
first := true
|
||||||
|
for pos < len(encoded) {
|
||||||
|
fmt.Print("\033_G")
|
||||||
|
if first {
|
||||||
|
fmt.Printf("q=2,a=T,f=32,s=%d,v=%d,", width, height)
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
chunkLen := len(encoded) - pos
|
||||||
|
if chunkLen > chunkSize {
|
||||||
|
chunkLen = chunkSize
|
||||||
|
}
|
||||||
|
if pos+chunkLen < len(encoded) {
|
||||||
|
fmt.Print("m=1")
|
||||||
|
}
|
||||||
|
fmt.Printf(";%s\033\\", encoded[pos:pos+chunkLen])
|
||||||
|
pos += chunkLen
|
||||||
|
}
|
||||||
|
fmt.Print("\033[u")
|
||||||
|
}
|
||||||
205
relay/client.go
Normal file
205
relay/client.go
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
package relay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"WhspBrd/menc"
|
||||||
|
"WhspBrd/owner"
|
||||||
|
"WhspBrd/thrembio"
|
||||||
|
"WhspBrd/typio/splco"
|
||||||
|
"crypto/mlkem"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client interface {
|
||||||
|
Register(tokenId uint32, token []byte) error
|
||||||
|
Login() error
|
||||||
|
PublishKem() error
|
||||||
|
GetKem(whos owner.Identity) (*mlkem.EncapsulationKey1024, error)
|
||||||
|
Send(to owner.Identity, encap *mlkem.EncapsulationKey1024, data []byte) error
|
||||||
|
MessageLen() uint32
|
||||||
|
MessageGet(id uint32) (sender owner.Identity, timestamp uint32, data []byte, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type client struct {
|
||||||
|
th thrembio.Client
|
||||||
|
sc owner.Secret
|
||||||
|
spuha owner.Identity
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(addr string, secret owner.Secret, spuha owner.Identity) (Client, error) {
|
||||||
|
host, portStr, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid address, expected ip:port: %w", err)
|
||||||
|
}
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid port: %w", err)
|
||||||
|
}
|
||||||
|
ip := net.ParseIP(host)
|
||||||
|
if ip == nil {
|
||||||
|
return nil, fmt.Errorf("invalid ip: %s", host)
|
||||||
|
}
|
||||||
|
th, err := thrembio.NewClient(&net.UDPAddr{IP: ip, Port: port}, secret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &client{
|
||||||
|
th: th,
|
||||||
|
sc: secret,
|
||||||
|
spuha: spuha,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) Register(tokenId uint32, token []byte) error {
|
||||||
|
return c.th.Register(tokenId, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) Login() error {
|
||||||
|
return c.th.Login()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) PublishKem() error {
|
||||||
|
// [serverPuha] [kek] [osign(serverPuha | kek)]
|
||||||
|
data := append(c.spuha[:], c.sc.EncapKey()...)
|
||||||
|
osign, err := c.sc.Sign(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data = append(data, osign...)
|
||||||
|
data = append([]byte("pubkem"), data...)
|
||||||
|
err = c.th.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
res, err := c.th.Read()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if string(res) != "done" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) GetKem(whos owner.Identity) (*mlkem.EncapsulationKey1024, error) {
|
||||||
|
// [receiverPuha]
|
||||||
|
// <- ([serverPuha] [kek] [osign(serverPuha | kek)])(of that receiverPuha)
|
||||||
|
err := c.th.Write(append([]byte("getkem"), whos[:]...))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res, err := c.th.Read()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(res) < owner.IdentitySize+mlkem.EncapsulationKeySize1024 {
|
||||||
|
return nil, errors.New("invalid response length")
|
||||||
|
}
|
||||||
|
serverPuha := res[:owner.IdentitySize]
|
||||||
|
kek := res[owner.IdentitySize : owner.IdentitySize+mlkem.EncapsulationKeySize1024]
|
||||||
|
osign := res[owner.IdentitySize+mlkem.EncapsulationKeySize1024:]
|
||||||
|
whosver, err := owner.Verify(append(serverPuha, kek...), osign)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if whosver != whos {
|
||||||
|
return nil, errors.New("not signed by the expected owner")
|
||||||
|
}
|
||||||
|
return mlkem.NewEncapsulationKey1024(kek)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) Send(to owner.Identity, encap *mlkem.EncapsulationKey1024, data []byte) error {
|
||||||
|
// -> [serverPuha] [receiverPuha] [kct] [time] [encData] [osign(serverPuha | receiverPuha | kct | time | encData)]
|
||||||
|
// <- "send"
|
||||||
|
shared, kct := encap.Encapsulate()
|
||||||
|
key := sha256.Sum256(shared)
|
||||||
|
timee := make([]byte, 8)
|
||||||
|
binary.BigEndian.PutUint64(timee, uint64(time.Now().Unix()))
|
||||||
|
encData, err := menc.AESGCM_Quick_Encrypt(key[:], data, timee)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
payload := splco.Append(c.spuha[:], to[:], kct, timee, encData)
|
||||||
|
osign, err := c.sc.Sign(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
payload = append(payload, osign...)
|
||||||
|
err = c.th.Write(append([]byte("send"), payload...))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
res, err := c.th.Read()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if string(res) != "send" {
|
||||||
|
return errors.New("unexpected response")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) MessageLen() uint32 {
|
||||||
|
// -> "msglen"
|
||||||
|
// <- [len]
|
||||||
|
err := c.th.Write([]byte("msglen"))
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
res, err := c.th.Read()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if len(res) != 4 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return binary.BigEndian.Uint32(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) MessageGet(id uint32) (sender owner.Identity, timestamp uint32, data []byte, err error) {
|
||||||
|
// -> [id]
|
||||||
|
// <- [serverPuha] [receiverPuha] [kct] [time] [encData] [osign(serverPuha | senderPuha | kct | time | encData)]
|
||||||
|
idB := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint32(idB, id)
|
||||||
|
err = c.th.Write(append([]byte("msgget"), idB...))
|
||||||
|
if err != nil {
|
||||||
|
return owner.Identity{}, 0, nil, err
|
||||||
|
}
|
||||||
|
res, err := c.th.Read()
|
||||||
|
if err != nil {
|
||||||
|
return owner.Identity{}, 0, nil, err
|
||||||
|
}
|
||||||
|
if len(res) < owner.IdentitySize*2+mlkem.CiphertextSize1024+8 {
|
||||||
|
return owner.Identity{}, 0, nil, errors.New("invalid response length")
|
||||||
|
}
|
||||||
|
serverPuha := owner.Identity(res[:owner.IdentitySize])
|
||||||
|
//receiverPuha := owner.Identity(res[owner.IdentitySize : owner.IdentitySize*2])
|
||||||
|
kct := res[owner.IdentitySize*2 : owner.IdentitySize*2+mlkem.CiphertextSize1024]
|
||||||
|
timee := res[owner.IdentitySize*2+mlkem.CiphertextSize1024 : owner.IdentitySize*2+mlkem.CiphertextSize1024+8]
|
||||||
|
encData := res[owner.IdentitySize*2+mlkem.CiphertextSize1024+8 : owner.IdentitySize*2+mlkem.CiphertextSize1024+8+len(res)-owner.IdentitySize*2-mlkem.CiphertextSize1024-8]
|
||||||
|
osign := res[len(res)-owner.IdentitySize:]
|
||||||
|
verData := res[:len(res)-owner.IdentitySize]
|
||||||
|
whosver, err := owner.Verify(verData, osign)
|
||||||
|
if err != nil {
|
||||||
|
return owner.Identity{}, 0, nil, err
|
||||||
|
}
|
||||||
|
shared, err := c.sc.Decapsulate(kct)
|
||||||
|
if err != nil {
|
||||||
|
return owner.Identity{}, 0, nil, err
|
||||||
|
}
|
||||||
|
key := sha256.Sum256(shared)
|
||||||
|
data, err = menc.AESGCM_Quick_Decrypt(key[:], encData, timee)
|
||||||
|
if err != nil {
|
||||||
|
return owner.Identity{}, 0, nil, err
|
||||||
|
}
|
||||||
|
if owner.IdentityEq(c.spuha, serverPuha) {
|
||||||
|
return owner.Identity{}, 0, nil, errors.New("unexpected sender")
|
||||||
|
}
|
||||||
|
return whosver, binary.BigEndian.Uint32(timee), data, nil
|
||||||
|
|
||||||
|
}
|
||||||
242
relay/server.go
Normal file
242
relay/server.go
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
package relay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"WhspBrd/owner"
|
||||||
|
"WhspBrd/thrembio"
|
||||||
|
"crypto/mlkem"
|
||||||
|
"encoding/binary"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server interface {
|
||||||
|
Start() error
|
||||||
|
Close()
|
||||||
|
GetId() owner.Identity
|
||||||
|
AddRegisterToken(tokenId uint32, token []byte) error
|
||||||
|
GetRegisterTokens() map[uint32][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type server struct {
|
||||||
|
db thrembio.ServerDB
|
||||||
|
th thrembio.Server
|
||||||
|
sc owner.Secret
|
||||||
|
messages map[owner.Identity][][]byte
|
||||||
|
userKems map[owner.Identity]*mlkem.EncapsulationKey1024
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(port uint16, secret owner.Secret, path string) (Server, error) {
|
||||||
|
db, err := thrembio.NewSQLiteDB(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
th, err := thrembio.NewServer(port, secret, db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
th.SetFlags(thrembio.SF_NoUserErrorLog | thrembio.SF_NoRegisterLog | thrembio.SF_NoLoginLog)
|
||||||
|
|
||||||
|
return &server{
|
||||||
|
db: db,
|
||||||
|
th: th,
|
||||||
|
sc: secret,
|
||||||
|
messages: make(map[owner.Identity][][]byte),
|
||||||
|
userKems: make(map[owner.Identity]*mlkem.EncapsulationKey1024),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) Start() error {
|
||||||
|
err := s.th.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.handleData()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handleData() error {
|
||||||
|
for {
|
||||||
|
rt, user, _, data, err := s.th.Read()
|
||||||
|
if err != nil {
|
||||||
|
if rt == thrembio.RT_UserError {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch rt {
|
||||||
|
case thrembio.RT_Register:
|
||||||
|
// New user registered
|
||||||
|
continue
|
||||||
|
case thrembio.RT_Login:
|
||||||
|
// User logged in
|
||||||
|
continue
|
||||||
|
case thrembio.RT_Data:
|
||||||
|
// Parse relay protocol command
|
||||||
|
if len(data) < 6 {
|
||||||
|
s.th.Write([]byte("error"), user)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := string(data[:6])
|
||||||
|
payload := data[6:]
|
||||||
|
|
||||||
|
switch cmd {
|
||||||
|
case "pubkem":
|
||||||
|
s.handlePublishKem(user, payload)
|
||||||
|
case "getkem":
|
||||||
|
s.handleGetKem(user, payload)
|
||||||
|
case "send":
|
||||||
|
s.handleSend(user, payload)
|
||||||
|
case "msglen":
|
||||||
|
s.handleMessageLen(user)
|
||||||
|
case "msgget":
|
||||||
|
s.handleMessageGet(user, payload)
|
||||||
|
default:
|
||||||
|
s.th.Write([]byte("error"), user)
|
||||||
|
}
|
||||||
|
case thrembio.RT_UserError, thrembio.RT_InsideError:
|
||||||
|
// Handle errors or continue
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handlePublishKem(user owner.Identity, payload []byte) {
|
||||||
|
// Expected: [serverPuha] [kek] [osign(serverPuha | kek)]
|
||||||
|
if len(payload) < owner.IdentitySize+mlkem.EncapsulationKeySize1024+owner.SignatureSize {
|
||||||
|
s.th.Write([]byte("error"), user)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverPuha := owner.Identity(payload[:owner.IdentitySize])
|
||||||
|
kek := payload[owner.IdentitySize : owner.IdentitySize+mlkem.EncapsulationKeySize1024]
|
||||||
|
osign := payload[owner.IdentitySize+mlkem.EncapsulationKeySize1024:]
|
||||||
|
|
||||||
|
// Verify signature signed by the user
|
||||||
|
whosver, err := owner.Verify(append(serverPuha[:], kek...), osign)
|
||||||
|
if err != nil || !owner.IdentityEq(whosver, user) {
|
||||||
|
s.th.Write([]byte("error"), user)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store KEM for this user
|
||||||
|
kemKey, err := mlkem.NewEncapsulationKey1024(kek)
|
||||||
|
if err != nil {
|
||||||
|
s.th.Write([]byte("error"), user)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
s.userKems[user] = kemKey
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
s.th.Write([]byte("done"), user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handleGetKem(user owner.Identity, payload []byte) {
|
||||||
|
// Expected: [receiverPuha]
|
||||||
|
if len(payload) < owner.IdentitySize {
|
||||||
|
s.th.Write([]byte("error"), user)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
receiverPuha := owner.Identity(payload[:owner.IdentitySize])
|
||||||
|
|
||||||
|
// Get receiver's KEM
|
||||||
|
s.mu.RLock()
|
||||||
|
kemKey, exists := s.userKems[receiverPuha]
|
||||||
|
s.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists || kemKey == nil {
|
||||||
|
s.th.Write([]byte("error"), user)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return [serverPuha] [kek] [osign(serverPuha | kek)]
|
||||||
|
id := s.sc.Identity()
|
||||||
|
responseData := append(id[:], kemKey.Bytes()...)
|
||||||
|
osign, err := s.sc.Sign(responseData)
|
||||||
|
if err != nil {
|
||||||
|
s.th.Write([]byte("error"), user)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
responseData = append(responseData, osign...)
|
||||||
|
|
||||||
|
s.th.Write(responseData, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handleSend(user owner.Identity, payload []byte) {
|
||||||
|
// Expected: [serverPuha] [receiverPuha] [kct] [time] [encData] [osign(...)]
|
||||||
|
// Minimum: owner.IdentitySize*2 + mlkem.CiphertextSize1024 + 8 + owner.SignatureSize
|
||||||
|
if len(payload) < owner.IdentitySize*2+mlkem.CiphertextSize1024+8+owner.SignatureSize {
|
||||||
|
s.th.Write([]byte("error"), user)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract receiverPuha (sender is verified by thrembio layer)
|
||||||
|
receiverPuha := owner.Identity(payload[owner.IdentitySize : owner.IdentitySize*2])
|
||||||
|
|
||||||
|
// Store the full message payload for the receiver
|
||||||
|
s.mu.Lock()
|
||||||
|
s.messages[receiverPuha] = append(s.messages[receiverPuha], payload)
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
// Acknowledge
|
||||||
|
s.th.Write([]byte("send"), user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handleMessageLen(user owner.Identity) {
|
||||||
|
// Return message count as 4-byte big-endian
|
||||||
|
s.mu.RLock()
|
||||||
|
msgCount := len(s.messages[user])
|
||||||
|
s.mu.RUnlock()
|
||||||
|
|
||||||
|
lenB := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint32(lenB, uint32(msgCount))
|
||||||
|
s.th.Write(lenB, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handleMessageGet(user owner.Identity, payload []byte) {
|
||||||
|
// Expected: [id] (4 bytes)
|
||||||
|
if len(payload) < 4 {
|
||||||
|
s.th.Write([]byte("error"), user)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msgId := binary.BigEndian.Uint32(payload[:4])
|
||||||
|
|
||||||
|
s.mu.RLock()
|
||||||
|
msgs := s.messages[user]
|
||||||
|
if msgId >= uint32(len(msgs)) {
|
||||||
|
s.mu.RUnlock()
|
||||||
|
s.th.Write([]byte("error"), user)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the full message payload: [serverPuha] [senderPuha] [kct] [time] [encData] [osign(...)]
|
||||||
|
msg := msgs[msgId]
|
||||||
|
s.mu.RUnlock()
|
||||||
|
|
||||||
|
s.th.Write(msg, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) Close() {
|
||||||
|
s.th.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) GetId() owner.Identity {
|
||||||
|
return s.sc.Identity()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) AddRegisterToken(tokenId uint32, token []byte) error {
|
||||||
|
return s.db.SetRegisterToken(tokenId, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) GetRegisterTokens() map[uint32][]byte {
|
||||||
|
tokens, err := s.db.GetRegisterTokens()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
243
sfudp/sfudp.go
Normal file
243
sfudp/sfudp.go
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
// SFUDP = Safely Fragmented UDP
|
||||||
|
package sfudp
|
||||||
|
|
||||||
|
// 80% of AI generated code
|
||||||
|
// human designed; not human reviewed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MaxFragmentSize = 1024
|
||||||
|
HeaderSize = 8 // [4B ID][2B Index][1B Count][1B Reserved]
|
||||||
|
MaxPayload = MaxFragmentSize - HeaderSize
|
||||||
|
MaxFragments = 255
|
||||||
|
FragmentTTL = 20 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// fragmentSet holds state for a fragmented message.
|
||||||
|
type fragmentSet struct {
|
||||||
|
frags [][]byte
|
||||||
|
received int
|
||||||
|
total int
|
||||||
|
totalSize int
|
||||||
|
timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// SFUDPConn = classic UDPConn + transparent fragmentation.
|
||||||
|
type SFUDPConn struct {
|
||||||
|
udp *net.UDPConn
|
||||||
|
fragments map[uint32]*fragmentSet
|
||||||
|
mu sync.Mutex
|
||||||
|
|
||||||
|
nextID uint32
|
||||||
|
stopGC chan struct{}
|
||||||
|
readBuf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SFUDPConn) GetUDP() *net.UDPConn { return c.udp }
|
||||||
|
|
||||||
|
func ListenSFUDP(network string, laddr *net.UDPAddr) (*SFUDPConn, error) {
|
||||||
|
u, err := net.ListenUDP(network, laddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newConn(u), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DialSFUDP(network string, laddr, raddr *net.UDPAddr) (*SFUDPConn, error) {
|
||||||
|
u, err := net.DialUDP(network, laddr, raddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newConn(u), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConn(u *net.UDPConn) *SFUDPConn {
|
||||||
|
c := &SFUDPConn{
|
||||||
|
udp: u,
|
||||||
|
fragments: make(map[uint32]*fragmentSet),
|
||||||
|
stopGC: make(chan struct{}),
|
||||||
|
readBuf: make([]byte, MaxFragmentSize),
|
||||||
|
}
|
||||||
|
go c.gcLoop()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SFUDPConn) Close() error {
|
||||||
|
close(c.stopGC)
|
||||||
|
return c.udp.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// periodic cleanup of stale fragment sets
|
||||||
|
func (c *SFUDPConn) gcLoop() {
|
||||||
|
t := time.NewTicker(FragmentTTL / 2)
|
||||||
|
defer t.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
now := time.Now()
|
||||||
|
c.mu.Lock()
|
||||||
|
for id, fs := range c.fragments {
|
||||||
|
if now.Sub(fs.timestamp) > FragmentTTL {
|
||||||
|
delete(c.fragments, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
case <-c.stopGC:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Writing ---
|
||||||
|
|
||||||
|
func (c *SFUDPConn) Write(data []byte) (int, error) {
|
||||||
|
return c.writeFragments(data, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SFUDPConn) WriteToSFUDP(data []byte, addr *net.UDPAddr) (int, error) {
|
||||||
|
return c.writeFragments(data, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SFUDPConn) writeFragments(data []byte, addr *net.UDPAddr) (int, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
write := func(b []byte) (int, error) {
|
||||||
|
if addr != nil {
|
||||||
|
return c.udp.WriteToUDP(b, addr)
|
||||||
|
}
|
||||||
|
return c.udp.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// single packet
|
||||||
|
if len(data) <= MaxPayload {
|
||||||
|
pkt := make([]byte, HeaderSize+len(data))
|
||||||
|
binary.LittleEndian.PutUint32(pkt[0:4], 0)
|
||||||
|
binary.LittleEndian.PutUint16(pkt[4:6], 0)
|
||||||
|
pkt[6] = 1
|
||||||
|
copy(pkt[HeaderSize:], data)
|
||||||
|
_, err := write(pkt)
|
||||||
|
return len(data), err
|
||||||
|
}
|
||||||
|
|
||||||
|
fragCount := (len(data) + MaxPayload - 1) / MaxPayload
|
||||||
|
if fragCount > MaxFragments {
|
||||||
|
return 0, errors.New("message too large")
|
||||||
|
}
|
||||||
|
|
||||||
|
id := atomic.AddUint32(&c.nextID, 1)
|
||||||
|
total := 0
|
||||||
|
|
||||||
|
for i := 0; i < fragCount; i++ {
|
||||||
|
start := i * MaxPayload
|
||||||
|
end := start + MaxPayload
|
||||||
|
if end > len(data) {
|
||||||
|
end = len(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt := make([]byte, HeaderSize+(end-start))
|
||||||
|
binary.LittleEndian.PutUint32(pkt[0:4], id)
|
||||||
|
binary.LittleEndian.PutUint16(pkt[4:6], uint16(i))
|
||||||
|
pkt[6] = uint8(fragCount)
|
||||||
|
|
||||||
|
copy(pkt[HeaderSize:], data[start:end])
|
||||||
|
|
||||||
|
if _, err := write(pkt); err != nil {
|
||||||
|
return total, err
|
||||||
|
}
|
||||||
|
total += end - start
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Reading ---
|
||||||
|
|
||||||
|
func (c *SFUDPConn) Read(b []byte) (int, error) {
|
||||||
|
n, _, err := c.ReadFromSFUDP(b)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SFUDPConn) ReadFromSFUDP(b []byte) (int, *net.UDPAddr, error) {
|
||||||
|
for {
|
||||||
|
n, addr, err := c.udp.ReadFromUDP(c.readBuf)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
if n < HeaderSize {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
h := c.readBuf[:HeaderSize]
|
||||||
|
id := binary.LittleEndian.Uint32(h[0:4])
|
||||||
|
index := binary.LittleEndian.Uint16(h[4:6])
|
||||||
|
count := h[6]
|
||||||
|
|
||||||
|
if count == 0 || int(index) >= int(count) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := c.readBuf[HeaderSize:n]
|
||||||
|
|
||||||
|
// single fragment
|
||||||
|
if id == 0 && count == 1 {
|
||||||
|
if len(payload) > len(b) {
|
||||||
|
return 0, addr, errors.New("buffer too small")
|
||||||
|
}
|
||||||
|
copy(b, payload)
|
||||||
|
return len(payload), addr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
fs := c.fragments[id]
|
||||||
|
if fs == nil {
|
||||||
|
fs = &fragmentSet{
|
||||||
|
frags: make([][]byte, count),
|
||||||
|
total: int(count),
|
||||||
|
timestamp: time.Now(),
|
||||||
|
}
|
||||||
|
c.fragments[id] = fs
|
||||||
|
}
|
||||||
|
fs.timestamp = time.Now()
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if fs.frags[index] == nil {
|
||||||
|
cp := make([]byte, len(payload))
|
||||||
|
copy(cp, payload)
|
||||||
|
fs.frags[index] = cp
|
||||||
|
fs.received++
|
||||||
|
fs.totalSize += len(cp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fs.received == fs.total {
|
||||||
|
if fs.totalSize > len(b) {
|
||||||
|
c.mu.Lock()
|
||||||
|
delete(c.fragments, id)
|
||||||
|
c.mu.Unlock()
|
||||||
|
return 0, addr, errors.New("buffer too small")
|
||||||
|
}
|
||||||
|
|
||||||
|
off := 0
|
||||||
|
for _, f := range fs.frags {
|
||||||
|
copy(b[off:], f)
|
||||||
|
off += len(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
delete(c.fragments, id)
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
return off, addr, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
264
sfudp/sfudp_test.go
Normal file
264
sfudp/sfudp_test.go
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
package sfudp_test
|
||||||
|
|
||||||
|
// 100% of AI generated code
|
||||||
|
// not reviewed at all
|
||||||
|
|
||||||
|
import (
|
||||||
|
"WhspBrd/sfudp"
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testTimeout = 2 * time.Second
|
||||||
|
|
||||||
|
// --- helpers ---
|
||||||
|
|
||||||
|
func newPair(t *testing.T) (*sfudp.SFUDPConn, *sfudp.SFUDPConn) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
s, err := sfudp.ListenSFUDP("udp", &net.UDPAddr{Port: 0})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := sfudp.DialSFUDP("udp", nil, s.GetUDP().LocalAddr().(*net.UDPAddr))
|
||||||
|
if err != nil {
|
||||||
|
s.Close()
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = s.GetUDP().SetReadDeadline(time.Now().Add(testTimeout))
|
||||||
|
_ = c.GetUDP().SetReadDeadline(time.Now().Add(testTimeout))
|
||||||
|
|
||||||
|
return s, c
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustWrite(t *testing.T, c *sfudp.SFUDPConn, b []byte) {
|
||||||
|
t.Helper()
|
||||||
|
n, err := c.Write(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if n != len(b) {
|
||||||
|
t.Fatalf("short write: %d != %d", n, len(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustRead(t *testing.T, s *sfudp.SFUDPConn, expected []byte) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
buf := make([]byte, len(expected))
|
||||||
|
n, err := s.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if n != len(expected) {
|
||||||
|
t.Fatalf("size mismatch: %d != %d", n, len(expected))
|
||||||
|
}
|
||||||
|
if !bytes.Equal(buf, expected) {
|
||||||
|
t.Fatal("payload mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- tests ---
|
||||||
|
|
||||||
|
func TestMessageSizes(t *testing.T) {
|
||||||
|
s, c := newPair(t)
|
||||||
|
defer s.Close()
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
tests := []int{
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
sfudp.MaxPayload - 1,
|
||||||
|
sfudp.MaxPayload,
|
||||||
|
sfudp.MaxPayload + 1,
|
||||||
|
sfudp.MaxPayload*3 + 123,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, size := range tests {
|
||||||
|
t.Run("size_"+itoa(size), func(t *testing.T) {
|
||||||
|
msg := make([]byte, size)
|
||||||
|
for i := range msg {
|
||||||
|
msg[i] = byte(i)
|
||||||
|
}
|
||||||
|
mustWrite(t, c, msg)
|
||||||
|
mustRead(t, s, msg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTooLargeMessage(t *testing.T) {
|
||||||
|
s, c := newPair(t)
|
||||||
|
defer s.Close()
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
msg := make([]byte, sfudp.MaxPayload*sfudp.MaxFragments+1)
|
||||||
|
_, err := c.Write(msg)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBufferTooSmall(t *testing.T) {
|
||||||
|
s, c := newPair(t)
|
||||||
|
defer s.Close()
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
msg := make([]byte, sfudp.MaxPayload*2)
|
||||||
|
mustWrite(t, c, msg)
|
||||||
|
|
||||||
|
small := make([]byte, 10)
|
||||||
|
_, err := s.Read(small)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected buffer too small error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConcurrentWrites(t *testing.T) {
|
||||||
|
s, c := newPair(t)
|
||||||
|
defer s.Close()
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
const count = 20
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(v int) {
|
||||||
|
defer wg.Done()
|
||||||
|
msg := bytes.Repeat([]byte{byte(v)}, sfudp.MaxPayload+50)
|
||||||
|
mustWrite(t, c, msg)
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
buf := make([]byte, sfudp.MaxPayload+50)
|
||||||
|
_, err := s.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFragmentTimeoutGC(t *testing.T) {
|
||||||
|
s, _ := newPair(t)
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
raw, err := net.DialUDP("udp", nil, s.GetUDP().LocalAddr().(*net.UDPAddr))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer raw.Close()
|
||||||
|
|
||||||
|
// send incomplete fragment set
|
||||||
|
pkt := make([]byte, sfudp.HeaderSize+10)
|
||||||
|
binary.LittleEndian.PutUint32(pkt[0:4], 999)
|
||||||
|
binary.LittleEndian.PutUint16(pkt[4:6], 0)
|
||||||
|
pkt[6] = 2
|
||||||
|
_, _ = raw.Write(pkt)
|
||||||
|
|
||||||
|
time.Sleep(sfudp.FragmentTTL + time.Second)
|
||||||
|
|
||||||
|
// IMPORTANT: reset deadline after long sleep
|
||||||
|
_ = s.GetUDP().SetReadDeadline(time.Now().Add(testTimeout))
|
||||||
|
|
||||||
|
// send valid single fragment
|
||||||
|
valid := []byte("ok")
|
||||||
|
pkt = make([]byte, sfudp.HeaderSize+len(valid))
|
||||||
|
pkt[6] = 1
|
||||||
|
copy(pkt[sfudp.HeaderSize:], valid)
|
||||||
|
_, _ = raw.Write(pkt)
|
||||||
|
|
||||||
|
buf := make([]byte, 10)
|
||||||
|
n, err := s.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(buf[:n], valid) {
|
||||||
|
t.Fatal("unexpected payload")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidFragmentIgnored(t *testing.T) {
|
||||||
|
s, _ := newPair(t)
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
raw, err := net.DialUDP("udp", nil, s.GetUDP().LocalAddr().(*net.UDPAddr))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer raw.Close()
|
||||||
|
|
||||||
|
// invalid index >= count
|
||||||
|
pkt := make([]byte, sfudp.HeaderSize+10)
|
||||||
|
binary.LittleEndian.PutUint32(pkt[0:4], 111)
|
||||||
|
binary.LittleEndian.PutUint16(pkt[4:6], 5)
|
||||||
|
pkt[6] = 2
|
||||||
|
_, _ = raw.Write(pkt)
|
||||||
|
|
||||||
|
// valid single packet
|
||||||
|
valid := []byte("good")
|
||||||
|
pkt = make([]byte, sfudp.HeaderSize+len(valid))
|
||||||
|
pkt[6] = 1
|
||||||
|
copy(pkt[sfudp.HeaderSize:], valid)
|
||||||
|
_, _ = raw.Write(pkt)
|
||||||
|
|
||||||
|
buf := make([]byte, 16)
|
||||||
|
n, err := s.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(buf[:n], valid) {
|
||||||
|
t.Fatal("invalid fragment was not ignored")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyWrite(t *testing.T) {
|
||||||
|
s, c := newPair(t)
|
||||||
|
defer s.Close()
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
n, err := c.Write(nil)
|
||||||
|
if err != nil || n != 0 {
|
||||||
|
t.Fatal("empty write failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 16)
|
||||||
|
_, err = s.Read(buf)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected timeout")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, net.ErrClosed) && !isTimeout(err) {
|
||||||
|
t.Fatal("unexpected error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// small int to string helper without fmt
|
||||||
|
func itoa(v int) string {
|
||||||
|
if v == 0 {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
var b [20]byte
|
||||||
|
i := len(b)
|
||||||
|
for v > 0 {
|
||||||
|
i--
|
||||||
|
b[i] = byte('0' + v%10)
|
||||||
|
v /= 10
|
||||||
|
}
|
||||||
|
return string(b[i:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTimeout(err error) bool {
|
||||||
|
nerr, ok := err.(net.Error)
|
||||||
|
return ok && nerr.Timeout()
|
||||||
|
}
|
||||||
375
thrembio/client.go
Normal file
375
thrembio/client.go
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
package thrembio
|
||||||
|
|
||||||
|
// 80% of AI generated code
|
||||||
|
// human designed & reviewed-ish
|
||||||
|
|
||||||
|
import (
|
||||||
|
"WhspBrd/menc"
|
||||||
|
"WhspBrd/owner"
|
||||||
|
"WhspBrd/sfudp"
|
||||||
|
"WhspBrd/typio/bit"
|
||||||
|
"bytes"
|
||||||
|
"crypto/mlkem"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client interface {
|
||||||
|
Close() error
|
||||||
|
ping() error
|
||||||
|
GetServerId() owner.Identity
|
||||||
|
|
||||||
|
Register(tokenId uint32, token []byte) error
|
||||||
|
Login() error
|
||||||
|
Write(data []byte) error
|
||||||
|
Read() ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type client struct {
|
||||||
|
conn *sfudp.SFUDPConn
|
||||||
|
secret owner.Secret
|
||||||
|
|
||||||
|
serverId owner.Identity
|
||||||
|
aes *menc.AESGCM_AutoNonce
|
||||||
|
sequence uint32
|
||||||
|
|
||||||
|
last []byte
|
||||||
|
|
||||||
|
debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(addr *net.UDPAddr, secret owner.Secret) (Client, error) {
|
||||||
|
if secret == nil {
|
||||||
|
return nil, ErrSecretCantBeNil
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := sfudp.DialSFUDP("udp", nil, addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &client{
|
||||||
|
conn: conn,
|
||||||
|
secret: secret,
|
||||||
|
debug: os.Getenv("WHSPBRD_DEBUG") != "",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.ping()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) Close() error {
|
||||||
|
return c.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) ping() error {
|
||||||
|
err := c.rawWrite(c.rawHeader(Rq_Ping), none)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t, _, id, err := c.receive()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if t != Rs_Ack {
|
||||||
|
return fmt.Errorf("expected ack")
|
||||||
|
}
|
||||||
|
if len(id) <= 0 {
|
||||||
|
return fmt.Errorf("expected valid id")
|
||||||
|
}
|
||||||
|
c.serverId = id
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) receive() (PacketResType, []byte, owner.Identity, error) {
|
||||||
|
buf := make([]byte, 16384)
|
||||||
|
if err := c.setReadDeadline(); err != nil {
|
||||||
|
return 0, nil, owner.Identity{}, err
|
||||||
|
}
|
||||||
|
n, err := c.conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, owner.Identity{}, err
|
||||||
|
}
|
||||||
|
if c.debug {
|
||||||
|
log.Printf("thrembio receive bytes=%d", n)
|
||||||
|
}
|
||||||
|
return c.rawRead(buf[:n])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) setReadDeadline() error {
|
||||||
|
ms := readTimeoutMs()
|
||||||
|
if ms <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.conn.GetUDP().SetReadDeadline(time.Now().Add(time.Duration(ms) * time.Millisecond))
|
||||||
|
}
|
||||||
|
|
||||||
|
func readTimeoutMs() int {
|
||||||
|
val := os.Getenv("WHSPBRD_TIMEOUT_MS")
|
||||||
|
if val == "" {
|
||||||
|
return 5000
|
||||||
|
}
|
||||||
|
ms, err := strconv.Atoi(val)
|
||||||
|
if err != nil {
|
||||||
|
return 5000
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) GetServerId() owner.Identity {
|
||||||
|
return c.serverId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) Register(tokenId uint32, token []byte) error {
|
||||||
|
header := c.rawHeader(Rq_Register)
|
||||||
|
tokenIdB := make([]byte, bit.SizeUint32_B)
|
||||||
|
binary.BigEndian.PutUint32(tokenIdB, tokenId)
|
||||||
|
|
||||||
|
identity := c.secret.Identity()
|
||||||
|
checkHash := sha256.Sum256(append(append(append(header, tokenIdB...), token...), identity[:]...))
|
||||||
|
|
||||||
|
signData := make([]byte, 0, len(header)+len(tokenIdB)+len(checkHash))
|
||||||
|
signData = append(signData, header...)
|
||||||
|
signData = append(signData, tokenIdB...)
|
||||||
|
signData = append(signData, checkHash[:]...)
|
||||||
|
|
||||||
|
sign, err := c.secret.Sign(signData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := make([]byte, 0, len(tokenIdB)+len(checkHash)+len(sign))
|
||||||
|
payload = append(payload, tokenIdB...)
|
||||||
|
payload = append(payload, checkHash[:]...)
|
||||||
|
payload = append(payload, sign...)
|
||||||
|
|
||||||
|
if err := c.rawWrite(header, payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t, data, id, err := c.receive()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(id) > 0 {
|
||||||
|
c.serverId = id
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case Rs_Ack:
|
||||||
|
return nil
|
||||||
|
case Rs_Error:
|
||||||
|
return fmt.Errorf("register failed: %s", string(data))
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected response type %d", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) Login() error {
|
||||||
|
decKey, err := mlkem.GenerateKey1024()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
encapKey := decKey.EncapsulationKey().Bytes()
|
||||||
|
header := c.rawHeader(Rq_Login)
|
||||||
|
|
||||||
|
signData := make([]byte, 0, len(header)+len(encapKey))
|
||||||
|
signData = append(signData, header...)
|
||||||
|
signData = append(signData, encapKey...)
|
||||||
|
|
||||||
|
sign, err := c.secret.Sign(signData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := make([]byte, 0, len(encapKey)+len(sign))
|
||||||
|
payload = append(payload, encapKey...)
|
||||||
|
payload = append(payload, sign...)
|
||||||
|
|
||||||
|
if err := c.rawWrite(header, payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t, data, id, err := c.receive()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(id) > 0 {
|
||||||
|
c.serverId = id
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case Rs_Error:
|
||||||
|
return fmt.Errorf("login failed: %s", string(data))
|
||||||
|
case Rs_Data:
|
||||||
|
shared, err := decKey.Decapsulate(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key := sha256.Sum256(shared)
|
||||||
|
c.aes, err = menc.NewAESGCM_AutoNonce(key[:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.sequence = 0
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected response type %d", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) Write(data []byte) error {
|
||||||
|
if c.aes == nil {
|
||||||
|
return errors.New("not logged in")
|
||||||
|
}
|
||||||
|
|
||||||
|
header := c.rawHeader(Rq_Data)
|
||||||
|
userID := c.secret.Identity()
|
||||||
|
seqB := make([]byte, bit.SizeUint32_B)
|
||||||
|
binary.BigEndian.PutUint32(seqB, c.sequence)
|
||||||
|
|
||||||
|
aad := make([]byte, 0, len(header)+len(seqB))
|
||||||
|
aad = append(aad, header...)
|
||||||
|
aad = append(aad, seqB...)
|
||||||
|
|
||||||
|
ciphertext, err := c.aes.Encrypt(data, aad)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := make([]byte, 0, len(userID)+len(seqB)+len(ciphertext))
|
||||||
|
payload = append(payload, userID[:]...)
|
||||||
|
payload = append(payload, seqB...)
|
||||||
|
payload = append(payload, ciphertext...)
|
||||||
|
|
||||||
|
if err := c.rawWrite(header, payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t, data, id, err := c.receive()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(id) > 0 {
|
||||||
|
c.serverId = id
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case Rs_Ack:
|
||||||
|
c.sequence++
|
||||||
|
return nil
|
||||||
|
case Rs_Error:
|
||||||
|
return fmt.Errorf("write failed: %s", string(data))
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected response type %d", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) Read() ([]byte, error) {
|
||||||
|
if c.aes == nil {
|
||||||
|
return nil, errors.New("not logged in")
|
||||||
|
}
|
||||||
|
|
||||||
|
t, data, id, err := c.receive()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(id) > 0 {
|
||||||
|
c.serverId = id
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case Rs_Data:
|
||||||
|
if len(c.last) == 0 {
|
||||||
|
return nil, errors.New("missing last request for decryption")
|
||||||
|
}
|
||||||
|
plain, err := c.aes.Decrypt(data, c.last)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return plain, nil
|
||||||
|
case Rs_Error:
|
||||||
|
return nil, fmt.Errorf("server error: %s", string(data))
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unexpected response type %d", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) rawHeader(reqType PacketReqType) []byte {
|
||||||
|
buf := make([]byte, commonHeaderSize)
|
||||||
|
|
||||||
|
buf[0] = magicBytes[0]
|
||||||
|
buf[1] = magicBytes[1]
|
||||||
|
buf[2] = magicBytes[2]
|
||||||
|
buf[3] = version
|
||||||
|
|
||||||
|
nowMs := uint64(time.Now().UnixMilli())
|
||||||
|
binary.LittleEndian.PutUint64(buf[4:12], nowMs)
|
||||||
|
|
||||||
|
buf[12] = byte(reqType)
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) rawWrite(header, data []byte) error {
|
||||||
|
last := append(header, data...)
|
||||||
|
_, err := c.conn.Write(last)
|
||||||
|
c.last = last
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) rawRead(p []byte) (PacketResType, []byte, owner.Identity, error) {
|
||||||
|
if len(p) < 37 ||
|
||||||
|
p[0] != magicBytes[0] ||
|
||||||
|
p[1] != magicBytes[1] ||
|
||||||
|
p[2] != magicBytes[2] ||
|
||||||
|
p[3] != version {
|
||||||
|
return 0, nil, owner.Identity{}, fmt.Errorf("bad packet")
|
||||||
|
}
|
||||||
|
|
||||||
|
tp := PacketResType(p[4])
|
||||||
|
if tp >= Rs_Unknown {
|
||||||
|
return 0, nil, owner.Identity{}, fmt.Errorf("bad type")
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := p[5:37]
|
||||||
|
data := p[37:]
|
||||||
|
|
||||||
|
expectedHash := sha256.Sum256(c.last)
|
||||||
|
if !bytes.Equal(hash, expectedHash[:]) {
|
||||||
|
return 0, nil, owner.Identity{}, fmt.Errorf("hash mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) >= owner.SignatureSize {
|
||||||
|
payload := data[:len(data)-owner.SignatureSize]
|
||||||
|
sig := data[len(data)-owner.SignatureSize:]
|
||||||
|
|
||||||
|
buf := make([]byte, len(hash)+len(payload))
|
||||||
|
copy(buf, hash)
|
||||||
|
copy(buf[len(hash):], payload)
|
||||||
|
|
||||||
|
id, err := owner.Verify(buf, sig)
|
||||||
|
if err == nil {
|
||||||
|
return tp, payload, id, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tp == Rs_Data {
|
||||||
|
return tp, data, owner.Identity{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, nil, owner.Identity{}, fmt.Errorf("bad packet")
|
||||||
|
}
|
||||||
46
thrembio/global.go
Normal file
46
thrembio/global.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package thrembio
|
||||||
|
|
||||||
|
// 0% of AI generated code
|
||||||
|
|
||||||
|
import (
|
||||||
|
"WhspBrd/owner"
|
||||||
|
"WhspBrd/typio/bit"
|
||||||
|
"crypto/mlkem"
|
||||||
|
"crypto/sha256"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrSecretCantBeNil = errors.New("secret cant be nil")
|
||||||
|
)
|
||||||
|
var magicBytes = []byte{0x70, 0x03, 0xEA} // 'p' + 'Ϫ' (U+03EA)
|
||||||
|
const version byte = 3
|
||||||
|
|
||||||
|
var magicWithVersion = append(magicBytes, version)
|
||||||
|
|
||||||
|
type PacketReqType byte
|
||||||
|
type PacketResType byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
Rq_Ping PacketReqType = 0
|
||||||
|
Rq_Register PacketReqType = 1
|
||||||
|
Rq_Login PacketReqType = 2
|
||||||
|
Rq_Data PacketReqType = 3
|
||||||
|
Rq_Unknown PacketReqType = 4
|
||||||
|
|
||||||
|
Rs_Ack PacketResType = 0
|
||||||
|
Rs_Data PacketResType = 1
|
||||||
|
Rs_Error PacketResType = 2
|
||||||
|
Rs_Unknown PacketResType = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
notLoggedB = []byte("not_logged")
|
||||||
|
notRegisteredB = []byte("not_registered")
|
||||||
|
none = []byte{0}
|
||||||
|
)
|
||||||
|
|
||||||
|
var commonHeaderSize = len(magicWithVersion) + bit.Size64b_B + 1
|
||||||
|
var registerRestSize = bit.Size64b_B + sha256.BlockSize + owner.SignatureSize
|
||||||
|
var loginRestSize = mlkem.EncapsulationKeySize1024 + owner.SignatureSize
|
||||||
|
var dataRestMinSize = sha256.BlockSize + bit.Size32b_B + 1
|
||||||
603
thrembio/server.go
Normal file
603
thrembio/server.go
Normal file
@ -0,0 +1,603 @@
|
|||||||
|
package thrembio
|
||||||
|
|
||||||
|
// 10% of AI generated code
|
||||||
|
// human made; AI details/comments
|
||||||
|
// FINAL
|
||||||
|
|
||||||
|
import (
|
||||||
|
"WhspBrd/menc"
|
||||||
|
"WhspBrd/owner"
|
||||||
|
"WhspBrd/sfudp"
|
||||||
|
"WhspBrd/thrembio/taskpool"
|
||||||
|
"WhspBrd/typio/bit"
|
||||||
|
"bytes"
|
||||||
|
"crypto/mlkem"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrServerAlreadyOpen = errors.New("server already open")
|
||||||
|
ErrMalformedPacket = errors.New("user provided malformed packet")
|
||||||
|
ErrNoAssociatedRegisterTokenFound = errors.New("user provided register token ID that doesn't exist")
|
||||||
|
ErrInvalidRegisterCheckHash = errors.New("user provided invalid register check hash (binds token + identity)")
|
||||||
|
ErrUserAlreadyRegistered = errors.New("user is already registered")
|
||||||
|
ErrNotRegisteredTriedToLogin = errors.New("user that is not registered tried to login")
|
||||||
|
ErrReplayAttackSuspected = errors.New("replay attack suspected due to repeated packet")
|
||||||
|
ErrUserNotLoggedIn = errors.New("user not logged in tried to send data")
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReadType bit.Bit8
|
||||||
|
|
||||||
|
const (
|
||||||
|
RT_None ReadType = iota
|
||||||
|
// New user registered
|
||||||
|
RT_Register
|
||||||
|
// User logged in
|
||||||
|
RT_Login
|
||||||
|
// Data from authenticated user
|
||||||
|
RT_Data
|
||||||
|
// Client-related error
|
||||||
|
RT_UserError
|
||||||
|
// Internal server error
|
||||||
|
RT_InsideError
|
||||||
|
)
|
||||||
|
|
||||||
|
type serverFlag bit.Bit8
|
||||||
|
|
||||||
|
const (
|
||||||
|
SF_None serverFlag = 0
|
||||||
|
// Don't log user errors (like invalid register token, invalid login, etc.)
|
||||||
|
SF_NoUserErrorLog serverFlag = 1 << 0
|
||||||
|
// Don't log register events (successful ones)
|
||||||
|
SF_NoRegisterLog serverFlag = 1 << 1
|
||||||
|
// Don't log login events (successful ones)
|
||||||
|
SF_NoLoginLog serverFlag = 1 << 2
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f *serverFlag) has(flag serverFlag) bool {
|
||||||
|
return (*f)&flag != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type reqPacket struct {
|
||||||
|
from net.UDPAddr // The address of the client that sent the request.
|
||||||
|
|
||||||
|
time uint64
|
||||||
|
reqType PacketReqType // The type of the request (register, login, data, etc.)
|
||||||
|
|
||||||
|
fullPacket []byte // The full packet that was sent by the client, including everything.
|
||||||
|
header []byte
|
||||||
|
packet []byte // The packet without the common header (magic, version, timestamp, type), so just the payload-ish.
|
||||||
|
|
||||||
|
// register
|
||||||
|
_got_tokenID_B []byte
|
||||||
|
_got_checkHash []byte
|
||||||
|
_got_sign []byte
|
||||||
|
_got_tokenID uint32
|
||||||
|
_token []byte
|
||||||
|
|
||||||
|
// login
|
||||||
|
_got_encapKey []byte
|
||||||
|
//_got_sign []byte
|
||||||
|
|
||||||
|
// data
|
||||||
|
_got_user []byte
|
||||||
|
_got_seq_B []byte
|
||||||
|
_got_payload []byte
|
||||||
|
_got_seq uint32
|
||||||
|
_sesh [32]byte
|
||||||
|
|
||||||
|
// temps
|
||||||
|
__tempBool bool
|
||||||
|
__tempBytes []byte
|
||||||
|
|
||||||
|
// for auto parts
|
||||||
|
__offset int
|
||||||
|
}
|
||||||
|
|
||||||
|
const rP_ReadAll = -1
|
||||||
|
|
||||||
|
func (pkt *reqPacket) startParts() {
|
||||||
|
pkt.__offset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pkt *reqPacket) nextPart(size int) []byte {
|
||||||
|
start := pkt.__offset
|
||||||
|
if start > len(pkt.packet) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var end int
|
||||||
|
switch {
|
||||||
|
case size == rP_ReadAll:
|
||||||
|
end = len(pkt.packet)
|
||||||
|
case size >= 0 && start+size <= len(pkt.packet):
|
||||||
|
end = start + size
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt.__offset = end
|
||||||
|
return pkt.packet[start:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
type readPacket struct {
|
||||||
|
rt ReadType
|
||||||
|
u owner.Identity
|
||||||
|
fa net.UDPAddr
|
||||||
|
d []byte
|
||||||
|
e error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Server interface {
|
||||||
|
Open() error
|
||||||
|
Close()
|
||||||
|
SetFlags(flags serverFlag)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Read waits for incoming packets until a valid one is processed.
|
||||||
|
Invalid packets are ignored.
|
||||||
|
|
||||||
|
Returns one of:
|
||||||
|
RT_Register:
|
||||||
|
(RT_Register, newUser, fromAddr, nil, nil)
|
||||||
|
Ignored if SF_NoRegisterLog is enabled.
|
||||||
|
|
||||||
|
RT_Login:
|
||||||
|
(RT_Login, user, fromAddr, nil, nil)
|
||||||
|
Ignored if SF_NoLoginLog is enabled.
|
||||||
|
|
||||||
|
RT_Data:
|
||||||
|
(RT_Data, user, fromAddr, requestData, nil)
|
||||||
|
|
||||||
|
RT_UserError:
|
||||||
|
(RT_UserError, ?user, fromAddr, nil, error)
|
||||||
|
Ignored if SF_NoUserError is enabled.
|
||||||
|
|
||||||
|
RT_InsideError:
|
||||||
|
(RT_InsideError, emptyUser, emptyAddr, nil, error)
|
||||||
|
*/
|
||||||
|
Read() (readType ReadType, user owner.Identity, fromAddr net.UDPAddr, data []byte, err error)
|
||||||
|
Write(data []byte, to owner.Identity) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type server struct {
|
||||||
|
port uint16
|
||||||
|
secret owner.Secret
|
||||||
|
db ServerDB
|
||||||
|
|
||||||
|
flags serverFlag
|
||||||
|
listener *sfudp.SFUDPConn
|
||||||
|
debug bool
|
||||||
|
|
||||||
|
bufPool sync.Pool
|
||||||
|
taskPool *taskpool.Pool[reqPacket]
|
||||||
|
|
||||||
|
packets chan readPacket
|
||||||
|
|
||||||
|
done chan struct{}
|
||||||
|
doneMu sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(port uint16, secret owner.Secret, db ServerDB) (Server, error) {
|
||||||
|
if secret == nil {
|
||||||
|
return nil, ErrSecretCantBeNil
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := &server{
|
||||||
|
port: port,
|
||||||
|
secret: secret,
|
||||||
|
db: db,
|
||||||
|
listener: nil,
|
||||||
|
taskPool: taskpool.New[reqPacket](0, 0),
|
||||||
|
bufPool: sync.Pool{
|
||||||
|
New: func() any {
|
||||||
|
b := make([]byte, 16384)
|
||||||
|
return &b
|
||||||
|
},
|
||||||
|
},
|
||||||
|
debug: os.Getenv("WHSPBRD_DEBUG") != "",
|
||||||
|
}
|
||||||
|
|
||||||
|
return srv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) SetFlags(flags serverFlag) {
|
||||||
|
s.flags = flags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) Open() error {
|
||||||
|
if s.listener != nil {
|
||||||
|
return ErrServerAlreadyOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := sfudp.ListenSFUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: int(s.port)})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.listener = l
|
||||||
|
s.packets = make(chan readPacket, 1024)
|
||||||
|
s.done = make(chan struct{})
|
||||||
|
|
||||||
|
s.taskPool.Open()
|
||||||
|
|
||||||
|
go s.serve()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) serve() {
|
||||||
|
defer close(s.packets)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
bufPtr := s.bufPool.Get().(*[]byte)
|
||||||
|
buf := *bufPtr
|
||||||
|
|
||||||
|
length, addr, err := s.listener.ReadFromSFUDP(buf[:])
|
||||||
|
if err != nil {
|
||||||
|
s.bufPool.Put(bufPtr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if length < commonHeaderSize ||
|
||||||
|
buf[0] != magicBytes[0] ||
|
||||||
|
buf[1] != magicBytes[1] ||
|
||||||
|
buf[2] != magicBytes[2] ||
|
||||||
|
buf[3] != version {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := binary.LittleEndian.Uint64(buf[4:12])
|
||||||
|
now := time.Now()
|
||||||
|
nowMs := uint64(now.UnixMilli())
|
||||||
|
if ts > nowMs || nowMs-ts > 8_000 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
reqType := PacketReqType(buf[12])
|
||||||
|
if reqType >= Rq_Unknown {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt := reqPacket{
|
||||||
|
from: *addr,
|
||||||
|
time: ts,
|
||||||
|
reqType: reqType,
|
||||||
|
fullPacket: buf[:length],
|
||||||
|
}
|
||||||
|
pkt.header = pkt.fullPacket[:13]
|
||||||
|
pkt.packet = pkt.fullPacket[13:]
|
||||||
|
|
||||||
|
s.taskPool.Dispatch(taskpool.Task[reqPacket]{
|
||||||
|
Value: pkt,
|
||||||
|
Handle: s.handlePacket,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handlePacket(pkt reqPacket) {
|
||||||
|
defer s.bufPool.Put(&pkt.fullPacket)
|
||||||
|
|
||||||
|
var (
|
||||||
|
user owner.Identity
|
||||||
|
data []byte
|
||||||
|
userErr error
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
switch pkt.reqType {
|
||||||
|
|
||||||
|
case Rq_Ping:
|
||||||
|
s.rawWriteAck(&pkt.from, pkt.fullPacket, true)
|
||||||
|
return
|
||||||
|
case Rq_Register:
|
||||||
|
user, userErr, err = s.handleRegister(pkt)
|
||||||
|
case Rq_Login:
|
||||||
|
user, userErr, err = s.handleLogin(pkt)
|
||||||
|
case Rq_Data:
|
||||||
|
user, data, userErr, err = s.handleData(pkt)
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.emitResult(pkt, user, data, userErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) emitResult(
|
||||||
|
pkt reqPacket,
|
||||||
|
user owner.Identity,
|
||||||
|
data []byte,
|
||||||
|
userErr error,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
var rp readPacket
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rp = readPacket{RT_InsideError, owner.Identity{}, net.UDPAddr{}, nil, err}
|
||||||
|
} else if userErr != nil {
|
||||||
|
if s.debug {
|
||||||
|
log.Printf("user error: %v", userErr)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
_ = s.rawWriteError(&pkt.from, pkt.fullPacket, userErrPayload(userErr), true)
|
||||||
|
}
|
||||||
|
if s.flags.has(SF_NoUserErrorLog) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rp = readPacket{RT_UserError, owner.Identity{}, pkt.from, nil, userErr}
|
||||||
|
} else {
|
||||||
|
switch pkt.reqType {
|
||||||
|
case Rq_Register:
|
||||||
|
if s.flags.has(SF_NoRegisterLog) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rp = readPacket{RT_Register, user, pkt.from, nil, nil}
|
||||||
|
case Rq_Login:
|
||||||
|
if s.flags.has(SF_NoLoginLog) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rp = readPacket{RT_Login, user, pkt.from, nil, nil}
|
||||||
|
case Rq_Data:
|
||||||
|
rp = readPacket{RT_Data, user, pkt.from, data, nil}
|
||||||
|
}
|
||||||
|
s.db.SetLastIp(user, &pkt.from)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case s.packets <- rp:
|
||||||
|
case <-s.done:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) Close() {
|
||||||
|
s.doneMu.Do(func() {
|
||||||
|
if s.listener == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
close(s.done)
|
||||||
|
s.listener.Close()
|
||||||
|
s.taskPool.Close()
|
||||||
|
s.listener = nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) Read() (ReadType, owner.Identity, net.UDPAddr, []byte, error) {
|
||||||
|
p, ok := <-s.packets
|
||||||
|
if !ok {
|
||||||
|
return RT_InsideError, owner.Identity{}, net.UDPAddr{}, nil, net.ErrClosed
|
||||||
|
}
|
||||||
|
return p.rt, p.u, p.fa, p.d, p.e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) Write(data []byte, user owner.Identity) error {
|
||||||
|
ip, ex := s.db.GetLastIp(user)
|
||||||
|
if !ex {
|
||||||
|
return errors.New("no known IP for user")
|
||||||
|
}
|
||||||
|
req, ex := s.db.GetLastReq(user)
|
||||||
|
if !ex {
|
||||||
|
return errors.New("no known last request for user")
|
||||||
|
}
|
||||||
|
key, ex, err := s.db.GetActiveSession(user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ex {
|
||||||
|
return errors.New("no active session for user")
|
||||||
|
}
|
||||||
|
ciphertext, err := menc.AESGCM_Quick_Encrypt(key[:], data, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.rawWriteData(ip, req, ciphertext, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handleRegister(pkt reqPacket) (newUser owner.Identity, userErr error, err error) {
|
||||||
|
pkt.startParts()
|
||||||
|
pkt._got_tokenID_B = pkt.nextPart(bit.SizeUint32_B)
|
||||||
|
pkt._got_checkHash = pkt.nextPart(bit.SizeSha256_B)
|
||||||
|
pkt._got_sign = pkt.nextPart(owner.SignatureSize)
|
||||||
|
pkt.__tempBytes = pkt.nextPart(rP_ReadAll)
|
||||||
|
if pkt._got_tokenID_B == nil || pkt._got_checkHash == nil || pkt._got_sign == nil || len(pkt.__tempBytes) != 0 {
|
||||||
|
userErr = ErrMalformedPacket
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pkt._got_tokenID = binary.BigEndian.Uint32(pkt._got_tokenID_B)
|
||||||
|
|
||||||
|
// --- ATOMIC token consumption ---
|
||||||
|
pkt._token, pkt.__tempBool, err = s.db.ConsumeRegisterToken(pkt._got_tokenID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
} else if !pkt.__tempBool {
|
||||||
|
userErr = ErrNoAssociatedRegisterTokenFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- verify identity signature ---
|
||||||
|
newUser, userErr = owner.Verify(
|
||||||
|
pkt.fullPacket[:len(pkt.fullPacket)-owner.SignatureSize],
|
||||||
|
pkt._got_sign,
|
||||||
|
)
|
||||||
|
if userErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- check hash binds token + identity ---
|
||||||
|
sha := sha256.New()
|
||||||
|
sha.Write(pkt.header)
|
||||||
|
sha.Write(pkt._got_tokenID_B)
|
||||||
|
sha.Write(pkt._token)
|
||||||
|
sha.Write(newUser[:])
|
||||||
|
if !bytes.Equal(sha.Sum(nil), pkt._got_checkHash) {
|
||||||
|
userErr = ErrInvalidRegisterCheckHash
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ATOMIC register user ---
|
||||||
|
alreadyExists, err := s.db.AddRegisteredUserAtomic(newUser)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
} else if alreadyExists {
|
||||||
|
userErr = ErrUserAlreadyRegistered
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.rawWriteAck(&pkt.from, pkt.fullPacket, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handleLogin(pkt reqPacket) (user owner.Identity, userErr error, err error) {
|
||||||
|
pkt.startParts()
|
||||||
|
pkt._got_encapKey = pkt.nextPart(mlkem.EncapsulationKeySize1024)
|
||||||
|
pkt._got_sign = pkt.nextPart(owner.SignatureSize)
|
||||||
|
pkt.__tempBytes = pkt.nextPart(rP_ReadAll)
|
||||||
|
|
||||||
|
if pkt._got_encapKey == nil || pkt._got_sign == nil || len(pkt.__tempBytes) != 0 {
|
||||||
|
userErr = ErrMalformedPacket
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- verify user signature ---
|
||||||
|
user, userErr = owner.Verify(
|
||||||
|
pkt.fullPacket[:len(pkt.fullPacket)-owner.SignatureSize],
|
||||||
|
pkt._got_sign,
|
||||||
|
)
|
||||||
|
if userErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- check registered user ---
|
||||||
|
pkt.__tempBool, err = s.db.HasRegisteredUser(user)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
} else if !pkt.__tempBool {
|
||||||
|
err = s.rawWriteError(&pkt.from, pkt.fullPacket, notRegisteredB, true)
|
||||||
|
userErr = ErrNotRegisteredTriedToLogin
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ATOMIC anti replay check ---
|
||||||
|
pkt.__tempBool, err = s.db.CheckAndSetLastReq(user, pkt.fullPacket)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
} else if pkt.__tempBool {
|
||||||
|
userErr = ErrReplayAttackSuspected
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- generate encapsulation key ---
|
||||||
|
encKey, userErr := mlkem.NewEncapsulationKey1024(pkt._got_encapKey)
|
||||||
|
if userErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shared, ciphertext := encKey.Encapsulate()
|
||||||
|
|
||||||
|
// --- set active session ---
|
||||||
|
err = s.db.SetActiveSession(user, sha256.Sum256(shared))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.rawWriteData(&pkt.from, pkt.fullPacket, ciphertext, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handleData(pkt reqPacket) (user owner.Identity, data []byte, userErr error, err error) {
|
||||||
|
pkt.startParts()
|
||||||
|
pkt._got_user = pkt.nextPart(owner.IdentitySize)
|
||||||
|
pkt._got_seq_B = pkt.nextPart(bit.SizeUint32_B)
|
||||||
|
pkt._got_payload = pkt.nextPart(rP_ReadAll)
|
||||||
|
if pkt._got_user == nil || pkt._got_seq_B == nil || pkt._got_payload == nil {
|
||||||
|
userErr = ErrMalformedPacket
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pkt._got_seq = binary.BigEndian.Uint32(pkt._got_seq_B)
|
||||||
|
|
||||||
|
// --- check logged in user ---
|
||||||
|
pkt._sesh, pkt.__tempBool, err = s.db.GetActiveSession(owner.Identity(pkt._got_user))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
} else if !pkt.__tempBool {
|
||||||
|
err = s.rawWriteError(&pkt.from, pkt.fullPacket, notLoggedB, true)
|
||||||
|
userErr = ErrUserNotLoggedIn
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, userErr = menc.AESGCM_Quick_Decrypt(pkt._sesh[:], pkt._got_payload, append(pkt.header, pkt._got_seq_B...))
|
||||||
|
if userErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ATOMIC anti replay check ---
|
||||||
|
pkt.__tempBool, err = s.db.CheckAndSetLastReq(user, pkt.fullPacket)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
} else if pkt.__tempBool {
|
||||||
|
userErr = ErrReplayAttackSuspected
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.rawWriteAck(&pkt.from, pkt.fullPacket, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) rawWriteAck(to *net.UDPAddr, tohash []byte, signed bool) error {
|
||||||
|
return s.rawWrite(to, Rs_Ack, tohash, none, signed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) rawWriteData(to *net.UDPAddr, tohash []byte, data []byte, signed bool) error {
|
||||||
|
return s.rawWrite(to, Rs_Data, tohash, data, signed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) rawWriteError(to *net.UDPAddr, tohash []byte, err []byte, signed bool) error {
|
||||||
|
return s.rawWrite(to, Rs_Error, tohash, err, signed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) rawWrite(to *net.UDPAddr, tp PacketResType, tohash []byte, payload []byte, signed bool) error {
|
||||||
|
hash := sha256.Sum256(tohash)
|
||||||
|
data := append(hash[:], payload...)
|
||||||
|
if signed {
|
||||||
|
signature, err := s.secret.Sign(data)
|
||||||
|
if err != nil {
|
||||||
|
panic("klokotek")
|
||||||
|
}
|
||||||
|
data = append(data, signature...)
|
||||||
|
}
|
||||||
|
data = append(magicWithVersion, append([]byte{byte(tp)}, data...)...)
|
||||||
|
_, err := s.listener.WriteToSFUDP(data, to)
|
||||||
|
return err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func userErrPayload(err error) []byte {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, ErrMalformedPacket):
|
||||||
|
return []byte("malformed")
|
||||||
|
case errors.Is(err, ErrNoAssociatedRegisterTokenFound):
|
||||||
|
return []byte("no_token")
|
||||||
|
case errors.Is(err, ErrInvalidRegisterCheckHash):
|
||||||
|
return []byte("invalid_check")
|
||||||
|
case errors.Is(err, ErrUserAlreadyRegistered):
|
||||||
|
return []byte("already_registered")
|
||||||
|
case errors.Is(err, ErrNotRegisteredTriedToLogin):
|
||||||
|
return []byte("not_registered")
|
||||||
|
case errors.Is(err, ErrUserNotLoggedIn):
|
||||||
|
return []byte("not_logged")
|
||||||
|
case errors.Is(err, ErrReplayAttackSuspected):
|
||||||
|
return []byte("replay")
|
||||||
|
default:
|
||||||
|
return []byte("user_error")
|
||||||
|
}
|
||||||
|
}
|
||||||
310
thrembio/serverDb.go
Normal file
310
thrembio/serverDb.go
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
package thrembio
|
||||||
|
|
||||||
|
// 90% of AI generated code
|
||||||
|
// human designed & reviewed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"WhspBrd/owner"
|
||||||
|
"bytes"
|
||||||
|
"database/sql"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerDB interface {
|
||||||
|
// Registration tokens (atomic usage)
|
||||||
|
GetRegisterTokens() (map[uint32][]byte, error) // optional read-only snapshot
|
||||||
|
SetRegisterToken(id uint32, token []byte) error // add new token
|
||||||
|
ConsumeRegisterToken(id uint32) ([]byte, bool, error) // atomic get + remove
|
||||||
|
|
||||||
|
// Registered users
|
||||||
|
GetRegisteredUsers() ([]owner.Identity, error) // optional snapshot
|
||||||
|
HasRegisteredUser(user owner.Identity) (bool, error)
|
||||||
|
AddRegisteredUserAtomic(user owner.Identity) (alreadyExists bool, err error) // atomic insert
|
||||||
|
RemoveRegisteredUser(user owner.Identity) error
|
||||||
|
|
||||||
|
// Active sessions
|
||||||
|
GetActiveSessions() (map[owner.Identity][32]byte, error)
|
||||||
|
SetActiveSession(user owner.Identity, secret [32]byte) error
|
||||||
|
GetActiveSession(user owner.Identity) ([32]byte, bool, error)
|
||||||
|
RemoveActiveSession(user owner.Identity) error
|
||||||
|
|
||||||
|
/*
|
||||||
|
Replay prevention (atomic)
|
||||||
|
Not db persistent.
|
||||||
|
*/
|
||||||
|
CheckAndSetLastReq(user owner.Identity, reqData []byte) (isReplay bool, err error)
|
||||||
|
CheckAndSetLastSeq(user owner.Identity, seq uint32) (isReplay bool, err error)
|
||||||
|
GetLastReq(user owner.Identity) ([]byte, bool)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Not db persistent
|
||||||
|
*/
|
||||||
|
SetLastIp(user owner.Identity, ip *net.UDPAddr)
|
||||||
|
GetLastIp(user owner.Identity) (*net.UDPAddr, bool)
|
||||||
|
|
||||||
|
// Close DB
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverDB struct {
|
||||||
|
db *sql.DB
|
||||||
|
|
||||||
|
lastReqsMu sync.Mutex
|
||||||
|
lastReqs map[owner.Identity][]byte
|
||||||
|
|
||||||
|
lastSeqsMu sync.Mutex
|
||||||
|
lastSeqs map[owner.Identity]uint32
|
||||||
|
|
||||||
|
lastIpsMu sync.Mutex
|
||||||
|
lastIps map[owner.Identity]*net.UDPAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSQLiteDB(path string) (ServerDB, error) {
|
||||||
|
db, err := sql.Open("sqlite3", path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Ping(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &serverDB{
|
||||||
|
db: db,
|
||||||
|
lastReqs: make(map[owner.Identity][]byte),
|
||||||
|
lastSeqs: make(map[owner.Identity]uint32),
|
||||||
|
lastIps: make(map[owner.Identity]*net.UDPAddr),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.init(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
func (s *serverDB) init() error {
|
||||||
|
_, err := s.db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS register_tokens (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
token BLOB NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS registered_users (
|
||||||
|
username BLOB PRIMARY KEY
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS active_sessions (
|
||||||
|
username BLOB PRIMARY KEY,
|
||||||
|
session_id BLOB NOT NULL
|
||||||
|
);`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- helpers ---
|
||||||
|
func idToBytes(id owner.Identity) []byte {
|
||||||
|
b := make([]byte, len(id))
|
||||||
|
copy(b, id[:])
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesToID(b []byte) owner.Identity {
|
||||||
|
var id owner.Identity
|
||||||
|
copy(id[:], b)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- register tokens ---
|
||||||
|
func (s *serverDB) GetRegisterTokens() (map[uint32][]byte, error) {
|
||||||
|
rows, err := s.db.Query(`SELECT id, token FROM register_tokens`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
tokens := make(map[uint32][]byte)
|
||||||
|
for rows.Next() {
|
||||||
|
var id uint32
|
||||||
|
var token []byte
|
||||||
|
if err := rows.Scan(&id, &token); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tokens[id] = token
|
||||||
|
}
|
||||||
|
return tokens, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverDB) SetRegisterToken(id uint32, token []byte) error {
|
||||||
|
_, err := s.db.Exec(`
|
||||||
|
INSERT INTO register_tokens(id, token)
|
||||||
|
VALUES(?, ?)
|
||||||
|
ON CONFLICT(id) DO UPDATE SET token = excluded.token
|
||||||
|
`, id, token)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ATOMIC: consume token ---
|
||||||
|
func (s *serverDB) ConsumeRegisterToken(id uint32) ([]byte, bool, error) {
|
||||||
|
row := s.db.QueryRow(`
|
||||||
|
DELETE FROM register_tokens
|
||||||
|
WHERE id = ?
|
||||||
|
RETURNING token
|
||||||
|
`, id)
|
||||||
|
|
||||||
|
var token []byte
|
||||||
|
err := row.Scan(&token)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
return token, err == nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- registered users ---
|
||||||
|
func (s *serverDB) GetRegisteredUsers() ([]owner.Identity, error) {
|
||||||
|
rows, err := s.db.Query(`SELECT username FROM registered_users`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var users []owner.Identity
|
||||||
|
for rows.Next() {
|
||||||
|
var u []byte
|
||||||
|
if err := rows.Scan(&u); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
users = append(users, bytesToID(u))
|
||||||
|
}
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverDB) HasRegisteredUser(user owner.Identity) (bool, error) {
|
||||||
|
var exists int
|
||||||
|
err := s.db.QueryRow(
|
||||||
|
`SELECT EXISTS(SELECT 1 FROM registered_users WHERE username = ?)`,
|
||||||
|
idToBytes(user),
|
||||||
|
).Scan(&exists)
|
||||||
|
return exists == 1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ATOMIC: add user if not exists ---
|
||||||
|
func (s *serverDB) AddRegisteredUserAtomic(user owner.Identity) (alreadyExists bool, err error) {
|
||||||
|
res, err := s.db.Exec(`
|
||||||
|
INSERT OR IGNORE INTO registered_users(username) VALUES(?)
|
||||||
|
`, idToBytes(user))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows == 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverDB) RemoveRegisteredUser(user owner.Identity) error {
|
||||||
|
_, err := s.db.Exec(`DELETE FROM registered_users WHERE username = ?`, idToBytes(user))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- active sessions ---
|
||||||
|
func (s *serverDB) GetActiveSessions() (map[owner.Identity][32]byte, error) {
|
||||||
|
rows, err := s.db.Query(`SELECT username, session_id FROM active_sessions`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
sessions := make(map[owner.Identity][32]byte)
|
||||||
|
for rows.Next() {
|
||||||
|
var username []byte
|
||||||
|
var key [32]byte
|
||||||
|
if err := rows.Scan(&username, &key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sessions[bytesToID(username)] = key
|
||||||
|
}
|
||||||
|
return sessions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverDB) SetActiveSession(user owner.Identity, key [32]byte) error {
|
||||||
|
_, err := s.db.Exec(`
|
||||||
|
INSERT INTO active_sessions(username, session_id)
|
||||||
|
VALUES(?, ?)
|
||||||
|
ON CONFLICT(username) DO UPDATE SET session_id = excluded.session_id
|
||||||
|
`, idToBytes(user), key[:])
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverDB) GetActiveSession(user owner.Identity) ([32]byte, bool, error) {
|
||||||
|
var key [32]byte
|
||||||
|
err := s.db.QueryRow(
|
||||||
|
`SELECT session_id FROM active_sessions WHERE username = ?`,
|
||||||
|
idToBytes(user),
|
||||||
|
).Scan(&key)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return [32]byte{}, false, nil
|
||||||
|
}
|
||||||
|
return key, err == nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverDB) RemoveActiveSession(user owner.Identity) error {
|
||||||
|
_, err := s.db.Exec(`DELETE FROM active_sessions WHERE username = ?`, idToBytes(user))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ATOMIC: login replay prevention ---
|
||||||
|
func (s *serverDB) CheckAndSetLastReq(user owner.Identity, reqData []byte) (isReplay bool, err error) {
|
||||||
|
s.lastReqsMu.Lock()
|
||||||
|
defer s.lastReqsMu.Unlock()
|
||||||
|
|
||||||
|
last, exists := s.lastReqs[user]
|
||||||
|
if exists && bytes.Equal(last, reqData) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.lastReqs[user] = reqData
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverDB) CheckAndSetLastSeq(user owner.Identity, seq uint32) (isReplay bool, err error) {
|
||||||
|
s.lastSeqsMu.Lock()
|
||||||
|
defer s.lastSeqsMu.Unlock()
|
||||||
|
|
||||||
|
last, exists := s.lastSeqs[user]
|
||||||
|
if exists && seq <= last {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.lastSeqs[user] = seq
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverDB) GetLastReq(user owner.Identity) ([]byte, bool) {
|
||||||
|
s.lastReqsMu.Lock()
|
||||||
|
defer s.lastReqsMu.Unlock()
|
||||||
|
|
||||||
|
req, exists := s.lastReqs[user]
|
||||||
|
return req, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// just ip save
|
||||||
|
func (s *serverDB) SetLastIp(user owner.Identity, ip *net.UDPAddr) {
|
||||||
|
s.lastIpsMu.Lock()
|
||||||
|
defer s.lastIpsMu.Unlock()
|
||||||
|
|
||||||
|
s.lastIps[user] = ip
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverDB) GetLastIp(user owner.Identity) (*net.UDPAddr, bool) {
|
||||||
|
s.lastIpsMu.Lock()
|
||||||
|
defer s.lastIpsMu.Unlock()
|
||||||
|
|
||||||
|
ip, exists := s.lastIps[user]
|
||||||
|
return ip, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- close ---
|
||||||
|
func (s *serverDB) Close() error { return s.db.Close() }
|
||||||
127
thrembio/taskpool/taskpool.go
Normal file
127
thrembio/taskpool/taskpool.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
// taskpool: simple bounded worker pool with safe shutdown + fast dispatch
|
||||||
|
package taskpool
|
||||||
|
|
||||||
|
// 80% of AI generated code
|
||||||
|
// human designed & reviewed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Task = value + handler
|
||||||
|
type Task[T any] struct {
|
||||||
|
Value T
|
||||||
|
Handle func(T)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pool manages worker goroutines processing tasks from a queue.
|
||||||
|
type Pool[T any] struct {
|
||||||
|
workers int // number of worker goroutines
|
||||||
|
queueSize int // channel capacity
|
||||||
|
|
||||||
|
mu sync.RWMutex // RLock for dispatch, Lock for open/close
|
||||||
|
tasks chan Task[T] // task queue (nil when closed)
|
||||||
|
wg sync.WaitGroup
|
||||||
|
closed bool // prevents sends after close
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a pool.
|
||||||
|
// nWorkers <= 0 → NumCPU
|
||||||
|
// queueSize <= 0 → 256
|
||||||
|
func New[T any](nWorkers, queueSize int) *Pool[T] {
|
||||||
|
if nWorkers <= 0 {
|
||||||
|
nWorkers = runtime.NumCPU()
|
||||||
|
}
|
||||||
|
if queueSize <= 0 {
|
||||||
|
queueSize = 256
|
||||||
|
}
|
||||||
|
return &Pool[T]{workers: nWorkers, queueSize: queueSize}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open starts workers (idempotent, restartable after Close).
|
||||||
|
func (p *Pool[T]) Open() {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
if !p.closed && p.tasks != nil { // already running
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.tasks = make(chan Task[T], p.queueSize)
|
||||||
|
p.closed = false
|
||||||
|
|
||||||
|
for i := 0; i < p.workers; i++ {
|
||||||
|
p.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer p.wg.Done()
|
||||||
|
for t := range p.tasks { // exits on close
|
||||||
|
t.Handle(t.Value)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch: try queue → fallback inline.
|
||||||
|
// Non-blocking, safe under concurrent Close.
|
||||||
|
func (p *Pool[T]) Dispatch(t Task[T]) {
|
||||||
|
p.mu.RLock()
|
||||||
|
tasks := p.tasks
|
||||||
|
closed := p.closed
|
||||||
|
|
||||||
|
if closed || tasks == nil {
|
||||||
|
p.mu.RUnlock()
|
||||||
|
t.Handle(t.Value) // pool unavailable → run inline
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case tasks <- t: // fast path
|
||||||
|
p.mu.RUnlock()
|
||||||
|
default:
|
||||||
|
p.mu.RUnlock()
|
||||||
|
t.Handle(t.Value) // queue full → backpressure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TryDispatch: enqueue only, no fallback.
|
||||||
|
func (p *Pool[T]) TryDispatch(t Task[T]) bool {
|
||||||
|
p.mu.RLock()
|
||||||
|
tasks := p.tasks
|
||||||
|
closed := p.closed
|
||||||
|
|
||||||
|
if closed || tasks == nil {
|
||||||
|
p.mu.RUnlock()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case tasks <- t:
|
||||||
|
p.mu.RUnlock()
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
p.mu.RUnlock()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close stops workers and waits for completion.
|
||||||
|
// Safe to call multiple times.
|
||||||
|
func (p *Pool[T]) Close() {
|
||||||
|
p.mu.Lock()
|
||||||
|
if p.closed {
|
||||||
|
p.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.closed = true
|
||||||
|
tasks := p.tasks
|
||||||
|
p.tasks = nil // prevent future sends
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
if tasks != nil {
|
||||||
|
close(tasks) // safe: no send can happen (RLock prevents race)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.wg.Wait() // wait for workers to drain
|
||||||
|
}
|
||||||
69
typio/base58/base58.go
Normal file
69
typio/base58/base58.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package base58
|
||||||
|
|
||||||
|
// 20% of AI generated code
|
||||||
|
// human made; AI enhanced
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidBase58Character = errors.New("invalid character in base58 string")
|
||||||
|
)
|
||||||
|
|
||||||
|
const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||||
|
|
||||||
|
var revAlphabet = func() map[byte]int {
|
||||||
|
m := make(map[byte]int)
|
||||||
|
for i := range len(alphabet) {
|
||||||
|
m[alphabet[i]] = i
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}()
|
||||||
|
|
||||||
|
var b58 = big.NewInt(58)
|
||||||
|
var b256 = big.NewInt(256)
|
||||||
|
var estimate = math.Log(256) / math.Log(58)
|
||||||
|
|
||||||
|
func Encode(input []byte) string {
|
||||||
|
number := new(big.Int).SetBytes(input)
|
||||||
|
if number.Sign() == 0 {
|
||||||
|
return string(alphabet[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
bufCap := int(float64(len(input))*estimate) + 8
|
||||||
|
buf := make([]byte, 0, bufCap)
|
||||||
|
|
||||||
|
mod := new(big.Int)
|
||||||
|
for number.Sign() > 0 {
|
||||||
|
number.DivMod(number, b58, mod)
|
||||||
|
if cap(buf) > len(buf) {
|
||||||
|
buf = buf[:len(buf)+1]
|
||||||
|
copy(buf[1:], buf[:len(buf)-1+1])
|
||||||
|
buf[0] = alphabet[mod.Int64()]
|
||||||
|
} else {
|
||||||
|
tmp := make([]byte, 1, cap(buf)+1)
|
||||||
|
tmp[0] = alphabet[mod.Int64()]
|
||||||
|
buf = append(tmp, buf...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Decode(input string) ([]byte, error) {
|
||||||
|
number := big.NewInt(0)
|
||||||
|
for i := 0; i < len(input); i++ {
|
||||||
|
c := input[i]
|
||||||
|
val, ok := revAlphabet[c]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrInvalidBase58Character
|
||||||
|
}
|
||||||
|
number.Mul(number, b58)
|
||||||
|
number.Add(number, big.NewInt(int64(val)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return number.Bytes(), nil
|
||||||
|
}
|
||||||
55
typio/bit/bit.go
Normal file
55
typio/bit/bit.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package bit
|
||||||
|
|
||||||
|
// 0% of AI generated code
|
||||||
|
// simple helper
|
||||||
|
|
||||||
|
const (
|
||||||
|
Size8b_B = 1
|
||||||
|
Size16b_B = 2
|
||||||
|
Size32b_B = 4
|
||||||
|
Size64b_B = 8
|
||||||
|
Size128b_B = 16
|
||||||
|
Size256b_B = 32
|
||||||
|
Size512b_B = 64
|
||||||
|
|
||||||
|
SizeUint8_B = Size8b_B
|
||||||
|
SizeUint16_B = Size16b_B
|
||||||
|
SizeUint32_B = Size32b_B
|
||||||
|
SizeUint64_B = Size64b_B
|
||||||
|
|
||||||
|
SizeSha128_B = Size128b_B
|
||||||
|
SizeSha256_B = Size256b_B
|
||||||
|
SizeSha512_B = Size512b_B
|
||||||
|
|
||||||
|
Size1B_b = 8
|
||||||
|
Size2B_b = 16
|
||||||
|
Size4B_b = 32
|
||||||
|
Size8B_b = 64
|
||||||
|
Size16B_b = 128
|
||||||
|
Size32B_b = 256
|
||||||
|
Size64B_b = 512
|
||||||
|
|
||||||
|
Size1KB_b = 1024
|
||||||
|
Size2KB_b = 2048
|
||||||
|
Size4KB_b = 4096
|
||||||
|
Size8KB_b = 8192
|
||||||
|
Size16KB_b = 16384
|
||||||
|
Size32KB_b = 32768
|
||||||
|
Size64KB_b = 65536
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Bit8 = uint8
|
||||||
|
Bit16 = uint16
|
||||||
|
Bit32 = uint32
|
||||||
|
Bit64 = uint64
|
||||||
|
Bit128 = [16]byte
|
||||||
|
Bit256 = [32]byte
|
||||||
|
Bit384 = [48]byte
|
||||||
|
Bit512 = [64]byte
|
||||||
|
|
||||||
|
Sha128 = Bit128
|
||||||
|
Sha256 = Bit256
|
||||||
|
Sha384 = Bit384
|
||||||
|
Sha512 = Bit512
|
||||||
|
)
|
||||||
47
typio/splco/splco.go
Normal file
47
typio/splco/splco.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package splco
|
||||||
|
|
||||||
|
// why doesnt Go have this built in
|
||||||
|
// 30% of AI generated code
|
||||||
|
// simple helper
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrDataTooShort = errors.New("data too short for specified sizes")
|
||||||
|
ErrInvalidDataLength = errors.New("data length does not match sum of specified sizes")
|
||||||
|
)
|
||||||
|
|
||||||
|
func Append(data ...[]byte) []byte {
|
||||||
|
expLen := 0
|
||||||
|
for _, b := range data {
|
||||||
|
expLen += len(b)
|
||||||
|
}
|
||||||
|
return AppendWithSize(expLen, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AppendWithSize(size int, data ...[]byte) []byte {
|
||||||
|
return AppendInto(make([]byte, 0, size), data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AppendInto(dest []byte, data ...[]byte) []byte {
|
||||||
|
for _, b := range data {
|
||||||
|
dest = append(dest, b...)
|
||||||
|
}
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
func Split(data []byte, sizes ...int) ([][]byte, error) {
|
||||||
|
result := make([][]byte, len(sizes))
|
||||||
|
offset := 0
|
||||||
|
for i, size := range sizes {
|
||||||
|
if offset+size > len(data) {
|
||||||
|
return nil, ErrDataTooShort
|
||||||
|
}
|
||||||
|
result[i] = data[offset : offset+size]
|
||||||
|
offset += size
|
||||||
|
}
|
||||||
|
if offset != len(data) {
|
||||||
|
return nil, ErrInvalidDataLength
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
114
typio/yum/yum.go
Normal file
114
typio/yum/yum.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package yum
|
||||||
|
|
||||||
|
// 95% of AI generated code
|
||||||
|
// human designed & reviewed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/argon2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
saltSize = 16
|
||||||
|
keySize = 32
|
||||||
|
nonceSize = 12
|
||||||
|
|
||||||
|
// Argon2 params (tune if needed)
|
||||||
|
argonTime = 1
|
||||||
|
argonMemory = 64 * 1024 // 64 MB
|
||||||
|
argonThreads = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
// YumSave encrypts and saves data securely to disk.
|
||||||
|
func YumSave(path string, data []byte, password []byte) error {
|
||||||
|
// Generate salt
|
||||||
|
salt := make([]byte, saltSize)
|
||||||
|
if _, err := rand.Read(salt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive key
|
||||||
|
key := argon2.IDKey(password, salt, argonTime, argonMemory, argonThreads, keySize)
|
||||||
|
|
||||||
|
// Create AES cipher
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate nonce
|
||||||
|
nonce := make([]byte, nonceSize)
|
||||||
|
if _, err := rand.Read(nonce); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt
|
||||||
|
ciphertext := gcm.Seal(nil, nonce, data, nil)
|
||||||
|
|
||||||
|
// File format: [salt | nonce | ciphertext]
|
||||||
|
fileData := append(salt, nonce...)
|
||||||
|
fileData = append(fileData, ciphertext...)
|
||||||
|
|
||||||
|
return os.WriteFile(path, fileData, 0600)
|
||||||
|
}
|
||||||
|
|
||||||
|
// YumLoad loads and decrypts data from disk.
|
||||||
|
func YumLoad(path string, password []byte) ([]byte, error) {
|
||||||
|
fileData, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fileData) < saltSize+nonceSize {
|
||||||
|
return nil, errors.New("file too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract components
|
||||||
|
salt := fileData[:saltSize]
|
||||||
|
nonce := fileData[saltSize : saltSize+nonceSize]
|
||||||
|
ciphertext := fileData[saltSize+nonceSize:]
|
||||||
|
|
||||||
|
// Derive key
|
||||||
|
key := argon2.IDKey(password, salt, argonTime, argonMemory, argonThreads, keySize)
|
||||||
|
|
||||||
|
// Recreate cipher
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt
|
||||||
|
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("decryption failed (wrong password or corrupted data)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func YumSeed(buf []byte) error {
|
||||||
|
_, err := io.ReadFull(rand.Reader, buf)
|
||||||
|
if err != nil {
|
||||||
|
for i := range buf {
|
||||||
|
buf[i] = 0
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user