2024-03-22 18:38:19 +00:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2024-03-24 13:09:36 +00:00
|
|
|
"net/http"
|
2024-03-24 16:10:02 +00:00
|
|
|
"os"
|
2024-03-24 13:09:36 +00:00
|
|
|
"strings"
|
2024-03-22 18:38:19 +00:00
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
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-03-22 18:38:19 +00:00
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"krzyzanowski.dev/p2pchat/common"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Account struct {
|
|
|
|
nickname string
|
|
|
|
passHash []byte
|
|
|
|
}
|
|
|
|
|
|
|
|
type ServerContext struct {
|
2024-03-24 13:09:36 +00:00
|
|
|
idCounter int
|
|
|
|
idCounterLock sync.RWMutex
|
2024-03-22 18:38:19 +00:00
|
|
|
peersList []*Peer
|
|
|
|
peersListLock sync.RWMutex
|
|
|
|
accounts map[string]*Account
|
|
|
|
accountsLock sync.RWMutex
|
|
|
|
}
|
|
|
|
|
|
|
|
type HandlerContext struct {
|
2024-03-24 13:09:36 +00:00
|
|
|
peer *Peer
|
|
|
|
*ServerContext
|
2024-03-22 18:38:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type Peer struct {
|
|
|
|
id int
|
2024-03-24 13:09:36 +00:00
|
|
|
conn *websocket.Conn
|
2024-03-22 18:38:19 +00:00
|
|
|
hasAccount bool
|
|
|
|
account *Account
|
|
|
|
}
|
|
|
|
|
2024-03-24 16:10:02 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-24 13:09:36 +00:00
|
|
|
func NewPeer(conn *websocket.Conn) *Peer {
|
|
|
|
return &Peer{-1, conn, false, nil}
|
|
|
|
}
|
|
|
|
|
2024-03-22 18:38:19 +00:00
|
|
|
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]
|
|
|
|
}
|
|
|
|
|
2024-03-24 13:09:36 +00:00
|
|
|
func (srvCtx *ServerContext) removePeer(peer *Peer) {
|
|
|
|
srvCtx.peersListLock.Lock()
|
|
|
|
peerSliceRemove(&srvCtx.peersList, peerSliceIndexOf(srvCtx.peersList, peer.id))
|
|
|
|
srvCtx.peersListLock.Unlock()
|
2024-03-22 18:38:19 +00:00
|
|
|
}
|
|
|
|
|
2024-03-24 13:09:36 +00:00
|
|
|
func handleDisconnection(handlerCtx *HandlerContext) {
|
|
|
|
handlerCtx.removePeer(handlerCtx.peer)
|
2024-03-24 16:10:02 +00:00
|
|
|
logger.Infof("%s disconnected", handlerCtx.peer.conn.RemoteAddr())
|
2024-03-22 18:38:19 +00:00
|
|
|
}
|
|
|
|
|
2024-03-24 13:09:36 +00:00
|
|
|
func (hdlCtx *HandlerContext) handleEcho(reqFrame *common.RequestFrame) (res common.Response, err error) {
|
|
|
|
echoReq, err := common.RequestFromFrame[common.EchoRequest](*reqFrame)
|
2024-03-22 18:38:19 +00:00
|
|
|
if err != nil {
|
2024-03-24 16:10:02 +00:00
|
|
|
logger.Error("could not read request from frame")
|
2024-03-22 18:38:19 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
echoRes := common.EchoResponse(echoReq)
|
2024-03-24 13:09:36 +00:00
|
|
|
return echoRes, nil
|
2024-03-22 18:38:19 +00:00
|
|
|
}
|
|
|
|
|
2024-03-24 13:09:36 +00:00
|
|
|
func (hdlCtx *HandlerContext) handleListPeers(reqFrame *common.RequestFrame) (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)
|
2024-03-22 18:38:19 +00:00
|
|
|
if err != nil {
|
2024-03-24 16:10:02 +00:00
|
|
|
logger.Error("could not read request from frame")
|
2024-03-22 18:38:19 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-03-24 13:09:36 +00:00
|
|
|
hdlCtx.peersListLock.RLock()
|
|
|
|
peersFreeze := make([]*Peer, len(hdlCtx.peersList))
|
|
|
|
copy(peersFreeze, hdlCtx.peersList)
|
|
|
|
hdlCtx.peersListLock.RUnlock()
|
2024-03-22 18:38:19 +00:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-03-24 13:09:36 +00:00
|
|
|
return listPeersRes, nil
|
2024-03-22 18:38:19 +00:00
|
|
|
}
|
|
|
|
|
2024-03-24 13:09:36 +00:00
|
|
|
func (hdlCtx *HandlerContext) handleAuth(reqFrame *common.RequestFrame) (res common.Response, err error) {
|
|
|
|
authReq, err := common.RequestFromFrame[common.AuthRequest](*reqFrame)
|
2024-03-22 18:38:19 +00:00
|
|
|
if err != nil {
|
2024-03-24 16:10:02 +00:00
|
|
|
logger.Error("could not read request from frame")
|
2024-03-22 18:38:19 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if account already exists
|
2024-03-24 13:09:36 +00:00
|
|
|
hdlCtx.accountsLock.RLock()
|
|
|
|
account, ok := hdlCtx.accounts[authReq.Nickname]
|
|
|
|
hdlCtx.accountsLock.RUnlock()
|
|
|
|
var authRes *common.AuthResponse
|
2024-03-22 18:38:19 +00:00
|
|
|
|
|
|
|
if ok {
|
|
|
|
// Check if password matches
|
|
|
|
if bcrypt.CompareHashAndPassword(account.passHash, []byte(authReq.Password)) == nil {
|
2024-03-24 13:09:36 +00:00
|
|
|
authRes = &common.AuthResponse{IsSuccess: true}
|
|
|
|
hdlCtx.peersListLock.Lock()
|
|
|
|
hdlCtx.peer.hasAccount = true
|
|
|
|
hdlCtx.peer.account = account
|
|
|
|
hdlCtx.peersListLock.Unlock()
|
2024-03-22 18:38:19 +00:00
|
|
|
} else {
|
2024-03-24 13:09:36 +00:00
|
|
|
authRes = &common.AuthResponse{IsSuccess: false}
|
2024-03-22 18:38:19 +00:00
|
|
|
}
|
|
|
|
} else {
|
2024-03-24 13:09:36 +00:00
|
|
|
authRes = &common.AuthResponse{IsSuccess: true}
|
2024-03-22 18:38:19 +00:00
|
|
|
passHash, err := bcrypt.GenerateFromPassword([]byte(authReq.Password), bcrypt.DefaultCost)
|
|
|
|
|
|
|
|
if err != nil {
|
2024-03-24 13:09:36 +00:00
|
|
|
authRes = &common.AuthResponse{IsSuccess: false}
|
2024-03-22 18:38:19 +00:00
|
|
|
} else {
|
|
|
|
newAcc := Account{authReq.Nickname, passHash}
|
2024-03-24 13:09:36 +00:00
|
|
|
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()
|
2024-03-22 18:38:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-24 13:09:36 +00:00
|
|
|
return authRes, nil
|
2024-03-22 18:38:19 +00:00
|
|
|
}
|
|
|
|
|
2024-03-24 13:09:36 +00:00
|
|
|
func (srvCtx *ServerContext) printConnectedPeers() {
|
2024-03-22 18:38:19 +00:00
|
|
|
srvCtx.peersListLock.RLock()
|
2024-03-24 16:10:02 +00:00
|
|
|
logger.Debug("displaying all connections:")
|
2024-03-22 18:38:19 +00:00
|
|
|
|
|
|
|
for _, p := range srvCtx.peersList {
|
|
|
|
nick := "-"
|
|
|
|
|
|
|
|
if p.hasAccount {
|
|
|
|
nick = p.account.nickname
|
|
|
|
}
|
|
|
|
|
2024-03-24 16:10:02 +00:00
|
|
|
log.Debugf("ID#%d, Addr:%s, Auth:%t, Nick:%s", p.id, p.conn.RemoteAddr(), p.hasAccount, nick)
|
2024-03-22 18:38:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
srvCtx.peersListLock.RUnlock()
|
|
|
|
}
|
|
|
|
|
2024-03-24 13:09:36 +00:00
|
|
|
func (hdlCtx *HandlerContext) handleRequest(reqJsonBytes []byte) error {
|
2024-03-24 16:10:02 +00:00
|
|
|
logger.Debugf("got message text: %s", strings.Trim(string(reqJsonBytes), "\n"))
|
2024-03-24 13:09:36 +00:00
|
|
|
var reqFrame common.RequestFrame
|
|
|
|
json.Unmarshal(reqJsonBytes, &reqFrame)
|
2024-03-24 16:10:02 +00:00
|
|
|
log.Debugf("unmarshalled request frame (ID=%d)", reqFrame.ID)
|
2024-03-24 13:09:36 +00:00
|
|
|
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)
|
|
|
|
}
|
2024-03-22 18:38:19 +00:00
|
|
|
|
|
|
|
if err != nil {
|
2024-03-24 16:10:02 +00:00
|
|
|
logger.Errorf("could not handle request ID=%d", reqFrame.ID)
|
2024-03-24 13:09:36 +00:00
|
|
|
return err
|
2024-03-22 18:38:19 +00:00
|
|
|
}
|
|
|
|
|
2024-03-24 13:09:36 +00:00
|
|
|
resFrame, err := common.ResponseFrameFrom(res)
|
|
|
|
if err != nil {
|
2024-03-24 16:10:02 +00:00
|
|
|
logger.Errorf("could not create frame from response")
|
2024-03-24 13:09:36 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
resJsonBytes, err := json.Marshal(resFrame)
|
|
|
|
if err != nil {
|
2024-03-24 16:10:02 +00:00
|
|
|
logger.Errorf("error marshalling frame to json")
|
2024-03-24 13:09:36 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-03-24 16:10:02 +00:00
|
|
|
logger.Debugf("sending %s", string(resJsonBytes))
|
2024-03-24 13:09:36 +00:00
|
|
|
err = hdlCtx.peer.conn.WriteMessage(websocket.TextMessage, resJsonBytes)
|
|
|
|
if err != nil {
|
2024-03-24 16:10:02 +00:00
|
|
|
logger.Errorf("error writing response frame")
|
2024-03-24 13:09:36 +00:00
|
|
|
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 {
|
2024-03-24 16:10:02 +00:00
|
|
|
logger.Errorf("upgrade failed")
|
2024-03-24 13:09:36 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
peer := NewPeer(conn)
|
|
|
|
srvCtx.addPeer(peer)
|
|
|
|
handlerCtx := &HandlerContext{peer, srvCtx}
|
|
|
|
defer handleDisconnection(handlerCtx)
|
|
|
|
defer conn.Close()
|
2024-03-24 16:10:02 +00:00
|
|
|
logger.Infof("%s connected", conn.RemoteAddr())
|
2024-03-22 18:38:19 +00:00
|
|
|
|
|
|
|
for {
|
2024-03-24 13:09:36 +00:00
|
|
|
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 {
|
2024-03-24 16:10:02 +00:00
|
|
|
logger.Debugf("[Server] error sending close message due to unsupported data")
|
2024-03-24 13:09:36 +00:00
|
|
|
}
|
2024-03-22 18:38:19 +00:00
|
|
|
|
2024-03-24 13:09:36 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err = handlerCtx.handleRequest(messBytes)
|
2024-03-22 18:38:19 +00:00
|
|
|
if err != nil {
|
2024-03-24 16:10:02 +00:00
|
|
|
logger.Debug(err)
|
2024-03-22 18:38:19 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-03-24 13:09:36 +00:00
|
|
|
|
|
|
|
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)
|
2024-03-24 16:10:02 +00:00
|
|
|
logger.Info("Starting server...")
|
2024-03-24 13:09:36 +00:00
|
|
|
http.ListenAndServe(":8080", nil)
|
|
|
|
}
|