backend/servers/servers.go
2024-05-14 15:12:18 +03:00

391 lines
11 KiB
Go

package servers
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"time"
"acooldomain.co/backend/auth"
"acooldomain.co/backend/models"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"github.com/gin-gonic/gin"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
type Connection struct {
connection *mongo.Client
apiClient *client.Client
}
type ContainerLabels struct {
OwnerId string `json:"user_id"`
ImageId string `json:"image_id"`
VolumeId string `json:"volume_id"`
Type string `json:"type"`
}
type VolumeLabels struct {
OwnerId string `json:"user_id"`
ImageId string `json:"image_id"`
Type string `json:"type"`
}
type ImageLabels struct {
Type string `json:"type"`
}
func (con Connection) getServerInfo(volume volume.Volume) (*models.ServerInfo, error) {
var volumeLabels VolumeLabels
jsonData, err := json.Marshal(volume.Labels)
if err != nil {
return nil, err
}
json.Unmarshal(jsonData, &volumeLabels)
imageList, err := con.apiClient.ImageList(context.Background(), image.ListOptions{Filters: filters.NewArgs(filters.Arg("reference", volumeLabels.ImageId))})
if len(imageList) == 0 {
return nil, fmt.Errorf("ImageId %s does not exist", volumeLabels.ImageId)
}
imageSummery := imageList[0]
imageInspect, _, err := con.apiClient.ImageInspectWithRaw(context.Background(), imageSummery.ID)
if err != nil {
return nil, err
}
var imagePorts []models.Port = make([]models.Port, len(imageInspect.Config.ExposedPorts))
i := 0
for imagePort := range imageInspect.Config.ExposedPorts {
imagePorts[i] = models.Port{Protocol: imagePort.Proto(), Number: imagePort.Int()}
i += 1
}
imageNameAndVersion := strings.Split(volumeLabels.ImageId, ":")
imageName := imageNameAndVersion[0]
imageVersion := imageNameAndVersion[1]
containers, err := con.apiClient.ContainerList(context.Background(), container.ListOptions{
All: true,
Filters: filters.NewArgs(filters.Arg("label", "type=GAME"), filters.Arg("label", fmt.Sprintf("volume_id=%s", volume.Name))),
})
var state bool
var ports []models.Port
if err != nil || len(containers) == 0 {
state = false
ports = nil
} else {
container := containers[0]
state = container.State == "running"
ports = transformContainerPortsToModel(container.Ports)
}
var serverData models.ServerData
con.connection.Database("backend").Collection("servers").FindOne(context.TODO(), bson.D{{Key: "volume_id", Value: volume.Name}}).Decode(&serverData)
serverInfo := models.ServerInfo{
Id: volume.Name,
Image: models.ImageInfo{
Name: imageName,
Version: imageVersion,
Ports: imagePorts,
},
OwnerId: volumeLabels.OwnerId,
On: state,
Ports: ports,
Nickname: serverData.Nickname,
}
return &serverInfo, nil
}
func (con Connection) getServerInfoFromId(ServerId string) (*models.ServerInfo, error) {
volume, err := con.apiClient.VolumeInspect(context.Background(), ServerId)
if err != nil {
return nil, err
}
return con.getServerInfo(volume)
}
func transformContainerPortsToModel(ports []types.Port) []models.Port {
modelPorts := make([]models.Port, len(ports))
for index, port := range ports {
modelPorts[index] = models.Port{
Number: int(port.PublicPort),
Protocol: port.Type,
}
}
return modelPorts
}
func convertLabelsToMap(v any) (map[string]string, error) {
data, err := json.Marshal(v)
if err != nil {
return nil, err
}
x := map[string]string{}
json.Unmarshal(data, &x)
return x, nil
}
type CreateServerRequest struct {
ImageId string `json:"image_id"`
}
func (con Connection) CreateServer(ctx *gin.Context) {
claims, exists := ctx.Get("claims")
if !exists {
ctx.AbortWithStatus(500)
return
}
var request CreateServerRequest
err := json.NewDecoder(ctx.Request.Body).Decode(&request)
if err != nil {
ctx.AbortWithError(400, err)
return
}
imageList, err := con.apiClient.ImageList(context.Background(), image.ListOptions{All: true, Filters: filters.NewArgs(filters.Arg("reference", request.ImageId))})
if err != nil {
ctx.AbortWithError(400, err)
return
}
if len(imageList) == 0 {
ctx.AbortWithStatusJSON(404, "imageNotFound")
return
}
imageSummary := imageList[0]
labels, err := convertLabelsToMap(VolumeLabels{OwnerId: claims.(*auth.AuthClaims).Username, ImageId: imageSummary.RepoTags[0], Type: "GAME"})
if err != nil {
ctx.AbortWithError(400, err)
return
}
volumeResponse, err := con.apiClient.VolumeCreate(context.Background(), volume.CreateOptions{
Labels: labels,
})
if err != nil {
ctx.AbortWithError(500, err)
return
}
ctx.JSON(200, volumeResponse.Name)
}
func (con Connection) StartServer(ctx *gin.Context) {
serverId := ctx.Param("server_id")
claims, exists := ctx.Get("claims")
if !exists {
ctx.AbortWithStatus(403)
return
}
// command := ctx.Param("command")
serverInfo, err := con.getServerInfoFromId(serverId)
if err != nil {
ctx.AbortWithError(500, err)
return
}
if serverInfo.On {
ctx.Status(200)
return
}
imageId := serverInfo.Image.Name + ":" + serverInfo.Image.Version
labels := ContainerLabels{
OwnerId: claims.(*auth.AuthClaims).Username,
ImageId: imageId,
VolumeId: serverInfo.Id,
Type: "GAME",
}
jsonString, err := json.Marshal(labels)
if err != nil {
ctx.AbortWithError(500, err)
return
}
jsonLabels := make(map[string]string)
json.Unmarshal(jsonString, &jsonLabels)
volumes := make(map[string]struct{})
image, _, err := con.apiClient.ImageInspectWithRaw(context.Background(), imageId)
if err != nil {
ctx.AbortWithError(500, err)
return
}
var portMapping nat.PortMap = make(nat.PortMap)
for _, port := range serverInfo.Image.Ports {
dockerPort, err := nat.NewPort(port.Protocol, fmt.Sprint(port.Number))
if err != nil {
ctx.AbortWithError(500, err)
return
}
portMapping[dockerPort] = []nat.PortBinding{{HostIP: "0.0.0.0"}}
}
response, err := con.apiClient.ContainerCreate(
context.Background(),
&container.Config{
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Tty: true,
OpenStdin: false,
StdinOnce: false,
Image: imageId,
Volumes: volumes,
Labels: jsonLabels,
},
&container.HostConfig{
AutoRemove: true,
Mounts: []mount.Mount{{Source: serverInfo.Id, Target: image.Config.WorkingDir, Type: "volume"}},
PortBindings: portMapping,
},
&network.NetworkingConfig{},
&v1.Platform{},
"",
)
if err != nil {
ctx.AbortWithError(500, err)
return
}
if err := con.apiClient.ContainerStart(ctx, response.ID, container.StartOptions{}); err != nil {
ctx.AbortWithError(500, err)
return
}
UPNPPath, exists := os.LookupEnv("UPNP_PATH")
HostIP, hostIPexists := os.LookupEnv("HOST_IP")
if exists && hostIPexists {
time.Sleep(time.Millisecond * 100)
containerData, err := con.apiClient.ContainerInspect(context.Background(), response.ID)
if err != nil {
ctx.AbortWithError(500, err)
return
}
fo, err := os.OpenFile(UPNPPath, os.O_APPEND, os.ModeAppend)
if err != nil {
panic(err)
}
defer func() {
if err := fo.Close(); err != nil {
panic(err)
}
}()
for containerPort, portBindings := range containerData.NetworkSettings.Ports {
for _, hostPort := range portBindings {
number, proto := hostPort.HostPort, containerPort.Proto()
_, err := fo.Write([]byte(fmt.Sprintf("%s|%s|%s|%s|%s\n", HostIP, number, "0.0.0.0", number, strings.ToUpper(proto))))
if err != nil {
ctx.AbortWithError(500, err)
return
}
}
}
}
ctx.JSON(200, response.ID)
}
func (con Connection) GetServers(ctx *gin.Context) {
volumes, err := con.apiClient.VolumeList(
context.TODO(),
volume.ListOptions{
Filters: filters.NewArgs(filters.Arg("label", "type=GAME")),
},
)
if err != nil {
ctx.AbortWithError(500, err)
}
var servers []models.ServerInfo
for _, volume := range volumes.Volumes {
serverInfo, err := con.getServerInfo(*volume)
if err != nil {
continue
}
servers = append(servers, *serverInfo)
}
if err != nil {
ctx.AbortWithError(500, err)
}
ctx.JSON(200, servers)
}
func (con Connection) StopServer(ctx *gin.Context) {
serverId := ctx.Param("server_id")
containersList, err := con.apiClient.ContainerList(context.Background(), container.ListOptions{
Filters: filters.NewArgs(filters.Arg("label", "volume_id="+serverId), filters.Arg("label", "type=GAME")),
})
if err != nil {
ctx.AbortWithError(500, err)
return
}
if len(containersList) == 0 {
ctx.Status(200)
return
}
for _, containerData := range containersList {
con.apiClient.ContainerStop(context.Background(), containerData.ID, container.StopOptions{})
}
ctx.Status(200)
}
func (con Connection) serverAuthorized(permissions models.Permission) func(*gin.Context) bool {
return func(ctx *gin.Context) bool {
claims, exists := ctx.Get("claims")
if !exists {
return false
}
server_id := ctx.Param("server_id")
if server_id == "" {
return false
}
var serverData models.ServerData
con.connection.Database("Backend").Collection("Servers").FindOne(context.Background(), bson.D{{Key: "ServerId", Value: server_id}}).Decode(&serverData)
if serverData.OwnerId == claims.(*auth.AuthClaims).Username {
return true
}
userPermissions := serverData.UserPermissions[claims.(*auth.AuthClaims).Username]
if userPermissions&permissions == permissions || userPermissions&models.Admin == models.Admin {
return true
}
return false
}
}
func LoadGroup(group *gin.RouterGroup, mongo_client *mongo.Client) {
apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}
defer apiClient.Close()
connection := Connection{connection: mongo_client, apiClient: apiClient}
group.POST("/:server_id/start", auth.AuthorizedTo(models.Start, connection.serverAuthorized(models.Start)), connection.StartServer)
group.POST("/", auth.AuthorizedTo(models.Create), connection.CreateServer)
group.GET("/", auth.AuthorizedTo(0), connection.GetServers)
group.POST("/:server_id/stop", auth.AuthorizedTo(models.Stop, connection.serverAuthorized(models.Stop)), connection.StopServer)
}