mirror of
https://github.com/renorris/openfsd
synced 2026-03-22 06:25:35 +08:00
177 lines
4.7 KiB
Go
177 lines
4.7 KiB
Go
package fsd
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/renorris/openfsd/db"
|
|
"log/slog"
|
|
"maps"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// runServiceHTTP starts the admin service HTTP server used for
|
|
// internal communication between the API HTTP server and this FSD server.
|
|
func (s *Server) runServiceHTTP(ctx context.Context) {
|
|
e := s.setupRoutes()
|
|
if err := e.Run(s.cfg.ServiceHTTPListenAddr); err != nil {
|
|
slog.Error(err.Error())
|
|
}
|
|
}
|
|
|
|
func (s *Server) setupRoutes() (e *gin.Engine) {
|
|
e = gin.New()
|
|
|
|
// Verify administrator service JWT
|
|
e.Use(s.authMiddleware)
|
|
e.GET("/online_users", s.handleGetOnlineUsers)
|
|
e.POST("/kick_user", s.handleKickUser)
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Server) authMiddleware(c *gin.Context) {
|
|
authHeader, found := strings.CutPrefix(c.GetHeader("Authorization"), "Bearer ")
|
|
if !found {
|
|
c.AbortWithStatus(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
jwtSecret, err := s.dbRepo.ConfigRepo.Get(db.ConfigJwtSecretKey)
|
|
if err != nil {
|
|
slog.Error(err.Error())
|
|
c.AbortWithStatus(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
accessToken, err := ParseJwtToken(authHeader, []byte(jwtSecret))
|
|
if err != nil {
|
|
c.AbortWithStatus(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
claims := accessToken.CustomClaims()
|
|
if claims.TokenType != "fsd_service" || claims.NetworkRating < NetworkRatingAdministator {
|
|
c.AbortWithStatus(http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
c.Next()
|
|
}
|
|
|
|
type OnlineUserGeneralData struct {
|
|
Callsign string `json:"callsign"`
|
|
CID int `json:"cid"`
|
|
Name string `json:"name"`
|
|
NetworkRating int `json:"network_rating"`
|
|
MaxNetworkRating int `json:"max_network_rating"`
|
|
Latitude float64 `json:"latitude"`
|
|
Longitude float64 `json:"longitude"`
|
|
LogonTime time.Time `json:"logon_time"`
|
|
LastUpdated time.Time `json:"last_updated"`
|
|
}
|
|
|
|
type OnlineUserPilot struct {
|
|
OnlineUserGeneralData
|
|
Altitude int `json:"altitude"`
|
|
Groundspeed int `json:"groundspeed"`
|
|
Heading int `json:"heading"`
|
|
Transponder string `json:"transponder"`
|
|
}
|
|
|
|
type OnlineUserATC struct {
|
|
OnlineUserGeneralData
|
|
Frequency string `json:"frequency"`
|
|
Facility int `json:"facility"`
|
|
VisRange int `json:"visual_range"`
|
|
}
|
|
|
|
type OnlineUsersResponseData struct {
|
|
Pilots []OnlineUserPilot `json:"pilots"`
|
|
ATC []OnlineUserATC `json:"atc"`
|
|
}
|
|
|
|
func (s *Server) handleGetOnlineUsers(c *gin.Context) {
|
|
s.postOffice.clientMapLock.RLock()
|
|
mapLen := len(s.postOffice.clientMap)
|
|
s.postOffice.clientMapLock.RUnlock()
|
|
|
|
clientMap := make(map[string]*Client, mapLen+16)
|
|
|
|
s.postOffice.clientMapLock.RLock()
|
|
maps.Copy(clientMap, s.postOffice.clientMap)
|
|
s.postOffice.clientMapLock.RUnlock()
|
|
|
|
resData := OnlineUsersResponseData{
|
|
Pilots: make([]OnlineUserPilot, 0, 512),
|
|
ATC: make([]OnlineUserATC, 0, 128),
|
|
}
|
|
|
|
for _, client := range clientMap {
|
|
latLon := client.latLon()
|
|
genData := OnlineUserGeneralData{
|
|
Callsign: client.callsign,
|
|
CID: client.cid,
|
|
Name: client.realName,
|
|
NetworkRating: int(client.networkRating),
|
|
MaxNetworkRating: int(client.maxNetworkRating),
|
|
Latitude: latLon[0],
|
|
Longitude: latLon[1],
|
|
LogonTime: client.loginTime,
|
|
LastUpdated: client.lastUpdated.Load(),
|
|
}
|
|
|
|
if client.isAtc {
|
|
atc := OnlineUserATC{
|
|
OnlineUserGeneralData: genData,
|
|
Frequency: client.frequency.Load(),
|
|
Facility: client.facilityType,
|
|
VisRange: int(client.visRange.Load() * 0.000539957), // Convert meters to nautical miles
|
|
}
|
|
resData.ATC = append(resData.ATC, atc)
|
|
} else {
|
|
pilot := OnlineUserPilot{
|
|
OnlineUserGeneralData: genData,
|
|
Altitude: int(client.altitude.Load()),
|
|
Groundspeed: int(client.groundspeed.Load()),
|
|
Heading: int(client.heading.Load()),
|
|
Transponder: client.transponder.Load(),
|
|
}
|
|
resData.Pilots = append(resData.Pilots, pilot)
|
|
}
|
|
}
|
|
|
|
c.Writer.Header().Set("Content-Type", "application/json")
|
|
c.Writer.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(c.Writer).Encode(&resData)
|
|
}
|
|
|
|
func (s *Server) handleKickUser(c *gin.Context) {
|
|
type RequestBody struct {
|
|
Callsign string `json:"callsign" binding:"required"`
|
|
}
|
|
|
|
var reqBody RequestBody
|
|
if err := c.ShouldBindJSON(&reqBody); err != nil {
|
|
c.AbortWithStatus(http.StatusBadRequest)
|
|
}
|
|
|
|
client, err := s.postOffice.find(reqBody.Callsign)
|
|
if err != nil {
|
|
if !errors.Is(err, ErrCallsignDoesNotExist) {
|
|
c.AbortWithStatus(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
c.AbortWithStatus(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Cancelling the context will cause the client's event loop to close
|
|
client.cancelCtx()
|
|
|
|
c.AbortWithStatus(http.StatusNoContent)
|
|
}
|