mirror of
https://github.com/originalmk/archat-server.git
synced 2024-10-03 19:26:10 +00:00
Maciej Krzyżanowski
f16b53278b
Server may now send requests to client and client will respond to them. Mechanics similar to how server handled requests was added to client. Also server can now receive responses from client :)
360 lines
8.6 KiB
Go
360 lines
8.6 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/log"
|
|
"github.com/gorilla/websocket"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"krzyzanowski.dev/p2pchat/common"
|
|
)
|
|
|
|
type Account struct {
|
|
nickname string
|
|
passHash []byte
|
|
}
|
|
|
|
type Context struct {
|
|
idCounter int
|
|
idCounterLock sync.RWMutex
|
|
peersList []*Peer
|
|
peersListLock sync.RWMutex
|
|
accounts map[string]*Account
|
|
accountsLock sync.RWMutex
|
|
}
|
|
|
|
type HandlerContext struct {
|
|
peer *Peer
|
|
*Context
|
|
resFromClient chan common.RFrame
|
|
reqFromClient chan common.RFrame
|
|
rToClient chan common.RFrame
|
|
}
|
|
|
|
func NewHandlerContext(peer *Peer, srvCtx *Context) *HandlerContext {
|
|
return &HandlerContext{
|
|
peer,
|
|
srvCtx,
|
|
make(chan common.RFrame),
|
|
make(chan common.RFrame),
|
|
make(chan common.RFrame),
|
|
}
|
|
}
|
|
|
|
func (hdlCtx *HandlerContext) clientHandler(hdlWg *sync.WaitGroup) error {
|
|
defer hdlWg.Done()
|
|
|
|
for {
|
|
reqFrame := <-hdlCtx.reqFromClient
|
|
var res common.Response
|
|
var err error
|
|
|
|
if reqFrame.ID == common.AuthReqID {
|
|
res, err = hdlCtx.handleAuth(&reqFrame)
|
|
} else if reqFrame.ID == common.ListPeersReqID {
|
|
res, err = hdlCtx.handleListPeers(&reqFrame)
|
|
} else if reqFrame.ID == common.EchoReqID {
|
|
res, err = hdlCtx.handleEcho(&reqFrame)
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Errorf("could not handle request ID=%d", reqFrame.ID)
|
|
return err
|
|
}
|
|
|
|
resFrame, err := common.ResponseFrameFrom(res)
|
|
if err != nil {
|
|
logger.Errorf("could not create frame from response")
|
|
return err
|
|
}
|
|
|
|
hdlCtx.rToClient <- resFrame
|
|
}
|
|
}
|
|
|
|
func (hdlCtx *HandlerContext) clientWriter(hdlWg *sync.WaitGroup) error {
|
|
defer hdlWg.Done()
|
|
|
|
for {
|
|
rFrame := <-hdlCtx.rToClient
|
|
resJsonBytes, err := json.Marshal(rFrame)
|
|
if err != nil {
|
|
logger.Errorf("error marshalling frame to json")
|
|
return err
|
|
}
|
|
|
|
logger.Debugf("sending %s", string(resJsonBytes))
|
|
err = hdlCtx.peer.conn.WriteMessage(websocket.TextMessage, resJsonBytes)
|
|
if err != nil {
|
|
logger.Errorf("error writing rframe")
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
func (hdlCtx *HandlerContext) clientReader(hdlWg *sync.WaitGroup) error {
|
|
defer hdlWg.Done()
|
|
|
|
for {
|
|
messType, messBytes, err := hdlCtx.peer.conn.ReadMessage()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if messType != 1 {
|
|
err := hdlCtx.peer.conn.WriteMessage(websocket.CloseUnsupportedData, []byte("Only JSON text is supported"))
|
|
if err != nil {
|
|
logger.Debugf("[Server] error sending unsupported data close message")
|
|
return err
|
|
}
|
|
}
|
|
|
|
logger.Debugf("got message text: %s", strings.Trim(string(messBytes), "\n"))
|
|
var rFrame common.RFrame
|
|
json.Unmarshal(messBytes, &rFrame)
|
|
logger.Debugf("unmarshalled request frame (ID=%d)", rFrame.ID)
|
|
|
|
if rFrame.ID > 128 {
|
|
logger.Debug("it is response frame", "id", rFrame.ID)
|
|
hdlCtx.resFromClient <- rFrame
|
|
} else {
|
|
logger.Debug("it is request frame", "id", rFrame.ID)
|
|
hdlCtx.reqFromClient <- rFrame
|
|
}
|
|
}
|
|
}
|
|
|
|
func (hdlCtx *HandlerContext) sendRequest(req common.Request) error {
|
|
rf, err := common.RequestFrameFrom(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
hdlCtx.rToClient <- rf
|
|
return nil
|
|
}
|
|
|
|
func (hdlCtx *HandlerContext) getResponseFrame() common.RFrame {
|
|
return <-hdlCtx.resFromClient
|
|
}
|
|
|
|
type Peer struct {
|
|
id int
|
|
conn *websocket.Conn
|
|
hasAccount bool
|
|
account *Account
|
|
}
|
|
|
|
var logger = log.NewWithOptions(os.Stdout, log.Options{
|
|
ReportTimestamp: true,
|
|
TimeFormat: time.TimeOnly,
|
|
Prefix: "⚙️ Server",
|
|
})
|
|
|
|
func init() {
|
|
if common.IsProd {
|
|
logger.SetLevel(log.InfoLevel)
|
|
} else {
|
|
logger.SetLevel(log.DebugLevel)
|
|
}
|
|
}
|
|
|
|
func NewPeer(conn *websocket.Conn) *Peer {
|
|
return &Peer{-1, conn, false, nil}
|
|
}
|
|
|
|
func peerSliceIndexOf(s []*Peer, id int) int {
|
|
i := 0
|
|
var p *Peer
|
|
for i, p = range s {
|
|
if p.id == id {
|
|
break
|
|
}
|
|
}
|
|
return i
|
|
}
|
|
|
|
func peerSliceRemove(s *[]*Peer, i int) {
|
|
(*s)[i] = (*s)[len(*s)-1]
|
|
*s = (*s)[:len(*s)-1]
|
|
}
|
|
|
|
func (srvCtx *Context) removePeer(peer *Peer) {
|
|
srvCtx.peersListLock.Lock()
|
|
peerSliceRemove(&srvCtx.peersList, peerSliceIndexOf(srvCtx.peersList, peer.id))
|
|
srvCtx.peersListLock.Unlock()
|
|
}
|
|
|
|
func handleDisconnection(handlerCtx *HandlerContext) {
|
|
handlerCtx.removePeer(handlerCtx.peer)
|
|
logger.Infof("%s disconnected", handlerCtx.peer.conn.RemoteAddr())
|
|
}
|
|
|
|
func (hdlCtx *HandlerContext) handleEcho(reqFrame *common.RFrame) (res common.Response, err error) {
|
|
echoReq, err := common.RequestFromFrame[common.EchoRequest](*reqFrame)
|
|
if err != nil {
|
|
logger.Error("could not read request from frame")
|
|
return nil, err
|
|
}
|
|
|
|
echoRes := common.EchoResponse(echoReq)
|
|
return echoRes, nil
|
|
}
|
|
|
|
func (hdlCtx *HandlerContext) handleListPeers(reqFrame *common.RFrame) (res common.Response, err error) {
|
|
// Currently list peers request is empty, so we can ignore it - we won't use it
|
|
_, err = common.RequestFromFrame[common.ListPeersRequest](*reqFrame)
|
|
if err != nil {
|
|
logger.Error("could not read request from frame")
|
|
return nil, err
|
|
}
|
|
|
|
hdlCtx.peersListLock.RLock()
|
|
peersFreeze := make([]*Peer, len(hdlCtx.peersList))
|
|
copy(peersFreeze, hdlCtx.peersList)
|
|
hdlCtx.peersListLock.RUnlock()
|
|
listPeersRes := common.ListPeersResponse{PeersInfo: make([]common.PeerInfo, 0)}
|
|
|
|
for _, peer := range peersFreeze {
|
|
listPeersRes.PeersInfo = append(
|
|
listPeersRes.PeersInfo,
|
|
common.PeerInfo{
|
|
ID: peer.id,
|
|
Addr: peer.conn.RemoteAddr().String(),
|
|
HasNickaname: peer.hasAccount,
|
|
Nickname: peer.account.nickname,
|
|
},
|
|
)
|
|
}
|
|
|
|
return listPeersRes, nil
|
|
}
|
|
|
|
func (hdlCtx *HandlerContext) handleAuth(reqFrame *common.RFrame) (res common.Response, err error) {
|
|
authReq, err := common.RequestFromFrame[common.AuthRequest](*reqFrame)
|
|
if err != nil {
|
|
logger.Error("could not read request from frame")
|
|
return nil, err
|
|
}
|
|
|
|
// Check if account already exists
|
|
hdlCtx.accountsLock.RLock()
|
|
account, ok := hdlCtx.accounts[authReq.Nickname]
|
|
hdlCtx.accountsLock.RUnlock()
|
|
var authRes *common.AuthResponse
|
|
|
|
if ok {
|
|
// Check if password matches
|
|
if bcrypt.CompareHashAndPassword(account.passHash, []byte(authReq.Password)) == nil {
|
|
authRes = &common.AuthResponse{IsSuccess: true}
|
|
hdlCtx.peersListLock.Lock()
|
|
hdlCtx.peer.hasAccount = true
|
|
hdlCtx.peer.account = account
|
|
hdlCtx.peersListLock.Unlock()
|
|
} else {
|
|
authRes = &common.AuthResponse{IsSuccess: false}
|
|
}
|
|
} else {
|
|
authRes = &common.AuthResponse{IsSuccess: true}
|
|
passHash, err := bcrypt.GenerateFromPassword([]byte(authReq.Password), bcrypt.DefaultCost)
|
|
|
|
if err != nil {
|
|
authRes = &common.AuthResponse{IsSuccess: false}
|
|
} else {
|
|
newAcc := Account{authReq.Nickname, passHash}
|
|
hdlCtx.accountsLock.Lock()
|
|
hdlCtx.accounts[newAcc.nickname] = &newAcc
|
|
hdlCtx.accountsLock.Unlock()
|
|
hdlCtx.peersListLock.Lock()
|
|
hdlCtx.peer.hasAccount = true
|
|
hdlCtx.peer.account = &newAcc
|
|
hdlCtx.peersListLock.Unlock()
|
|
}
|
|
}
|
|
|
|
return authRes, nil
|
|
}
|
|
|
|
func (srvCtx *Context) printConnectedPeers() {
|
|
srvCtx.peersListLock.RLock()
|
|
logger.Debug("displaying all connections:")
|
|
|
|
for _, p := range srvCtx.peersList {
|
|
nick := "-"
|
|
|
|
if p.hasAccount {
|
|
nick = p.account.nickname
|
|
}
|
|
|
|
logger.Debugf("ID#%d, Addr:%s, Auth:%t, Nick:%s", p.id, p.conn.RemoteAddr(), p.hasAccount, nick)
|
|
}
|
|
|
|
srvCtx.peersListLock.RUnlock()
|
|
}
|
|
|
|
func (srvCtx *Context) 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 *Context) wsapiHandler(w http.ResponseWriter, r *http.Request) {
|
|
upgrader := websocket.Upgrader{}
|
|
conn, err := upgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
logger.Errorf("upgrade failed")
|
|
return
|
|
}
|
|
|
|
peer := NewPeer(conn)
|
|
srvCtx.addPeer(peer)
|
|
handlerCtx := NewHandlerContext(peer, srvCtx)
|
|
defer handleDisconnection(handlerCtx)
|
|
defer conn.Close()
|
|
logger.Infof("%s connected", conn.RemoteAddr())
|
|
|
|
var handlerWg sync.WaitGroup
|
|
handlerWg.Add(3)
|
|
go handlerCtx.clientWriter(&handlerWg)
|
|
go handlerCtx.clientHandler(&handlerWg)
|
|
go handlerCtx.clientReader(&handlerWg)
|
|
|
|
logger.Debug("sending echo request...")
|
|
handlerCtx.sendRequest(common.EchoRequest{EchoByte: 123})
|
|
logger.Debug("sent")
|
|
echoResF := handlerCtx.getResponseFrame()
|
|
logger.Debug("got response")
|
|
echoRes, err := common.ResponseFromFrame[common.EchoResponse](echoResF)
|
|
if err != nil {
|
|
logger.Error(err)
|
|
return
|
|
}
|
|
logger.Debug("test echo done", "byteSent", 123, "byteReceived", echoRes.EchoByte)
|
|
|
|
handlerWg.Wait()
|
|
}
|
|
|
|
func RunServer() {
|
|
srvCtx := &Context{peersList: make([]*Peer, 0), accounts: make(map[string]*Account)}
|
|
|
|
go func() {
|
|
for {
|
|
srvCtx.printConnectedPeers()
|
|
time.Sleep(time.Second * 5)
|
|
}
|
|
}()
|
|
|
|
http.HandleFunc("/wsapi", srvCtx.wsapiHandler)
|
|
logger.Info("Starting server...")
|
|
http.ListenAndServe(":8080", nil)
|
|
}
|