fix(all): stabilize login and proxy-aware game startup
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Unity reads these variables before managed plugins can run.
|
||||
func gameProxyEnvironment(environment []string, systemProxy string) []string {
|
||||
result := append([]string(nil), environment...)
|
||||
lookup := func(name string) (int, string) {
|
||||
for index, entry := range result {
|
||||
key, value, found := strings.Cut(entry, "=")
|
||||
if found && strings.EqualFold(key, name) {
|
||||
return index, value
|
||||
}
|
||||
}
|
||||
return -1, ""
|
||||
}
|
||||
if index, _ := lookup("UNITY_PROXYSERVER"); index < 0 && validUnityProxy(systemProxy) {
|
||||
result = append(result, "UNITY_PROXYSERVER="+systemProxy)
|
||||
}
|
||||
index, bypass := lookup("UNITY_NOPROXY")
|
||||
parts := strings.FieldsFunc(bypass, func(character rune) bool { return character == ',' || character == ';' })
|
||||
for _, local := range []string{"localhost", "127.0.0.1", "::1"} {
|
||||
present := false
|
||||
for _, part := range parts {
|
||||
if strings.EqualFold(strings.TrimSpace(part), local) {
|
||||
present = true
|
||||
}
|
||||
}
|
||||
if !present {
|
||||
parts = append(parts, local)
|
||||
}
|
||||
}
|
||||
entry := "UNITY_NOPROXY=" + strings.Join(parts, ",")
|
||||
if index >= 0 {
|
||||
result[index] = entry
|
||||
} else {
|
||||
result = append(result, entry)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func validUnityProxy(proxy string) bool {
|
||||
parsed, err := url.Parse(proxy)
|
||||
if err != nil || parsed.Scheme != "http" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" || parsed.Path != "" {
|
||||
return false
|
||||
}
|
||||
host, port, err := net.SplitHostPort(parsed.Host)
|
||||
if err != nil || host == "" || port == "" {
|
||||
return false
|
||||
}
|
||||
_, err = net.LookupPort("tcp", port)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Unity accepts one proxy, so per-scheme configurations must agree.
|
||||
func sharedWindowsProxy(raw string) string {
|
||||
if !strings.Contains(raw, "=") {
|
||||
proxy := "http://" + strings.TrimSpace(raw)
|
||||
if validUnityProxy(proxy) {
|
||||
return proxy
|
||||
}
|
||||
return ""
|
||||
}
|
||||
var httpProxy, httpsProxy string
|
||||
for _, entry := range strings.Split(raw, ";") {
|
||||
key, value, found := strings.Cut(strings.TrimSpace(entry), "=")
|
||||
if !found {
|
||||
return ""
|
||||
}
|
||||
switch strings.ToLower(key) {
|
||||
case "http":
|
||||
httpProxy = value
|
||||
case "https":
|
||||
httpsProxy = value
|
||||
}
|
||||
}
|
||||
if httpProxy == "" || !strings.EqualFold(httpProxy, httpsProxy) {
|
||||
return ""
|
||||
}
|
||||
proxy := "http://" + httpProxy
|
||||
if validUnityProxy(proxy) {
|
||||
return proxy
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//go:build darwin
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func systemGameProxy() string {
|
||||
output, err := exec.Command("/usr/sbin/scutil", "--proxy").Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
values := map[string]string{}
|
||||
for _, line := range strings.Split(string(output), "\n") {
|
||||
key, value, found := strings.Cut(strings.TrimSpace(line), " : ")
|
||||
if found {
|
||||
values[key] = strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
if values["ProxyAutoConfigEnable"] == "1" || values["ProxyAutoDiscoveryEnable"] == "1" ||
|
||||
values["HTTPEnable"] != "1" || values["HTTPSEnable"] != "1" ||
|
||||
values["HTTPProxy"] != values["HTTPSProxy"] || values["HTTPPort"] != values["HTTPSPort"] {
|
||||
return ""
|
||||
}
|
||||
port, err := strconv.Atoi(values["HTTPPort"])
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
return ""
|
||||
}
|
||||
proxy := "http://" + net.JoinHostPort(values["HTTPProxy"], strconv.Itoa(port))
|
||||
if validUnityProxy(proxy) {
|
||||
return proxy
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGameProxyEnvironmentPreservesExplicitAndIsIdempotent(t *testing.T) {
|
||||
input := []string{"PATH=kept", "UNITY_PROXYSERVER=http://explicit:8080", "UNITY_NOPROXY=example.org;localhost"}
|
||||
got := gameProxyEnvironment(input, "http://system:8888")
|
||||
if !reflect.DeepEqual(got, gameProxyEnvironment(got, "http://other:9999")) {
|
||||
t.Fatal("environment is not idempotent")
|
||||
}
|
||||
if got[1] != input[1] || input[2] != "UNITY_NOPROXY=example.org;localhost" {
|
||||
t.Fatal("explicit environment changed or input mutated")
|
||||
}
|
||||
if !strings.Contains(got[2], "127.0.0.1") || !strings.Contains(got[2], "::1") {
|
||||
t.Fatal("loopback bypass missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedWindowsProxy(t *testing.T) {
|
||||
for _, test := range []struct{ input, want string }{
|
||||
{"127.0.0.1:12451", "http://127.0.0.1:12451"},
|
||||
{"http=proxy:8080;https=proxy:8080", "http://proxy:8080"},
|
||||
{"http=proxy:8080;https=other:8080", ""},
|
||||
{"https=proxy:8080", ""}, {"user:password@proxy:8080", ""}, {"proxy:99999", ""},
|
||||
} {
|
||||
if got := sharedWindowsProxy(test.input); got != test.want {
|
||||
t.Errorf("proxy configuration result mismatch")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGameProxyEnvironmentIgnoresInvalidSystemProxy(t *testing.T) {
|
||||
for _, proxy := range []string{"", "https://proxy:443", "http://user:password@proxy:8080", "http://proxy:8080/path"} {
|
||||
for _, entry := range gameProxyEnvironment(nil, proxy) {
|
||||
if strings.HasPrefix(entry, "UNITY_PROXYSERVER=") {
|
||||
t.Fatal("invalid system proxy accepted")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//go:build windows
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/windows"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func systemGameProxy() string {
|
||||
var config struct {
|
||||
AutoDetect int32
|
||||
AutoConfigURL, Proxy, Bypass *uint16
|
||||
}
|
||||
procedure := syscall.NewLazyDLL("winhttp.dll").NewProc("WinHttpGetIEProxyConfigForCurrentUser")
|
||||
globalFree := syscall.NewLazyDLL("kernel32.dll").NewProc("GlobalFree")
|
||||
result, _, _ := procedure.Call(uintptr(unsafe.Pointer(&config)))
|
||||
defer func() {
|
||||
for _, pointer := range []*uint16{config.AutoConfigURL, config.Proxy, config.Bypass} {
|
||||
if pointer != nil {
|
||||
globalFree.Call(uintptr(unsafe.Pointer(pointer)))
|
||||
}
|
||||
}
|
||||
}()
|
||||
if result == 0 || config.AutoDetect != 0 || config.AutoConfigURL != nil || config.Proxy == nil {
|
||||
return ""
|
||||
}
|
||||
return sharedWindowsProxy(windows.UTF16PtrToString(config.Proxy))
|
||||
}
|
||||
@@ -25,6 +25,16 @@ func launchGame(target string) error {
|
||||
if err := exec.Command("pgrep", "-x", "BrownDust II").Run(); err == nil {
|
||||
return errGameAlreadyRunning
|
||||
}
|
||||
args := append([]string{target, "--args"}, gameLaunchArguments()...)
|
||||
// LaunchServices does not inherit open's environment; --env explicitly
|
||||
// supplies the Unity variables to the newly launched application.
|
||||
args := []string{target}
|
||||
for _, entry := range gameProxyEnvironment(os.Environ(), systemGameProxy()) {
|
||||
key, _, found := strings.Cut(entry, "=")
|
||||
if found && (strings.EqualFold(key, "UNITY_PROXYSERVER") || strings.EqualFold(key, "UNITY_NOPROXY")) {
|
||||
args = append(args, "--env", entry)
|
||||
}
|
||||
}
|
||||
args = append(args, "--args")
|
||||
args = append(args, gameLaunchArguments()...)
|
||||
return exec.Command("open", args...).Start()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -79,6 +80,7 @@ func launchGame(target string) error {
|
||||
}
|
||||
command := visibleCommand(target, gameLaunchArguments()...)
|
||||
command.Dir = filepath.Dir(target)
|
||||
command.Env = gameProxyEnvironment(os.Environ(), systemGameProxy())
|
||||
if err := command.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user