mirror of
https://github.com/renorris/openfsd
synced 2026-03-22 14:35:36 +08:00
426 lines
10 KiB
Go
426 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/renorris/openfsd/protocol"
|
|
"github.com/renorris/openfsd/vatsimauth"
|
|
"log"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type FSDClient struct {
|
|
Conn *net.TCPConn
|
|
Reader *bufio.Reader
|
|
|
|
AuthVerify *vatsimauth.VatsimAuth // Auth state to verify client's auth responses
|
|
PendingAuthVerifyChallenge string // Store the pending challenge sent to the client
|
|
AuthSelf *vatsimauth.VatsimAuth // Auth state for interrogating client
|
|
|
|
Callsign string
|
|
CID int
|
|
NetworkRating int
|
|
SimulatorType int
|
|
RealName string
|
|
CurrentGeohash uint64
|
|
SendFastEnabled bool
|
|
|
|
Kill chan string // Signal to disconnect this client
|
|
Mailbox chan string // Incoming messages
|
|
}
|
|
|
|
// EventLoop runs the main event loop for an FSD client.
|
|
// All clients that reach this stage are logged in
|
|
func EventLoop(client *FSDClient) {
|
|
|
|
// Reader goroutine
|
|
packetsRead := make(chan string)
|
|
readerCtx, cancelReader := context.WithCancel(context.Background())
|
|
go func(ctx context.Context) {
|
|
for {
|
|
// Reset the deadline
|
|
err := client.Conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
if err != nil {
|
|
close(packetsRead)
|
|
return
|
|
}
|
|
|
|
var buf []byte
|
|
buf, err = client.Reader.ReadSlice('\n')
|
|
if err != nil {
|
|
close(packetsRead)
|
|
return
|
|
}
|
|
|
|
packet := string(buf)
|
|
|
|
// Validate delimiter
|
|
if len(packet) < 2 || string(packet[len(packet)-2:]) != "\r\n" {
|
|
close(packetsRead)
|
|
return
|
|
}
|
|
|
|
// Send the packet over the channel
|
|
// (also watch for context.Done)
|
|
select {
|
|
case <-ctx.Done():
|
|
close(packetsRead)
|
|
return
|
|
case packetsRead <- packet:
|
|
}
|
|
}
|
|
}(readerCtx)
|
|
|
|
// Defer reader cancellation
|
|
defer cancelReader()
|
|
|
|
// Writer goroutine
|
|
packetsToWrite := make(chan string, 16)
|
|
writerClosed := make(chan struct{})
|
|
go func() {
|
|
for {
|
|
var packet string
|
|
var ok bool
|
|
// Wait for a packet
|
|
select {
|
|
case packet, ok = <-packetsToWrite:
|
|
if !ok {
|
|
close(writerClosed)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Reset the deadline
|
|
err := client.Conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
|
if err != nil {
|
|
close(writerClosed)
|
|
// Exhaust packetsToWrite
|
|
for {
|
|
select {
|
|
case _, ok := <-packetsToWrite:
|
|
if !ok {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Attempt to write the packet
|
|
_, err = client.Conn.Write([]byte(packet))
|
|
if err != nil {
|
|
close(writerClosed)
|
|
// Exhaust packetsToWrite
|
|
for {
|
|
select {
|
|
case _, ok := <-packetsToWrite:
|
|
if !ok {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Wait max 100 milliseconds for writer to flush before continuing the connection shutdown
|
|
defer func() {
|
|
timer := time.NewTimer(100 * time.Millisecond)
|
|
select {
|
|
case <-writerClosed:
|
|
case <-timer.C:
|
|
}
|
|
}()
|
|
|
|
// Defer writer close
|
|
defer close(packetsToWrite)
|
|
|
|
// Defer "delete pilot" broadcast
|
|
defer func() {
|
|
deletePilotPDU := protocol.DeletePilotPDU{
|
|
From: client.Callsign,
|
|
CID: client.CID,
|
|
}
|
|
|
|
mail := NewMail(client)
|
|
mail.SetType(MailTypeBroadcastAll)
|
|
mail.AddPacket(deletePilotPDU.Serialize())
|
|
PO.SendMail([]Mail{*mail})
|
|
}()
|
|
|
|
// Main loop
|
|
for {
|
|
select {
|
|
case packet, ok := <-packetsRead:
|
|
// Check if the reader closed
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Find the processor for this packet
|
|
processor, err := GetProcessor(packet)
|
|
if err != nil {
|
|
packetsToWrite <- protocol.NewGenericFSDError(protocol.SyntaxError).Serialize()
|
|
return
|
|
}
|
|
|
|
result := processor(client, packet)
|
|
|
|
// Send replies to the client
|
|
for _, replyPacket := range result.Replies {
|
|
packetsToWrite <- replyPacket
|
|
}
|
|
|
|
// Send mail
|
|
PO.SendMail(result.Mail)
|
|
|
|
// Disconnect the client if flagged
|
|
if result.ShouldDisconnect {
|
|
return
|
|
}
|
|
case <-writerClosed:
|
|
return
|
|
case mailPacket := <-client.Mailbox:
|
|
packetsToWrite <- mailPacket
|
|
case s, ok := <-client.Kill:
|
|
if ok {
|
|
select {
|
|
case packetsToWrite <- s:
|
|
default:
|
|
}
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func sendSyntaxError(conn *net.TCPConn) {
|
|
conn.Write([]byte(protocol.NewGenericFSDError(protocol.SyntaxError).Serialize()))
|
|
}
|
|
|
|
func HandleConnection(conn *net.TCPConn) {
|
|
defer func() {
|
|
err := conn.Close()
|
|
if err != nil {
|
|
log.Printf("Error closing connection for %s\n%s", conn.RemoteAddr().String(), err.Error())
|
|
}
|
|
}()
|
|
|
|
initChallenge, err := vatsimauth.GenerateChallenge()
|
|
if err != nil {
|
|
log.Printf("Error generating challenge string:\n%s", err.Error())
|
|
return
|
|
}
|
|
|
|
serverIdentPDU := protocol.ServerIdentificationPDU{
|
|
From: protocol.ServerCallsign,
|
|
To: "CLIENT",
|
|
Version: "openfsd",
|
|
InitialChallenge: initChallenge,
|
|
}
|
|
serverIdentPacket := serverIdentPDU.Serialize()
|
|
|
|
// The client has 2 seconds to log in
|
|
if err = conn.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
|
|
return
|
|
}
|
|
if err = conn.SetWriteDeadline(time.Now().Add(2 * time.Second)); err != nil {
|
|
return
|
|
}
|
|
|
|
_, err = conn.Write([]byte(serverIdentPacket))
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
reader := bufio.NewReaderSize(conn, 1024)
|
|
var buf []byte
|
|
|
|
buf, err = reader.ReadSlice('\n')
|
|
if err != nil {
|
|
sendSyntaxError(conn)
|
|
return
|
|
}
|
|
packet := string(buf)
|
|
|
|
// Validate delimiter
|
|
if len(packet) < 2 || string(packet[len(packet)-2:]) != "\r\n" {
|
|
sendSyntaxError(conn)
|
|
return
|
|
}
|
|
|
|
clientIdentPDU, err := protocol.ParseClientIdentificationPDU(packet)
|
|
if err != nil {
|
|
var fsdError *protocol.FSDError
|
|
if errors.As(err, &fsdError) {
|
|
conn.Write([]byte(fsdError.Serialize()))
|
|
}
|
|
return
|
|
}
|
|
|
|
buf, err = reader.ReadSlice('\n')
|
|
if err != nil {
|
|
sendSyntaxError(conn)
|
|
return
|
|
}
|
|
packet = string(buf)
|
|
|
|
// Validate delimiter
|
|
if len(packet) < 2 || string(packet[len(packet)-2:]) != "\r\n" {
|
|
sendSyntaxError(conn)
|
|
return
|
|
}
|
|
|
|
addPilotPDU, err := protocol.ParseAddPilotPDU(packet)
|
|
if err != nil {
|
|
var fsdError *protocol.FSDError
|
|
if errors.As(err, &fsdError) {
|
|
conn.Write([]byte(fsdError.Serialize()))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Verify callsign
|
|
switch clientIdentPDU.From {
|
|
case protocol.ServerCallsign, protocol.ClientQueryBroadcastRecipient, protocol.ClientQueryBroadcastRecipientPilots:
|
|
conn.Write([]byte(protocol.NewGenericFSDError(protocol.CallsignInvalidError).Serialize()))
|
|
return
|
|
}
|
|
|
|
// Verify protocol revision
|
|
if addPilotPDU.ProtocolRevision != protocol.ProtoRevisionVatsim2022 {
|
|
conn.Write([]byte(protocol.NewGenericFSDError(protocol.InvalidProtocolRevisionError).Serialize()))
|
|
return
|
|
}
|
|
|
|
// Verify if we support this client
|
|
_, ok := vatsimauth.Keys[clientIdentPDU.ClientID]
|
|
if !ok {
|
|
conn.Write([]byte(protocol.NewGenericFSDError(protocol.UnauthorizedSoftwareError).Serialize()))
|
|
return
|
|
}
|
|
|
|
// Verify fields in login PDUs
|
|
if clientIdentPDU.From != addPilotPDU.From ||
|
|
clientIdentPDU.CID != addPilotPDU.CID {
|
|
sendSyntaxError(conn)
|
|
return
|
|
}
|
|
|
|
// Validate token signature
|
|
claims := jwt.MapClaims{}
|
|
token, err := jwt.ParseWithClaims(addPilotPDU.Token, &claims, func(token *jwt.Token) (interface{}, error) {
|
|
return JWTKey, nil
|
|
})
|
|
if err != nil {
|
|
conn.Write([]byte(protocol.NewGenericFSDError(protocol.InvalidLogonError).Serialize()))
|
|
return
|
|
}
|
|
|
|
// Check for expiry
|
|
exp, err := token.Claims.GetExpirationTime()
|
|
if err != nil {
|
|
conn.Write([]byte(protocol.NewGenericFSDError(protocol.InvalidLogonError).Serialize()))
|
|
return
|
|
}
|
|
if time.Now().After(exp.Time) {
|
|
conn.Write([]byte(protocol.NewGenericFSDError(protocol.InvalidLogonError).Serialize()))
|
|
return
|
|
}
|
|
|
|
// Verify claimed CID
|
|
claimedCID, err := claims.GetSubject()
|
|
if err != nil {
|
|
conn.Write([]byte(protocol.NewGenericFSDError(protocol.InvalidLogonError).Serialize()))
|
|
return
|
|
}
|
|
|
|
cidInt, err := strconv.Atoi(claimedCID)
|
|
if err != nil {
|
|
sendSyntaxError(conn)
|
|
return
|
|
}
|
|
|
|
if cidInt != clientIdentPDU.CID {
|
|
conn.Write([]byte(protocol.NewGenericFSDError(protocol.InvalidLogonError).Serialize()))
|
|
return
|
|
}
|
|
|
|
// Verify controller rating
|
|
claimedRating, ok := claims["controller_rating"].(float64)
|
|
if !ok {
|
|
sendSyntaxError(conn)
|
|
return
|
|
}
|
|
|
|
if addPilotPDU.NetworkRating > int(claimedRating) {
|
|
conn.Write([]byte(protocol.NewGenericFSDError(protocol.RequestedLevelTooHighError).Serialize()))
|
|
return
|
|
}
|
|
|
|
// Configure client
|
|
fsdClient := FSDClient{
|
|
Conn: conn,
|
|
Reader: reader,
|
|
AuthVerify: &vatsimauth.VatsimAuth{},
|
|
PendingAuthVerifyChallenge: "",
|
|
AuthSelf: &vatsimauth.VatsimAuth{},
|
|
Callsign: clientIdentPDU.From,
|
|
CID: clientIdentPDU.CID,
|
|
NetworkRating: int(claimedRating),
|
|
SimulatorType: addPilotPDU.SimulatorType,
|
|
RealName: addPilotPDU.RealName,
|
|
CurrentGeohash: 0,
|
|
SendFastEnabled: false,
|
|
Kill: make(chan string, 1),
|
|
Mailbox: make(chan string, 16),
|
|
}
|
|
|
|
// Register callsign to the post office. End the connection if callsign already exists
|
|
{
|
|
err := PO.RegisterCallsign(clientIdentPDU.From, &fsdClient)
|
|
if err != nil {
|
|
if errors.Is(err, CallsignAlreadyRegisteredError) {
|
|
pdu := protocol.NewGenericFSDError(protocol.CallsignInUseError)
|
|
conn.Write([]byte(pdu.Serialize()))
|
|
}
|
|
return
|
|
}
|
|
}
|
|
defer PO.DeregisterCallsign(clientIdentPDU.From)
|
|
|
|
// Configure vatsim auth states
|
|
fsdClient.AuthSelf = vatsimauth.NewVatsimAuth(clientIdentPDU.ClientID, vatsimauth.Keys[clientIdentPDU.ClientID])
|
|
fsdClient.AuthSelf.SetInitialChallenge(clientIdentPDU.InitialChallenge)
|
|
fsdClient.AuthVerify = vatsimauth.NewVatsimAuth(clientIdentPDU.ClientID, vatsimauth.Keys[clientIdentPDU.ClientID])
|
|
fsdClient.AuthVerify.SetInitialChallenge(initChallenge)
|
|
|
|
// Broadcast AddPilot packet to network
|
|
addPilotPDU.Token = ""
|
|
mail := NewMail(&fsdClient)
|
|
mail.SetType(MailTypeBroadcastAll)
|
|
mail.AddPacket(addPilotPDU.Serialize())
|
|
PO.SendMail([]Mail{*mail})
|
|
|
|
// Send MOTD
|
|
lines := strings.Split(SC.MOTD, "\n")
|
|
for _, line := range lines {
|
|
pdu := protocol.TextMessagePDU{
|
|
From: protocol.ServerCallsign,
|
|
To: clientIdentPDU.From,
|
|
Message: line,
|
|
}
|
|
_, err := conn.Write([]byte(pdu.Serialize()))
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Start the event loop
|
|
EventLoop(&fsdClient)
|
|
}
|