Changed to purego ncurses, fixed ping, and started to add session invite flow

live
noah metz 2024-04-23 14:16:25 -06:00
parent 6b3bf3eda2
commit 017734e40b
7 changed files with 160 additions and 87 deletions

3
.gitmodules vendored

@ -0,0 +1,3 @@
[submodule "go-ncurses"]
path = go-ncurses
url = gitea@git.metznet.ca:MetzNet/go-ncurses

@ -10,7 +10,6 @@ import (
"fmt" "fmt"
"os" "os"
"os/signal" "os/signal"
"slices"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"time" "time"
@ -19,7 +18,7 @@ import (
"github.com/gen2brain/malgo" "github.com/gen2brain/malgo"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hraban/opus" "github.com/hraban/opus"
"seehuhn.de/go/ncurses" "git.metznet.ca/MetzNet/go-ncurses"
) )
var decoders = map[pnyx.PeerID]chan[]byte{} var decoders = map[pnyx.PeerID]chan[]byte{}
@ -107,12 +106,7 @@ func mixer(data_chan chan []int16, speaker_chan chan []int16) {
} }
} }
func setup_audio() (*malgo.AllocatedContext, *malgo.Device, *malgo.Device) {
func main() {
key_file_arg := flag.String("key", "${HOME}/.pnyx.key", "Path to the private key file to load/save")
generate_key_arg := flag.Bool("genkey", false, "Set to generate a key if none exists")
flag.Parse()
ctx, err := malgo.InitContext(nil, malgo.ContextConfig{}, nil) ctx, err := malgo.InitContext(nil, malgo.ContextConfig{}, nil)
if err != nil { if err != nil {
panic(err) panic(err)
@ -120,9 +114,6 @@ func main() {
go mixer(audio_data, speaker) go mixer(audio_data, speaker)
defer ctx.Free()
defer ctx.Uninit()
infos, err := ctx.Devices(malgo.Playback) infos, err := ctx.Devices(malgo.Playback)
if err != nil { if err != nil {
panic(err) panic(err)
@ -205,9 +196,6 @@ func main() {
panic(err) panic(err)
} }
defer outDevice.Uninit()
defer outDevice.Stop()
onRecvFrames := func(output_samples []byte, input_samples []byte, framecount uint32) { onRecvFrames := func(output_samples []byte, input_samples []byte, framecount uint32) {
if encoder != nil { if encoder != nil {
pcm := make([]int16, len(input_samples)/2) pcm := make([]int16, len(input_samples)/2)
@ -241,13 +229,11 @@ func main() {
panic(err) panic(err)
} }
defer inDevice.Uninit() return ctx, outDevice, inDevice
defer inDevice.Stop() }
var key ed25519.PrivateKey = nil
key_file_path := os.ExpandEnv(*key_file_arg) func get_private_key(path string, generate bool) ed25519.PrivateKey {
key_file_bytes, err := os.ReadFile(key_file_path) key_file_bytes, err := os.ReadFile(path)
if err == nil { if err == nil {
key_pem, _ := pem.Decode(key_file_bytes) key_pem, _ := pem.Decode(key_file_bytes)
if key_pem.Type != "PRIVATE KEY" { if key_pem.Type != "PRIVATE KEY" {
@ -260,12 +246,13 @@ func main() {
} }
var ok bool var ok bool
key, ok = private_key.(ed25519.PrivateKey) key, ok := private_key.(ed25519.PrivateKey)
if ok == false { if ok == false {
panic("Private key is not ed25519.PrivateKey") panic("Private key is not ed25519.PrivateKey")
} }
} else if *generate_key_arg { return key
_, key, err = ed25519.GenerateKey(rand.Reader) } else if generate {
_, key, err := ed25519.GenerateKey(rand.Reader)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -280,37 +267,35 @@ func main() {
Bytes: key_pkcs8, Bytes: key_pkcs8,
}) })
err = os.WriteFile(key_file_path, key_pem, 0o600) err = os.WriteFile(path, key_pem, 0o600)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return key
} else {
panic(fmt.Sprintf("Failed to read key from %s", path))
} }
}
client, err := pnyx.NewClient(key, flag.Arg(0)) func main_loop(client *pnyx.Client, window ncurses.Window, active *atomic.Bool, packet_chan chan pnyx.Payload, user_chan chan rune) {
if err != nil { max_y := ncurses.GetMaxY.Load()(window)
panic(err) max_x := ncurses.GetMaxX.Load()(window)
} titlebar := ncurses.NewWin.Load()(1, max_x, 0, 0)
channels := ncurses.NewWin.Load()(max_y - 1, max_x / 3, 1, 0)
body := ncurses.NewWin.Load()(max_y - 1, max_x * 2 / 3, 1, max_x / 3)
go func() { server_name := client.Connection.RemoteAddr().String()
var buf [1024]byte
for true {
read, _, err := client.Connection.ReadFromUDP(buf[:])
if err != nil {
break
}
data, err := pnyx.ParseSessionData(&client.Session, buf[pnyx.COMMAND_LENGTH + pnyx.SESSION_ID_LENGTH:read]) ncurses.MvWAddStr.Load()(titlebar, 0, 0, fmt.Sprintf("pnyx client %X:%X", client.Key.Public().(ed25519.PublicKey)[:2], client.Session.ID[:2]))
if err != nil { ncurses.MvWAddStr.Load()(body, 0, max_x-len(server_name), server_name)
continue ncurses.WRefresh.Load()(titlebar)
}
packet, err := pnyx.ParsePacket(data)
if err != nil {
continue
}
for active.Load() {
select {
case packet := <-packet_chan:
switch packet := packet.(type) { switch packet := packet.(type) {
case pnyx.PingPacket: case pnyx.PingPacket:
_ = client.Send(pnyx.NewPingPacket())
case pnyx.ChannelCommandPacket: case pnyx.ChannelCommandPacket:
if packet.Channel == pnyx.ChannelID(0) { if packet.Channel == pnyx.ChannelID(0) {
if packet.Mode == pnyx.MODE_AUDIO { if packet.Mode == pnyx.MODE_AUDIO {
@ -350,61 +335,120 @@ func main() {
} }
default: default:
} }
case char := <-user_chan:
ncurses.MvWAddStr.Load()(body, 0, 0, string(char))
ncurses.WRefresh.Load()(body)
ncurses.MvWAddStr.Load()(channels, 0, 0, string(char))
ncurses.WRefresh.Load()(channels)
}
} }
}() }
join_packet := pnyx.NewChannelCommandPacket(uuid.New(), pnyx.ChannelID(0), pnyx.MODE_CHANNEL, pnyx.CHANNEL_COMMAND_JOIN, nil) func bitmatch(b byte, pattern byte, length int) bool {
err = client.Send(join_packet) mask := ^(byte(1 << (8 - length)) - 1)
return (b ^ pattern) & mask == 0
}
func ch_listen(active *atomic.Bool, user_chan chan rune) {
b := [4]byte{}
for active.Load() {
os.Stdin.Read(b[0:1])
if bitmatch(b[0], 0b00000000, 1) {
user_chan <- rune(b[0])
} // TODO: further utf-8 support
}
}
func udp_listen(client *pnyx.Client, active *atomic.Bool, packet_chan chan pnyx.Payload) {
var buf [1024]byte
for active.Load() {
read, _, err := client.Connection.ReadFromUDP(buf[:])
if err != nil { if err != nil {
panic(err) break
} }
get_sample_rate_packet := pnyx.NewChannelCommandPacket(uuid.New(), pnyx.ChannelID(0), pnyx.MODE_AUDIO, pnyx.AUDIO_SET_SAMPLE_RATE, []byte{byte(pnyx.SAMPLE_RATE_48KHZ)}) data, err := pnyx.ParseSessionData(&client.Session, buf[pnyx.COMMAND_LENGTH + pnyx.SESSION_ID_LENGTH:read])
err = client.Send(get_sample_rate_packet)
if err != nil { if err != nil {
panic(err) continue
} }
go func(){ packet, err := pnyx.ParsePacket(data)
if err != nil {
continue
}
packet_chan <- packet
}
}
func process_mic(client *pnyx.Client) {
for true { for true {
data := <- mic data := <- mic
err = client.Send(pnyx.NewDataPacket(pnyx.ChannelID(0), pnyx.MODE_AUDIO, data)) err := client.Send(pnyx.NewDataPacket(pnyx.ChannelID(0), pnyx.MODE_AUDIO, data))
if err != nil { if err != nil {
panic(err) panic(err)
} }
} }
}() }
func main() {
key_file_arg := flag.String("key", "${HOME}/.pnyx.key", "Path to the private key file to load/save")
generate_key_arg := flag.Bool("genkey", false, "Set to generate a key if none exists")
flag.Parse()
ctx, outDevice, inDevice := setup_audio()
defer ctx.Free()
defer ctx.Uninit()
defer outDevice.Uninit()
defer outDevice.Stop()
defer inDevice.Uninit()
defer inDevice.Stop()
key := get_private_key(os.ExpandEnv(*key_file_arg), *generate_key_arg)
client, err := pnyx.NewClient(key, flag.Arg(0))
if err != nil {
panic(err)
}
packet_chan := make(chan pnyx.Payload, 1024)
user_chan := make(chan rune, 1024)
window := ncurses.Init()
active := atomic.Bool{} active := atomic.Bool{}
active.Store(true) active.Store(true)
go func() { go udp_listen(client, &active, packet_chan)
ncurses.ColorPair(1).Init(ncurses.ColorBlue, ncurses.ColorRed)
window.AddStr("pnyx client")
for active.Load() { join_packet := pnyx.NewChannelCommandPacket(uuid.New(), pnyx.ChannelID(0), pnyx.MODE_CHANNEL, pnyx.CHANNEL_COMMAND_JOIN, nil)
window.Refresh() err = client.Send(join_packet)
time.Sleep(200*time.Millisecond) if err != nil {
peers := make([]pnyx.PeerID, 0, len(decoders)) panic(err)
for peer_id := range(decoders) {
peers = append(peers, peer_id)
} }
slices.SortFunc(peers, func(a, b pnyx.PeerID) int { get_sample_rate_packet := pnyx.NewChannelCommandPacket(uuid.New(), pnyx.ChannelID(0), pnyx.MODE_AUDIO, pnyx.AUDIO_SET_SAMPLE_RATE, []byte{byte(pnyx.SAMPLE_RATE_48KHZ)})
return slices.Compare(a[:], b[:]) err = client.Send(get_sample_rate_packet)
}) if err != nil {
panic(err)
for i, peer_id := range(peers) {
window.MvAddStr(i+1, 0, fmt.Sprintf("%x", peer_id))
} }
go process_mic(client)
err = ncurses.Init()
if err != nil {
panic(err)
} }
}()
window := ncurses.InitScr.Load()()
go ch_listen(&active, user_chan)
go main_loop(client, window, &active, packet_chan, user_chan)
os_sigs := make(chan os.Signal, 1) os_sigs := make(chan os.Signal, 1)
signal.Notify(os_sigs, syscall.SIGINT, syscall.SIGABRT) signal.Notify(os_sigs, syscall.SIGINT, syscall.SIGABRT)
<-os_sigs <-os_sigs
active.Store(false) active.Store(false)
ncurses.EndWin() ncurses.EndWin.Load()()
} }

@ -0,0 +1 @@
Subproject commit 10449a124f00077b5ee1b6b50e1b475ec3d66721

@ -1,13 +1,18 @@
module git.metznet.ca/MetzNet/pnyx module git.metznet.ca/MetzNet/pnyx
go 1.22.0 go 1.22.2
replace git.metznet.ca/MetzNet/go-ncurses => ./go-ncurses
require ( require (
filippo.io/edwards25519 v1.1.0 filippo.io/edwards25519 v1.1.0
git.metznet.ca/MetzNet/go-ncurses v0.0.0
github.com/gen2brain/malgo v0.11.21 github.com/gen2brain/malgo v0.11.21
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/hraban/opus v0.0.0-20230925203106-0188a62cb302 github.com/hraban/opus v0.0.0-20230925203106-0188a62cb302
seehuhn.de/go/ncurses v0.2.0
) )
require golang.org/x/sys v0.14.0 // indirect require (
github.com/ebitengine/purego v0.7.1 // indirect
golang.org/x/sys v0.7.0 // indirect
)

@ -1,13 +1,12 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA=
github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/gen2brain/malgo v0.11.21 h1:qsS4Dh6zhZgmvAW5CtKRxDjQzHbc2NJlBG9eE0tgS8w= github.com/gen2brain/malgo v0.11.21 h1:qsS4Dh6zhZgmvAW5CtKRxDjQzHbc2NJlBG9eE0tgS8w=
github.com/gen2brain/malgo v0.11.21/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww= github.com/gen2brain/malgo v0.11.21/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hraban/opus v0.0.0-20230925203106-0188a62cb302 h1:K7bmEmIesLcvCW0Ic2rCk6LtP5++nTnPmrO8mg5umlA= github.com/hraban/opus v0.0.0-20230925203106-0188a62cb302 h1:K7bmEmIesLcvCW0Ic2rCk6LtP5++nTnPmrO8mg5umlA=
github.com/hraban/opus v0.0.0-20230925203106-0188a62cb302/go.mod h1:YQQXrWHN3JEvCtw5ImyTCcPeU/ZLo/YMA+TpB64XdrU= github.com/hraban/opus v0.0.0-20230925203106-0188a62cb302/go.mod h1:YQQXrWHN3JEvCtw5ImyTCcPeU/ZLo/YMA+TpB64XdrU=
golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
seehuhn.de/go/ncurses v0.2.0 h1:ZV256n0GIMVEHJnECliGMffzdFsEoT7krJqdfGoYD1E=
seehuhn.de/go/ncurses v0.2.0/go.mod h1:oAc9Y+UN0tflNV0iME++z0ij9uNmIjdxQFkpGoRMd2E=

@ -148,9 +148,12 @@ func handle_session_incoming(session *ServerSession, server *Server) {
server.sessions_lock.Lock() server.sessions_lock.Lock()
server.close_session(session) server.close_session(session)
server.sessions_lock.Unlock() server.sessions_lock.Unlock()
} else if time.Now().Add(-1*SESSION_PING_TIME).Compare(session.LastSeen) != -1 {
server.Log("Pinging %s after being inactive since %s", session.ID, session.LastSeen)
session.OutgoingPackets <- NewPingPacket()
ping_timer = time.After(SESSION_PING_TIME)
} else { } else {
server.Log("%s passed keep-alive check, last seen %s", session.ID, session.LastSeen) server.Log("%s passed keep-alive check, last seen %s", session.ID, session.LastSeen)
session.OutgoingPackets <- NewPingPacket()
ping_timer = time.After(SESSION_PING_TIME) ping_timer = time.After(SESSION_PING_TIME)
} }
case encrypted := <- session.IncomingPackets: case encrypted := <- session.IncomingPackets:

@ -48,9 +48,11 @@ const (
HMAC_LENGTH = 64 HMAC_LENGTH = 64
COMMAND_LENGTH = 1 COMMAND_LENGTH = 1
TIME_LENGTH = 8 TIME_LENGTH = 8
INVITE_LENGTH = 32
SESSION_OPEN_LENGTH = PUBKEY_LENGTH + PUBKEY_LENGTH + SIGNATURE_LENGTH SESSION_OPEN_LENGTH = PUBKEY_LENGTH + PUBKEY_LENGTH + SIGNATURE_LENGTH
SESSION_OPENED_LENGTH = PUBKEY_LENGTH + PUBKEY_LENGTH + SESSION_ID_LENGTH + SIGNATURE_LENGTH SESSION_OPENED_LENGTH = PUBKEY_LENGTH + PUBKEY_LENGTH + SESSION_ID_LENGTH + SIGNATURE_LENGTH
SESSION_INVITE_LENGTH = INVITE_LENGTH + PUBKEY_LENGTH
SESSION_TIMED_LENGTH = SESSION_ID_LENGTH + TIME_LENGTH + SIGNATURE_LENGTH SESSION_TIMED_LENGTH = SESSION_ID_LENGTH + TIME_LENGTH + SIGNATURE_LENGTH
SESSION_TIMED_RESP_LENGTH = TIME_LENGTH + SIGNATURE_LENGTH SESSION_TIMED_RESP_LENGTH = TIME_LENGTH + SIGNATURE_LENGTH
@ -59,6 +61,8 @@ const (
*/ */
SESSION_OPEN SessionPacketType = iota SESSION_OPEN SessionPacketType = iota
SESSION_OPENED SESSION_OPENED
SESSION_INVITE
SESSION_INVITED
SESSION_CONNECT SESSION_CONNECT
SESSION_CONNECTED SESSION_CONNECTED
SESSION_CLOSE SESSION_CLOSE
@ -84,6 +88,20 @@ func ECDH(public ed25519.PublicKey, private ed25519.PrivateKey) ([]byte, error)
return shared_point.BytesMontgomery(), nil return shared_point.BytesMontgomery(), nil
} }
type Invite [INVITE_LENGTH]byte
func NewSessionInvite(key ed25519.PublicKey, invite Invite) ([]byte, error) {
if key == nil {
return nil, fmt.Errorf("Cannot create a SESSION_INVITE packet without a key")
}
packet := make([]byte, COMMAND_LENGTH + SESSION_INVITE_LENGTH)
// TODO
return packet, nil
}
func NewSessionOpen(key ed25519.PrivateKey) ([]byte, ed25519.PrivateKey, error) { func NewSessionOpen(key ed25519.PrivateKey) ([]byte, ed25519.PrivateKey, error) {
if key == nil { if key == nil {
return nil, nil, fmt.Errorf("Cannot create a SESSION_OPEN packet without a key") return nil, nil, fmt.Errorf("Cannot create a SESSION_OPEN packet without a key")