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