mirror of
https://github.com/Octops/quake-kube.git
synced 2026-04-07 02:10:34 +00:00
Initial commit
This commit is contained in:
116
internal/quake/client/proxy.go
Normal file
116
internal/quake/client/proxy.go
Normal 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
|
||||
}
|
||||
}
|
||||
130
internal/quake/client/router.go
Normal file
130
internal/quake/client/router.go
Normal 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)
|
||||
}
|
||||
58
internal/quake/client/server.go
Normal file
58
internal/quake/client/server.go
Normal 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)
|
||||
}
|
||||
156
internal/quake/content/download.go
Normal file
156
internal/quake/content/download.go
Normal 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
|
||||
}
|
||||
56
internal/quake/content/files.go
Normal file
56
internal/quake/content/files.go
Normal 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)
|
||||
})
|
||||
}
|
||||
72
internal/quake/content/maps.go
Normal file
72
internal/quake/content/maps.go
Normal 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
|
||||
}
|
||||
133
internal/quake/content/router.go
Normal file
133
internal/quake/content/router.go
Normal 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
|
||||
}
|
||||
221
internal/quake/server/config.go
Normal file
221
internal/quake/server/config.go
Normal 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"`
|
||||
}
|
||||
80
internal/quake/server/config_test.go
Normal file
80
internal/quake/server/config_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
178
internal/quake/server/eula.go
Normal file
178
internal/quake/server/eula.go
Normal 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.
|
||||
`
|
||||
18
internal/quake/server/server.go
Normal file
18
internal/quake/server/server.go
Normal 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()
|
||||
}
|
||||
41
internal/util/net/http/http.go
Normal file
41
internal/util/net/http/http.go
Normal 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
25
internal/util/net/net.go
Normal 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")
|
||||
}
|
||||
Reference in New Issue
Block a user