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

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

@ -1,13 +1,18 @@
module git.metznet.ca/MetzNet/pnyx
go 1.22.0
go 1.22.2
replace git.metznet.ca/MetzNet/go-ncurses => ./go-ncurses
require (
filippo.io/edwards25519 v1.1.0
git.metznet.ca/MetzNet/go-ncurses v0.0.0
github.com/gen2brain/malgo v0.11.21
github.com/google/uuid v1.6.0
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/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/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww=
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/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=
golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
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=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

@ -148,9 +148,12 @@ func handle_session_incoming(session *ServerSession, server *Server) {
server.sessions_lock.Lock()
server.close_session(session)
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 {
server.Log("%s passed keep-alive check, last seen %s", session.ID, session.LastSeen)
session.OutgoingPackets <- NewPingPacket()
ping_timer = time.After(SESSION_PING_TIME)
}
case encrypted := <- session.IncomingPackets:

@ -48,9 +48,11 @@ const (
HMAC_LENGTH = 64
COMMAND_LENGTH = 1
TIME_LENGTH = 8
INVITE_LENGTH = 32
SESSION_OPEN_LENGTH = PUBKEY_LENGTH + PUBKEY_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_RESP_LENGTH = TIME_LENGTH + SIGNATURE_LENGTH
@ -59,6 +61,8 @@ const (
*/
SESSION_OPEN SessionPacketType = iota
SESSION_OPENED
SESSION_INVITE
SESSION_INVITED
SESSION_CONNECT
SESSION_CONNECTED
SESSION_CLOSE
@ -84,6 +88,20 @@ func ECDH(public ed25519.PublicKey, private ed25519.PrivateKey) ([]byte, error)
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) {
if key == nil {
return nil, nil, fmt.Errorf("Cannot create a SESSION_OPEN packet without a key")