Finished implementation for B, C, D stages of initiation

This commit is contained in:
Maciej Krzyżanowski 2024-04-29 13:46:26 +02:00
parent d5b891e4f5
commit 62e3ee780b
4 changed files with 229 additions and 68 deletions

View File

@ -1,5 +1,6 @@
# 🔀 Archat
*It is not working application yet, work in progress!*
Simple P2P server (and client - for testing purposes only, client for end user will be written in NodeJS using Electron, soon).
## Starting

View File

@ -7,8 +7,10 @@ import (
"golang.org/x/sync/errgroup"
"net/url"
"os"
"slices"
"strconv"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
@ -20,9 +22,11 @@ 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
resFromServer chan cm.RFrame
reqFromServer chan cm.RFrame
rToServer chan cm.RFrame
initiations []*cm.Initiation
initiationsLock sync.RWMutex
}
func NewClientContext(conn *websocket.Conn) *Context {
@ -37,6 +41,7 @@ func NewClientContext(conn *websocket.Conn) *Context {
func (cliCtx *Context) serverHandler(syncCtx context.Context) error {
defer logger.Debug("server handler last line...")
handleNext:
for {
select {
case <-syncCtx.Done():
@ -44,25 +49,83 @@ func (cliCtx *Context) serverHandler(syncCtx context.Context) error {
case reqFrame := <-cliCtx.reqFromServer:
logger.Debug("got request from server", "id", reqFrame.ID)
var res cm.Response
var err error
if reqFrame.ID == cm.EchoReqID {
echoReq, err := cm.RequestFromFrame[cm.EchoRequest](reqFrame)
if err != nil {
return err
}
resFrame, err := cm.ResponseFrameFrom(cm.EchoResponse(echoReq))
if err != nil {
return err
}
cliCtx.rToServer <- resFrame
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)
} 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()
cliCtx.initiations = append(cliCtx.initiations, &cm.Initiation{
AbANick: startChatBReq.Nickname,
AbBNick: "",
Stage: cm.InitiationStageB,
})
cliCtx.initiationsLock.Unlock()
return nil, nil
}
func (cliCtx *Context) handleStartChatD(reqFrame cm.RFrame) (res cm.Response, err error) {
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)
logger.Warn("handleStartChatD not implemented yet")
return nil, nil
}
func (cliCtx *Context) serverWriter(syncCtx context.Context) error {
defer logger.Debug("server writer last line...")
@ -133,7 +196,7 @@ func init() {
}
func sendAuth(ctx *Context, nick, pass string) {
logger.Info("Trying to authenticate as krzmaciek...")
logger.Info("trying to authenticate as krzmaciek...")
err := ctx.sendRequest(cm.AuthRequest{Nickname: nick, Password: pass})
if err != nil {
@ -141,7 +204,7 @@ func sendAuth(ctx *Context, nick, pass string) {
return
}
logger.Debug("Request sent, waiting for response...")
logger.Debug("request sent, waiting for response...")
arf := ctx.getResponseFrame()
ar, err := cm.ResponseFromFrame[cm.AuthResponse](arf)
@ -154,7 +217,7 @@ func sendAuth(ctx *Context, nick, pass string) {
}
func sendEcho(ctx *Context, echoByte byte) {
logger.Info("Testing echo...", "echoByte", echoByte)
logger.Info("testing echo...", "echoByte", echoByte)
err := ctx.sendRequest(cm.EchoRequest{EchoByte: echoByte})
if err != nil {
@ -162,7 +225,7 @@ func sendEcho(ctx *Context, echoByte byte) {
return
}
logger.Debug("Request sent, waiting for response...")
logger.Debug("request sent, waiting for response...")
ereqf := ctx.getResponseFrame()
ereq, err := cm.ResponseFromFrame[cm.EchoResponse](ereqf)
@ -171,11 +234,11 @@ func sendEcho(ctx *Context, echoByte byte) {
return
}
logger.Info("Got response", "echoByte", ereq.EchoByte)
logger.Info("got response", "echoByte", ereq.EchoByte)
}
func sendListPeers(ctx *Context) {
logger.Info("Trying to get list of peers...")
logger.Info("trying to get list of peers...")
err := ctx.sendRequest(cm.ListPeersRequest{})
if err != nil {
@ -183,7 +246,7 @@ func sendListPeers(ctx *Context) {
return
}
logger.Debug("Request sent, waiting for response...")
logger.Debug("request sent, waiting for response...")
lpreqf := ctx.getResponseFrame()
lpreq, err := cm.ResponseFromFrame[cm.ListPeersResponse](lpreqf)
@ -196,7 +259,7 @@ func sendListPeers(ctx *Context) {
}
func sendStartChatA(ctx *Context, nick string) {
logger.Info("Doing chat start A...")
logger.Info("doing chat start A...")
err := ctx.sendRequest(cm.StartChatARequest{Nickname: nick})
if err != nil {
@ -204,7 +267,27 @@ func sendStartChatA(ctx *Context, nick string) {
return
}
logger.Debug("Request sent, no wait for response")
logger.Debug("request sent, no wait for response")
}
func sendStartChatC(ctx *Context, nick string) {
idx := slices.IndexFunc(ctx.initiations, func(i *cm.Initiation) bool {
return i.AbANick == 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")
}
func RunClient() {
@ -297,6 +380,21 @@ func RunClient() {
}
sendStartChatA(cliCtx, cmdArgs[0])
} else if cmdName == "initations" {
logger.Info("displaying all initations...")
cliCtx.initiationsLock.RLock()
for _, i := range cliCtx.initiations {
logger.Debugf("from %s, stage: %d", i.AbANick, i.Stage)
}
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])
}
}
}()

View File

@ -173,10 +173,25 @@ func (StartChatCRequest) ID() int {
}
type StartChatDRequest struct {
Nickname string `json:"nickname"`
Accept bool `json:"accept"`
Nickname string `json:"nickname"`
PunchCode string `json:"punchCode"`
}
func (StartChatDRequest) ID() int {
return StartChatDReqID
}
type Initiation struct {
AbANick string
AbBNick string
Stage int
AbAPunchCode string
AbBPunchCode string
}
const (
InitiationStageA = 1
InitiationStageB = 2
InitiationStageC = 3
InitiationStageD = 4
)

View File

@ -2,6 +2,7 @@ package server
import (
"context"
"crypto/rand"
"encoding/json"
"errors"
"net/http"
@ -30,26 +31,21 @@ func NewPeer(conn *websocket.Conn) *Peer {
return &Peer{-1, conn, false, nil}
}
func (p *Peer) NicknameOrEmpty() string {
if p.hasAccount {
return p.account.nickname
} else {
return ""
}
}
type Account struct {
nickname string
passHash []byte
}
const (
InitiationStageA = 1
InitiationStageB = 2
InitiationStageC = 3
InitiationStageD = 4
)
type Initiation struct {
abANick string
abBNick string
stage int
}
func NewInitiation(abA string, abB string) *Initiation {
return &Initiation{abA, abB, InitiationStageA}
func NewInitiation(abA string, abB string) *common.Initiation {
return &common.Initiation{AbANick: abA, AbBNick: abB, Stage: common.InitiationStageA}
}
type Context struct {
@ -59,7 +55,7 @@ type Context struct {
peersListLock sync.RWMutex
accounts map[string]*Account
accountsLock sync.RWMutex
initiations []*Initiation
initiations []*common.Initiation
initiationsLock sync.RWMutex
handlerContexts []*HandlerContext
handlerContextsLock sync.RWMutex
@ -69,7 +65,7 @@ func NewContext() *Context {
return &Context{
peersList: make([]*Peer, 0),
accounts: make(map[string]*Account),
initiations: make([]*Initiation, 0),
initiations: make([]*common.Initiation, 0),
}
}
@ -134,6 +130,11 @@ handleNext:
res, err = hdlCtx.handleEcho(&reqFrame)
} else if reqFrame.ID == common.StartChatAReqID {
res, err = hdlCtx.handleChatStartA(&reqFrame)
} else if reqFrame.ID == common.StartChatCReqID {
res, err = hdlCtx.handleChatStartC(&reqFrame)
} else {
logger.Warnf("can't handle request of ID=%d", reqFrame.ID)
continue
}
if err != nil {
@ -271,10 +272,10 @@ func (ctx *Context) removePeer(peer *Peer) {
return p.id == peer.id
})
ctx.initiations = slices.DeleteFunc[[]*Initiation, *Initiation](
ctx.initiations = slices.DeleteFunc[[]*common.Initiation, *common.Initiation](
ctx.initiations,
func(i *Initiation) bool {
return peer.hasAccount && (peer.account.nickname == i.abANick || peer.account.nickname == i.abBNick)
func(i *common.Initiation) bool {
return peer.hasAccount && (peer.account.nickname == i.AbANick || peer.account.nickname == i.AbBNick)
})
// TODO: Inform the other side about peer leaving
@ -317,21 +318,13 @@ func (hdlCtx *HandlerContext) handleListPeers(reqFrame *common.RFrame) (res comm
listPeersRes := common.ListPeersResponse{PeersInfo: make([]common.PeerInfo, 0)}
for _, peer := range peersFreeze {
var nickname string
if peer.hasAccount {
nickname = peer.account.nickname
} else {
nickname = ""
}
listPeersRes.PeersInfo = append(
listPeersRes.PeersInfo,
common.PeerInfo{
ID: peer.id,
Addr: peer.conn.RemoteAddr().String(),
HasNickname: peer.hasAccount,
Nickname: nickname,
Nickname: peer.NicknameOrEmpty(),
},
)
}
@ -418,47 +411,101 @@ func (hdlCtx *HandlerContext) handleChatStartA(reqFrame *common.RFrame) (res com
receiverPeerCtx.rToClient <- chatStartBReqF
hdlCtx.initiationsLock.Lock()
//
idx := slices.IndexFunc(hdlCtx.initiations, func(i *common.Initiation) bool {
return i.AbANick == hdlCtx.peer.account.nickname && i.AbBNick == startChatAReq.Nickname
})
hdlCtx.initiations[idx].Stage = common.InitiationStageB
hdlCtx.initiationsLock.Unlock()
return nil, nil
}
func (hdlCtx *HandlerContext) handleChatStartC(reqFrame *common.RFrame) (res common.Response, err error) {
startChatAReq, err := common.RequestFromFrame[common.StartChatARequest](*reqFrame)
hdlCtx.initiationsLock.Lock()
startChatCReq, err := common.RequestFromFrame[common.StartChatCRequest](*reqFrame)
if err != nil {
return nil, err
}
receiverPeerCtx, err := hdlCtx.getCtxByNick(startChatAReq.Nickname)
logger.Debugf("got chat start c for %s", startChatCReq.Nickname)
receiverPeerCtx, err := hdlCtx.getCtxByNick(startChatCReq.Nickname)
if err != nil {
logger.Debug("receiver peer not found")
return nil, nil
}
// initation started
hdlCtx.initiationsLock.Lock()
hdlCtx.initiations = append(hdlCtx.initiations, NewInitiation(hdlCtx.peer.account.nickname, startChatAReq.Nickname))
hdlCtx.initiationsLock.Unlock()
idx := slices.IndexFunc(hdlCtx.initiations, func(i *common.Initiation) bool {
return i.AbBNick == hdlCtx.peer.account.nickname && i.AbANick == startChatCReq.Nickname
})
chatStartB := common.StartChatBRequest{
Nickname: hdlCtx.peer.account.nickname,
if idx == -1 {
logger.Debug("initation not found, won't handle")
return nil, nil
}
chatStartBReqF, err := common.RequestFrameFrom(chatStartB)
if hdlCtx.initiations[idx].Stage != common.InitiationStageB {
logger.Debug("initation found, but is not in stage B, won't handle")
return nil, nil
}
hdlCtx.initiations[idx].Stage = common.InitiationStageC
aCode, err := generatePunchCode()
if err != nil {
logger.Error("failed generating punch code for a")
return nil, nil
}
bCode, err := generatePunchCode()
if err != nil {
logger.Error("failed generating punch code for b")
return nil, nil
}
hdlCtx.initiations[idx].AbAPunchCode = aCode
hdlCtx.initiations[idx].AbBPunchCode = bCode
dReqToA := common.StartChatDRequest{Nickname: hdlCtx.peer.account.nickname, PunchCode: aCode}
dReqToB := common.StartChatDRequest{Nickname: startChatCReq.Nickname, PunchCode: bCode}
err = hdlCtx.sendRequest(dReqToB)
if err != nil {
logger.Debug("chat start B req frame creation failed")
return nil, err
logger.Errorf("could not send chatstartd to B=%s", hdlCtx.peer.account.nickname)
return nil, nil
}
receiverPeerCtx.rToClient <- chatStartBReqF
err = receiverPeerCtx.sendRequest(dReqToA)
if err != nil {
logger.Errorf("could not send chatstartd to A=%s", startChatCReq.Nickname)
return nil, nil
}
hdlCtx.initiations[idx].Stage = common.InitiationStageD
hdlCtx.initiationsLock.Unlock()
return nil, nil
}
func generatePunchCode() (string, error) {
codeBytes := make([]byte, 8)
_, err := rand.Read(codeBytes)
if err != nil {
return "", err
}
for idx, cb := range codeBytes {
codeBytes[idx] = 65 + (cb % 26)
}
return string(codeBytes), nil
}
func (ctx *Context) printDebugInfo() {
ctx.peersListLock.RLock()
logger.Debug("================================ server state")
@ -477,7 +524,7 @@ func (ctx *Context) printDebugInfo() {
logger.Debug("displaying all initiations:")
for _, i := range ctx.initiations {
logger.Debugf("from %s to %s, stage: %d", i.abBNick, i.abBNick, i.stage)
logger.Debugf("from %s to %s, stage: %d", i.AbANick, i.AbBNick, i.Stage)
}
ctx.peersListLock.RUnlock()