Initial commit

This commit is contained in:
Chris Marshall
2020-08-02 13:23:17 -04:00
committed by chris
commit ffca54fdb8
60 changed files with 34080 additions and 0 deletions

View File

@ -0,0 +1,116 @@
package client
import (
"bytes"
"context"
"fmt"
"log"
"net"
"net/http"
"time"
"github.com/gorilla/websocket"
)
var DefaultUpgrader = &websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type WebsocketUDPProxy struct {
Upgrader *websocket.Upgrader
addr net.Addr
}
func NewProxy(addr string) (*WebsocketUDPProxy, error) {
raddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
return &WebsocketUDPProxy{addr: raddr}, nil
}
func (w *WebsocketUDPProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
upgrader := w.Upgrader
if w.Upgrader == nil {
upgrader = DefaultUpgrader
}
upgradeHeader := http.Header{}
if hdr := req.Header.Get("Sec-Websocket-Protocol"); hdr != "" {
upgradeHeader.Set("Sec-Websocket-Protocol", hdr)
}
ws, err := upgrader.Upgrade(rw, req, upgradeHeader)
if err != nil {
log.Printf("wsproxy: couldn't upgrade %v", err)
return
}
defer ws.Close()
backend, err := net.ListenPacket("udp", "0.0.0.0:0")
if err != nil {
return
}
defer backend.Close()
errc := make(chan error, 1)
go func() {
for {
_, msg, err := ws.ReadMessage()
if err != nil {
m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err))
if e, ok := err.(*websocket.CloseError); ok {
if e.Code != websocket.CloseNoStatusReceived {
m = websocket.FormatCloseMessage(e.Code, e.Text)
}
}
errc <- err
ws.WriteMessage(websocket.CloseMessage, m)
return
}
if bytes.HasPrefix(msg, []byte("\xff\xff\xff\xffport")) {
continue
}
if err := backend.SetWriteDeadline(time.Now().Add(5 * time.Second)); err != nil {
errc <- err
return
}
_, err = backend.WriteTo(msg, w.addr)
if err != nil {
errc <- err
return
}
}
}()
go func() {
buffer := make([]byte, 1024*1024)
for {
n, _, err := backend.ReadFrom(buffer)
if err != nil {
errc <- err
return
}
if err := ws.WriteMessage(websocket.BinaryMessage, buffer[:n]); err != nil {
errc <- err
return
}
}
}()
select {
case err = <-errc:
if e, ok := err.(*websocket.CloseError); !ok || e.Code == websocket.CloseAbnormalClosure {
log.Printf("wsproxy: %v", err)
}
case <-ctx.Done():
return
}
}

View File

@ -0,0 +1,130 @@
package client
import (
"bytes"
"html/template"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type Config struct {
ContentServerURL string
ServerAddr string
Files http.FileSystem
}
func NewRouter(cfg *Config) (*echo.Echo, error) {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept},
}))
f, err := cfg.Files.Open("index.html")
if err != nil {
return nil, err
}
defer f.Close()
data, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
templates, err := template.New("index").Parse(string(data))
if err != nil {
return nil, err
}
e.Renderer = &TemplateRenderer{templates}
// default route
e.GET("/", func(c echo.Context) error {
return c.Render(http.StatusOK, "index", map[string]string{
"ServerAddr": cfg.ServerAddr,
})
})
raddr, err := net.ResolveUDPAddr("udp", cfg.ServerAddr)
if err != nil {
return nil, err
}
e.GET("/info", func(c echo.Context) error {
conn, err := net.ListenPacket("udp", "0.0.0.0:0")
if err != nil {
return err
}
defer conn.Close()
buffer := make([]byte, 1024*1024)
if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
return err
}
n, err := conn.WriteTo([]byte("\xff\xff\xff\xffgetinfo xxx"), raddr)
if err != nil {
return err
}
n, _, err = conn.ReadFrom(buffer)
if err != nil {
return err
}
resp := buffer[:n]
resp = bytes.TrimPrefix(resp, []byte("\xff\xff\xff\xffinfoResponse\n\\"))
resp = bytes.TrimSuffix(resp, []byte("\\xxx"))
parts := bytes.Split(resp, []byte("\\"))
m := make(map[string]string)
for i := 0; i < len(parts)-1; i += 2 {
m[string(parts[i])] = string(parts[i+1])
}
return c.JSON(http.StatusOK, m)
})
// static files
e.GET("/*", echo.WrapHandler(http.FileServer(cfg.Files)))
// Quake3 assets requests must be proxied to the content server. The host
// header is manipulated to ensure that services like CloudFlare will not
// reject requests based upon incorrect host header.
csurl, err := url.Parse(cfg.ContentServerURL)
if err != nil {
return nil, err
}
g := e.Group("/assets")
g.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{
Balancer: middleware.NewRoundRobinBalancer([]*middleware.ProxyTarget{
{URL: csurl},
}),
Transport: &HostHeaderTransport{RoundTripper: http.DefaultTransport, Host: csurl.Host},
}))
return e, nil
}
type HostHeaderTransport struct {
http.RoundTripper
Host string
}
func (t *HostHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.Host = t.Host
return t.RoundTripper.RoundTrip(req)
}
type TemplateRenderer struct {
*template.Template
}
func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.ExecuteTemplate(w, name, data)
}

View File

@ -0,0 +1,58 @@
package client
import (
"net"
"net/http"
"time"
"github.com/cockroachdb/cmux"
)
type Server struct {
Addr string
Handler http.Handler
ServerAddr string
}
func (s *Server) Serve(l net.Listener) error {
m := cmux.New(l)
websocketL := m.Match(cmux.HTTP1HeaderField("Upgrade", "websocket"))
httpL := m.Match(cmux.Any())
go func() {
s := &http.Server{
Addr: s.Addr,
Handler: s.Handler,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
if err := s.Serve(httpL); err != cmux.ErrListenerClosed {
panic(err)
}
}()
wsproxy, err := NewProxy(s.ServerAddr)
if err != nil {
return err
}
go func() {
s := &http.Server{
Handler: wsproxy,
}
if err := s.Serve(websocketL); err != cmux.ErrListenerClosed {
panic(err)
}
}()
return m.Serve()
}
func (s *Server) ListenAndServe() error {
l, err := net.Listen("tcp", s.Addr)
if err != nil {
return err
}
return s.Serve(l)
}

View File

@ -0,0 +1,156 @@
package content
import (
"archive/tar"
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
httputil "github.com/criticalstack/quake-kube/internal/util/net/http"
)
func CopyAssets(u *url.URL, dir string) error {
url := strings.TrimSuffix(u.String(), "/")
files, err := getManifest(url)
if err != nil {
return err
}
for _, f := range files {
path := filepath.Join(dir, f.Name)
if _, err := os.Stat(path); !os.IsNotExist(err) {
continue
}
data, err := httputil.GetBody(url + fmt.Sprintf("/assets/%d-%s", f.Checksum, f.Name))
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
if err := ioutil.WriteFile(path, data, 0644); err != nil {
return err
}
if strings.HasPrefix(f.Name, "linuxq3ademo") {
if err := extractDemoPack(path, dir); err != nil {
return err
}
}
if strings.HasPrefix(f.Name, "linuxq3apoint") {
if err := extractPointPacks(path, dir); err != nil {
return err
}
}
}
return nil
}
func getManifest(url string) ([]*File, error) {
data, err := httputil.GetBody(url + "/assets/manifest.json")
if err != nil {
return nil, err
}
files := make([]*File, 0)
if err := json.Unmarshal(data, &files); err != nil {
return nil, err
}
return files, nil
}
var gzipMagicHeader = []byte{'\x1f', '\x8b'}
func extractDemoPack(path, dir string) error {
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
idx := bytes.Index(data, gzipMagicHeader)
data = data[idx:]
gr, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return err
}
defer gr.Close()
data, err = ioutil.ReadAll(gr)
if err != nil {
return err
}
tr := tar.NewReader(bytes.NewReader(data))
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if strings.HasSuffix(hdr.Name, ".pk3") {
fmt.Printf("Downloaded %s\n", hdr.Name)
data, err := ioutil.ReadAll(tr)
if err != nil {
return err
}
path := filepath.Join(dir, "baseq3", filepath.Base(hdr.Name))
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
if err := ioutil.WriteFile(path, data, 0644); err != nil {
return err
}
}
}
return nil
}
func extractPointPacks(path, dir string) error {
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
idx := bytes.Index(data, gzipMagicHeader)
data = data[idx:]
gr, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return err
}
defer gr.Close()
data, err = ioutil.ReadAll(gr)
if err != nil {
return err
}
tr := tar.NewReader(bytes.NewReader(data))
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if strings.HasSuffix(hdr.Name, ".pk3") {
fmt.Printf("Downloaded %s\n", hdr.Name)
data, err := ioutil.ReadAll(tr)
if err != nil {
return err
}
path := filepath.Join(dir, hdr.Name)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
if err := ioutil.WriteFile(path, data, 0644); err != nil {
return err
}
}
}
return nil
}

View File

@ -0,0 +1,56 @@
package content
import (
"hash/crc32"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
type File struct {
Name string `json:"name"`
Compressed int64 `json:"compressed"`
Checksum uint32 `json:"checksum"`
}
func getAssets(dir string) (files []*File, err error) {
err = walk(dir, func(path string, info os.FileInfo, err error) error {
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
n := crc32.ChecksumIEEE(data)
path = strings.TrimPrefix(path, dir+"/")
files = append(files, &File{path, info.Size(), n})
return nil
}, ".pk3", ".sh", ".run")
return
}
func hasExts(path string, exts ...string) bool {
for _, ext := range exts {
if strings.HasSuffix(path, ext) {
return true
}
}
return false
}
func walk(root string, walkFn filepath.WalkFunc, exts ...string) error {
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
if os.IsPermission(err) {
return nil
}
return err
}
if !hasExts(path, exts...) {
return nil
}
if info.IsDir() {
return nil
}
return walkFn(path, info, err)
})
}

View File

@ -0,0 +1,72 @@
package content
import (
"archive/zip"
"os"
"path/filepath"
"strings"
)
type Map struct {
File string `json:"file"`
Name string `json:"name"`
}
func getMaps(dir string) (result []*Map, err error) {
err = walk(dir, func(path string, info os.FileInfo, err error) error {
mp, err := OpenMapPack(path)
if err != nil {
return err
}
defer mp.Close()
maps, err := mp.Maps()
if err != nil {
return err
}
result = append(result, maps...)
return err
}, ".pk3")
return
}
type MapPack struct {
*os.File
*zip.Reader
path string
}
func OpenMapPack(path string) (*MapPack, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
info, err := f.Stat()
if err != nil {
return nil, err
}
r, err := zip.NewReader(f, info.Size())
if err != nil {
return nil, err
}
mp := &MapPack{
File: f,
Reader: r,
path: path,
}
return mp, nil
}
func (m *MapPack) Maps() ([]*Map, error) {
maps := make([]*Map, 0)
for _, f := range m.Reader.File {
if !hasExts(f.Name, ".bsp") {
continue
}
path := filepath.Join(filepath.Base(filepath.Dir(m.path)), filepath.Base(m.path))
mapName := strings.TrimSuffix(filepath.Base(f.Name), ".bsp")
maps = append(maps, &Map{File: path, Name: mapName})
}
return maps, nil
}

View File

@ -0,0 +1,133 @@
package content
import (
"archive/zip"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type Config struct {
AssetsDir string
}
func NewRouter(cfg *Config) (*echo.Echo, error) {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
//e.Use(middleware.BodyLimit("100M"))
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept},
}))
e.GET("/", func(c echo.Context) error {
return c.HTML(http.StatusOK, `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Map pack upload</title>
</head>
<body>
<a href="/maps">Show maps</a>
<h1>Upload map pack file</h1>
<form action="/maps" method="post" enctype="multipart/form-data">
GameName: <input type="text" name="name" value="baseq3" /><br>
Files: <input type="file" name="file"><br><br>
<input type="submit" value="Submit">
</form>
</body>
</html>`)
})
e.GET("/assets/manifest.json", func(c echo.Context) error {
files, err := getAssets(cfg.AssetsDir)
if err != nil {
return err
}
return c.JSONPretty(http.StatusOK, files, " ")
})
e.GET("/assets/*", func(c echo.Context) error {
path := filepath.Join(cfg.AssetsDir, c.Param("*"))
d, f := filepath.Split(path)
f = f[strings.Index(f, "-")+1:]
path = filepath.Join(d, f)
if _, err := os.Stat(path); os.IsNotExist(err) {
return c.String(http.StatusNotFound, "file not found")
}
return c.File(path)
})
e.GET("/maps", func(c echo.Context) error {
maps, err := getMaps(cfg.AssetsDir)
if err != nil {
return err
}
return c.JSONPretty(http.StatusOK, maps, " ")
})
e.POST("/maps", func(c echo.Context) error {
name := c.FormValue("name")
file, err := c.FormFile("file")
if err != nil {
return err
}
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
if hasExts(file.Filename, ".zip") {
r, err := zip.NewReader(src, file.Size)
if err != nil {
return err
}
files := make([]string, 0)
for _, f := range r.File {
if !hasExts(f.Name, ".pk3") {
continue
}
pak, err := f.Open()
if err != nil {
return err
}
defer pak.Close()
dst, err := os.Create(filepath.Join(cfg.AssetsDir, name, filepath.Base(f.Name)))
if err != nil {
return err
}
defer dst.Close()
if _, err = io.Copy(dst, pak); err != nil {
return err
}
files = append(files, filepath.Base(f.Name))
}
if len(files) == 0 {
return c.HTML(http.StatusOK, fmt.Sprintf("<p>File %s did not contain any map pack files.</p>", file.Filename))
}
for i, _ := range files {
files[i] = "<li>" + files[i] + "</li>"
}
return c.HTML(http.StatusOK, fmt.Sprintf("<p>Loaded the following map packs from file %s:</p><ul>%s</ul>", file.Filename, strings.Join(files, "")))
}
dst, err := os.Create(filepath.Join(cfg.AssetsDir, name, file.Filename))
if err != nil {
return err
}
defer dst.Close()
if _, err = io.Copy(dst, src); err != nil {
return err
}
return c.HTML(http.StatusOK, fmt.Sprintf("<p>File %s uploaded successfully.</p>", filepath.Join(name, file.Filename)))
})
return e, nil
}

View File

@ -0,0 +1,221 @@
package server
import (
"bytes"
"fmt"
"reflect"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
)
type GameType int
const (
FreeForAll GameType = 0
Tournament GameType = 1
SinglePlayer GameType = 2
TeamDeathmatch GameType = 3
CaptureTheFlag GameType = 4
)
func (gt GameType) String() string {
switch gt {
case FreeForAll:
return "FreeForAll"
case Tournament:
return "Tournament"
case SinglePlayer:
return "SinglePlayer"
case TeamDeathmatch:
return "TeamDeathmatch"
case CaptureTheFlag:
return "CaptureTheFlag"
default:
return "Unknown"
}
}
func (gt *GameType) UnmarshalText(data []byte) error {
switch string(data) {
case "FreeForAll", "FFA":
*gt = FreeForAll
case "Tournament":
*gt = Tournament
case "SinglePlayer":
*gt = SinglePlayer
case "TeamDeathmatch":
*gt = TeamDeathmatch
case "CaptureTheFlag", "CTF":
*gt = CaptureTheFlag
default:
return errors.Errorf("unknown GameType: %s", data)
}
return nil
}
type Config struct {
FragLimit int `name:"fraglimit"`
TimeLimit time.Duration `name:"timelimit"`
GameConfig
ServerConfig
}
type GameConfig struct {
ForceRespawn bool `name:"g_forcerespawn"`
GameType GameType `name:"g_gametype"`
Inactivity time.Duration `name:"g_inactivity"`
Log string `name:"g_log"`
MOTD string `name:"g_motd"`
QuadFactor int `name:"g_quadfactor"`
WeaponRespawn int `name:"g_weaponrespawn"`
}
type FileServerConfig struct {
// allows people to base mods upon mods syntax to follow
BaseGame string `name:"fs_basegame"`
// set base path root C:\Program Files\Quake III Arena for files to be
// downloaded from this path may change for TC's and MOD's
BasePath string `name:"fs_basepath"`
// toggle if files can be copied from servers or if client will download
CopyFiles bool `name:"fs_copyfiles"`
// possibly enables file server debug mode for download/uploads or
// something
Debug bool `name:"fs_debug"`
// set gamedir set the game folder/dir default is baseq3
Game string `name:"fs_game"`
// possibly for TC's and MODS the default is the path to quake3.exe
HomePath string `name:"fs_homepath"`
}
type ServerConfig struct {
AllowDownload bool `name:"sv_allowDownload"`
DownloadURL string `name:"sv_dlURL"`
Hostname string `name:"sv_hostname"`
MaxClients int `name:"sv_maxclients"`
Password string `name:"rconpassword"`
}
func (c *Config) Marshal() ([]byte, error) {
return writeStruct(reflect.Indirect(reflect.ValueOf(c)))
}
func writeStruct(v reflect.Value) ([]byte, error) {
if v.Kind() != reflect.Struct {
return nil, errors.Errorf("expected struct, received %T", v.Kind())
}
var b bytes.Buffer
for i := 0; i < v.Type().NumField(); i++ {
fv := v.Field(i)
switch fv.Kind() {
case reflect.Struct:
data, err := writeStruct(fv)
if err != nil {
return nil, err
}
b.Write(data)
default:
tv, ok := v.Type().Field(i).Tag.Lookup("name")
if !ok {
continue
}
s := toString(v.Type().Field(i).Name, fv)
switch tv {
case "sv_dlURL":
if s != "" {
b.WriteString(fmt.Sprintf("sets %s %s\n", tv, s))
}
default:
b.WriteString(fmt.Sprintf("seta %s %s\n", tv, strconv.Quote(s)))
}
}
}
return b.Bytes(), nil
}
func toString(name string, v reflect.Value) string {
switch val := v.Interface().(type) {
case string:
return val
case int:
return strconv.Itoa(val)
case time.Duration:
switch name {
case "TimeLimit":
return fmt.Sprintf("%d", int(val.Minutes()))
default:
return fmt.Sprintf("%d", int(val.Seconds()))
}
case bool:
if val {
return "1"
}
return "0"
case GameType:
return fmt.Sprintf("%d", val)
default:
panic(fmt.Errorf("received unknown type %T", v.Interface()))
}
}
func Default() *Config {
return &Config{
TimeLimit: 15 * time.Minute,
FragLimit: 25,
GameConfig: GameConfig{
Log: "",
MOTD: "Welcome to Critical Stack",
QuadFactor: 3,
GameType: FreeForAll,
WeaponRespawn: 3,
Inactivity: 10 * time.Minute,
ForceRespawn: false,
},
ServerConfig: ServerConfig{
MaxClients: 12,
Hostname: "quakekube",
Password: "changeme",
},
}
}
type Maps []Map
func (maps Maps) Marshal() ([]byte, error) {
var b bytes.Buffer
for i, m := range maps {
cmds := []string{
fmt.Sprintf("g_gametype %d", m.Type),
}
if m.Type == CaptureTheFlag && m.CaptureLimit != 0 {
cmds = append(cmds, fmt.Sprintf("capturelimit %d", m.CaptureLimit))
}
if m.FragLimit != 0 {
cmds = append(cmds, fmt.Sprintf("fraglimit %d", m.FragLimit))
}
if m.TimeLimit != 0 {
cmds = append(cmds, fmt.Sprintf("timelimit %d", int(m.TimeLimit.Minutes())))
}
cmds = append(cmds, fmt.Sprintf("map %s", m.Name))
nextmap := "d0"
if i < len(maps)-1 {
nextmap = fmt.Sprintf("d%d", i+1)
}
cmds = append(cmds, fmt.Sprintf("set nextmap vstr %s", nextmap))
b.WriteString(fmt.Sprintf("set d%d \"seta %s\"\n", i, strings.Join(cmds, " ; ")))
}
b.WriteString("vstr d0")
return b.Bytes(), nil
}
type Map struct {
Name string `json:"name"`
Type GameType `json:"type"`
CaptureLimit int `json:"captureLimit"`
FragLimit int `json:"fragLimit"`
TimeLimit time.Duration `json:"timeLimit"`
}

View File

@ -0,0 +1,80 @@
package server
import (
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"sigs.k8s.io/yaml"
)
const expectedConfig = `seta fraglimit "25"
seta timelimit "15"
seta g_forcerespawn "0"
seta g_gametype "0"
seta g_inactivity "600"
seta g_log ""
seta g_motd "Welcome to Critical Stack"
seta g_quadfactor "3"
seta g_weaponrespawn "3"
seta sv_allowDownload "0"
seta sv_hostname "quakekube"
seta sv_maxclients "12"
seta rconpassword "changeme"
`
func TestConfigMarshal(t *testing.T) {
c := Default()
data, err := c.Marshal()
if err != nil {
t.Fatal(err)
}
fmt.Printf("data = %s\n", data)
if diff := cmp.Diff(string(data), expectedConfig); diff != "" {
t.Fatalf(diff)
}
}
const mapConfig = `- name: q3dm7
type: FreeForAll
- name: q3dm17
type: FreeForAll
- name: q3wctf1
type: CaptureTheFlag
captureLimit: 8
- name: q3tourney2
type: Tournament
- name: q3wctf3
type: CaptureTheFlag
captureLimit: 8
- name: ztn3tourney1
type: Tournament
`
const expectedMapConfig = `set d0 "seta g_gametype 0 ; map q3dm7 ; set nextmap vstr d1"
set d1 "seta g_gametype 0 ; map q3dm17 ; set nextmap vstr d2"
set d2 "seta g_gametype 4 ; capturelimit 8 ; map q3wctf1 ; set nextmap vstr d3"
set d3 "seta g_gametype 1 ; map q3tourney2 ; set nextmap vstr d4"
set d4 "seta g_gametype 4 ; capturelimit 8 ; map q3wctf3 ; set nextmap vstr d5"
set d5 "seta g_gametype 1 ; map ztn3tourney1 ; set nextmap vstr d0"
vstr d0`
func TestMapRead(t *testing.T) {
var maps Maps
if err := yaml.Unmarshal([]byte(mapConfig), &maps); err != nil {
t.Fatal(err)
}
for _, m := range maps {
fmt.Printf("m = %+v\n", m)
}
data, err := maps.Marshal()
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(string(data), expectedMapConfig); diff != "" {
t.Fatalf(diff)
}
}

View File

@ -0,0 +1,178 @@
package server
const Q3DemoEULA = `LIMITED USE SOFTWARE LICENSE AGREEMENT
This Limited Use Software License Agreement (the "Agreement") is a legal
agreement between you, the end-user, and Id Software, Inc. ("ID"). BY
CONTINUING THE INSTALLATION OF THIS GAME DEMO PROGRAM ENTITLED QUAKE III:
ARENA (THE "SOFTWARE"), BY LOADING OR RUNNING THE SOFTWARE, OR BY PLACING
OR COPYING THE SOFTWARE ONTO YOUR COMPUTER HARD DRIVE, COMPUTER RAM OR
OTHER STORAGE, YOU ARE AGREEING TO BE BOUND BY THE TERMS OF THIS
AGREEMENT.
1. Grant of License. Subject to the terms and provisions of this
Agreement, ID grants to you the non-exclusive and limited right to use the
Software only in executable or object code form. The term "Software"
includes all elements of the Software, including, without limitation, data
files and screen displays. You are not receiving any ownership or
proprietary right, title or interest in or to the Software or the
copyright, trademarks, or other rights related thereto. For purposes of
this section, "use" means loading the Software into RAM and/or onto
computer hard drive, as well as installation of the Software on a hard
disk or other storage device and means the uses permitted in section 3.
hereinbelow. You agree that the Software will not be shipped,
transferred or exported into any country in violation of the U.S. Export
Administration Act (or any other law governing such matters) by you or
anyone at your direction and that you will not utilize and will not
authorize anyone to utilize, in any other manner, the Software in
violation of any applicable law. The Software may not be downloaded
or otherwise exported or exported into (or to a national or resident
of) any country to which the U.S. has embargoed goods or to anyone
or into any country who/which are prohibited, by applicable law, from
receiving such property.
2. Prohibitions. You, either directly or indirectly, shall not do
any of the following acts:
a. rent the Software;
b. sell the Software;
c. lease or lend the Software;
d. offer the Software on a "pay-per-play" basis;
e. distribute the Software (except as permitted by section 3.
hereinbelow);
f. in any other manner and through any medium whatsoever
commercially exploit the Software or use the Software for any commercial
purpose;
g. disassemble, reverse engineer, decompile, modify or alter the
Software including, without limitation, creating or developing extra or
add-on levels for the Software;
h. translate the Software;
i. reproduce or copy the Software (except as permitted by section
3. hereinbelow);
j. publicly display the Software;
k. prepare or develop derivative works based upon the Software; or
l. remove or alter any legal notices or other markings or
legends, such as trademark and copyright notices, affixed on or within
the Software.
3. Permitted Distribution and Copying. So long as this Agreement
accompanies each copy you make of the Software, and so long as you fully
comply, at all times, with this Agreement, ID grants to you the
non-exclusive and limited right to copy the Software and to distribute
such copies of the Software free of charge for non-commercial purposes
which shall include the free of charge distribution of copies of the
Software as mounted on the covers of magazines; provided, however, you
shall not copy or distribute the Software in any infringing manner or
in any manner which violates any law or third party right and you shall
not distribute the Software together with any material which is
infringing, libelous, defamatory, obscene, false, misleading, or
otherwise illegal or unlawful. You agree to label conspicuously as
"SHAREWARE" or "DEMO" each CD or other non-electronic copy of the
Software that you make and distribute. ID reserves all rights not
granted in this Agreement. You shall not commercially distribute the
Software unless you first enter into a separate contract with ID, a
copy of which you may request, but which ID may decline to execute.
For more information visit www.quake3arena.com.
4. Intellectual Property Rights. The Software and all copyrights,
trademarks and all other conceivable intellectual property rights related
to the Software are owned by ID and are protected by United States
copyright laws, international treaty provisions and all applicable law,
such as the Lanham Act. You must treat the Software like any other
copyrighted material, as required by 17 U.S.C., §101 et seq. and other
applicable law. You agree to use your best efforts to see that any user
of the Software licensed hereunder complies with this Agreement. You
agree that you are receiving a copy of the Software by license only
and not by sale and that the "first sale" doctrine of 17 U.S.C. §109
does not apply to your receipt or use of the Software.
5. NO WARRANTIES. ID DISCLAIMS ALL WARRANTIES, WHETHER EXPRESS OR
IMPLIED, INCLUDING, BUT NOT LIMITED TO, IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE WITH RESPECT TO THE
SOFTWARE. ID DOES NOT WARRANT THAT THE OPERATION OF THE SOFTWARE WILL BE
UNINTERRUPTED OR ERROR FREE OR THAT THE SOFTWARE WILL MEET YOUR SPECIFIC
REQUIREMENTS. ADDITIONAL STATEMENTS SUCH AS PRESENTATIONS, WHETHER ORAL
OR WRITTEN, DO NOT CONSTITUTE WARRANTIES BY ID AND SHOULD NOT BE RELIED
UPON. THIS SECTION 5. SHALL SURVIVE CANCELLATION OR TERMINATION OF THIS
AGREEMENT.
6. Governing Law, Venue, Indemnity and Liability Limitation. This
Agreement shall be construed in accordance with and governed by the
applicable laws of the State of Texas and applicable United States federal
law. Copyright and other proprietary matters will be governed by United
States laws and international treaties. Exclusive venue for all
litigation regarding this Agreement shall be in Dallas County, Texas
and you agree to submit to the jurisdiction of the courts in Dallas,
Texas for any such litigation. You agree to indemnify, defend and hold
harmless ID and ID's officers, employees, directors, agents, licensees
(excluding you), successors and assigns from and against all losses,
lawsuits, damages, causes of action and claims relating to and/or
arising from your breach of this Agreement. You agree that your
unauthorized use of the Software, or any part thereof, may immediately
and irreparably damage ID such that ID could not be adequately
compensated solely by a monetary award and that at ID's option ID shall
be entitled to an injunctive order, in addition to all other available
remedies including a monetary award, appropriately restraining and/or
prohibiting such unauthorized use without the necessity of ID posting
bond or other security. IN ANY CASE, ID AND ID'S OFFICERS, EMPLOYEES,
DIRECTORS, AGENTS, LICENSEES, SUBLICENSEES, SUCCESSORS AND ASSIGNS
SHALL NOT BE LIABLE FOR LOSS OF DATA, LOSS OF PROFITS, LOST SAVINGS,
SPECIAL, INCIDENTAL, CONSEQUENTIAL, INDIRECT, PUNITIVE OR OTHER SIMILAR
DAMAGES ARISING FROM ANY ALLEGED CLAIM FOR BREACH OF WARRANTY, BREACH
OF CONTRACT, NEGLIGENCE, STRICT PRODUCT LIABILITY, OR OTHER LEGAL
THEORY EVEN IF ID OR ITS AGENT HAVE BEEN ADVISED OF THE POSSIBILITY
OF SUCH DAMAGES OR EVEN IF SUCH DAMAGES ARE FORESEEABLE, OR LIABLE
FOR ANY CLAIM BY ANY OTHER PARTY. Some jurisdictions do not allow
the exclusion or limitation of incidental or consequential damages,
so the above limitation or exclusion may not apply to you. This
Section 6. shall survive cancellation or termination of this Agreement.
7. U.S. Government Restricted Rights. To the extent applicable,
the United States Government shall only have those rights to use the
Software as expressly stated and expressly limited and restricted in
this Agreement, as provided in 48 C.F.R. §§ 227.7201 through 227.7204,
inclusive.
8. General Provisions. Neither this Agreement nor any part or
portion hereof shall be assigned or sublicensed by you. ID may assign its
rights under this Agreement in ID's sole discretion. Should any provision
of this Agreement be held to be void, invalid, unenforceable or illegal by
a court of competent jurisdiction, the validity and enforceability of the
other provisions shall not be affected thereby. If any provision is
determined to be unenforceable by a court of competent jurisdiction, you
agree to a modification of such provision to provide for enforcement of
the provision's intent, to the extent permitted by applicable law.
Failure of ID to enforce any provision of this Agreement shall not
constitute or be construed as a waiver of such provision or of the right
to enforce such provision. Immediately upon your failure to comply with
or breach of any term or provision of this Agreement, THIS AGREEMENT
AND YOUR LICENSE SHALL AUTOMATICALLY TERMINATE, WITHOUT NOTICE, AND ID
MAY PURSUE ALL RELIEF AND REMEDIES AGAINST YOU WHICH ARE AVAILABLE UNDER
APPLICABLE LAW AND/OR THIS AGREEMENT. In the event this Agreement is
terminated, you shall have no right to use the Software, in any manner,
and you shall immediately destroy all copies of the Software in your
possession, custody or control.
YOU ACKNOWLEDGE THAT YOU HAVE READ THIS AGREEMENT, YOU UNDERSTAND THIS
AGREEMENT, AND UNDERSTAND THAT BY CONTINUING THE INSTALLATION OF THE
SOFTWARE, BY LOADING OR RUNNING THE SOFTWARE, OR BY PLACING OR COPYING
THE SOFTWARE ONTO YOUR COMPUTER HARD DRIVE OR RAM, YOU AGREE TO BE BOUND
BY THE TERMS AND CONDITIONS OF THIS AGREEMENT. YOU FURTHER AGREE THAT,
EXCEPT FOR WRITTEN SEPARATE AGREEMENTS BETWEEN ID AND YOU, THIS
AGREEMENT IS A COMPLETE AND EXCLUSIVE STATEMENT OF THE RIGHTS AND
LIABILITIES OF THE PARTIES HERETO. THIS AGREEMENT SUPERSEDES ALL PRIOR
ORAL AGREEMENTS, PROPOSALS OR UNDERSTANDINGS, AND ANY OTHER
COMMUNICATIONS BETWEEN ID AND YOU RELATING TO THE SUBJECT MATTER OF
THIS AGREEMENT.
`

View File

@ -0,0 +1,18 @@
package server
import (
"context"
"os"
"os/exec"
)
func Start(ctx context.Context, dir string) error {
cmd := exec.CommandContext(ctx, "ioq3ded", "+set", "dedicated", "1", "+exec", "server.cfg", "+exec", "maps.cfg")
cmd.Dir = dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
return err
}
return cmd.Wait()
}

View File

@ -0,0 +1,41 @@
package http
import (
"io/ioutil"
"net/http"
"time"
"github.com/pkg/errors"
)
func GetBody(url string) ([]byte, error) {
client := http.Client{
Timeout: 30 * time.Second,
}
resp, err := client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
func GetUntil(url string, stop <-chan struct{}) error {
client := http.Client{
Timeout: 1 * time.Second,
}
for {
select {
case <-stop:
return errors.Errorf("not available: %q", url)
default:
resp, err := client.Get(url)
if err != nil {
continue
}
resp.Body.Close()
return nil
}
}
}

25
internal/util/net/net.go Normal file
View File

@ -0,0 +1,25 @@
package net
import (
"net"
"github.com/pkg/errors"
)
// DetectHostIPv4 attempts to determine the host IPv4 address by finding the
// first non-loopback device with an assigned IPv4 address.
func DetectHostIPv4() (string, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return "", errors.WithStack(err)
}
for _, a := range addrs {
if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() == nil {
continue
}
return ipnet.IP.String(), nil
}
}
return "", errors.New("cannot detect host IPv4 address")
}