From de94e668f08f181350ff138895915603f88fcc69 Mon Sep 17 00:00:00 2001 From: Reese Norris Date: Sat, 14 Sep 2024 12:20:34 -0700 Subject: [PATCH] update 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.) --- README.md | 20 +- database_util.go | 8 +- fsd_client.go | 347 ++++++++++++++++-------------- fsd_client_login_test.go | 63 +----- go.mod | 16 +- go.sum | 25 ++- http_server.go | 26 +-- main.go | 15 +- post_office.go | 9 +- processor.go | 2 +- protocol/add_pilot.go | 14 +- protocol/client_identification.go | 6 +- 12 files changed, 274 insertions(+), 277 deletions(-) diff --git a/README.md b/README.md index d68a672..0c70627 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/database_util.go b/database_util.go index 97e5151..120b6ac 100644 --- a/database_util.go +++ b/database_util.go @@ -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 } diff --git a/fsd_client.go b/fsd_client.go index 1caf359..e90bbee 100644 --- a/fsd_client.go +++ b/fsd_client.go @@ -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 +} diff --git a/fsd_client_login_test.go b/fsd_client_login_test.go index a57053c..7b2213c 100644 --- a/fsd_client_login_test.go +++ b/fsd_client_login_test.go @@ -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 diff --git a/go.mod b/go.mod index 1d8853a..a46b4ac 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 84cc932..a7d7277 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/http_server.go b/http_server.go index ba2156d..6d536e0 100644 --- a/http_server.go +++ b/http_server.go @@ -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) diff --git a/main.go b/main.go index 08d1a4a..936007c 100644 --- a/main.go +++ b/main.go @@ -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 diff --git a/post_office.go b/post_office.go index f28abd5..fe35c62 100644 --- a/post_office.go +++ b/post_office.go @@ -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] diff --git a/processor.go b/processor.go index c7486ea..9f8da4c 100644 --- a/processor.go +++ b/processor.go @@ -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] { diff --git a/protocol/add_pilot.go b/protocol/add_pilot.go index 76c54ea..fb399bb 100644 --- a/protocol/add_pilot.go +++ b/protocol/add_pilot.go @@ -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 { diff --git a/protocol/client_identification.go b/protocol/client_identification.go index e6c5e9a..d5223dd 100644 --- a/protocol/client_identification.go +++ b/protocol/client_identification.go @@ -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"`