Bug fixes:
- Fix re-hashing an already hashed password in UpdateUserRecord
- Fix validator incorrectly flagging swift client name
- Switched a > to a < when checking field length for #TM packets

Misc:
- Simplify client reader goroutine
- Simplify client writer logic

Features:
- Add an option to enable plaintext passwords (replacing the JWT token in the AddPilotPDU `token` field.)
This commit is contained in:
Reese Norris
2024-09-14 12:20:34 -07:00
parent 257d891df5
commit de94e668f0
12 changed files with 274 additions and 277 deletions

View File

@@ -17,16 +17,16 @@ A default admin user will be printed to stdout on first startup. A simple web in
The server is configured via environment variables:
| Variable Name | Default Value | Description |
|-----------------|---------------|------------------------------------------------------------|
| `FSD_ADDR` | 0.0.0.0:6809 | FSD listen address |
| `HTTP_ADDR` | 0.0.0.0:9086 | HTTP listen address |
| `HTTPS_ENABLED` | false | Enable HTTPS |
| `TLS_CERT_FILE` | | TLS certificate file path |
| `TLS_KEY_FILE` | | TLS key file path |
| `DATABASE_FILE` | ./fsd.db | SQLite database file path |
| `MOTD` | openfsd | Message to send on FSD client login (line feeds supported) |
| Variable Name | Default Value | Description |
|-----------------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------|
| `FSD_ADDR` | 0.0.0.0:6809 | FSD listen address |
| `HTTP_ADDR` | 0.0.0.0:9086 | HTTP listen address |
| `HTTPS_ENABLED` | false | Enable HTTPS |
| `TLS_CERT_FILE` | | TLS certificate file path |
| `TLS_KEY_FILE` | | TLS key file path |
| `DATABASE_FILE` | ./fsd.db | SQLite database file path |
| `MOTD` | openfsd | Message to send on FSD client login (line feeds supported) |
| `PLAINTEXT_PASSWORDS` | false | Setting this to true treats the "token" field in the #AP packet to be a plaintext password, rather than a VATSIM-esque JWT token. |
## Overview
Various clients such as [vPilot](https://vpilot.rosscarlson.dev/), [xPilot](https://docs.xpilot-project.org/) and [swift](https://swift-project.org/) are used to connect to VATSIM FSD servers.

View File

@@ -15,6 +15,8 @@ type FSDUserRecord struct {
CreationTime time.Time `json:"creation_time"`
}
// GetUserRecord returns a user record for a given CID
// If no user is found, this is not an error. FSDUserRecord will be nil, and error will be nil.
func GetUserRecord(db *sql.DB, cid int) (*FSDUserRecord, error) {
row := db.QueryRow("SELECT * FROM users WHERE cid=? LIMIT 1", cid)
@@ -24,7 +26,11 @@ func GetUserRecord(db *sql.DB, cid int) (*FSDUserRecord, error) {
var realName string
var creationTime time.Time
if err := row.Scan(&cidRecord, &pwd, &rating, &realName, &creationTime); errors.Is(err, sql.ErrNoRows) {
err := row.Scan(&cidRecord, &pwd, &rating, &realName, &creationTime)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}

View File

@@ -2,11 +2,14 @@ package main
import (
"bufio"
"bytes"
"context"
"errors"
"github.com/golang-jwt/jwt/v5"
"github.com/renorris/openfsd/protocol"
"github.com/renorris/openfsd/vatsimauth"
"golang.org/x/crypto/bcrypt"
"io"
"log"
"net"
"strconv"
@@ -15,6 +18,9 @@ import (
)
type FSDClient struct {
Ctx context.Context
CancelCtx func()
Conn *net.TCPConn
Reader *bufio.Reader
@@ -38,22 +44,21 @@ type FSDClient struct {
// 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) {
// Setup reader goroutine
incomingPackets := make(chan string)
go func(ctx context.Context, packetChan chan string) {
defer close(packetChan)
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
}
@@ -61,7 +66,6 @@ func EventLoop(client *FSDClient) {
// Validate delimiter
if len(packet) < 2 || string(packet[len(packet)-2:]) != "\r\n" {
close(packetsRead)
return
}
@@ -69,78 +73,14 @@ func EventLoop(client *FSDClient) {
// (also watch for context.Done)
select {
case <-ctx.Done():
close(packetsRead)
return
case packetsRead <- packet:
case packetChan <- 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)
}(client.Ctx, incomingPackets)
// Defer "delete pilot" broadcast
defer func() {
defer func(client *FSDClient) {
deletePilotPDU := protocol.DeletePilotPDU{
From: client.Callsign,
CID: client.CID,
@@ -150,12 +90,15 @@ func EventLoop(client *FSDClient) {
mail.SetType(MailTypeBroadcastAll)
mail.AddPacket(deletePilotPDU.Serialize())
PO.SendMail([]Mail{*mail})
}()
}(client)
// Main loop
for {
select {
case packet, ok := <-packetsRead:
case <-client.Ctx.Done(): // Check for context cancel
return
case packet, ok := <-incomingPackets: // Read incoming packets
// Check if the reader closed
if !ok {
return
@@ -164,7 +107,7 @@ func EventLoop(client *FSDClient) {
// Find the processor for this packet
processor, err := GetProcessor(packet)
if err != nil {
packetsToWrite <- protocol.NewGenericFSDError(protocol.SyntaxError).Serialize()
sendSyntaxError(client.Conn)
return
}
@@ -172,7 +115,10 @@ func EventLoop(client *FSDClient) {
// Send replies to the client
for _, replyPacket := range result.Replies {
packetsToWrite <- replyPacket
err := client.writePacket(5*time.Second, replyPacket)
if err != nil {
return
}
}
// Send mail
@@ -182,17 +128,25 @@ func EventLoop(client *FSDClient) {
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:
}
case mailPacket := <-client.Mailbox: // Read incoming mail messages
err := client.writePacket(5*time.Second, mailPacket)
if err != nil {
return
}
case killPacket, ok := <-client.Kill: // Read incoming kill signals
if !ok {
return
}
// Write the kill packet
err := client.writePacket(5*time.Second, killPacket)
if err != nil {
return
}
// Close connection
return
}
}
@@ -203,13 +157,22 @@ func sendSyntaxError(conn *net.TCPConn) {
}
func HandleConnection(conn *net.TCPConn) {
defer func() {
// Set the linger value to 1 second
err := conn.SetLinger(1)
if err != nil {
log.Printf("error setting connection linger value")
return
}
// Defer connection close
defer func(conn *net.TCPConn) {
err := conn.Close()
if err != nil {
log.Printf("Error closing connection for %s\n%s", conn.RemoteAddr().String(), err.Error())
log.Println("error closing connection: " + err.Error())
}
}()
}(conn)
// Generate the initial challenge
initChallenge, err := vatsimauth.GenerateChallenge()
if err != nil {
log.Printf("Error generating challenge string:\n%s", err.Error())
@@ -224,15 +187,15 @@ func HandleConnection(conn *net.TCPConn) {
}
serverIdentPacket := serverIdentPDU.Serialize()
// The client has 2 seconds to log in
if err = conn.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
// The client has 5 seconds to log in
if err = conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
return
}
if err = conn.SetWriteDeadline(time.Now().Add(2 * time.Second)); err != nil {
if err = conn.SetWriteDeadline(time.Now().Add(5 * time.Second)); err != nil {
return
}
_, err = conn.Write([]byte(serverIdentPacket))
_, err = io.Copy(conn, bytes.NewReader([]byte(serverIdentPacket)))
if err != nil {
return
}
@@ -284,6 +247,46 @@ func HandleConnection(conn *net.TCPConn) {
return
}
// Handle authentication
var networkRating int
if SC.PlaintextPasswords { // Treat token field as a plaintext password
plaintextPassword := addPilotPDU.Token
networkRating = 0
userRecord, err := GetUserRecord(DB, clientIdentPDU.CID)
if err != nil { // Check for error
log.Printf("error fetching user record: " + err.Error())
return
}
if userRecord == nil {
conn.Write([]byte(protocol.NewGenericFSDError(protocol.InvalidLogonError).Serialize()))
return
}
if userRecord.Rating < addPilotPDU.NetworkRating {
conn.Write([]byte(protocol.NewGenericFSDError(protocol.RequestedLevelTooHighError).Serialize()))
return
}
err = bcrypt.CompareHashAndPassword([]byte(userRecord.Password), []byte(plaintextPassword))
if err != nil {
conn.Write([]byte(protocol.NewGenericFSDError(protocol.InvalidLogonError).Serialize()))
return
}
} else { // Treat token field as a JWT token
networkRating, err = verifyJWTToken(clientIdentPDU.CID, addPilotPDU.NetworkRating, addPilotPDU.Token)
if err != nil {
var fsdError *protocol.FSDError
if errors.As(err, &fsdError) {
conn.Write([]byte(fsdError.Serialize()))
} else {
conn.Write([]byte(protocol.NewGenericFSDError(protocol.InvalidLogonError).Serialize()))
}
return
}
}
// Verify callsign
switch clientIdentPDU.From {
case protocol.ServerCallsign, protocol.ClientQueryBroadcastRecipient, protocol.ClientQueryBroadcastRecipientPilots:
@@ -311,59 +314,17 @@ func HandleConnection(conn *net.TCPConn) {
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
ctx, cancelCtx := context.WithCancel(context.Background())
// Defer context close
defer func(cancelCtx func()) {
cancelCtx()
}(cancelCtx)
fsdClient := FSDClient{
Ctx: ctx,
CancelCtx: cancelCtx,
Conn: conn,
Reader: reader,
AuthVerify: &vatsimauth.VatsimAuth{},
@@ -371,7 +332,7 @@ func HandleConnection(conn *net.TCPConn) {
AuthSelf: &vatsimauth.VatsimAuth{},
Callsign: clientIdentPDU.From,
CID: clientIdentPDU.CID,
NetworkRating: int(claimedRating),
NetworkRating: networkRating,
SimulatorType: addPilotPDU.SimulatorType,
RealName: addPilotPDU.RealName,
CurrentGeohash: 0,
@@ -381,17 +342,22 @@ func HandleConnection(conn *net.TCPConn) {
}
// 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
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)
// Defer deregistration
defer func(callsign string) {
err := PO.DeregisterCallsign(callsign)
if err != nil {
log.Printf("error deregistering callsign: " + err.Error())
}
}(clientIdentPDU.From)
// Configure vatsim auth states
fsdClient.AuthSelf = vatsimauth.NewVatsimAuth(clientIdentPDU.ClientID, vatsimauth.Keys[clientIdentPDU.ClientID])
@@ -423,3 +389,72 @@ func HandleConnection(conn *net.TCPConn) {
// Start the event loop
EventLoop(&fsdClient)
}
// writePacket writes a packet to this client's connection
// timeout sets the write deadline (relative to time.Now). No deadline will be set if timeout = -1
func (c *FSDClient) writePacket(timeout time.Duration, packet string) error {
// Reset the deadline
if timeout > 0 {
err := c.Conn.SetWriteDeadline(time.Now().Add(timeout * time.Second))
if err != nil {
return err
}
}
// Attempt to write the packet
_, err := io.Copy(c.Conn, bytes.NewReader([]byte(packet)))
if err != nil {
return err
}
return nil
}
// verifyJWTToken compares the claimed fields token `token` to cid and networkRating (from the plaintext FSD packet)
// Returns the signed network rating on success
func verifyJWTToken(cid, networkRating int, token string) (signedNetworkRating int, err error) {
// Validate token signature
claims := jwt.MapClaims{}
t, err := jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) {
return JWTKey, nil
})
if err != nil {
return -1, protocol.NewGenericFSDError(protocol.InvalidLogonError)
}
// Check for expiry
exp, err := t.Claims.GetExpirationTime()
if err != nil {
return -1, protocol.NewGenericFSDError(protocol.InvalidLogonError)
}
if time.Now().After(exp.Time) {
return -1, protocol.NewGenericFSDError(protocol.InvalidLogonError)
}
// Verify claimed CID
claimedCID, err := claims.GetSubject()
if err != nil {
return -1, protocol.NewGenericFSDError(protocol.InvalidLogonError)
}
cidInt, err := strconv.Atoi(claimedCID)
if err != nil {
return -1, errors.Join(errors.New("error parsing CID"))
}
if cidInt != cid {
return -1, protocol.NewGenericFSDError(protocol.InvalidLogonError)
}
// Verify controller rating
claimedRating, ok := claims["controller_rating"].(float64)
if !ok {
return -1, protocol.NewGenericFSDError(protocol.InvalidLogonError)
}
if networkRating > int(claimedRating) {
return -1, protocol.NewGenericFSDError(protocol.RequestedLevelTooHighError)
}
return int(claimedRating), nil
}

View File

@@ -28,11 +28,12 @@ func addUserToDatabase(t *testing.T, cid int, password string, rating int) {
func TestFSDClientLogin(t *testing.T) {
// Setup config for testing environment
SC = &ServerConfig{
FsdListenAddr: "localhost:6809",
HttpListenAddr: "localhost:9086",
HttpsEnabled: false,
DatabaseFile: "./test.db",
MOTD: "motd line 1\nmotd line 2",
FsdListenAddr: "localhost:6809",
HttpListenAddr: "localhost:9086",
HttpsEnabled: false,
DatabaseFile: "./test.db",
MOTD: "motd line 1\nmotd line 2",
PlaintextPasswords: false,
}
// Delete any existing database file so a new one is created
@@ -439,58 +440,6 @@ func TestFSDClientLogin(t *testing.T) {
conn.Close()
}
// Test jibberish token
{
conn, err := net.Dial("tcp", "localhost:6809")
assert.Nil(t, err)
err = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
assert.Nil(t, err)
reader := bufio.NewReader(conn)
serverIdent, err := reader.ReadString('\n')
assert.Nil(t, err)
assert.NotEmpty(t, serverIdent)
assert.True(t, strings.HasPrefix(serverIdent, "$DISERVER:CLIENT:"))
clientIdentPDU := protocol.ClientIdentificationPDU{
From: "N123",
To: "SERVER",
ClientID: 35044,
ClientName: "vPilot",
MajorVersion: 3,
MinorVersion: 8,
CID: 1000000,
SysUID: -99999,
InitialChallenge: "0123456789abcdef",
}
addPilotPDU := protocol.AddPilotPDU{
From: "N123",
To: "SERVER",
CID: 1000000,
Token: "garbage",
NetworkRating: 1,
ProtocolRevision: 101,
SimulatorType: 2,
RealName: "real name",
}
_, err = conn.Write([]byte(clientIdentPDU.Serialize()))
assert.Nil(t, err)
_, err = conn.Write([]byte(addPilotPDU.Serialize()))
assert.Nil(t, err)
responseMsg, err := reader.ReadString('\n')
assert.Nil(t, err)
assert.NotEmpty(t, serverIdent)
expectedPDU := protocol.NewGenericFSDError(protocol.SyntaxError)
assert.Equal(t, expectedPDU.Serialize(), responseMsg)
conn.Close()
}
// Test token with invalid signature
{
// Fake jwt

16
go.mod
View File

@@ -1,26 +1,26 @@
module github.com/renorris/openfsd
go 1.22.2
go 1.23
require (
github.com/go-playground/validator/v10 v10.19.0
github.com/go-playground/validator/v10 v10.22.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/mattn/go-sqlite3 v1.14.22
github.com/mmcloughlin/geohash v0.10.0
github.com/sethvargo/go-envconfig v1.0.1
github.com/sethvargo/go-envconfig v1.1.0
github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.22.0
golang.org/x/crypto v0.25.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

25
go.sum
View File

@@ -2,14 +2,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@@ -22,23 +24,26 @@ github.com/mmcloughlin/geohash v0.10.0 h1:9w1HchfDfdeLc+jFEf/04D27KP7E2QmpDu52wP
github.com/mmcloughlin/geohash v0.10.0/go.mod h1:oNZxQo5yWJh0eMQEP/8hwQuVx9Z9tjwFUqcTB1SmG0c=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sethvargo/go-envconfig v1.0.1 h1:9wglip/5fUfaH0lQecLM8AyOClMw0gT0A9K2c2wozao=
github.com/sethvargo/go-envconfig v1.0.1/go.mod h1:OKZ02xFaD3MvWBBmEW45fQr08sJEsonGrrOdicvQmQA=
github.com/sethvargo/go-envconfig v1.1.0 h1:cWZiJxeTm7AlCvzGXrEXaSTCNgip5oJepekh/BOQuog=
github.com/sethvargo/go-envconfig v1.1.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -4,7 +4,6 @@ import (
"bytes"
"context"
"crypto/rand"
"database/sql"
_ "embed"
"encoding/base64"
"encoding/json"
@@ -67,13 +66,13 @@ func fsdJwtApiHandler(w http.ResponseWriter, r *http.Request) {
}
userRecord, userRecordErr := GetUserRecord(DB, cid)
if userRecordErr != nil && !errors.Is(userRecordErr, sql.ErrNoRows) {
if userRecordErr != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
// If user not found
if errors.Is(userRecordErr, sql.ErrNoRows) {
if userRecord == nil {
jwtResponse := JwtResponse{
Success: false,
Token: "",
@@ -228,6 +227,10 @@ func userAPIHandler(w http.ResponseWriter, r *http.Request) {
userRecord, err := GetUserRecord(DB, cid)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if userRecord == nil {
res := UserApiResponse{
Success: false,
Message: "Error: user not found",
@@ -321,15 +324,6 @@ func userAPIHandler(w http.ResponseWriter, r *http.Request) {
return
}
if len(req.Password) > 0 {
bcryptBytes, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
req.Password = string(bcryptBytes)
}
err = UpdateUserRecord(DB, &req)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
@@ -421,6 +415,14 @@ func dashboardHandler(w http.ResponseWriter, r *http.Request) {
userRecord, err := GetUserRecord(DB, cid)
if err != nil {
w.Header().Add("Content-Type", "text/plain")
w.Header().Add("WWW-Authenticate", `Basic realm="dashboard", charset="UTF-8"`)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("internal server error"))
return
}
if userRecord == nil {
w.Header().Add("Content-Type", "text/plain")
w.Header().Add("WWW-Authenticate", `Basic realm="dashboard", charset="UTF-8"`)
w.WriteHeader(http.StatusUnauthorized)

15
main.go
View File

@@ -20,13 +20,14 @@ import (
)
type ServerConfig struct {
FsdListenAddr string `env:"FSD_ADDR, default=0.0.0.0:6809"`
HttpListenAddr string `env:"HTTP_ADDR, default=0.0.0.0:9086"`
HttpsEnabled bool `env:"HTTPS_ENABLED, default=false"`
TLSCertFile string `env:"TLS_CERT_FILE"`
TLSKeyFile string `env:"TLS_KEY_FILE"`
DatabaseFile string `env:"DATABASE_FILE, default=./db/fsd.db"`
MOTD string `env:"MOTD, default=openfsd"`
FsdListenAddr string `env:"FSD_ADDR, default=0.0.0.0:6809"`
HttpListenAddr string `env:"HTTP_ADDR, default=0.0.0.0:9086"`
HttpsEnabled bool `env:"HTTPS_ENABLED, default=false"`
TLSCertFile string `env:"TLS_CERT_FILE"`
TLSKeyFile string `env:"TLS_KEY_FILE"`
DatabaseFile string `env:"DATABASE_FILE, default=./db/fsd.db"`
MOTD string `env:"MOTD, default=openfsd"`
PlaintextPasswords bool `env:"PLAINTEXT_PASSWORDS, default=false"`
}
var SC *ServerConfig

View File

@@ -52,7 +52,7 @@ func (m *Mail) AddRecipient(callsign string) {
func (m *Mail) AddPacket(packet string) {
if m.Packets == nil {
m.Packets = make([]string, 0)
m.Packets = make([]string, 0, 1)
}
m.Packets = append(m.Packets, packet)
}
@@ -113,7 +113,7 @@ func (p *PostOffice) DeregisterCallsign(callsign string) error {
}
// Remove client from geohash registry
p.removeClientFromGeohashRegistry(client, client.CurrentGeohash)
p.removeClientFromGeohashRegistry(client)
return nil
}
@@ -182,11 +182,10 @@ func (p *PostOffice) SendMail(messages []Mail) {
}
}
}
}
}
func (p *PostOffice) removeClientFromGeohashRegistry(client *FSDClient, hash uint64) {
func (p *PostOffice) removeClientFromGeohashRegistry(client *FSDClient) {
clientList, ok := p.geohashRegistry[client.CurrentGeohash]
if !ok {
return
@@ -226,7 +225,7 @@ func (p *PostOffice) SetLocation(client *FSDClient, lat, lng float64) {
defer p.lock.Unlock()
// Remove client from old geohash bucket
p.removeClientFromGeohashRegistry(client, client.CurrentGeohash)
p.removeClientFromGeohashRegistry(client)
// Find the new client list
newClientList, ok := p.geohashRegistry[hash]

View File

@@ -104,7 +104,7 @@ func GetProcessor(rawPacket string) (Processor, error) {
return PlaneInfoResponseProcessor, nil
}
case "#TM":
if len(fields) > 3 {
if len(fields) < 3 {
return nil, InvalidPacketError
}
switch fields[1] {

View File

@@ -7,13 +7,13 @@ import (
)
type AddPilotPDU struct {
From string `validate:"required,alphanum,max=7"`
To string `validate:"required,alphanum,max=7"`
From string `validate:"required,alphanum,max=16"`
To string `validate:"required,alphanum,max=16"`
CID int `validate:"required,min=100000,max=9999999"`
Token string `validate:"required,jwt"`
NetworkRating int `validate:"min=1,max=12"`
Token string `validate:"required"`
NetworkRating int `validate:"min=1,max=16"`
ProtocolRevision int `validate:""`
SimulatorType int `validate:"min=0,max=6"`
SimulatorType int `validate:"min=0,max=32"`
RealName string `validate:"required,max=32"`
}
@@ -21,8 +21,8 @@ func (p *AddPilotPDU) Serialize() string {
return fmt.Sprintf("#AP%s:%s:%d:%s:%d:%d:%d:%s%s", p.From, p.To, p.CID, p.Token, p.NetworkRating, p.ProtocolRevision, p.SimulatorType, p.RealName, PacketDelimeter)
}
func ParseAddPilotPDU(rawPacket string) (*AddPilotPDU, error) {
rawPacket = strings.TrimSuffix(rawPacket, PacketDelimeter)
func ParseAddPilotPDU(packet string) (*AddPilotPDU, error) {
rawPacket := strings.TrimSuffix(string(packet), PacketDelimeter)
rawPacket = strings.TrimPrefix(rawPacket, "#AP")
fields := strings.Split(rawPacket, Delimeter)
if len(fields) != 8 {

View File

@@ -9,10 +9,10 @@ import (
)
type ClientIdentificationPDU struct {
From string `validate:"required,alphanum,max=7"`
To string `validate:"required,alphanum,max=7"`
From string `validate:"required,alphanum,max=16"`
To string `validate:"required,alphanum,max=16"`
ClientID uint16 `validate:"required"`
ClientName string `validate:"required,alphanum,max=16"`
ClientName string `validate:"required,max=32"`
MajorVersion int `validate:""`
MinorVersion int `validate:""`
CID int `validate:"required,min=100000,max=9999999"`