mirror of
https://github.com/renorris/openfsd
synced 2026-03-22 06:25:35 +08:00
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.)
This commit is contained in:
@@ -18,7 +18,7 @@ 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 |
|
||||
@@ -26,7 +26,7 @@ The server is configured via environment variables:
|
||||
| `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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
331
fsd_client.go
331
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:
|
||||
|
||||
case mailPacket := <-client.Mailbox: // Read incoming mail messages
|
||||
err := client.writePacket(5*time.Second, mailPacket)
|
||||
if err != nil {
|
||||
return
|
||||
case mailPacket := <-client.Mailbox:
|
||||
packetsToWrite <- mailPacket
|
||||
case s, ok := <-client.Kill:
|
||||
if ok {
|
||||
select {
|
||||
case packetsToWrite <- s:
|
||||
default:
|
||||
}
|
||||
|
||||
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,8 +342,7 @@ func HandleConnection(conn *net.TCPConn) {
|
||||
}
|
||||
|
||||
// Register callsign to the post office. End the connection if callsign already exists
|
||||
{
|
||||
err := PO.RegisterCallsign(clientIdentPDU.From, &fsdClient)
|
||||
err = PO.RegisterCallsign(clientIdentPDU.From, &fsdClient)
|
||||
if err != nil {
|
||||
if errors.Is(err, CallsignAlreadyRegisteredError) {
|
||||
pdu := protocol.NewGenericFSDError(protocol.CallsignInUseError)
|
||||
@@ -390,8 +350,14 @@ func HandleConnection(conn *net.TCPConn) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Defer deregistration
|
||||
defer func(callsign string) {
|
||||
err := PO.DeregisterCallsign(callsign)
|
||||
if err != nil {
|
||||
log.Printf("error deregistering callsign: " + err.Error())
|
||||
}
|
||||
defer PO.DeregisterCallsign(clientIdentPDU.From)
|
||||
}(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
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ func TestFSDClientLogin(t *testing.T) {
|
||||
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
16
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
|
||||
)
|
||||
|
||||
25
go.sum
25
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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
1
main.go
1
main.go
@@ -27,6 +27,7 @@ type ServerConfig struct {
|
||||
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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user