feat: initial commit

This commit is contained in:
redtea committed 2026-10-13 00:25:29 +03:00
1 parent 353f62e257
commit c6852c6f1b
62 files changed
+5911 -2

No files matched your search

BIN
View File
Binary file not shown.
+64
View File
@@ -0,0 +1,64 @@
// Command agent is the Linux client installed on machines without a public IP.
// It runs as a root systemd service, dials the server over HTTP long-polling,
// and opens a root PTY when an admin requests a terminal in the web panel.
package main
import (
"context"
"log"
"net"
"net/url"
"os"
"os/signal"
"syscall"
"github.com/x-grk/longpolling-remote-access/internal/agent"
)
func main() {
host := envOr("HOST", "https://REDACTED:8445")
cfg := agent.Config{
Host: host,
EnrollSecret: os.Getenv("ENROLL_SECRET"),
Name: os.Getenv("AGENT_NAME"),
StatePath: envOr("STATE_PATH", "/etc/lpagent/state.json"),
TunnelAddr: tunnelAddr(host),
}
if cfg.EnrollSecret == "" {
// Only required for the very first run (before enrollment).
if _, err := os.Stat(cfg.StatePath); os.IsNotExist(err) {
log.Fatal("ENROLL_SECRET is required for first enrollment")
}
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
if err := agent.New(cfg).Run(ctx); err != nil {
log.Fatalf("agent: %v", err)
}
}
func envOr(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
// tunnelAddr derives the direct-tunnel address (host:8446) from the server URL.
// TUNNEL_ADDR overrides it entirely; TUNNEL_PORT overrides just the port.
// Set TUNNEL_ADDR=off to disable the direct tunnel.
func tunnelAddr(hostURL string) string {
if v := os.Getenv("TUNNEL_ADDR"); v != "" {
if v == "off" {
return ""
}
return v
}
u, err := url.Parse(hostURL)
if err != nil || u.Hostname() == "" {
return ""
}
return net.JoinHostPort(u.Hostname(), envOr("TUNNEL_PORT", "8446"))
}
+62
View File
@@ -0,0 +1,62 @@
// Command server is the host: it serves the admin web panel and acts as the
// rendezvous point that NAT'd agents dial into over HTTP long-polling.
//
// TLS is terminated by nginx in front of this process, so it listens on plain
// HTTP on localhost by default (see deploy/nginx.conf.example).
package main
import (
"flag"
"log"
"net/http"
"os"
"time"
"github.com/x-grk/longpolling-remote-access/internal/server"
)
func main() {
addr := flag.String("addr", envOr("LPRA_ADDR", "127.0.0.1:8091"), "listen address (behind nginx)")
dbPath := flag.String("db", envOr("LPRA_DB", "lpra.db"), "path to SQLite database")
distDir := flag.String("dist", envOr("LPRA_DIST", "dist"), "directory with agent binaries served at /download/")
tunnelAddr := flag.String("tunnel", envOr("LPRA_TUNNEL_ADDR", ":8446"), "listen address for the direct TCP+UDP forward tunnel (empty disables)")
insecureCookies := flag.Bool("insecure-cookies", envOr("LPRA_INSECURE_COOKIES", "") == "1",
"do not mark cookies Secure (plain-HTTP local testing only)")
flag.Parse()
db, err := server.OpenDB(*dbPath)
if err != nil {
log.Fatalf("open db: %v", err)
}
defer db.Close()
srv, err := server.New(server.Options{
DB: db,
DistDir: *distDir,
SecureCookies: !*insecureCookies,
TunnelAddr: *tunnelAddr,
})
if err != nil {
log.Fatalf("init server: %v", err)
}
httpSrv := &http.Server{
Addr: *addr,
Handler: srv.Handler(),
// No ReadTimeout/WriteTimeout: long-poll connections are intentionally
// long-lived. nginx bounds them instead.
ReadHeaderTimeout: 15 * time.Second,
IdleTimeout: 130 * time.Second,
}
log.Printf("lpra server listening on %s (db=%s dist=%s)", *addr, *dbPath, *distDir)
if err := httpSrv.ListenAndServe(); err != nil {
log.Fatalf("serve: %v", err)
}
}
func envOr(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}