From 1572e52c195b6e21b26a0150f58632eef728092f Mon Sep 17 00:00:00 2001 From: Serge Bazanski Date: Thu, 3 Dec 2020 23:19:28 +0100 Subject: [PATCH] wow: init This is a shitty MMORPG server. Private. Do not touch. Change-Id: Iddfce069f5895632d305a73fcaa2d963e25dc600 --- WORKSPACE | 67 ++- personal/q3k/wow/lib.libsonnet | 301 +++++++++++ personal/q3k/wow/panel/BUILD.bazel | 51 ++ personal/q3k/wow/panel/main.go | 466 ++++++++++++++++++ personal/q3k/wow/panel/soap.go | 215 ++++++++ personal/q3k/wow/prod.jsonnet | 90 ++++ personal/q3k/wow/secrets/cipher/motd.txt | 46 ++ .../wow/secrets/cipher/mysql-root-password | 40 ++ personal/q3k/wow/secrets/cipher/oauth-secret | 40 ++ personal/q3k/wow/secrets/cipher/panel-secret | 40 ++ personal/q3k/wow/secrets/cipher/soap-password | 40 ++ personal/q3k/wow/secrets/plain/.gitignore | 1 + 12 files changed, 1389 insertions(+), 8 deletions(-) create mode 100644 personal/q3k/wow/lib.libsonnet create mode 100644 personal/q3k/wow/panel/BUILD.bazel create mode 100644 personal/q3k/wow/panel/main.go create mode 100644 personal/q3k/wow/panel/soap.go create mode 100644 personal/q3k/wow/prod.jsonnet create mode 100644 personal/q3k/wow/secrets/cipher/motd.txt create mode 100644 personal/q3k/wow/secrets/cipher/mysql-root-password create mode 100644 personal/q3k/wow/secrets/cipher/oauth-secret create mode 100644 personal/q3k/wow/secrets/cipher/panel-secret create mode 100644 personal/q3k/wow/secrets/cipher/soap-password create mode 100644 personal/q3k/wow/secrets/plain/.gitignore diff --git a/WORKSPACE b/WORKSPACE index 9dd649cc..2c302460 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -15,6 +15,7 @@ http_archive( ) load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") + protobuf_deps() # Force rules_python at a bleeding edge version (for pip3_import). @@ -31,7 +32,9 @@ http_archive( urls = ["https://github.com/tweag/rules_nixpkgs/archive/dc24090573d74adcf38730422941fd69b87682c7.tar.gz"], sha256 = "aca86baa64174478c57f74ed09d5c2313113abe94aa3af030486d1b14032d3ed", ) + load("//third_party/nix:repository_rules.bzl", "hscloud_setup_nix") + hscloud_setup_nix( revision = "1179841f9a88b8a548f4b11d1a03aa25a790c379", sha256 = "8b64041bfb9760de9e797c0a985a4830880c21732489f397e217d877edd9a990", @@ -46,6 +49,7 @@ http_archive( "https://github.com/bazelbuild/rules_go/releases/download/v0.24.7/rules_go-v0.24.7.tar.gz", ], ) + http_archive( name = "bazel_gazelle", sha256 = "bfd86b3cbe855d6c16c6fce60d76bd51f5c8dbc9cfcaef7a2bb5c1aafd0710e8", @@ -58,26 +62,30 @@ http_archive( # Python rules # Important: rules_python must be loaded before protobuf (and grpc) because they load an older version otherwise load("@rules_python//python:repositories.bzl", "py_repositories") + py_repositories() load("@rules_python//python:pip.bzl", "pip_repositories") + pip_repositories() load("@hscloud_pip_imports//:imports.bzl", "hscloud_pip3_import") + hscloud_pip3_import( - name = "pydeps", - requirements = "//third_party/py:requirements.txt", + name = "pydeps", + requirements = "//third_party/py:requirements.txt", ) load("@pydeps//:requirements.bzl", "pip_install") -pip_install() +pip_install() # Setup Go toolchain. # This workspace is generated by hscloud_setup_nixpkgs. It will either call # go_register_toolchains() to automagically get Go toolchains from the Internet # or, if nix is present, instead setup a toolchain from nixpkgs. load("@hscloud_go_toolchain//:imports.bzl", "hscloud_go_register_toolchains") + hscloud_go_register_toolchains() # IMPORTANT: match protobuf version above with the one loaded by grpc @@ -90,19 +98,25 @@ http_archive( # Load grpc deps after Go, to prevent overriding Go toolchains/SDK. load("@com_github_grpc_grpc//bazel:grpc_deps.bzl", "grpc_deps") + grpc_deps() + load("@com_github_grpc_grpc//bazel:grpc_extra_deps.bzl", "grpc_extra_deps") + grpc_extra_deps() load("@io_bazel_rules_go//go:deps.bzl", "go_rules_dependencies") + go_rules_dependencies() # gazelle:repository_macro third_party/go/repositories.bzl%go_repositories -load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") +load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository") + gazelle_dependencies() # Load Go third-party packages. load("//third_party/go:repositories.bzl", "go_repositories") + go_repositories() # Docker rules @@ -127,6 +141,7 @@ load( "@io_bazel_rules_docker//repositories:repositories.bzl", container_repositories = "repositories", ) + container_repositories() # Docker base images @@ -151,6 +166,7 @@ container_pull( # third_party/factorio load("//third_party/factorio:factorio.bzl", "factorio_repositories") + factorio_repositories() # For devtools/gerrit/gerrit-oauth-provider and gerrit OWNERS plugin @@ -159,14 +175,17 @@ git_repository( name = "com_googlesource_gerrit_bazlets", remote = "https://gerrit.googlesource.com/bazlets", commit = "1d381f01c853e2c02ae35430a8e294e485635d62", - shallow_since = "1559431096 -0400" + shallow_since = "1559431096 -0400", ) load("@com_googlesource_gerrit_bazlets//:gerrit_api.bzl", "gerrit_api") + gerrit_api() -load("@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl", gerrit_maven_jar="maven_jar", GERRIT="GERRIT") +load("@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl", gerrit_maven_jar = "maven_jar", "GERRIT") + PROLOG_VERS = "1.4.3" + JACKSON_VER = "2.9.7" gerrit_maven_jar( @@ -186,21 +205,25 @@ gerrit_maven_jar( artifact = "com.fasterxml.jackson.core:jackson-core:" + JACKSON_VER, sha1 = "4b7f0e0dc527fab032e9800ed231080fdc3ac015", ) + gerrit_maven_jar( name = "jackson-databind", artifact = "com.fasterxml.jackson.core:jackson-databind:" + JACKSON_VER, sha1 = "e6faad47abd3179666e89068485a1b88a195ceb7", ) + gerrit_maven_jar( name = "jackson-annotations", artifact = "com.fasterxml.jackson.core:jackson-annotations:" + JACKSON_VER, sha1 = "4b838e5c4fc17ac02f3293e9a558bb781a51c46d", ) + gerrit_maven_jar( name = "jackson-dataformat-yaml", artifact = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:" + JACKSON_VER, sha1 = "a428edc4bb34a2da98a50eb759c26941d4e85960", ) + gerrit_maven_jar( name = "snakeyaml", artifact = "org.yaml:snakeyaml:1.23", @@ -214,6 +237,7 @@ gerrit_maven_jar( repository = GERRIT, sha1 = "d5206556cbc76ffeab21313ffc47b586a1efbcbb", ) + gerrit_maven_jar( name = "prolog-compiler", artifact = "com.googlecode.prolog-cafe:prolog-compiler:" + PROLOG_VERS, @@ -221,6 +245,7 @@ gerrit_maven_jar( repository = GERRIT, sha1 = "f37032cf1dec3e064427745bc59da5a12757a3b2", ) + gerrit_maven_jar( name = "prolog-io", artifact = "com.googlecode.prolog-cafe:prolog-io:" + PROLOG_VERS, @@ -240,6 +265,7 @@ http_archive( ) RULES_JVM_EXTERNAL_TAG = "3.0" + RULES_JVM_EXTERNAL_SHA = "62133c125bf4109dfd9d2af64830208356ce4ef8b165a6ef15bbff7460b35c3a" http_archive( @@ -258,7 +284,7 @@ maven_install( "org.spigotmc:spigot-api:1.15.2-R0.1-SNAPSHOT", "io.grpc:grpc-netty-shaded:1.29.0", "io.grpc:grpc-services:1.29.0", - ] + IO_GRPC_GRPC_JAVA_ARTIFACTS, + ] + IO_GRPC_GRPC_JAVA_ARTIFACTS, generate_compat_repositories = True, override_targets = IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS, repositories = [ @@ -270,12 +296,15 @@ maven_install( ) load("@maven//:defs.bzl", "pinned_maven_install") + pinned_maven_install() load("@maven//:compat.bzl", "compat_repositories") + compat_repositories() load("@io_grpc_grpc_java//:repositories.bzl", "grpc_java_repositories") + grpc_java_repositories() # Gerrit OWNERS plugins external repositories @@ -284,12 +313,13 @@ git_repository( name = "com_googlesource_gerrit_plugin_owners", remote = "https://gerrit.googlesource.com/plugins/owners/", commit = "5e691e87b8c00a04d261a8dd313f4d16c54797e8", - shallow_since = "1559729722 +0900" + shallow_since = "1559729722 +0900", ) # Go image repos for Docker load("@io_bazel_rules_docker//go:image.bzl", go_image_repositories = "repositories") + go_image_repositories() # oniguruma, with build from //third_party/oniguruma @@ -311,3 +341,24 @@ http_archive( sha256 = "5de8c8e29aaa3fb9cc6b47bb27299f271354ebb72514e3accadc7d38b5bbaa72", build_file = "@hscloud//third_party/jq:BUILD.external", ) + +go_repository( + name = "com_github_gorilla_sessions", + importpath = "github.com/gorilla/sessions", + sum = "h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=", + version = "v1.2.1", +) + +go_repository( + name = "com_github_boltdb_bolt", + importpath = "github.com/boltdb/bolt", + sum = "h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=", + version = "v1.3.1", +) + +go_repository( + name = "com_github_gorilla_securecookie", + importpath = "github.com/gorilla/securecookie", + sum = "h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=", + version = "v1.1.1", +) diff --git a/personal/q3k/wow/lib.libsonnet b/personal/q3k/wow/lib.libsonnet new file mode 100644 index 00000000..4a9517be --- /dev/null +++ b/personal/q3k/wow/lib.libsonnet @@ -0,0 +1,301 @@ +local kube = import "../../../kube/kube.libsonnet"; + +{ + local wow = self, + local cfg = wow.cfg, + local ns = wow.ns, + cfg:: { + namespace: error "namespace must be set", + prefix: "", + images: { + acore: "registry.k0.hswaw.net/q3k/azerothcore-wowtlk:1606950998", + panel: "registry.k0.hswaw.net/q3k/panel:1607033741-f18a531f9b84c5b33653c8db5d64aaa0af337541", + }, + db: { + local mkConfig = function(name) { + host: error ("db.%s.host must be set" % [name]), + port: error ("db.%s.prt must be set" % [name]), + user: error ("db.%s.user must be set" % [name]), + password: error ("db.%s.password must be set" % [name]), + database: "acore_%s" % [name], + }, + auth: mkConfig("auth"), + world: mkConfig("world"), + characters: mkConfig("characters"), + }, + panel: { + domain: error "panel.domain must be set", + soap: { + username: error "panel.soap.username must be set", + password: error "panel.soap.password must be set", + }, + secret: error "panel.secret must be set", + oauth: { + clientID: error "panel.oauth.clientID must set", + clientSecret: error "panel.oauth.clientSecret must set", + redirectURL: "https://%s/callback" % [cfg.panel.domain], + }, + motd: "", + }, + overrides: { + authserver: {}, + worldserver: {}, + ahbot: {}, + }, + }, + + ns: kube.Namespace(cfg.namespace), + + data: ns.Contain(kube.PersistentVolumeClaim(cfg.prefix + "data")) { + spec+: { + storageClassName: "waw-hdd-redundant-3", + accessModes: ["ReadWriteOnce"], + resources: { + requests: { + storage: "50Gi", + }, + }, + }, + }, + + // Make a *DatabaseInfo string for use by acore config. These are not any real + // standardized DSN format, just some semicolon-delimited proprietary format. + local mkDbString = function(config) ( + "%s;%d;%s;%s;%s" % [ + config.host, + config.port, + config.user, + config.password, + config.database, + ] + ), + + etc: ns.Contain(kube.Secret(cfg.prefix + "etc")) { + data: { + "worldserver.conf": std.base64(std.manifestIni({ + sections: { + worldserver: { + RealmID: 1, + DataDir: "/data/current", + LoginDatabaseInfo: mkDbString(cfg.db.auth), + WorldDatabaseInfo: mkDbString(cfg.db.world), + CharacterDatabaseInfo: mkDbString(cfg.db.characters), + LogLevel: 2, + + "Console.Enable": 0, + "Ra.Enable": 1, + "Ra.IP": "127.0.0.1", + "SOAP.Enabled": 1, + "SOAP.IP": "0.0.0.0", + + } + cfg.overrides.worldserver, + + }, + })), + "mod_ahbot.conf": std.base64(std.manifestIni({ + sections: { + worldserver: cfg.overrides.ahbot, + }, + })), + "authserver.conf": std.base64(std.manifestIni({ + sections: { + authserver: { + LoginDatabaseInfo: mkDbString(cfg.db.auth), + } + cfg.overrides.authserver, + }, + })), + }, + }, + + worldserverDeploy: ns.Contain(kube.Deployment(cfg.prefix + "worldserver")) { + spec+: { + template+: { + spec+: { + containers_: { + default: kube.Container("default") { + image: cfg.images.acore, + volumeMounts: [ + { name: "data", mountPath: "/data" }, + { name: "etc", mountPath: "/azeroth-server/etc/worldserver.conf", subPath: "worldserver.conf", }, + { name: "etc", mountPath: "/azeroth-server/etc/mod_ahbot.conf", subPath: "mod_ahbot.conf", }, + ], + command: [ + "/entrypoint.sh", + "/azeroth-server/bin/worldserver", + ], + }, + }, + securityContext: { + runAsUser: 999, + runAsGroup: 999, + fsGroup: 999, + }, + volumes_: { + data: kube.PersistentVolumeClaimVolume(wow.data), + etc: kube.SecretVolume(wow.etc), + }, + }, + }, + }, + }, + + authserverDeploy: ns.Contain(kube.Deployment(cfg.prefix + "authserver")) { + spec+: { + template+: { + spec+: { + containers_: { + default: kube.Container("default") { + image: cfg.images.acore, + volumeMounts_: { + etc: { mountPath: "/azeroth-server/etc/authserver.conf", subPath: "authserver.conf", }, + }, + command: [ + "/azeroth-server/bin/authserver", + ], + }, + }, + securityContext: { + runAsUser: 999, + runAsGroup: 999, + }, + volumes_: { + etc: kube.SecretVolume(wow.etc), + }, + }, + }, + }, + }, + + soapSvc: ns.Contain(kube.Service(cfg.prefix + "worldserver-soap")) { + target_pod:: wow.worldserverDeploy.spec.template, + spec+: { + ports: [ + { name: "soap", port: 7878, targetPort: 7878, protocol: "TCP" }, + ], + }, + }, + worldserverSvc: ns.Contain(kube.Service(cfg.prefix + "worldserver")) { + target_pod:: wow.worldserverDeploy.spec.template, + metadata+: { + annotations+: { + "metallb.universe.tf/allow-shared-ip": "%s/%ssvc" % [cfg.namespace, cfg.prefix], + }, + }, + spec+: { + ports: [ + { name: "worldserver", port: 8085, targetPort: 8085, protocol: "TCP" }, + ], + type: "LoadBalancer", + externalTrafficPolicy: "Cluster", + loadBalancerIP: cfg.address, + }, + }, + authserverSvc: ns.Contain(kube.Service(cfg.prefix + "authserver")) { + target_pod:: wow.authserverDeploy.spec.template, + metadata+: { + annotations+: { + "metallb.universe.tf/allow-shared-ip": "%s/%ssvc" % [cfg.namespace, cfg.prefix], + }, + }, + spec+: { + ports: [ + { name: "authserver", port: 3724, targetPort: 3724, protocol: "TCP" }, + ], + type: "LoadBalancer", + externalTrafficPolicy: "Cluster", + loadBalancerIP: cfg.address, + }, + }, + + panelSecret: ns.Contain(kube.Secret(cfg.prefix + "panel-secret")) { + data+: { + soapPassword: std.base64(cfg.panel.soap.password), + secret: std.base64(cfg.panel.secret), + oauthSecret: std.base64(cfg.panel.oauth.clientSecret), + "motd.txt": std.base64(cfg.panel.motd), + }, + }, + panelData: ns.Contain(kube.PersistentVolumeClaim(cfg.prefix + "panel-data")) { + spec+: { + storageClassName: "waw-hdd-redundant-3", + accessModes: ["ReadWriteOnce"], + resources: { + requests: { + storage: "128Mi", + }, + }, + }, + }, + panelDeploy: ns.Contain(kube.Deployment(cfg.prefix + "panel")) { + spec+: { + template+: { + spec+: { + containers_: { + default: kube.Container("default") { + image: cfg.images.panel, + env_: { + SOAP_PASSWORD: kube.SecretKeyRef(wow.panelSecret, "soapPassword"), + SECRET: kube.SecretKeyRef(wow.panelSecret, "secret"), + OAUTH_SECRET: kube.SecretKeyRef(wow.panelSecret, "oauthSecret"), + }, + command: [ + "/personal/q3k/wow/panel/panel", + "-listen", "0.0.0.0:8080", + "-db", "/data/panel.db", + "-soap_address", "http://%s" % [wow.soapSvc.host_colon_port], + "-soap_password", "$(SOAP_PASSWORD)", + "-secret", "$(SECRET)", + "-oauth_client_id", cfg.panel.oauth.clientID, + "-oauth_client_secret", "$(OAUTH_SECRET)", + "-oauth_redirect_url", cfg.panel.oauth.redirectURL, + "-motd", "/secret/motd.txt", + ], + volumeMounts_: { + data: { mountPath: "/data" }, + secret: { mountPath: "/secret" }, + }, + }, + }, + volumes_: { + data: kube.PersistentVolumeClaimVolume(wow.panelData), + secret: kube.SecretVolume(wow.panelSecret), + }, + }, + }, + }, + }, + panelSvc: ns.Contain(kube.Service(cfg.prefix + "panel")) { + target_pod:: wow.panelDeploy.spec.template, + spec+: { + ports: [ + { name: "web", port: 8080, targetPort: 8080, protocol: "TCP" }, + ], + }, + }, + panelIngress: ns.Contain(kube.Ingress(cfg.prefix + "panel")) { + metadata+: { + annotations+: { + "kubernetes.io/tls-acme": "true", + "certmanager.k8s.io/cluster-issuer": "letsencrypt-prod", + }, + }, + spec+: { + tls: [ + { + hosts: [cfg.panel.domain], + secretName: cfg.prefix + "panel-tls", + }, + ], + rules: [ + { + host: cfg.panel.domain, + http: { + paths: [ + { path: "/", backend: wow.panelSvc.name_port }, + ], + }, + } + ], + }, + }, +} diff --git a/personal/q3k/wow/panel/BUILD.bazel b/personal/q3k/wow/panel/BUILD.bazel new file mode 100644 index 00000000..44162a4c --- /dev/null +++ b/personal/q3k/wow/panel/BUILD.bazel @@ -0,0 +1,51 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") +load("@io_bazel_rules_docker//container:container.bzl", "container_image", "container_layer", "container_push") + + +go_library( + name = "go_default_library", + srcs = [ + "main.go", + "soap.go", + ], + importpath = "code.hackerspace.pl/hscloud/personal/q3k/wow/panel", + visibility = ["//visibility:private"], + deps = [ + "@com_github_boltdb_bolt//:go_default_library", + "@com_github_coreos_go_oidc//:go_default_library", + "@com_github_golang_glog//:go_default_library", + "@com_github_gorilla_sessions//:go_default_library", + "@org_golang_x_oauth2//:go_default_library", + ], +) + +go_binary( + name = "panel", + embed = [":go_default_library"], + visibility = ["//visibility:public"], +) + +container_layer( + name = "layer_bin", + files = [ + ":panel", + ], + directory = "/personal/q3k/wow/panel/", +) + +container_image( + name = "runtime", + base = "@prodimage-bionic//image", + layers = [ + ":layer_bin", + ], +) + +container_push( + name = "push", + image = ":runtime", + format = "Docker", + registry = "registry.k0.hswaw.net", + repository = "q3k/panel", + tag = "{BUILD_TIMESTAMP}-{STABLE_GIT_COMMIT}", +) diff --git a/personal/q3k/wow/panel/main.go b/personal/q3k/wow/panel/main.go new file mode 100644 index 00000000..cd1f586f --- /dev/null +++ b/personal/q3k/wow/panel/main.go @@ -0,0 +1,466 @@ +package main + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "flag" + "fmt" + "html/template" + "io/ioutil" + "net/http" + "sync" + "time" + + "github.com/boltdb/bolt" + oidc "github.com/coreos/go-oidc" + "github.com/golang/glog" + "github.com/gorilla/sessions" + "golang.org/x/oauth2" +) + +var ( + flagSOAPAddress string + flagSOAPUsername string + flagSOAPPassword string + flagListen string + flagSecret string + flagOAuthClientID string + flagOAuthClientSecret string + flagOAuthRedirectURL string + flagDB string + flagMOTD string +) + +func init() { + flag.Set("logtostderr", "true") +} + +func main() { + flag.StringVar(&flagSOAPAddress, "soap_address", "http://127.0.0.1:7878", "Address of AC SOAP server") + flag.StringVar(&flagSOAPUsername, "soap_username", "test1", "SOAP username") + flag.StringVar(&flagSOAPPassword, "soap_password", "", "SOAP password") + flag.StringVar(&flagListen, "listen", "127.0.0.1:8080", "HTTP listen address") + flag.StringVar(&flagSecret, "secret", "", "Cookie secret") + flag.StringVar(&flagOAuthClientID, "oauth_client_id", "", "OAuth client ID") + flag.StringVar(&flagOAuthClientSecret, "oauth_client_secret", "", "OAuth client secret") + flag.StringVar(&flagOAuthRedirectURL, "oauth_redirect_url", "", "OAuth redirect URL") + flag.StringVar(&flagDB, "db", "db.db", "Path to database") + flag.StringVar(&flagMOTD, "motd", "", "Path to MOTD") + flag.Parse() + + if flagSecret == "" { + glog.Exitf("-secret must be set") + } + + var err error + var motd []byte + if flagMOTD == "" { + glog.Warningf("no MOTD defined, set -motd to get one") + } else { + motd, err = ioutil.ReadFile(flagMOTD) + if err != nil { + glog.Exitf("cannot read MOTD %q: %v", flagMOTD, err) + } + } + + db, err := bolt.Open(flagDB, 0600, nil) + if err != nil { + glog.Exitf("opening database failed: %v", err) + } + + provider, err := oidc.NewProvider(context.Background(), "https://sso.hackerspace.pl") + if err != nil { + glog.Exitf("newprovider: %v", err) + } + oauth2Config := oauth2.Config{ + ClientID: flagOAuthClientID, + ClientSecret: flagOAuthClientSecret, + RedirectURL: flagOAuthRedirectURL, + + // Discovery returns the OAuth2 endpoints. + Endpoint: provider.Endpoint(), + + // "openid" is a required scope for OpenID Connect flows. + Scopes: []string{oidc.ScopeOpenID, "profile:read"}, + } + + err = db.Update(func(tx *bolt.Tx) error { + for _, name := range []string{ + "emailToAccount", + } { + _, err := tx.CreateBucketIfNotExists([]byte(name)) + if err != nil { + return fmt.Errorf("create bucket %q: %s", name, err) + } + } + return nil + }) + if err != nil { + glog.Exitf("db setup failed: %v", err) + } + + s := &server{ + db: db, + store: sessions.NewCookieStore([]byte(flagSecret)), + oauth2: &oauth2Config, + motd: string(motd), + } + + http.HandleFunc("/", s.viewIndex) + http.HandleFunc("/login", s.viewLogin) + http.HandleFunc("/oauth", s.viewOauth) + http.HandleFunc("/callback", s.viewOauthCallback) + http.HandleFunc("/setup", s.viewOauthSetup) + http.HandleFunc("/reset", s.viewReset) + http.HandleFunc("/logout", s.viewLogout) + + err = http.ListenAndServe(flagListen, nil) + if err != nil { + glog.Exitf("ListenAndServe: %v", err) + } +} + +type server struct { + db *bolt.DB + store *sessions.CookieStore + oauth2 *oauth2.Config + motd string + + onlineLock sync.RWMutex + onlineData []playerinfo + onlineDeadline time.Time +} + +var ( + tLogin = template.Must(template.New("login").Parse(` +super wow - who are you? + +
+
+ ___ _   _ _ __   ___ _ __  __      _______      __
+/ __| | | | '_ \ / _ \ '__| \ \ /\ / / _ \ \ /\ / /
+\__ \ |_| | |_) |  __/ |     \ V  V / (_) \ V  V /
+|___/\__,_| .__/ \___|_|      \_/\_/ \___/ \_/\_/
+          |_|
+ _         _       _             _           _       _
+| | _____ | | ___ (_)___ _ _ __ | | _____   | |_   _| |__
+| |/ / _ \| |/ _ \_  / _' | '_ \| |/ / _ \  | | | | | '_ \
+|   < (_) | |  __// / (_| | | | |   < (_) | | | |_| | |_) |
+|_|\_\___/|_|\___/___\__,_|_| |_|_|\_\___/  |_|\__,_|_.__/
+
+ _         _                    __
+| | _____ | | ___  __ _  ___    \ \
+| |/ / _ \| |/ _ \/ _' |/ _ \  (_) |
+|   < (_) | |  __/ (_| | (_) |  _| |
+|_|\_\___/|_|\___|\__, |\___/  (_) |
+                  |___/         /_/
+
+
+Sign in (or create new account) with HSWAW SSO.
+ +

+Not a hswaw member? Talk to q3k. +

+`)) + tSetup = template.Must(template.New("setup").Parse(` +super wow - setup account + +hi, please provide details for your new WoW account
+pick any username you want, pick a 3-16 character password that isn't the same as your sso password (duh)
+(this isn't your character name, this will only be used to log into WoW) +{{ if .Error }} +
error: {{ .Error }}
+{{ end }} +
+username:
+password:
+ +
+`)) + tIndex = template.Must(template.New("index").Parse(` +super wow + + +Hello, {{ .Username }}.
+Log out.
+{{ .MOTD }} +

+Your account name is {{ .Username }}. Use the password that you entered when logging in via SSO for the first time, or

.
+

+Currently in-game: + + +{{ range .Online }} + +{{ end }} +
AccountCharacter
{{ .Account }}{{ .Character }}
+`)) +) + +func (s *server) session(r *http.Request) *sessions.Session { + session, _ := s.store.Get(r, sessionName) + return session +} + +func (s *server) sessionGet(r *http.Request, k string) string { + v, ok := s.session(r).Values[k] + if !ok { + return "" + } + v2, ok := v.(string) + if !ok { + return "" + } + return v2 +} + +func (s *server) sessionPut(w http.ResponseWriter, r *http.Request, k, v string) { + session := s.session(r) + session.Values[k] = v + session.Save(r, w) +} + +func (s *server) online(ctx context.Context) []playerinfo { + s.onlineLock.RLock() + if s.onlineData == nil || time.Now().After(s.onlineDeadline) { + s.onlineLock.RUnlock() + s.onlineLock.Lock() + data, err := onlinelist(ctx) + if err != nil { + glog.Errorf("onlinelist fatch failed: %v", err) + s.onlineDeadline = time.Now().Add(10 * time.Second) + } else { + s.onlineData = data + s.onlineDeadline = time.Now().Add(60 * time.Second) + } + s.onlineLock.Unlock() + s.onlineLock.RLock() + } + + res := make([]playerinfo, len(s.onlineData)) + for i, pi := range s.onlineData { + res[i] = pi + } + s.onlineLock.RUnlock() + return res +} + +func (s *server) viewIndex(w http.ResponseWriter, r *http.Request) { + account := s.sessionGet(r, "account") + if account == "" { + http.Redirect(w, r, "/login", 302) + return + } + err := tIndex.Execute(w, map[string]interface{}{ + "Username": account, + "Online": s.online(r.Context()), + "MOTD": template.HTML(s.motd), + }) + if err != nil { + glog.Errorf("/: %v", err) + return + } +} + +const sessionName = "wow" + +func (s *server) viewLogin(w http.ResponseWriter, r *http.Request) { + account := s.sessionGet(r, "account") + if account != "" { + http.Redirect(w, r, "/", 302) + return + } + err := tLogin.Execute(w, nil) + if err != nil { + glog.Errorf("/login: %v", err) + return + } +} + +func (s *server) viewOauth(w http.ResponseWriter, r *http.Request) { + stateBytes := make([]byte, 8) + _, err := rand.Read(stateBytes) + if err != nil { + glog.Errorf("/oauth: random: %v", err) + return + } + state := hex.EncodeToString(stateBytes) + s.sessionPut(w, r, "ostate", state) + url := s.oauth2.AuthCodeURL(state) + http.Redirect(w, r, url, http.StatusTemporaryRedirect) +} + +func (s *server) viewOauthCallback(w http.ResponseWriter, r *http.Request) { + if r.FormValue("errors") != "" { + fmt.Fprintf(w, "Errors: %s", r.FormValue("errors")) + return + } + state := s.sessionGet(r, "ostate") + if state == "" { + glog.Errorf("No state") + http.Redirect(w, r, "/", 302) + return + } + if state != r.FormValue("state") { + glog.Errorf("Invalid state") + http.Redirect(w, r, "/", 302) + return + } + oauth2Token, err := s.oauth2.Exchange(r.Context(), r.FormValue("code")) + if err != nil { + glog.Errorf("Exchange failed: %v", err) + http.Redirect(w, r, "/", 302) + return + } + client := s.oauth2.Client(r.Context(), oauth2Token) + res, err := client.Get("https://sso.hackerspace.pl/api/1/userinfo") + if err != nil { + glog.Errorf("Userinfo failed: %v", err) + http.Redirect(w, r, "/", 302) + return + } + defer res.Body.Close() + data, _ := ioutil.ReadAll(res.Body) + + ui := userinfo{} + err = json.Unmarshal(data, &ui) + if err != nil || ui.Email == "" { + glog.Errorf("Userinfo unarshal failed: %v", err) + http.Redirect(w, r, "/", 302) + return + } + + account, err := s.accountForEmail(ui.Email) + if err != nil { + glog.Errorf("account get failed: %v", err) + http.Redirect(w, r, "/", 302) + return + } + if account != "" { + s.sessionPut(w, r, "account", account) + http.Redirect(w, r, "/", 302) + } else { + s.sessionPut(w, r, "email", ui.Email) + http.Redirect(w, r, "/setup", 302) + } +} + +func (s *server) viewOauthSetup(w http.ResponseWriter, r *http.Request) { + email := s.sessionGet(r, "email") + if email == "" { + glog.Errorf("No email") + http.Redirect(w, r, "/", 302) + return + } + + if r.Method == "GET" { + tSetup.Execute(w, nil) + return + } + + username := r.FormValue("username") + password := r.FormValue("password") + if !reAccount.MatchString(username) { + tSetup.Execute(w, map[string]string{ + "Username": username, + "Error": "Invalid username - must be 3-16 a-z 0-9 - _", + }) + return + } + if !rePassword.MatchString(password) { + tSetup.Execute(w, map[string]string{ + "Username": username, + "Error": "Invalid password - must be 3-16 a-z A-Z 0-9 - _", + }) + return + } + + // this races. ugh. no way to list users. yolo. + err := createAccount(r.Context(), username, password) + if err != nil { + tSetup.Execute(w, map[string]string{ + "Username": username, + "Error": "Account already exists, pick a different username", + }) + return + } + + err = s.db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte("emailToAccount")) + v := string(b.Get([]byte(email))) + if v != "" { + s.sessionPut(w, r, "account", v) + http.Redirect(w, r, "/", 302) + return nil + } + b.Put([]byte(email), []byte(username)) + s.sessionPut(w, r, "account", username) + http.Redirect(w, r, "/", 302) + return nil + }) + if err != nil { + glog.Errorf("setup tx: %v", err) + http.Redirect(w, r, "/", 302) + } +} +func (s *server) viewReset(w http.ResponseWriter, r *http.Request) { + account := s.sessionGet(r, "account") + if account == "" { + glog.Errorf("No account") + http.Redirect(w, r, "/", 302) + return + } + + password := r.FormValue("password") + if !rePassword.MatchString(password) { + fmt.Fprintf(w, "pick a 3-16 password with not too many special chars") + return + } + + // this races. ugh. no way to list users. yolo. + err := ensureAccount(r.Context(), account, password) + if err != nil { + glog.Errorf("ensureAccount(%q, _): %v", account, err) + fmt.Fprintf(w, "something went wrong, lol") + return + } + fmt.Fprintf(w, "new password set.") +} + +func (s *server) viewLogout(w http.ResponseWriter, r *http.Request) { + s.sessionPut(w, r, "account", "") + s.sessionPut(w, r, "email", "") + http.Redirect(w, r, "/", 302) +} + +func (s *server) accountForEmail(email string) (string, error) { + res := "" + err := s.db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte("emailToAccount")) + v := b.Get([]byte(email)) + res = string(v) + return nil + }) + return res, err +} + +type userinfo struct { + Email string `json:"email"` +} diff --git a/personal/q3k/wow/panel/soap.go b/personal/q3k/wow/panel/soap.go new file mode 100644 index 00000000..10198616 --- /dev/null +++ b/personal/q3k/wow/panel/soap.go @@ -0,0 +1,215 @@ +package main + +import ( + "bytes" + "context" + "encoding/xml" + "fmt" + "io/ioutil" + "net/http" + "regexp" + "strings" + + "github.com/golang/glog" +) + +type Envelope struct { + XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` + Body *Body +} + +type Body struct { + XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"` + Request *Request + Response *Response + Fault *Fault +} + +type Request struct { + XMLName xml.Name `xml:"urn:AC executeCommand"` + Command string `xml:"command"` +} +type Response struct { + XMLName xml.Name `xml:"urn:AC executeCommandResponse"` + Result string `xml:"result"` +} + +type Fault struct { + XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Fault"` + Code string `xml:"faultcode"` + String string `xml:"faultstring"` +} + +type commandRes struct { + result string + fault string +} + +var ( + reAccount = regexp.MustCompile(`^[a-z0-9\-_\.]{3,16}$`) + rePassword = regexp.MustCompile(`^[a-zA-Z0-9\-_\.]{3,16}$`) +) + +func createAccount(ctx context.Context, name, password string) error { + if !reAccount.MatchString(name) { + return fmt.Errorf("invalid account name") + } + if !rePassword.MatchString(password) { + return fmt.Errorf("invalid password name") + } + res, err := runCommand(ctx, fmt.Sprintf("account create %s %s", name, password)) + if err != nil { + glog.Errorf("Account create: %v", err) + return fmt.Errorf("server unavailable") + } + if res.result == fmt.Sprintf("Account created: %s", name) { + glog.Infof("Created account %q", name) + return nil + } + glog.Errorf("Account create fault: %q/%q", res.fault, res.result) + return fmt.Errorf("server error") +} + +func ensureAccount(ctx context.Context, name, password string) error { + if !reAccount.MatchString(name) { + return fmt.Errorf("invalid account name") + } + if !rePassword.MatchString(password) { + return fmt.Errorf("invalid password name") + } + res, err := runCommand(ctx, fmt.Sprintf("account create %s %s", name, password)) + if err != nil { + glog.Errorf("Account create: %v", err) + return fmt.Errorf("server unavailable") + } + if res.result == fmt.Sprintf("Account created: %s", name) { + glog.Infof("Created account %q", name) + return nil + } + if res.fault != "Account with this name already exist!" { + glog.Errorf("Account create fault: %q/%q", res.fault, res.result) + return fmt.Errorf("server error") + } + + res, err = runCommand(ctx, fmt.Sprintf("account set password %s %s %s", name, password, password)) + if res.result == "The password was changed" { + glog.Infof("Updated password for account %q", name) + return nil + } + glog.Infof("password update fault: %q/%q", res.fault, res.result) + return fmt.Errorf("server error") +} + +func runCommand(ctx context.Context, cmd string) (*commandRes, error) { + data, err := xml.Marshal(&Envelope{ + Body: &Body{ + Request: &Request{ + Command: cmd, + }, + }, + }) + if err != nil { + return nil, fmt.Errorf("marshal: %w", err) + } + buf := bytes.NewBuffer(data) + req, err := http.NewRequestWithContext(ctx, "POST", flagSOAPAddress, buf) + if err != nil { + return nil, fmt.Errorf("NewRequest(POST, %q): %w", flagSOAPAddress, err) + } + + req.SetBasicAuth(flagSOAPUsername, flagSOAPPassword) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("req.Do: %w", err) + } + defer resp.Body.Close() + + respBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("ReadAll response: %w", err) + } + + respEnvelope := Envelope{} + err = xml.Unmarshal(respBytes, &respEnvelope) + if err != nil { + return nil, fmt.Errorf("unmarshal: %w", err) + } + + if respEnvelope.Body == nil { + return nil, fmt.Errorf("no body returned") + } + + if respEnvelope.Body.Fault != nil { + fault := respEnvelope.Body.Fault + if fault.Code == "SOAP-ENV:Client" { + return &commandRes{ + fault: strings.TrimSpace(fault.String), + }, nil + } + return nil, fmt.Errorf("SOAP error %q: %v", fault.Code, fault.String) + } + + result := "" + if respEnvelope.Body.Response != nil { + result = respEnvelope.Body.Response.Result + } + + return &commandRes{ + result: strings.TrimSpace(result), + }, nil +} + +type playerinfo struct { + Account string + Character string +} + +func onlinelist(ctx context.Context) ([]playerinfo, error) { + res, err := runCommand(ctx, "account onlinelist") + if err != nil { + glog.Errorf("onlinelist: %v", err) + return nil, fmt.Errorf("server unavailable") + } + if res.fault != "" { + glog.Errorf("onlinelist fault: %q", res.fault) + return nil, fmt.Errorf("server unavailable") + } + + lines := strings.Split(res.result, "\n") + header := false + var pi []playerinfo + for _, line := range lines { + switch { + case strings.HasPrefix(line, "-="): + continue + case strings.HasPrefix(line, "-["): + default: + glog.Warningf("unparseable line %q", line) + continue + } + if !header { + header = true + continue + } + if len(line) != 69 { + glog.Warningf("wrong line length: %q", line) + continue + } + account := strings.ToLower(strings.TrimSpace(line[2:18])) + if line[18:20] != "][" { + glog.Warningf("unparseable line %q (wrong sep1)", line) + continue + } + character := strings.TrimSpace(line[20:32]) + if line[32:34] != "][" { + glog.Warningf("unparseable line %q (wrong sep2)", line) + continue + } + pi = append(pi, playerinfo{ + Account: account, + Character: character, + }) + } + glog.Infof("Onlinelist: %v", pi) + return pi, nil +} diff --git a/personal/q3k/wow/prod.jsonnet b/personal/q3k/wow/prod.jsonnet new file mode 100644 index 00000000..20912fbf --- /dev/null +++ b/personal/q3k/wow/prod.jsonnet @@ -0,0 +1,90 @@ +local wow = import "lib.libsonnet"; +local mysql = import "../../../kube/mysql.libsonnet"; + +{ + q3k: wow { + local sqlPassword = (std.split(importstr "secrets/plain/mysql-root-password", "\n"))[0], + local soapPassword = (std.split(importstr "secrets/plain/soap-password", "\n"))[0], + local panelSecret = (std.split(importstr "secrets/plain/panel-secret", "\n"))[0], + local oauthSecret = (std.split(importstr "secrets/plain/oauth-secret", "\n"))[0], + local motd = importstr "secrets/plain/motd.txt", + + local wow = self, + local cfg = self.cfg, + cfg+:: { + namespace: "personal-q3k", + prefix: "wow-", + address: "185.236.240.62", + db+: { + // Run everything as mysql root, #yolo. + local mkConfig = function(name) { + host: wow.mysql.svc.host, + port: wow.mysql.svc.port, + user: "root", + password: sqlPassword, + database: "acore_%s" % [name], + }, + auth+: mkConfig("auth"), + world+: mkConfig("world"), + characters+: mkConfig("characters"), + }, + panel+: { + domain: "wow.q3k.org", + soap+: { + username: "test1", + password: soapPassword, + }, + secret: panelSecret, + oauth+: { + clientID: "56403ef3-df6f-4893-b475-d6c18284ed42", + clientSecret: oauthSecret, + }, + motd: motd, + }, + overrides+: { + worldserver: { + RealmZone: 8, + Motd: "Welcome to Pabianice. Enjoy your grind.", + + "Rate.Drop.Item.Poor": 2, + "Rate.Drop.Item.Normal": 2, + "Rate.Drop.Item.Uncommon": 10, + "Rate.Drop.Item.Rare": 10, + "Rate.Drop.Item.Epic": 10, + "Rate.Drop.Item.Legendary": 10, + "Rate.Drop.Item.Artifact": 10, + "Rate.Drop.Item.Referenced": 10, + "Rate.Drop.Money": 10, + "Rate.XP.Kill": 5, + "Rate.XP.Explore": 5, + "Rate.XP.BattlegroundKill": 10, + "Rate.MoveSpeed": 2, + "SkillGain.Crafting": 5, + "SkillGain.Defense": 5, + "SkillGain.Gathering": 5, + "SkillGain.Weapon": 5, + + "MinPetitionSigns": 5, + "GM.AllowFriend": 1, + }, + ahbot: { + "AuctionHouseBot.EnableSeller": 1, + "AuctionHouseBot.EnableBuyer": 1, + "AuctionHouseBot.Account": 21, + "AuctionHouseBot.GUID": 12, + }, + }, + }, + + // Run a single shitty database. + mysql: mysql { + cfg+:: { + namespace: cfg.namespace, + appName: "wow", + prefix: cfg.prefix, + password: sqlPassword, + user: "acore", + }, + }, + }, +} diff --git a/personal/q3k/wow/secrets/cipher/motd.txt b/personal/q3k/wow/secrets/cipher/motd.txt new file mode 100644 index 00000000..2cf6885f --- /dev/null +++ b/personal/q3k/wow/secrets/cipher/motd.txt @@ -0,0 +1,46 @@ +-----BEGIN PGP MESSAGE----- + +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 +=+xOs +-----END PGP MESSAGE----- diff --git a/personal/q3k/wow/secrets/cipher/mysql-root-password b/personal/q3k/wow/secrets/cipher/mysql-root-password new file mode 100644 index 00000000..f323e613 --- /dev/null +++ b/personal/q3k/wow/secrets/cipher/mysql-root-password @@ -0,0 +1,40 @@ +-----BEGIN PGP MESSAGE----- + +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 +=BH/F +-----END PGP MESSAGE----- diff --git a/personal/q3k/wow/secrets/cipher/oauth-secret b/personal/q3k/wow/secrets/cipher/oauth-secret new file mode 100644 index 00000000..fedf4014 --- /dev/null +++ b/personal/q3k/wow/secrets/cipher/oauth-secret @@ -0,0 +1,40 @@ +-----BEGIN PGP MESSAGE----- + +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 +EWu/MPG8dkc8r8FJyy4oI0we4ec= +=KoHU +-----END PGP MESSAGE----- diff --git a/personal/q3k/wow/secrets/cipher/panel-secret b/personal/q3k/wow/secrets/cipher/panel-secret new file mode 100644 index 00000000..d43da27d --- /dev/null +++ b/personal/q3k/wow/secrets/cipher/panel-secret @@ -0,0 +1,40 @@ +-----BEGIN PGP MESSAGE----- + +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 +Z39r7G++Es9JHr2AqnoEY3s= +=E5nj +-----END PGP MESSAGE----- diff --git a/personal/q3k/wow/secrets/cipher/soap-password b/personal/q3k/wow/secrets/cipher/soap-password new file mode 100644 index 00000000..95c6e063 --- /dev/null +++ b/personal/q3k/wow/secrets/cipher/soap-password @@ -0,0 +1,40 @@ +-----BEGIN PGP MESSAGE----- + +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 +eFZDGjE5IA== +=l4Xr +-----END PGP MESSAGE----- diff --git a/personal/q3k/wow/secrets/plain/.gitignore b/personal/q3k/wow/secrets/plain/.gitignore new file mode 100644 index 00000000..72e8ffc0 --- /dev/null +++ b/personal/q3k/wow/secrets/plain/.gitignore @@ -0,0 +1 @@ +*