Working websocket version

This commit is contained in:
Maciej Krzyżanowski 2024-03-24 14:09:36 +01:00
parent 509d20948c
commit 361b84aac4
5 changed files with 340 additions and 243 deletions

View File

@ -1,91 +1,100 @@
package client package client
import ( import (
"bufio"
"encoding/json"
"log" "log"
"net" "net/url"
"time" "time"
"github.com/gorilla/websocket"
cm "krzyzanowski.dev/p2pchat/common" cm "krzyzanowski.dev/p2pchat/common"
) )
type ClientContext struct {
reader *bufio.Reader
writer *bufio.Writer
}
func perform[T cm.Request, U cm.Response](cliCtx *ClientContext, request T) (U, error) {
reqJsonBytes, err := json.Marshal(request)
if err != nil {
return *new(U), err
}
reqBytes := make([]byte, 0)
reqBytes = append(reqBytes, request.GetRID())
reqBytes = append(reqBytes, reqJsonBytes...)
reqBytes = append(reqBytes, '\n')
_, err = cliCtx.writer.Write(reqBytes)
if err != nil {
return *new(U), err
}
err = cliCtx.writer.Flush()
if err != nil {
return *new(U), err
}
resBytes, err := cliCtx.reader.ReadBytes('\n')
if err != nil {
return *new(U), err
}
var res U
json.Unmarshal(resBytes, &res)
return res, nil
}
func RunClient() { func RunClient() {
conn, err := net.Dial("tcp", ":8080") u := url.URL{Scheme: "ws", Host: ":8080", Path: "/wsapi"}
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil { if err != nil {
log.Println("[Client] err connecting") log.Println("[Client] could not connect to websocket")
return return
} }
defer func() { defer c.Close()
_ = conn.Close()
}()
br := bufio.NewReader(conn) log.Println("[Client] authenticating...")
bw := bufio.NewWriter(conn) rf, _ := cm.RequestFrameFrom(cm.AuthRequest{Nickname: "krzmaciek", Password: "9maciek1"})
cliCtx := &ClientContext{br, bw} err = c.WriteJSON(rf)
if err != nil {
log.Fatalln(err)
}
log.Println("[Client] connected to server") var authResFrame cm.ResponseFrame
err = c.ReadJSON(&authResFrame)
if err != nil {
log.Fatalln(err)
}
authRes, err := cm.ResponseFromFrame[cm.AuthResponse](authResFrame)
if err != nil {
log.Fatalln(err)
}
log.Printf("[Client] authentication result: %t\n", authRes.IsSuccess)
time.Sleep(time.Second * 1) time.Sleep(time.Second * 1)
echoRes, err := perform[cm.EchoRequest, cm.EchoResponse](cliCtx, cm.EchoRequest{EchoByte: 5}) log.Println("[Client] sending echo...")
echoByte := 123
rf, err = cm.RequestFrameFrom(cm.EchoRequest{EchoByte: byte(echoByte)})
if err != nil { if err != nil {
log.Fatalln("[Client] error performing echo") log.Fatalln(err)
} }
log.Printf("[Client] echo sent (5), got %d\n", echoRes) err = c.WriteJSON(rf)
if err != nil {
authRes, _ := perform[cm.AuthRequest, cm.AuthResponse](cliCtx, cm.AuthRequest{Nickname: "maciek", Password: "9maciek1"}) log.Fatalln(err)
log.Printf("[Client] authenticated: %t\n", authRes.IsSuccess)
listRes, _ := perform[cm.ListPeersRequest, cm.ListPeersResponse](cliCtx, cm.ListPeersRequest{})
log.Println("[Client] printing all peers:")
for _, peer := range listRes.PeersInfo {
log.Printf("[Client] Peer#%d from %s, hasNick: %t, nick: %s", peer.ID, peer.Addr, peer.HasNickaname, peer.Nickname)
} }
time.Sleep(time.Second * 10) var echoResFrame cm.ResponseFrame
err = c.ReadJSON(&echoResFrame)
if err != nil {
log.Fatalln(err)
}
echoRes, err := cm.ResponseFromFrame[cm.EchoResponse](echoResFrame)
if err != nil {
log.Fatalln(err)
}
log.Printf("[Client] sent echo of %d, got %d in return\n", echoByte, echoRes.EchoByte)
time.Sleep(time.Second)
log.Println("[Client] i want list of peers...")
rf, err = cm.RequestFrameFrom(cm.ListPeersRequest{})
if err != nil {
log.Fatalln(err)
}
err = c.WriteJSON(rf)
if err != nil {
log.Fatalln(err)
}
var listPeersResFrame cm.ResponseFrame
err = c.ReadJSON(&listPeersResFrame)
if err != nil {
log.Fatalln(err)
}
listPeersRes, err := cm.ResponseFromFrame[cm.ListPeersResponse](listPeersResFrame)
if err != nil {
log.Fatalln(err)
}
log.Println("[Client] printing list of peers:")
for _, p := range listPeersRes.PeersInfo {
log.Printf("[Client] %+v\n", p)
}
time.Sleep(time.Second * 5)
c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
} }

View File

@ -1,10 +1,18 @@
package common package common
type Request interface { import (
GetRID() byte "encoding/json"
} )
type Response Request // Constants:
const (
EchoRID = 1
ListPeersRID = 2
AuthRID = 3
)
// Requests & responses subtypes
type PeerInfo struct { type PeerInfo struct {
ID int `json:"id"` ID int `json:"id"`
@ -13,56 +21,121 @@ type PeerInfo struct {
Nickname string `json:"nickname"` Nickname string `json:"nickname"`
} }
// Requests & responses:
type RequestFrame struct {
ID int `json:"id"`
Rest json.RawMessage `json:"request"`
}
func RequestFrameFrom(req Request) (RequestFrame, error) {
jsonBytes, err := json.Marshal(req)
if err != nil {
return *new(RequestFrame), err
}
return RequestFrame{req.GetRID(), jsonBytes}, nil
}
func RequestFromFrame[T Request](reqFrame RequestFrame) (T, error) {
var req T
err := json.Unmarshal(reqFrame.Rest, &req)
if err != nil {
return *new(T), err
}
return req, nil
}
type ResponseFrame struct {
ID int `json:"id"`
Rest json.RawMessage `json:"response"`
}
func ResponseFrameFrom(res Response) (ResponseFrame, error) {
jsonBytes, err := json.Marshal(res)
if err != nil {
return *new(ResponseFrame), err
}
return ResponseFrame{res.GetRID(), jsonBytes}, nil
}
func ResponseFromFrame[T Response](resFrame ResponseFrame) (T, error) {
var res T
err := json.Unmarshal(resFrame.Rest, &res)
if err != nil {
return *new(T), err
}
return res, nil
}
type Request interface {
GetRID() int
}
type Response Request
type EchoRequest struct { type EchoRequest struct {
EchoByte byte `json:"echoByte"` EchoByte byte `json:"echoByte"`
} }
func (EchoRequest) GetRID() int {
return EchoRID
}
type EchoResponse struct { type EchoResponse struct {
EchoByte byte `json:"echoByte"` EchoByte byte `json:"echoByte"`
} }
func (EchoResponse) GetRID() int {
return EchoRID
}
type ListPeersRequest struct { type ListPeersRequest struct {
} }
func (ListPeersRequest) GetRID() int {
return ListPeersRID
}
type ListPeersResponse struct { type ListPeersResponse struct {
PeersInfo []PeerInfo `json:"peers"` PeersInfo []PeerInfo `json:"peers"`
} }
func (ListPeersResponse) GetRID() int {
return ListPeersRID
}
type AuthRequest struct { type AuthRequest struct {
Nickname string Nickname string `json:"nickname"`
Password string Password string `json:"password"`
}
func (req AuthRequest) MarshalJSON() ([]byte, error) {
type Alias AuthRequest
return json.Marshal(&struct {
ID int `json:"id"`
Alias
}{
AuthRID,
Alias(req),
})
}
func (AuthRequest) GetRID() int {
return AuthRID
} }
type AuthResponse struct { type AuthResponse struct {
IsSuccess bool IsSuccess bool
} }
const ( func (AuthResponse) GetRID() int {
EchoRID = 1
ListPeersRID = 2
AuthRID = 3
)
func (EchoRequest) GetRID() byte {
return EchoRID
}
func (EchoResponse) GetRID() byte {
return EchoRID
}
func (AuthRequest) GetRID() byte {
return AuthRID return AuthRID
} }
func (AuthResponse) GetRID() byte {
return AuthRID
}
func (ListPeersRequest) GetRID() byte {
return ListPeersRID
}
func (ListPeersResponse) GetRID() byte {
return ListPeersRID
}

7
go.mod
View File

@ -2,4 +2,9 @@ module krzyzanowski.dev/p2pchat
go 1.21.7 go 1.21.7
require golang.org/x/crypto v0.21.0 require (
github.com/gorilla/websocket v1.5.1
golang.org/x/crypto v0.21.0
)
require golang.org/x/net v0.21.0 // indirect

4
go.sum
View File

@ -1,2 +1,6 @@
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=

View File

@ -1,14 +1,14 @@
package server package server
import ( import (
"bufio"
"encoding/json" "encoding/json"
"io"
"log" "log"
"net" "net/http"
"strings"
"sync" "sync"
"time" "time"
"github.com/gorilla/websocket"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"krzyzanowski.dev/p2pchat/common" "krzyzanowski.dev/p2pchat/common"
) )
@ -19,6 +19,8 @@ type Account struct {
} }
type ServerContext struct { type ServerContext struct {
idCounter int
idCounterLock sync.RWMutex
peersList []*Peer peersList []*Peer
peersListLock sync.RWMutex peersListLock sync.RWMutex
accounts map[string]*Account accounts map[string]*Account
@ -27,16 +29,20 @@ type ServerContext struct {
type HandlerContext struct { type HandlerContext struct {
peer *Peer peer *Peer
srvCtx *ServerContext *ServerContext
} }
type Peer struct { type Peer struct {
id int id int
conn net.Conn conn *websocket.Conn
hasAccount bool hasAccount bool
account *Account account *Account
} }
func NewPeer(conn *websocket.Conn) *Peer {
return &Peer{-1, conn, false, nil}
}
func peerSliceIndexOf(s []*Peer, id int) int { func peerSliceIndexOf(s []*Peer, id int) int {
i := 0 i := 0
var p *Peer var p *Peer
@ -53,98 +59,40 @@ func peerSliceRemove(s *[]*Peer, i int) {
*s = (*s)[:len(*s)-1] *s = (*s)[:len(*s)-1]
} }
func (srvCtx *ServerContext) removePeer(peer *Peer) {
srvCtx.peersListLock.Lock()
peerSliceRemove(&srvCtx.peersList, peerSliceIndexOf(srvCtx.peersList, peer.id))
srvCtx.peersListLock.Unlock()
}
func handleDisconnection(handlerCtx *HandlerContext) { func handleDisconnection(handlerCtx *HandlerContext) {
handlerCtx.srvCtx.peersListLock.Lock() handlerCtx.removePeer(handlerCtx.peer)
p := handlerCtx.srvCtx.peersList[peerSliceIndexOf(handlerCtx.srvCtx.peersList, handlerCtx.peer.id)] log.Printf("[Server] %s disconnected\n", handlerCtx.peer.conn.RemoteAddr())
log.Printf("[Server] %s disconnected\n", p.conn.RemoteAddr())
peerSliceRemove(&handlerCtx.srvCtx.peersList, peerSliceIndexOf(handlerCtx.srvCtx.peersList, handlerCtx.peer.id))
handlerCtx.srvCtx.peersListLock.Unlock()
} }
func handlePeer(handlerCtx *HandlerContext) { func (hdlCtx *HandlerContext) handleEcho(reqFrame *common.RequestFrame) (res common.Response, err error) {
br := bufio.NewReader(handlerCtx.peer.conn) echoReq, err := common.RequestFromFrame[common.EchoRequest](*reqFrame)
bw := bufio.NewWriter(handlerCtx.peer.conn)
for {
reqBytes, err := br.ReadBytes('\n')
if err == io.EOF {
handleDisconnection(handlerCtx)
break
} else if err != nil {
log.Println(err)
break
}
if len(reqBytes) <= 1 {
log.Println("got request without id")
break
}
reqBytes = reqBytes[:len(reqBytes)-1]
operationCode := reqBytes[0]
reqJsonBytes := reqBytes[1:]
var resBytes []byte
if operationCode == common.EchoRID {
resBytes, err = handleEcho(handlerCtx, reqJsonBytes)
} else if operationCode == common.ListPeersRID {
resBytes, err = handleListPeers(handlerCtx, reqJsonBytes)
} else if operationCode == common.AuthRID {
resBytes, err = handleAuth(handlerCtx, reqJsonBytes)
}
if err != nil {
log.Println(err)
continue
}
resBytes = append(resBytes, '\n')
_, err = bw.Write(resBytes)
if err != nil {
log.Println(err)
continue
}
err = bw.Flush()
if err != nil {
log.Println(err)
}
}
}
func handleEcho(_ *HandlerContext, reqBytes []byte) (resBytes []byte, err error) {
var echoReq common.EchoRequest
err = json.Unmarshal(reqBytes, &echoReq)
if err != nil { if err != nil {
log.Println("[Server] could not read request from frame")
return nil, err return nil, err
} }
echoRes := common.EchoResponse(echoReq) echoRes := common.EchoResponse(echoReq)
resBytes, err = json.Marshal(echoRes) return echoRes, nil
if err != nil {
return nil, err
}
return resBytes, nil
} }
func handleListPeers(handlerCtx *HandlerContext, reqBytes []byte) (resBytes []byte, err error) { func (hdlCtx *HandlerContext) handleListPeers(reqFrame *common.RequestFrame) (res common.Response, err error) {
var listPeersReq common.ListPeersRequest // Currently list peers request is empty, so we can ignore it - we won't use it
err = json.Unmarshal(reqBytes, &listPeersReq) _, err = common.RequestFromFrame[common.ListPeersRequest](*reqFrame)
if err != nil { if err != nil {
log.Println("[Server] could not read request from frame")
return nil, err return nil, err
} }
handlerCtx.srvCtx.peersListLock.RLock() hdlCtx.peersListLock.RLock()
peersFreeze := make([]*Peer, len(handlerCtx.srvCtx.peersList)) peersFreeze := make([]*Peer, len(hdlCtx.peersList))
copy(peersFreeze, handlerCtx.srvCtx.peersList) copy(peersFreeze, hdlCtx.peersList)
handlerCtx.srvCtx.peersListLock.RUnlock() hdlCtx.peersListLock.RUnlock()
listPeersRes := common.ListPeersResponse{PeersInfo: make([]common.PeerInfo, 0)} listPeersRes := common.ListPeersResponse{PeersInfo: make([]common.PeerInfo, 0)}
for _, peer := range peersFreeze { for _, peer := range peersFreeze {
@ -159,68 +107,55 @@ func handleListPeers(handlerCtx *HandlerContext, reqBytes []byte) (resBytes []by
) )
} }
resBytes, err = json.Marshal(listPeersRes) return listPeersRes, nil
if err != nil {
return nil, err
}
return resBytes, nil
} }
func handleAuth(handlerCtx *HandlerContext, reqBytes []byte) (resBytes []byte, err error) { func (hdlCtx *HandlerContext) handleAuth(reqFrame *common.RequestFrame) (res common.Response, err error) {
var authReq common.AuthRequest authReq, err := common.RequestFromFrame[common.AuthRequest](*reqFrame)
err = json.Unmarshal(reqBytes, &authReq)
if err != nil { if err != nil {
log.Println("[Server] could not read request from frame")
return nil, err return nil, err
} }
// Check if account already exists // Check if account already exists
handlerCtx.srvCtx.accountsLock.RLock() hdlCtx.accountsLock.RLock()
account, ok := handlerCtx.srvCtx.accounts[authReq.Nickname] account, ok := hdlCtx.accounts[authReq.Nickname]
handlerCtx.srvCtx.accountsLock.RUnlock() hdlCtx.accountsLock.RUnlock()
var authRes common.AuthResponse var authRes *common.AuthResponse
if ok { if ok {
// Check if password matches // Check if password matches
if bcrypt.CompareHashAndPassword(account.passHash, []byte(authReq.Password)) == nil { if bcrypt.CompareHashAndPassword(account.passHash, []byte(authReq.Password)) == nil {
authRes = common.AuthResponse{IsSuccess: true} authRes = &common.AuthResponse{IsSuccess: true}
handlerCtx.srvCtx.peersListLock.Lock() hdlCtx.peersListLock.Lock()
handlerCtx.peer.hasAccount = true hdlCtx.peer.hasAccount = true
handlerCtx.peer.account = account hdlCtx.peer.account = account
handlerCtx.srvCtx.peersListLock.Unlock() hdlCtx.peersListLock.Unlock()
} else { } else {
authRes = common.AuthResponse{IsSuccess: false} authRes = &common.AuthResponse{IsSuccess: false}
} }
} else { } else {
authRes = common.AuthResponse{IsSuccess: true} authRes = &common.AuthResponse{IsSuccess: true}
passHash, err := bcrypt.GenerateFromPassword([]byte(authReq.Password), bcrypt.DefaultCost) passHash, err := bcrypt.GenerateFromPassword([]byte(authReq.Password), bcrypt.DefaultCost)
if err != nil { if err != nil {
authRes = common.AuthResponse{IsSuccess: false} authRes = &common.AuthResponse{IsSuccess: false}
} else { } else {
newAcc := Account{authReq.Nickname, passHash} newAcc := Account{authReq.Nickname, passHash}
handlerCtx.srvCtx.accountsLock.Lock() hdlCtx.accountsLock.Lock()
handlerCtx.srvCtx.accounts[newAcc.nickname] = &newAcc hdlCtx.accounts[newAcc.nickname] = &newAcc
handlerCtx.srvCtx.accountsLock.Unlock() hdlCtx.accountsLock.Unlock()
handlerCtx.srvCtx.peersListLock.Lock() hdlCtx.peersListLock.Lock()
handlerCtx.peer.hasAccount = true hdlCtx.peer.hasAccount = true
handlerCtx.peer.account = &newAcc hdlCtx.peer.account = &newAcc
handlerCtx.srvCtx.peersListLock.Unlock() hdlCtx.peersListLock.Unlock()
} }
} }
resBytes, err = json.Marshal(authRes) return authRes, nil
if err != nil {
return nil, err
}
return resBytes, nil
} }
func printConnectedPeers(srvCtx *ServerContext) { func (srvCtx *ServerContext) printConnectedPeers() {
srvCtx.peersListLock.RLock() srvCtx.peersListLock.RLock()
log.Println("[Server] displaying all connections:") log.Println("[Server] displaying all connections:")
@ -237,36 +172,107 @@ func printConnectedPeers(srvCtx *ServerContext) {
srvCtx.peersListLock.RUnlock() srvCtx.peersListLock.RUnlock()
} }
func RunServer() { func (hdlCtx *HandlerContext) handleRequest(reqJsonBytes []byte) error {
idCounter := 0 log.Printf("[Server] got message text: %s\n", strings.Trim(string(reqJsonBytes), "\n"))
srvCtx := &ServerContext{peersList: make([]*Peer, 0), accounts: make(map[string]*Account)} var reqFrame common.RequestFrame
ln, err := net.Listen("tcp", ":8080") json.Unmarshal(reqJsonBytes, &reqFrame)
log.Printf("[Server] unmarshalled request frame (ID=%d)\n", reqFrame.ID)
var res common.Response
var err error
if reqFrame.ID == common.AuthRID {
res, err = hdlCtx.handleAuth(&reqFrame)
} else if reqFrame.ID == common.ListPeersRID {
res, err = hdlCtx.handleListPeers(&reqFrame)
} else if reqFrame.ID == common.EchoRID {
res, err = hdlCtx.handleEcho(&reqFrame)
}
if err != nil { if err != nil {
log.Println(err) log.Printf("[Server] could not handle request ID=%d\n", reqFrame.ID)
return err
} }
go func() { resFrame, err := common.ResponseFrameFrom(res)
for { if err != nil {
printConnectedPeers(srvCtx) log.Println("[Server] could not create frame from response")
time.Sleep(time.Second * 5) return err
} }
}()
resJsonBytes, err := json.Marshal(resFrame)
if err != nil {
log.Println("[Server] error marshalling frame to json")
return err
}
log.Printf("[Server] sending %s\n", string(resJsonBytes))
err = hdlCtx.peer.conn.WriteMessage(websocket.TextMessage, resJsonBytes)
if err != nil {
log.Println("[Server] error writing response frame")
return err
}
return nil
}
func (srvCtx *ServerContext) addPeer(peer *Peer) {
srvCtx.idCounterLock.Lock()
srvCtx.idCounter++
peer.id = srvCtx.idCounter
srvCtx.idCounterLock.Unlock()
srvCtx.peersListLock.Lock()
srvCtx.peersList = append(srvCtx.peersList, peer)
srvCtx.peersListLock.Unlock()
}
func (srvCtx *ServerContext) wsapiHandler(w http.ResponseWriter, r *http.Request) {
upgrader := websocket.Upgrader{}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("[Server] upgrade failed")
return
}
peer := NewPeer(conn)
srvCtx.addPeer(peer)
handlerCtx := &HandlerContext{peer, srvCtx}
defer handleDisconnection(handlerCtx)
defer conn.Close()
log.Printf("[Server] %s connected\n", conn.RemoteAddr())
for { for {
c, err := ln.Accept() messType, messBytes, err := conn.ReadMessage()
if err != nil {
break
}
if messType != 1 {
err := conn.WriteMessage(websocket.CloseUnsupportedData, []byte("Only JSON text is supported"))
if err != nil {
log.Println("[Server] error sending close message due to unsupported data")
}
return
}
err = handlerCtx.handleRequest(messBytes)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
break break
} }
log.Printf("[Server] client connected %s\n", c.RemoteAddr())
idCounter++
newPeer := Peer{idCounter, c, false, nil}
srvCtx.peersListLock.Lock()
srvCtx.peersList = append(srvCtx.peersList, &newPeer)
srvCtx.peersListLock.Unlock()
go handlePeer(&HandlerContext{&newPeer, srvCtx})
} }
} }
func RunServer() {
srvCtx := &ServerContext{peersList: make([]*Peer, 0), accounts: make(map[string]*Account)}
go func() {
for {
srvCtx.printConnectedPeers()
time.Sleep(time.Second * 5)
}
}()
http.HandleFunc("/wsapi", srvCtx.wsapiHandler)
http.ListenAndServe(":8080", nil)
}