CI / build (push) Successful in 1m6s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
201 lines
5.9 KiB
Go
201 lines
5.9 KiB
Go
// Command t5de-proxy is T5DE's local proxy for IMVU Classic (see internal/proxy). The client starts one as it
|
|
// starts, and it exits with the client:
|
|
//
|
|
// t5de-proxy -parent <pid> -ready <file> [-log <file>] [-trace]
|
|
//
|
|
// It listens on a free port of 127.0.0.1 and, once it's listening, writes {"address", "token"} to the ready file
|
|
// (written whole, by renaming), which the client reads.
|
|
//
|
|
// t5de-proxy signin [-hub URL]
|
|
//
|
|
// signs the proxy in to T5DE's hub: it shows a code to enter on the hub's website and saves the sign-in (encrypted
|
|
// for the Windows user). From then on the proxy connects to the hub as it runs.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.t5de.dev/t5de/t5de-proxy/internal/buildinfo"
|
|
"git.t5de.dev/t5de/t5de-proxy/internal/hub"
|
|
"git.t5de.dev/t5de/t5de-proxy/internal/parent"
|
|
"git.t5de.dev/t5de/t5de-proxy/internal/products"
|
|
"git.t5de.dev/t5de/t5de-proxy/internal/proxy"
|
|
)
|
|
|
|
func main() {
|
|
if len(os.Args) > 1 && os.Args[1] == "signin" {
|
|
if err := signin(os.Args[2:]); err != nil {
|
|
fmt.Fprintln(os.Stderr, "t5de-proxy signin:", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
listen := flag.String("listen", "127.0.0.1:0", "address to listen on")
|
|
ready := flag.String("ready", "", "file to write the address and token to once listening")
|
|
parentPID := flag.Int("parent", 0, "exit when this process exits")
|
|
logFile := flag.String("log", "", "log file (default: standard error)")
|
|
trace := flag.Bool("trace", false, "log full URLs and every IMQ frame")
|
|
packs := flag.String("packs", "", "community product packs: directories of packs, separated by "+string(filepath.ListSeparator)+
|
|
" (default: %APPDATA%/T5DE/packs and $T5DE_PACKS)")
|
|
data := flag.String("data", "", "where the proxy keeps its state (default: %APPDATA%/T5DE/proxy)")
|
|
version := flag.Bool("version", false, "print the version and exit")
|
|
flag.Parse()
|
|
if *version {
|
|
fmt.Println(buildinfo.Version)
|
|
return
|
|
}
|
|
|
|
var out io.Writer = os.Stderr
|
|
if *logFile != "" {
|
|
f, err := os.Create(*logFile)
|
|
if err != nil {
|
|
os.Exit(1)
|
|
}
|
|
defer f.Close()
|
|
out = f
|
|
}
|
|
logger := log.New(out, "", log.Ldate|log.Ltime|log.Lmicroseconds)
|
|
if err := run(logger, *listen, *ready, *parentPID, *trace, packDirs(*packs), dataDir(*data)); err != nil {
|
|
logger.Printf("t5de-proxy: %v", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// packDirs is where community product packs are: the -packs flag, else T5DE's packs folder and $T5DE_PACKS.
|
|
func packDirs(flagValue string) []string {
|
|
if flagValue != "" {
|
|
return filepath.SplitList(flagValue)
|
|
}
|
|
var dirs []string
|
|
if config, err := os.UserConfigDir(); err == nil {
|
|
dirs = append(dirs, filepath.Join(config, "T5DE", "packs"))
|
|
}
|
|
return append(dirs, filepath.SplitList(os.Getenv("T5DE_PACKS"))...)
|
|
}
|
|
|
|
func dataDir(flagValue string) string {
|
|
if flagValue != "" {
|
|
return flagValue
|
|
}
|
|
if config, err := os.UserConfigDir(); err == nil {
|
|
return filepath.Join(config, "T5DE", "proxy")
|
|
}
|
|
return "."
|
|
}
|
|
|
|
func run(logger *log.Logger, listen, ready string, parentPID int, trace bool, packs []string, data string) error {
|
|
secret := make([]byte, 24)
|
|
if _, err := rand.Read(secret); err != nil {
|
|
return err
|
|
}
|
|
token := hex.EncodeToString(secret)
|
|
|
|
ln, err := net.Listen("tcp", listen)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
logger.Printf("t5de-proxy %s listening on %s (client process %d)", buildinfo.Version, ln.Addr(), parentPID)
|
|
if ready != "" {
|
|
if err := writeReady(ready, ln.Addr().String(), token); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if parentPID != 0 {
|
|
go func() {
|
|
err := parent.Wait(parentPID)
|
|
logger.Printf("client process %d exited (%v); exiting", parentPID, err)
|
|
os.Exit(0)
|
|
}()
|
|
}
|
|
handler := &proxy.Server{Token: token, Log: logger, Trace: trace}
|
|
prods, err := products.New(packs, data, logger.Printf)
|
|
if err != nil {
|
|
logger.Printf("community products are off: %v", err)
|
|
} else {
|
|
handler.Interceptor, handler.IMQFilter = prods, prods
|
|
logger.Printf("community products from %s", strings.Join(packs, ", "))
|
|
}
|
|
if saved, err := hub.Load(filepath.Join(data, "hub.json")); err == nil {
|
|
client := hub.New(filepath.Join(data, "hub.json"), saved, logger)
|
|
if prods != nil {
|
|
prods.Observer = client
|
|
}
|
|
go client.Run(context.Background())
|
|
} else if !errors.Is(err, hub.ErrSignedOut) {
|
|
logger.Printf("hub: %v", err)
|
|
}
|
|
server := &http.Server{
|
|
Handler: handler,
|
|
ReadHeaderTimeout: 30 * time.Second,
|
|
IdleTimeout: 2 * time.Minute,
|
|
ErrorLog: logger,
|
|
}
|
|
return server.Serve(ln)
|
|
}
|
|
|
|
func writeReady(path, address, token string) error {
|
|
data, err := json.Marshal(map[string]string{"address": address, "token": token})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmp := path + ".tmp"
|
|
if err := os.WriteFile(tmp, data, 0o600); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp, path)
|
|
}
|
|
|
|
// defaultHub is T5DE's hub.
|
|
const defaultHub = "https://hub.t5de.dev"
|
|
|
|
func signin(args []string) error {
|
|
fs := flag.NewFlagSet("signin", flag.ExitOnError)
|
|
hubURL := fs.String("hub", envOr("T5DE_HUB", defaultHub), "the hub's address (or $T5DE_HUB)")
|
|
data := fs.String("data", "", "where the proxy keeps its state (default: %APPDATA%/T5DE/proxy)")
|
|
fs.Parse(args)
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
|
defer stop()
|
|
saved, err := hub.SignIn(ctx, *hubURL, func(code, page string) {
|
|
fmt.Printf("To sign T5DE in, enter this code at %s:\n\n %s\n\nOpening it in your browser...\n", page, code)
|
|
openBrowser(page)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := saved.Save(filepath.Join(dataDir(*data), "hub.json")); err != nil {
|
|
return err
|
|
}
|
|
fmt.Println("Signed in. T5DE connects to the hub the next time the client starts.")
|
|
return nil
|
|
}
|
|
|
|
func envOr(name, fallback string) string {
|
|
if v := os.Getenv(name); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func openBrowser(page string) {
|
|
if runtime.GOOS == "windows" {
|
|
exec.Command("rundll32", "url.dll,FileProtocolHandler", page).Start()
|
|
}
|
|
}
|