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

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
bin
chart
tools

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
public/ioquake3.js linguist-vendored

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
vendor/
bin/

29
Dockerfile Normal file
View File

@ -0,0 +1,29 @@
FROM golang:1.13 as builder
WORKDIR /workspace
COPY go.mod go.mod
COPY go.sum go.sum
ARG GOPROXY
ARG GOSUMDB
RUN go mod download
COPY cmd cmd/
COPY internal internal/
COPY public public/
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o q3 ./cmd/q3
FROM alpine:3.12 as quake-n-bake
RUN apk add --no-cache git gcc make libc-dev sdl2-dev mesa-dev
RUN git clone https://github.com/ioquake/ioq3
RUN cd /ioq3 && make
RUN cp /ioq3/build/release-linux-x86_64/ioq3ded.x86_64 /usr/local/bin/ioq3ded
FROM alpine:3.12
COPY --from=builder /workspace/q3 /usr/local/bin
COPY --from=quake-n-bake /usr/local/bin/ioq3ded /usr/local/bin
COPY --from=quake-n-bake /lib/ld-musl-x86_64.so.1 /lib
ENTRYPOINT ["/usr/local/bin/q3"]

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

17
Makefile Normal file
View File

@ -0,0 +1,17 @@
BIN_DIR ?= bin
LDFLAGS := -s -w
GOFLAGS = -gcflags "all=-trimpath=$(PWD)" -asmflags "all=-trimpath=$(PWD)"
GO_BUILD_ENV_VARS := GO111MODULE=on CGO_ENABLED=0
q3: gen
@$(GO_BUILD_ENV_VARS) go build -o $(BIN_DIR)/q3 $(GOFLAGS) -ldflags '$(LDFLAGS)' ./cmd/q3
gen: ## Generate and embed templates
@go run tools/genstatic.go public public
VERSION ?= v1.0.0
IMAGE ?= docker.io/criticalstack/quake:$(VERSION)
.PHONY: build
build:
@docker build . --force-rm --build-arg GOPROXY --build-arg GOSUMDB -t $(IMAGE)

109
README.md Normal file
View File

@ -0,0 +1,109 @@
# QuakeKube
QuakeKube is a Kubernetes-ified version of [QuakeJS](https://github.com/inolen/quakejs) that runs a dedicated [Quake 3](https://en.wikipedia.org/wiki/Quake_III_Arena) server in a Kubernetes Deployment, and then allow clients to connect via QuakeJS in the browser.
## Quick start
Start an instance of Kubernetes locally using cinder (or kind):
```shell
$ cinder create cluster
```
Deploy the example manifest:
```shell
$ kubectl apply -f example.yaml
```
Finally, navigate to the `http://$(cinder get ip):30001` in the browser.
## How it works
QuakeKube makes use [ioquake](https://www.ioquake.org) for the Quake 3 dedicated server, and [QuakeJS](https://github.com/inolen/quakejs), a port of ioquake to javascript using [Emscripten](http://github.com/kripken/emscripten), to provide an in-browser game client.
### Networking
The client/server protocol of Quake 3 uses UDP to synchronize game state. Browsers do not natively supporting sending UDP packets so QuakeJS wraps the client and dedicated server net code in websockets, allowing the browser-based clients to send messages and enable multiplayer for other clients. This ends up preventing the browser client from using any other Quake 3 dedicated server. In order to use other Quake 3 dedicated servers, a proxy handles websocket traffic coming from browser clients and translates that into UDP to the backend. This gives the flexibility of being able to talk to other existing Quake 3 servers, but also allows using ioquake (instead of the javascript translation of it), which uses *considerably* less CPU and memory.
QuakeKube also uses a cool trick with [cmux](https://github.com/cockroachdb/cmux) to multiplex the client and websocket traffic into the same connection. Having all the traffic go through the same address makes routing a client to its backend much easier (since it can just use its `document.location.host`).
### Quake 3 demo EULA
The Quake 3 dedicated server requires an End-User License Agreement be agreed to by the user before distributing the Quake 3 demo files that are used (maps, textures, etc). To ensure that the installer is aware of, and agrees to, this EULA, the flag `--agree-eula` must be passed to `q3 server` at runtime. This flag is not set by default in the container image and is therefore required for the dedicated server to pass the prompt for EULA. The [example.yaml](example.yaml) manifest demonstrates usage of this flag to agree to the EULA.
## Configuration
### Server configuration
The server configuration is set by ConfigMap that is mounted to the container:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: quake3-server-config
data:
server.cfg: |
seta sv_hostname "quakekube"
seta g_log ""
seta sv_maxclients 12
seta g_motd "Welcome to Critical Stack"
seta g_quadfactor 3
seta timelimit 15
seta fraglimit 25
seta g_weaponrespawn 3
seta g_inactivity 600
seta g_forcerespawn 0
seta rconpassword "changeme"
```
Many of the config values seen [here](http://www.joz3d.net/html/q3console.html) will likely work, but be careful to not create an invalid server configuration (causing the pod to crash).
### Maps
The maps are configured via the server configuration ConfigMap by specifying a maps file that can be mounted into the container:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: quake3-server-config
data:
...
maps.yaml: |
- name: q3dm7
type: FreeForAll
- name: q3dm17
type: FreeForAll
```
These values are then used when specifying container arguments in the Kubernetes Deployment via `--maps`. For example usage, be sure to check out the [example.yaml](example.yaml) manifest.
The time limit and frag limit can be specified with each map (it will change it for subsequent maps in the list):
```yaml
- name: q3dm17
type: FreeForAll
fragLimit: 30
timeLimit: 30
```
Capture limit for CTF maps can also be configured:
```yaml
- name: q3wctf3
type: CaptureTheFlag
captureLimit: 8
```
## Add new maps
The content server hosts a small upload app to allow uploading `pk3` or `zip` files containing maps. The server must be restarted after maps have been added to be available. Currently, this happens after the map rotates to one of the previously loaded maps, however, in the future the server will respond to changes to the ConfigMap automatically.
## Credits
* [inolen/quakejs](https://github.com/inolen/quakejs) - The really awesome QuakeJS project that makes this possible.
* [ioquake/ioq3](https://github.com/ioquake/ioq3) - The community supported version of Quake 3 used by QuakeJS. It is licensed under the GPLv2.
* [begleysm/quakejs](https://github.com/begleysm/quakejs) - Information in the README.md (very helpful) was used as a guide, as well as, some forked assets of this project (which came from quakejs-web originally) were used.
* [joz3d.net](http://www.joz3d.net/html/q3console.html) - Useful information about configuration values.

14
cmd/q3/app/cmd/cmd.go Normal file
View File

@ -0,0 +1,14 @@
package cmd
import "github.com/spf13/cobra"
func NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "cmd",
Short: "send remote server commands",
RunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
return cmd
}

View File

@ -0,0 +1,70 @@
package content
import (
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"time"
"github.com/spf13/cobra"
quakecontent "github.com/criticalstack/quake-kube/internal/quake/content"
)
var opts struct {
Addr string
AssetsDir string
SeedContentURL string
}
func NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "content",
Short: "q3 content server",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) (err error) {
if !filepath.IsAbs(opts.AssetsDir) {
opts.AssetsDir, err = filepath.Abs(opts.AssetsDir)
if err != nil {
return err
}
}
if err := os.MkdirAll(opts.AssetsDir, 0755); err != nil {
return err
}
if opts.SeedContentURL != "" {
u, err := url.Parse(opts.SeedContentURL)
if err != nil {
return err
}
if err := quakecontent.CopyAssets(u, opts.AssetsDir); err != nil {
return err
}
}
e, err := quakecontent.NewRouter(&quakecontent.Config{
AssetsDir: opts.AssetsDir,
})
if err != nil {
return err
}
s := &http.Server{
Addr: opts.Addr,
Handler: e,
ReadTimeout: 600 * time.Second,
WriteTimeout: 600 * time.Second,
MaxHeaderBytes: 1 << 20,
}
fmt.Printf("Starting server %s\n", opts.Addr)
return s.ListenAndServe()
},
}
cmd.Flags().StringVarP(&opts.Addr, "addr", "a", ":9090", "address <host>:<port>")
cmd.Flags().StringVarP(&opts.AssetsDir, "assets-dir", "d", "assets", "assets directory")
cmd.Flags().StringVar(&opts.SeedContentURL, "seed-content-url", "", "seed content from another content server")
return cmd
}

46
cmd/q3/app/proxy/proxy.go Normal file
View File

@ -0,0 +1,46 @@
package proxy
import (
"fmt"
"net/http"
"github.com/spf13/cobra"
quakeclient "github.com/criticalstack/quake-kube/internal/quake/client"
netutil "github.com/criticalstack/quake-kube/internal/util/net"
)
var opts struct {
ClientAddr string
ServerAddr string
ContentServer string
}
func NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "proxy",
Short: "q3 websocket/udp proxy",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if opts.ClientAddr == "" {
hostIPv4, err := netutil.DetectHostIPv4()
if err != nil {
return err
}
opts.ClientAddr = fmt.Sprintf("%s:8080", hostIPv4)
}
p, err := quakeclient.NewProxy(opts.ServerAddr)
if err != nil {
return err
}
s := http.Server{
Addr: opts.ClientAddr,
Handler: p,
}
return s.ListenAndServe()
},
}
cmd.Flags().StringVarP(&opts.ClientAddr, "client-addr", "c", "", "client address <host>:<port>")
cmd.Flags().StringVarP(&opts.ServerAddr, "server-addr", "s", "", "dedicated server <host>:<port>")
return cmd
}

154
cmd/q3/app/server/server.go Normal file
View File

@ -0,0 +1,154 @@
package server
import (
"context"
"fmt"
"io/ioutil"
"net/url"
"path/filepath"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"sigs.k8s.io/yaml"
quakeclient "github.com/criticalstack/quake-kube/internal/quake/client"
"github.com/criticalstack/quake-kube/internal/quake/content"
quakeserver "github.com/criticalstack/quake-kube/internal/quake/server"
netutil "github.com/criticalstack/quake-kube/internal/util/net"
httputil "github.com/criticalstack/quake-kube/internal/util/net/http"
"github.com/criticalstack/quake-kube/public"
)
var opts struct {
ClientAddr string
ServerAddr string
ContentServer string
AcceptEula bool
AssetsDir string
ConfigFile string
Maps string
}
func NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "server",
Short: "q3 server",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if opts.ClientAddr == "" {
hostIPv4, err := netutil.DetectHostIPv4()
if err != nil {
return err
}
opts.ClientAddr = fmt.Sprintf("%s:8080", hostIPv4)
}
if opts.ServerAddr == "" {
hostIPv4, err := netutil.DetectHostIPv4()
if err != nil {
return err
}
opts.ServerAddr = fmt.Sprintf("%s:27960", hostIPv4)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
csurl, err := url.Parse(opts.ContentServer)
if err != nil {
return err
}
if !opts.AcceptEula {
fmt.Println(quakeserver.Q3DemoEULA)
return errors.New("You must agree to the EULA to continue")
}
if err := httputil.GetUntil(opts.ContentServer, ctx.Done()); err != nil {
return err
}
if err := content.CopyAssets(csurl, opts.AssetsDir); err != nil {
return err
}
if err := writeDefaultServerConfig(filepath.Join(opts.AssetsDir, "baseq3/server.cfg")); err != nil {
return err
}
if opts.ConfigFile != "" {
data, err := ioutil.ReadFile(opts.ConfigFile)
if err != nil {
return err
}
if err := ioutil.WriteFile(filepath.Join(opts.AssetsDir, "baseq3/server.cfg"), data, 0644); err != nil {
return err
}
}
if err := writeDefaultMapConfig(filepath.Join(opts.AssetsDir, "baseq3/maps.cfg")); err != nil {
return err
}
if opts.Maps != "" {
data, err := ioutil.ReadFile(opts.Maps)
if err != nil {
return err
}
var maps quakeserver.Maps
if err := yaml.Unmarshal(data, &maps); err != nil {
return err
}
data, err = maps.Marshal()
if err != nil {
return err
}
if err := ioutil.WriteFile(filepath.Join(opts.AssetsDir, "baseq3/maps.cfg"), data, 0644); err != nil {
return err
}
}
go func() {
if err := quakeserver.Start(ctx, opts.AssetsDir); err != nil {
panic(err)
}
}()
e, err := quakeclient.NewRouter(&quakeclient.Config{
ContentServerURL: opts.ContentServer,
ServerAddr: opts.ServerAddr,
Files: public.Files,
})
if err != nil {
return err
}
s := &quakeclient.Server{
Addr: opts.ClientAddr,
Handler: e,
ServerAddr: opts.ServerAddr,
}
fmt.Printf("Starting server %s\n", opts.ClientAddr)
return s.ListenAndServe()
},
}
cmd.Flags().StringVarP(&opts.ConfigFile, "config", "c", "", "server configuration file")
cmd.Flags().StringVar(&opts.ContentServer, "content-server", "http://content.quakejs.com", "content server url")
cmd.Flags().BoolVar(&opts.AcceptEula, "agree-eula", false, "agree to the Quake 3 demo EULA")
cmd.Flags().StringVar(&opts.AssetsDir, "assets-dir", "assets", "location for game files")
cmd.Flags().StringVar(&opts.ClientAddr, "client-addr", "", "client address <host>:<port>")
cmd.Flags().StringVar(&opts.ServerAddr, "server-addr", "", "dedicated server <host>:<port>")
cmd.Flags().StringVar(&opts.Maps, "maps", "", "map rotation")
return cmd
}
func writeDefaultMapConfig(path string) error {
maps := quakeserver.Maps{
{Name: "q3dm7", Type: quakeserver.FreeForAll},
{Name: "q3dm17", Type: quakeserver.FreeForAll},
}
data, err := maps.Marshal()
if err != nil {
return err
}
return ioutil.WriteFile(path, data, 0644)
}
func writeDefaultServerConfig(path string) error {
data, err := quakeserver.Default().Marshal()
if err != nil {
return err
}
return ioutil.WriteFile(path, data, 0644)
}

38
cmd/q3/main.go Normal file
View File

@ -0,0 +1,38 @@
package main
import (
"log"
"github.com/spf13/cobra"
q3cmd "github.com/criticalstack/quake-kube/cmd/q3/app/cmd"
q3content "github.com/criticalstack/quake-kube/cmd/q3/app/content"
q3proxy "github.com/criticalstack/quake-kube/cmd/q3/app/proxy"
q3server "github.com/criticalstack/quake-kube/cmd/q3/app/server"
)
var global struct {
Verbosity int
}
func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "q3",
Short: "",
}
cmd.AddCommand(
q3cmd.NewCommand(),
q3content.NewCommand(),
q3proxy.NewCommand(),
q3server.NewCommand(),
)
cmd.PersistentFlags().CountVarP(&global.Verbosity, "verbose", "v", "log output verbosity")
return cmd
}
func main() {
if err := NewRootCommand().Execute(); err != nil {
log.Fatal(err)
}
}

109
example.yaml Normal file
View File

@ -0,0 +1,109 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: quakejs
spec:
selector:
matchLabels:
run: quakejs
replicas: 1
template:
metadata:
labels:
run: quakejs
spec:
containers:
- command:
- q3
- server
- --config=/config/server.cfg
- --content-server=http://localhost:9090
- --maps=/config/maps.yaml
- --agree-eula
image: docker.io/criticalstack/quake:v1.0.0
imagePullPolicy: Always
name: server
ports:
- containerPort: 8080
readinessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 15
periodSeconds: 5
volumeMounts:
- name: quake3-server-config
mountPath: /config
- name: quake3-content
mountPath: /assets
- command:
- q3
- content
- --seed-content-url=http://content.quakejs.com
image: docker.io/criticalstack/quake:v1.0.0
name: content-server
ports:
- containerPort: 9090
volumeMounts:
- name: quake3-content
mountPath: /assets
volumes:
- name: quake3-server-config
configMap:
name: default-quake3-server-config
- name: quake3-content
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: quakejs
spec:
type: NodePort
selector:
run: quakejs
ports:
- port: 8080
targetPort: 8080
nodePort: 30001
name: client
- port: 27960
targetPort: 27960
nodePort: 30003
name: server
- port: 9090
targetPort: 9090
nodePort: 30002
name: content
---
apiVersion: v1
kind: ConfigMap
metadata:
name: default-quake3-server-config
data:
server.cfg: |
seta sv_hostname "quakekube"
seta g_log ""
seta sv_maxclients 12
seta g_motd "Welcome to Critical Stack"
seta g_quadfactor 3
seta timelimit 15
seta fraglimit 25
seta g_weaponrespawn 3
seta g_inactivity 600
seta g_forcerespawn 0
seta rconpassword "changeme"
maps.yaml: |
- 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

15
go.mod Normal file
View File

@ -0,0 +1,15 @@
module github.com/criticalstack/quake-kube
go 1.14
require (
github.com/cockroachdb/cmux v0.0.0-20170110192607-30d10be49292
github.com/google/go-cmp v0.2.0
github.com/gorilla/websocket v1.4.0
github.com/labstack/echo/v4 v4.1.16
github.com/pkg/errors v0.9.1
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect
github.com/shurcooL/vfsgen v0.0.0-20200627165143-92b8a710ab6c // indirect
github.com/spf13/cobra v1.0.0
sigs.k8s.io/yaml v1.2.0
)

183
go.sum Normal file
View File

@ -0,0 +1,183 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/cmux v0.0.0-20170110192607-30d10be49292 h1:dzj1/xcivGjNPwwifh/dWTczkwcuqsXXFHY1X/TZMtw=
github.com/cockroachdb/cmux v0.0.0-20170110192607-30d10be49292/go.mod h1:qRiX68mZX1lGBkTWyp3CLcenw9I94W2dLeRvMzcn9N4=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.1.16 h1:8swiwjE5Jkai3RPfZoahp8kjVCRNq+y7Q0hPji2Kz0o=
github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI=
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/vfsgen v0.0.0-20200627165143-92b8a710ab6c h1:XLPw6rny9Vrrvrzhw8pNLrC2+x/kH0a/3gOx5xWDa6Y=
github.com/shurcooL/vfsgen v0.0.0-20200627165143-92b8a710ab6c/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4=
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=

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")
}

11
public/browserconfig.xml Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="/images/ms-icon-70x70.png"/>
<square150x150logo src="/images/ms-icon-150x150.png"/>
<square310x310logo src="/images/ms-icon-310x310.png"/>
<TileColor>#ffffff</TileColor>
</tile>
</msapplication>
</browserconfig>

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

63
public/game.css Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

151
public/index.html Normal file
View File

@ -0,0 +1,151 @@
{{define "index"}}<!DOCTYPE html>
<html>
<head>
<title>QuakeJS Local</title>
<link rel="stylesheet" href="game.css"></link>
<script type="text/javascript" src="ioquake3.js"></script>
<link rel="apple-touch-icon" sizes="57x57" href="/images/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/images/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/images/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/images/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/images/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/images/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/images/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/images/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/images/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
<link rel="manifest" href="/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/images/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<script type="text/javascript">
function getQueryCommands() {
var search = /([^&=]+)/g;
var query = window.location.search.substring(1);
var args = [];
var match;
while (match = search.exec(query)) {
var val = decodeURIComponent(match[1]);
val = val.split(' ');
val[0] = '+' + val[0];
args.push.apply(args, val);
}
return args;
}
window.onload = function () {
function resizeViewport() {
if (!ioq3.canvas) {
// ignore if the canvas hasn't yet initialized
return;
}
if ((document['webkitFullScreenElement'] || document['webkitFullscreenElement'] ||
document['mozFullScreenElement'] || document['mozFullscreenElement'] ||
document['fullScreenElement'] || document['fullscreenElement'])) {
// ignore resize events due to going fullscreen
return;
}
ioq3.setCanvasSize(ioq3.viewport.offsetWidth, ioq3.viewport.offsetHeight);
}
ioq3.viewport = document.getElementById('viewport-frame');
ioq3.elementPointerLock = true;
ioq3.exitHandler = function (err) {
if (err) {
var form = document.createElement('form');
form.setAttribute('method', 'POST');
form.setAttribute('action', '/');
var hiddenField = document.createElement('input');
hiddenField.setAttribute('type', 'hidden');
hiddenField.setAttribute('name', 'error');
hiddenField.setAttribute('value', err);
form.appendChild(hiddenField);
document.body.appendChild(form);
form.submit();
return;
}
window.location.href = '/';
}
window.addEventListener('resize', resizeViewport);
};
</script>
<style>
.centered {
position: fixed;
top: 60%;
left: 50%;
transform: translate(-50%, -60%);
z-index: 1;
}
.form input[type=text] {
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
box-shadow: inset 0 1px 3px #ddd;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding-left: 20px;
padding-right: 20px;
padding-top: 16px;
padding-bottom: 16px;
}
.button {
background-color: #fe121e;
border: none;
color: white;
padding: 16px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
</style>
</head>
<body>
<div id="main">
<div id="bg"></div>
<div class="centered">
<form class="form">
<input type="text" id="playerName">
<button onclick="join()" type="submit" class="button">Join</button>
</form>
<script type="text/javascript">
placeholder = "enter player name"
if (localStorage.playerName) {
placeholder = localStorage.playerName
}
document.getElementById("playerName").placeholder = placeholder
function join(){
var inputPlayerName = document.getElementById("playerName");
if (inputPlayerName.value != "") {
localStorage.setItem("playerName", inputPlayerName.value);
}
host = document.location.host
if (!host.includes(":")) { host = host + ":80" }
var args = ['+set', 'fs_cdn', host, '+connect', host];
args.push.apply(args, ['+set', 'cl_allowDownload', '1'])
args.push.apply(args, ['+name', localStorage.playerName])
args.push.apply(args, getQueryCommands());
var element = document.getElementById("main");
element.parentNode.removeChild(element);
ioq3.callMain(args);
}
</script>
</div>
</div>
<div id="viewport-frame"></div>
</body>
</html>
{{end}}

30996
public/ioquake3.js vendored Normal file

File diff suppressed because one or more lines are too long

41
public/manifest.json Normal file
View File

@ -0,0 +1,41 @@
{
"name": "App",
"icons": [
{
"src": "\/images\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/images\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/images\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/images\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/images\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/images\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}

File diff suppressed because one or more lines are too long

70
tools/genstatic.go Normal file
View File

@ -0,0 +1,70 @@
// +build ignore
package main
import (
"log"
"net/http"
"os"
"path/filepath"
"time"
"github.com/shurcooL/httpfs/filter"
"github.com/shurcooL/vfsgen"
"github.com/spf13/cobra"
)
type modTimeFS struct {
fs http.FileSystem
}
func (fs modTimeFS) Open(name string) (http.File, error) {
f, err := fs.fs.Open(name)
if err != nil {
return nil, err
}
return modTimeFile{f}, nil
}
type modTimeFile struct {
http.File
}
func (f modTimeFile) Stat() (os.FileInfo, error) {
fi, err := f.File.Stat()
if err != nil {
return nil, err
}
return modTimeFileInfo{fi}, nil
}
type modTimeFileInfo struct {
os.FileInfo
}
func (modTimeFileInfo) ModTime() time.Time { return time.Time{} }
func StripModTime(fs http.FileSystem) http.FileSystem {
return modTimeFS{fs}
}
func main() {
cmd := &cobra.Command{
Use: "gen",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
templateDir := filter.Skip(StripModTime(http.Dir(args[0])), func(path string, fi os.FileInfo) bool {
return !fi.IsDir() && filepath.Ext(path) == ".go"
})
return vfsgen.Generate(templateDir, vfsgen.Options{
BuildTags: "!dev",
VariableName: "Files",
PackageName: args[1],
Filename: filepath.Join(args[1], "zz_generated.static.go"),
})
},
}
if err := cmd.Execute(); err != nil {
log.Fatal(err)
}
}