Files
openfsd/fsd/util.go
2025-05-12 17:21:16 -07:00

355 lines
9.6 KiB
Go

package fsd
import (
"bytes"
"encoding/base64"
"encoding/json"
"slices"
"strconv"
"strings"
)
// FSD error codes
const (
CallsignInUseError = 1 // Callsign is already in use
CallsignInvalidError = 2 // Callsign is invalid
AlreadyRegisteredError = 3 // Client is already registered
SyntaxError = 4 // Packet syntax is invalid
SourceInvalidError = 5 // Packet source is invalid
InvalidLogonError = 6 // Login credentials or token are invalid
NoSuchCallsignError = 7 // Specified callsign does not exist
NoFlightPlanError = 8 // No flight plan found for the Client
NoWeatherProfileError = 9 // No weather profile available
InvalidProtocolRevisionError = 10 // Client uses an unsupported protocol version
RequestedLevelTooHighError = 11 // Requested access level is too high
ServerFullError = 12 // Server has reached capacity
CertificateSuspendedError = 13 // Client's certificate is suspended
InvalidControlError = 14 // Invalid control command
InvalidPositionForRatingError = 15 // Position not allowed for Client's rating
UnauthorizedSoftwareError = 16 // Client software is not authorized
ClientAuthenticationResponseTimeoutError = 17 // Authentication response timed out
)
// FSD Network Ratings
type NetworkRating int
const (
NetworkRatingInactive NetworkRating = iota - 1
NetworkRatingSuspended
NetworkRatingObserver
NetworkRatingStudent1
NetworkRatingStudent2
NetworkRatingStudent3
NetworkRatingController1
NetworkRatingController2
NetworkRatingController3
NetworkRatingInstructor1
NetworkRatingInstructor2
NetworkRatingInstructor3
NetworkRatingSupervisor
NetworkRatingAdministator
)
func countFields(packet []byte) int {
return bytes.Count(packet, []byte(":")) + 1
}
func rebaseToNextField(packet []byte) []byte {
return packet[bytes.IndexByte(packet, ':')+1:]
}
func getField(packet []byte, index int) []byte {
for range index {
packet = rebaseToNextField(packet)
}
if i := bytes.IndexByte(packet, ':'); i != -1 {
packet = packet[:i]
}
packet, _ = bytes.CutSuffix(packet, []byte("\r\n"))
return packet
}
// mostLikelyJwt returns whether a given byte slice is most likely a JWT token
func mostLikelyJwt(token []byte) bool {
tmp := token
dotCount := 0
for {
if i := bytes.IndexByte(tmp, '.'); i > -1 {
tmp = tmp[i+1:]
dotCount++
if dotCount > 2 {
return false
}
continue
}
break
}
if dotCount != 2 {
return false
}
rawJwtHeader := token[:bytes.IndexByte(token, '.')]
buf := make([]byte, 0, 256)
buf, err := base64.StdEncoding.AppendDecode(buf, rawJwtHeader)
if err != nil {
return false
}
type jwtHeader struct {
Alg string `json:"alg"`
Typ string `json:"typ"`
}
header := jwtHeader{}
if err = json.Unmarshal(buf, &header); err != nil {
return false
}
if header.Alg == "" || header.Typ == "" {
return false
}
return true
}
func isValidCallsignLength(callsign []byte) bool {
return len(callsign) <= 10 && len(callsign) >= 2
}
var reservedCallsigns = []string{
"SERVER",
"CLIENT",
"FP",
}
func isValidClientCallsign(callsign []byte) bool {
if !isValidCallsignLength(callsign) {
return false
}
// Only uppercase alphanumeric characters and/or hyphen/underscores
for i := range callsign {
b := callsign[i]
if (b >= '0' && b <= '9') || (b >= 'A' && b <= 'Z') || (b == '-' || b == '_') {
continue
}
return false
}
// Check against reserved callsigns
if slices.Contains(reservedCallsigns, string(callsign)) {
return false
}
return true
}
// isAllowedFacilityType checks if a given network rating is allowed to connect as a given facility type.
func isAllowedFacilityType(rating NetworkRating, facilityType int) bool {
// Observer facility type is allowed for all ratings
if facilityType == 0 {
return true
}
// Map of facility types to minimum required rating protocol values
minRating := map[int]NetworkRating{
1: NetworkRatingController1, // Flight Service Station (FSS) - C1 and above
2: NetworkRatingStudent1, // Delivery (DEL) - S1 and above
3: NetworkRatingStudent1, // Ground (GND) - S1 and above
4: NetworkRatingStudent2, // Tower (TWR) - S2 and above
5: NetworkRatingStudent3, // Approach (APP) - S3 and above
6: NetworkRatingController1, // Centre (CTR) - C1 and above
}[facilityType]
// Return false for invalid facility types
if minRating == 0 {
return false
}
// Check if the rating meets the minimum requirement
return rating >= minRating
}
// parseLatLon extracts two base-10-encoded float64 values from a packet at the specified field indices
func parseLatLon(packet []byte, latIndex, lonIndex int) (lat float64, lon float64, ok bool) {
rawLat := getField(packet, latIndex)
rawLon := getField(packet, lonIndex)
lat, err := strconv.ParseFloat(string(rawLat), 64)
if err != nil {
return
}
lon, err = strconv.ParseFloat(string(rawLon), 64)
if err != nil {
return
}
ok = true
return
}
// parseVisRange parses an FSD-encoded visibility range and returns the distance in meters
func parseVisRange(packet []byte, index int) (visRange float64, ok bool) {
visRangeNauticalMiles, err := strconv.ParseFloat(string(getField(packet, index)), 10)
if err != nil {
return
}
// Convert to meters
visRange = visRangeNauticalMiles * 1852.0
ok = true
return
}
// broadcastRanged broadcasts a packet to all clients in range
func broadcastRanged(po *postOffice, client *Client, packet []byte) {
packetStr := string(packet)
po.search(client, func(recipient *Client) bool {
recipient.send(packetStr)
return true
})
}
// broadcastRangedVelocity broadcasts a packet to all clients in range
// supporting the Vatsim2022 (101) protocol revision.
func broadcastRangedVelocity(po *postOffice, client *Client, packet []byte) {
packetStr := string(packet)
po.search(client, func(recipient *Client) bool {
if client.protoRevision != 101 {
return true
}
recipient.send(packetStr)
return true
})
}
// broadcastRangedAtcOnly broadcasts a packet to all ATC clients in range
func broadcastRangedAtcOnly(po *postOffice, client *Client, packet []byte) {
packetStr := string(packet)
po.search(client, func(recipient *Client) bool {
if !client.isAtc {
return true
}
recipient.send(packetStr)
return true
})
}
// broadcastAll broadcasts a packet to the entire server
func broadcastAll(po *postOffice, client *Client, packet []byte) {
packetStr := string(packet)
po.all(client, func(recipient *Client) bool {
recipient.send(packetStr)
return true
})
}
// broadcastAllATC broadcasts a packet to all ATC on entire server
func broadcastAllATC(po *postOffice, client *Client, packet []byte) {
packetStr := string(packet)
po.all(client, func(recipient *Client) bool {
if !recipient.isAtc {
return true
}
recipient.send(packetStr)
return true
})
}
// broadcastAll broadcasts a packet to all supervisors on the server
func broadcastAllSupervisors(po *postOffice, client *Client, packet []byte) {
packetStr := string(packet)
po.all(client, func(recipient *Client) bool {
if client.networkRating < NetworkRatingSupervisor {
return true
}
recipient.send(packetStr)
return true
})
}
// sendDirectOrErr attempts to send a packet directly to a recipient.
// If the post office responds with an ErrCallsignDoesNotExist, the client
// is notified with a NoSuchCallsignError.
func sendDirectOrErr(po *postOffice, client *Client, recipient []byte, packet []byte) {
if err := po.send(string(recipient), string(packet)); err != nil {
client.sendError(NoSuchCallsignError, "No such callsign")
return
}
}
// extractFlightplanInfoSection extracts the useful flightplan information from an $FP or $AM packet
func extractFlightplanInfoSection(packet []byte) (fpl string) {
switch getPacketType(packet) {
case PacketTypeFlightPlan:
for range 2 {
packet = rebaseToNextField(packet)
}
default: // PacketTypeFlightPlanAmendment
for range 3 {
packet = rebaseToNextField(packet)
}
}
packet, _ = bytes.CutSuffix(packet, []byte("\r\n"))
return string(packet)
}
// buildFileFlightplanPacket builds an $FP packet
func buildFileFlightplanPacket(source, recipient, fplInfo string) (packet string) {
prefix := strings.Builder{}
prefix.WriteString("$FP")
prefix.WriteString(source)
prefix.WriteByte(':')
prefix.WriteString(recipient)
prefix.WriteByte(':')
return buildFlightplanPacket(prefix.String(), fplInfo)
}
// buildAmendFlightplanPacket builds an $AM packet
func buildAmendFlightplanPacket(source, recipient, targetCallsign, fplInfo string) (packet string) {
prefix := strings.Builder{}
prefix.Grow(36)
prefix.WriteString("$AM")
prefix.WriteString(source)
prefix.WriteByte(':')
prefix.WriteString(recipient)
prefix.WriteByte(':')
prefix.WriteString(targetCallsign)
prefix.WriteByte(':')
return buildFlightplanPacket(prefix.String(), fplInfo)
}
func buildFlightplanPacket(prefix, fplInfo string) (packet string) {
builder := strings.Builder{}
builder.Grow(len(prefix) + len(fplInfo) + 2)
builder.WriteString(prefix)
builder.WriteString(fplInfo)
builder.WriteString("\r\n")
return builder.String()
}
func buildBeaconCodePacket(source, recipient, targetCallsign, beaconCode string) (packet string) {
builder := strings.Builder{}
builder.Grow(48)
builder.WriteString("#PC")
builder.WriteString(source)
builder.WriteByte(':')
builder.WriteString(recipient)
builder.WriteString(":CCP:BC:")
builder.WriteString(targetCallsign)
builder.WriteByte(':')
builder.WriteString(beaconCode)
builder.WriteString("\r\n")
return builder.String()
}