archat-server/client/client.go

525 lines
12 KiB
Go
Raw Normal View History

package client
import (
2024-04-29 00:02:51 +00:00
"bufio"
"context"
2024-05-08 21:54:34 +00:00
"encoding/json"
2024-04-29 00:02:51 +00:00
"errors"
2024-05-08 21:54:34 +00:00
"net"
2024-03-24 13:09:36 +00:00
"net/url"
2024-03-24 16:10:02 +00:00
"os"
"slices"
2024-04-29 00:02:51 +00:00
"strconv"
"strings"
"sync"
"time"
2024-05-08 21:54:34 +00:00
"golang.org/x/sync/errgroup"
2024-03-24 16:10:02 +00:00
"github.com/charmbracelet/log"
2024-03-24 13:09:36 +00:00
"github.com/gorilla/websocket"
2024-05-08 21:54:34 +00:00
"krzyzanowski.dev/archat/common"
2024-04-29 00:02:51 +00:00
cm "krzyzanowski.dev/archat/common"
)
2024-05-08 21:54:34 +00:00
type InitiationInfo struct {
2024-06-01 21:41:40 +00:00
otherSideNick string
punchLaddrUsed string
2024-05-08 21:54:34 +00:00
}
type Context struct {
conn *websocket.Conn
// Assumption: size of 1 is enough, because first response read will be response for the last request
// no need to buffer
resFromServer chan cm.RFrame
reqFromServer chan cm.RFrame
rToServer chan cm.RFrame
2024-05-08 21:54:34 +00:00
initiations []InitiationInfo
initiationsLock sync.RWMutex
settings common.ClientSettings
}
2024-03-24 16:10:02 +00:00
func NewClientContext(conn *websocket.Conn, settings common.ClientSettings) *Context {
return &Context{
conn: conn,
resFromServer: make(chan cm.RFrame),
reqFromServer: make(chan cm.RFrame),
rToServer: make(chan cm.RFrame),
settings: settings,
2024-03-24 16:10:02 +00:00
}
}
2024-04-29 00:02:51 +00:00
func (cliCtx *Context) serverHandler(syncCtx context.Context) error {
defer logger.Debug("server handler last line...")
handleNext:
2024-04-29 00:02:51 +00:00
for {
select {
case <-syncCtx.Done():
return nil
case reqFrame := <-cliCtx.reqFromServer:
logger.Debug("got request from server", "id", reqFrame.ID)
var res cm.Response
var err error
2024-04-29 00:02:51 +00:00
if reqFrame.ID == cm.EchoReqID {
res, err = cliCtx.handleEcho(reqFrame)
} else if reqFrame.ID == cm.StartChatBReqID {
res, err = cliCtx.handleStartChatB(reqFrame)
} else if reqFrame.ID == cm.StartChatDReqID {
res, err = cliCtx.handleStartChatD(reqFrame)
2024-05-08 21:54:34 +00:00
} else if reqFrame.ID == cm.StartChatFinishReqID {
res, err = cliCtx.handleChatStartFinish(reqFrame)
2024-04-29 00:02:51 +00:00
} else {
logger.Warn("can't handle it!")
}
if err != nil {
logger.Errorf("could not handle request ID=%d", reqFrame.ID)
return err
}
if res == nil {
logger.Debugf("request without response ID=%d", reqFrame.ID)
continue handleNext
}
resFrame, err := cm.ResponseFrameFrom(res)
if err != nil {
logger.Errorf("could not create frame from response")
return err
}
cliCtx.rToServer <- resFrame
}
}
}
func (cliCtx *Context) handleEcho(reqFrame cm.RFrame) (res cm.Response, err error) {
echoReq, err := cm.RequestFromFrame[cm.EchoRequest](reqFrame)
if err != nil {
return nil, err
}
return cm.EchoResponse(echoReq), nil
}
func (cliCtx *Context) handleStartChatB(reqFrame cm.RFrame) (res cm.Response, err error) {
startChatBReq, err := cm.RequestFromFrame[cm.StartChatBRequest](reqFrame)
if err != nil {
return nil, err
}
logger.Infof("got start chat, %s wants to contact. use startchatc command to "+
"decide if you want to accept the chat", startChatBReq.Nickname)
cliCtx.initiationsLock.Lock()
2024-05-08 21:54:34 +00:00
cliCtx.initiations = append(cliCtx.initiations, InitiationInfo{
otherSideNick: startChatBReq.Nickname,
})
cliCtx.initiationsLock.Unlock()
return nil, nil
}
func (cliCtx *Context) handleStartChatD(reqFrame cm.RFrame) (res cm.Response, err error) {
cliCtx.initiationsLock.Lock()
defer cliCtx.initiationsLock.Unlock()
startChatDReq, err := cm.RequestFromFrame[cm.StartChatDRequest](reqFrame)
if err != nil {
return nil, err
}
logger.Infof("servers wants to be punched, got start chat d request for %s with code %s",
startChatDReq.Nickname, startChatDReq.PunchCode)
2024-05-08 21:54:34 +00:00
idx := slices.IndexFunc(cliCtx.initiations, func(i InitiationInfo) bool {
return i.otherSideNick == startChatDReq.Nickname
})
if idx == -1 {
2024-05-08 21:54:34 +00:00
logger.Error("there is no initation related to chatstartd's nickname, ignoring",
"nickname", startChatDReq.Nickname)
return nil, nil
}
conn, err := net.Dial("udp", cliCtx.settings.UdpAddr)
2024-05-08 21:54:34 +00:00
if err != nil {
logger.Error("error udp dialing for punch", err)
return nil, nil
}
2024-06-01 21:41:40 +00:00
cliCtx.initiations[idx].punchLaddrUsed = conn.LocalAddr().String()
2024-05-08 21:54:34 +00:00
enc := json.NewEncoder(conn)
err = enc.Encode(cm.PunchRequest{PunchCode: startChatDReq.PunchCode})
if err != nil {
logger.Error("error sending punch request data", "err", err)
return nil, nil
}
logger.Debug("punch request sent!")
2024-06-01 21:41:40 +00:00
conn.Close()
logger.Debug("UDP 'connection' closed", "laddr", conn.LocalAddr())
2024-05-08 21:54:34 +00:00
return nil, nil
}
func (ctx *Context) handleChatStartFinish(reqFrame common.RFrame) (res common.Response, err error) {
startChatFinishReq, err := common.RequestFromFrame[common.StartChatFinishRequest](reqFrame)
if err != nil {
return nil, err
}
logger.Info("got chat finish info!",
"nick", startChatFinishReq.OtherSideNickname,
"addr", startChatFinishReq.OtherSideAddress)
2024-06-01 21:41:40 +00:00
ctx.initiationsLock.RLock()
defer ctx.initiationsLock.RUnlock()
idx := slices.IndexFunc(ctx.initiations, func(i InitiationInfo) bool {
return i.otherSideNick == startChatFinishReq.OtherSideNickname
})
relatedInitiation := ctx.initiations[idx]
logger.Debug("punch laddr used", "laddr", relatedInitiation.punchLaddrUsed)
logger.Debug("resolving udp addrs")
laddr, err := net.ResolveUDPAddr("udp", relatedInitiation.punchLaddrUsed)
if err != nil {
logger.Error(err)
return nil, nil
}
logger.Debug("resolved laddr", "laddr", laddr.String())
raddr, err := net.ResolveUDPAddr("udp", startChatFinishReq.OtherSideAddress)
if err != nil {
logger.Error(err)
return nil, nil
}
logger.Debug("resolved raddr", "raddr", raddr.String())
logger.Debug("dialing udp")
conn, err := net.DialUDP("udp", laddr, raddr)
if err != nil {
logger.Error(err)
return nil, nil
}
log.Debugf("dialed udp L%s<->R%s", laddr.String(), raddr.String())
for i := 0; i < 3; i++ {
log.Debugf("writing Hello to other peer")
_, err = conn.Write([]byte("Hello"))
if err != nil {
logger.Error(err)
return nil, nil
}
log.Debug("waiting for message from other peer")
bb := make([]byte, 1024)
//conn.SetDeadline(time.Now().Add(time.Second * 5))
n, _, err := conn.ReadFrom(bb)
if err != nil {
logger.Error(err)
return nil, nil
}
bb = bb[:n]
logger.Debug("got info from other peer", "info", string(bb))
time.Sleep(time.Second)
}
return nil, nil
}
2024-04-29 00:02:51 +00:00
func (cliCtx *Context) serverWriter(syncCtx context.Context) error {
defer logger.Debug("server writer last line...")
for {
logger.Debug("waiting for a frame to write")
2024-04-29 00:02:51 +00:00
select {
case <-syncCtx.Done():
return nil
case frameToWrite := <-cliCtx.rToServer:
err := cliCtx.conn.WriteJSON(frameToWrite)
if err != nil {
return err
}
logger.Debug("frame written", "id", frameToWrite.ID)
}
}
}
2024-04-29 00:02:51 +00:00
func (cliCtx *Context) serverReader(syncCtx context.Context) error {
defer logger.Debug("server reader last line...")
for {
logger.Debug("waiting for a frame to read")
var rFrame cm.RFrame
err := cliCtx.conn.ReadJSON(&rFrame)
if err != nil {
return err
}
logger.Debug("frame read", "id", rFrame.ID)
if rFrame.IsResponse() {
cliCtx.resFromServer <- rFrame
} else {
cliCtx.reqFromServer <- rFrame
}
logger.Debug("frame pushed", "id", rFrame.ID)
2024-03-24 13:09:36 +00:00
}
}
func (cliCtx *Context) sendRequest(req cm.Request) error {
rf, err := cm.RequestFrameFrom(req)
2024-03-24 13:09:36 +00:00
if err != nil {
return err
2024-03-24 13:09:36 +00:00
}
cliCtx.rToServer <- rf
return nil
}
func (cliCtx *Context) getResponseFrame() cm.RFrame {
return <-cliCtx.resFromServer
}
var logger = log.NewWithOptions(os.Stdout, log.Options{
ReportTimestamp: true,
TimeFormat: time.TimeOnly,
Prefix: "👤 Client",
})
func init() {
if cm.IsProd {
logger.SetLevel(log.InfoLevel)
} else {
logger.SetLevel(log.DebugLevel)
2024-03-24 13:09:36 +00:00
}
}
2024-04-29 00:02:51 +00:00
func sendAuth(ctx *Context, nick, pass string) {
logger.Info("trying to authenticate...", "nick", nick)
2024-04-29 00:02:51 +00:00
err := ctx.sendRequest(cm.AuthRequest{Nickname: nick, Password: pass})
if err != nil {
logger.Error(err)
return
}
logger.Debug("request sent, waiting for response...")
arf := ctx.getResponseFrame()
ar, err := cm.ResponseFromFrame[cm.AuthResponse](arf)
if err != nil {
logger.Error(err)
return
}
logger.Infof("Authenticated?: %t", ar.IsSuccess)
}
2024-04-29 00:02:51 +00:00
func sendEcho(ctx *Context, echoByte byte) {
logger.Info("testing echo...", "echoByte", echoByte)
2024-04-29 00:02:51 +00:00
err := ctx.sendRequest(cm.EchoRequest{EchoByte: echoByte})
if err != nil {
logger.Error(err)
return
}
logger.Debug("request sent, waiting for response...")
ereqf := ctx.getResponseFrame()
ereq, err := cm.ResponseFromFrame[cm.EchoResponse](ereqf)
2024-03-24 13:09:36 +00:00
if err != nil {
logger.Error(err)
return
2024-03-24 13:09:36 +00:00
}
logger.Info("got response", "echoByte", ereq.EchoByte)
}
2024-04-29 00:02:51 +00:00
func sendListPeers(ctx *Context) {
logger.Info("trying to get list of peers...")
err := ctx.sendRequest(cm.ListPeersRequest{})
if err != nil {
logger.Error(err)
return
}
logger.Debug("request sent, waiting for response...")
lpreqf := ctx.getResponseFrame()
lpreq, err := cm.ResponseFromFrame[cm.ListPeersResponse](lpreqf)
2024-03-24 13:09:36 +00:00
if err != nil {
logger.Error(err)
return
2024-03-24 13:09:36 +00:00
}
logger.Info("Got that list", "peersList", lpreq.PeersInfo)
}
2024-04-29 00:02:51 +00:00
func sendStartChatA(ctx *Context, nick string) {
logger.Info("doing chat start A...")
2024-04-29 00:02:51 +00:00
err := ctx.sendRequest(cm.StartChatARequest{Nickname: nick})
if err != nil {
logger.Error(err)
return
}
logger.Debug("request sent, no wait for response")
}
func sendStartChatC(ctx *Context, nick string) {
ctx.initiationsLock.Lock()
defer ctx.initiationsLock.Unlock()
2024-05-08 21:54:34 +00:00
idx := slices.IndexFunc(ctx.initiations, func(i InitiationInfo) bool {
return i.otherSideNick == nick
})
if idx == -1 {
logger.Warn("user of that nick did not initiate connection, ignoring")
return
}
err := ctx.sendRequest(cm.StartChatCRequest{Nickname: nick})
if err != nil {
logger.Error(err)
return
}
logger.Debug("request sent, no wait for response")
2024-04-29 00:02:51 +00:00
}
func RunClient(settings common.ClientSettings) {
u := url.URL{Scheme: "ws", Host: settings.WsapiAddr, Path: "/wsapi"}
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
logger.Error("could not connect to websocket")
return
}
cliCtx := NewClientContext(c, settings)
2024-04-29 00:02:51 +00:00
errGroup, syncCtx := errgroup.WithContext(context.Background())
errGroup.Go(func() error {
return cliCtx.serverHandler(syncCtx)
})
errGroup.Go(func() error {
return cliCtx.serverReader(syncCtx)
})
errGroup.Go(func() error {
return cliCtx.serverWriter(syncCtx)
})
errGroup.Go(func() error {
<-syncCtx.Done()
logger.Info("closing client...")
time.Sleep(time.Second * 3)
close(cliCtx.rToServer)
close(cliCtx.resFromServer)
close(cliCtx.reqFromServer)
_ = c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
return c.Close()
})
closer := make(chan int)
errGroup.Go(func() error {
select {
case <-closer:
return errors.New("close")
case <-syncCtx.Done():
return nil
}
2024-04-29 00:02:51 +00:00
})
go func() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
cmd := strings.TrimRight(scanner.Text(), " \n\t")
cmdElements := strings.Split(cmd, " ")
cmdName := cmdElements[0]
cmdArgs := cmdElements[1:]
if cmdName == "exit" {
logger.Info("closing...")
closer <- 1
} else if cmdName == "echo" {
if len(cmdArgs) != 1 {
logger.Errorf("echo command requires 1 argument, but %d was provided", len(cmdArgs))
continue
}
num, err := strconv.Atoi(cmdArgs[0])
if err != nil {
logger.Errorf("%s is not a number", cmdArgs[0])
continue
}
sendEcho(cliCtx, byte(num))
} else if cmdName == "list" {
sendListPeers(cliCtx)
} else if cmdName == "auth" {
if len(cmdArgs) != 2 {
logger.Errorf("auth command requires 2 argument, but %d was provided", len(cmdArgs))
continue
}
nick := cmdArgs[0]
pass := cmdArgs[1]
sendAuth(cliCtx, nick, pass)
} else if cmdName == "startchata" {
if len(cmdArgs) != 1 {
logger.Errorf("startchata command requires 1 argument, but %d was provided", len(cmdArgs))
continue
}
sendStartChatA(cliCtx, cmdArgs[0])
2024-05-08 21:54:34 +00:00
cliCtx.initiationsLock.Lock()
cliCtx.initiations = append(cliCtx.initiations, InitiationInfo{
otherSideNick: cmdArgs[0],
})
cliCtx.initiationsLock.Unlock()
} else if cmdName == "initations" {
logger.Info("displaying all initations...")
cliCtx.initiationsLock.RLock()
for _, i := range cliCtx.initiations {
2024-06-01 21:41:40 +00:00
logger.Debugf("with %+v", i)
}
cliCtx.initiationsLock.RUnlock()
} else if cmdName == "startchatc" {
if len(cmdArgs) != 1 {
logger.Errorf("startchatc command requires 1 argument, but %d was provided", len(cmdArgs))
continue
}
sendStartChatC(cliCtx, cmdArgs[0])
2024-04-29 00:02:51 +00:00
}
}
}()
err = errGroup.Wait()
if err != nil {
logger.Error(err)
}
}