0488cdda72
Build tinqs-git / build (push) Failing after 16m16s
- Proxy: add bot.tinqs.com route alongside legacy bot.arikigame.com - Bot: add /api/v1/ai/* rewrite alias for inference proxy (Cursor endpoint) - Auth: update Gitea URL defaults from git.arikigame.com to tinqs.com - UI: update all landing page, team-tool, callback URLs to tinqs.com - Libs: update gitea.ts, design.ts, docs-search.ts, handoffs.ts, mcp-handler.ts, image-gen-context.ts to tinqs.com API base - Config: add tinqs-ai provider entry in deeptinqs providers.json - Tests: update smoke test default URL to bot.tinqs.com All endpoints work on both domains during transition. Old bot.arikigame.com stays in proxy routes for backwards compat. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
167 lines
4.7 KiB
Go
167 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/acme/autocert"
|
|
)
|
|
|
|
var version = "1.2.0"
|
|
|
|
// Routes: hostname → localhost:port.
|
|
// Compiled in — these change maybe once a quarter.
|
|
var routes = map[string]string{
|
|
"git.arikigame.com": "http://127.0.0.1:4100", // Gitea
|
|
"bot.arikigame.com": "http://127.0.0.1:5500", // bot platform (legacy — use bot.tinqs.com)
|
|
"bot.tinqs.com": "http://127.0.0.1:5500", // bot platform (new domain)
|
|
"taco.arikigame.com": "http://127.0.0.1:5500", // meeting assistant (same process as bot)
|
|
"admin.arikigame.com": "http://127.0.0.1:7700", // admin portal
|
|
"langfuse.arikigame.com": "http://127.0.0.1:3100", // Langfuse LLM observability
|
|
"staging.tinqs.com": "http://127.0.0.1:3115", // Tinqs Platform (staging) — pm2 platform-staging
|
|
"platform.tinqs.com": "http://127.0.0.1:3120", // Tinqs Platform (production) — pm2 platform
|
|
}
|
|
|
|
// Tailscale-only hosts — unreachable from public internet. Defense-in-depth.
|
|
var tailscaleOnly = map[string]bool{
|
|
"admin.arikigame.com": true,
|
|
"langfuse.arikigame.com": true,
|
|
}
|
|
|
|
// tailscaleCIDR is 100.64.0.0/10 (CGNAT range Tailscale uses).
|
|
var tailscaleCIDR *net.IPNet
|
|
|
|
func init() {
|
|
_, tailscaleCIDR, _ = net.ParseCIDR("100.64.0.0/10")
|
|
}
|
|
|
|
func isTailscaleIP(remoteAddr string) bool {
|
|
host, _, err := net.SplitHostPort(remoteAddr)
|
|
if err != nil {
|
|
host = remoteAddr
|
|
}
|
|
ip := net.ParseIP(host)
|
|
return ip != nil && tailscaleCIDR.Contains(ip)
|
|
}
|
|
|
|
func realIP(r *http.Request) string {
|
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
return r.RemoteAddr
|
|
}
|
|
return host
|
|
}
|
|
|
|
func main() {
|
|
proxies := make(map[string]*httputil.ReverseProxy, len(routes))
|
|
domains := make([]string, 0, len(routes))
|
|
|
|
for host, target := range routes {
|
|
u, err := url.Parse(target)
|
|
if err != nil {
|
|
log.Fatalf("bad target for %s: %v", host, err)
|
|
}
|
|
rp := &httputil.ReverseProxy{
|
|
Director: func(h string, u *url.URL) func(req *http.Request) {
|
|
return func(req *http.Request) {
|
|
req.URL.Scheme = u.Scheme
|
|
req.URL.Host = u.Host
|
|
req.Host = h
|
|
ip := realIP(req)
|
|
req.Header.Set("X-Real-IP", ip)
|
|
req.Header.Set("X-Forwarded-For", ip)
|
|
req.Header.Set("X-Forwarded-Proto", "https")
|
|
}
|
|
}(host, u),
|
|
ErrorLog: log.New(os.Stderr, "["+host+"] ", log.LstdFlags),
|
|
// Flush immediately for SSE / streaming responses
|
|
FlushInterval: -1,
|
|
}
|
|
proxies[host] = rp
|
|
domains = append(domains, host)
|
|
}
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
host := strings.ToLower(strings.Split(r.Host, ":")[0])
|
|
|
|
proxy, ok := proxies[host]
|
|
if !ok {
|
|
http.Error(w, "unknown host", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
if tailscaleOnly[host] && !isTailscaleIP(r.RemoteAddr) {
|
|
http.Error(w, "Tailscale required — install at https://tailscale.com/download and sign in with @tinqs.com", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
proxy.ServeHTTP(w, r)
|
|
})
|
|
|
|
// Certificate storage
|
|
certDir := "/var/lib/tinqs-proxy/certs"
|
|
if d := os.Getenv("CERT_DIR"); d != "" {
|
|
certDir = d
|
|
}
|
|
|
|
mgr := &autocert.Manager{
|
|
Cache: autocert.DirCache(certDir),
|
|
Prompt: autocert.AcceptTOS,
|
|
HostPolicy: autocert.HostWhitelist(domains...),
|
|
}
|
|
|
|
// :80 — ACME HTTP-01 challenges, Tailscale-only hosts served directly, rest redirect to HTTPS
|
|
go func() {
|
|
httpSrv := &http.Server{
|
|
Addr: ":80",
|
|
ReadTimeout: 5 * time.Second,
|
|
WriteTimeout: 5 * time.Second,
|
|
Handler: mgr.HTTPHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
host := strings.Split(r.Host, ":")[0]
|
|
// Tailscale-only hosts: serve on HTTP directly (WireGuard is already encrypted)
|
|
if tailscaleOnly[host] {
|
|
proxy, ok := proxies[host]
|
|
if ok && isTailscaleIP(r.RemoteAddr) {
|
|
proxy.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
http.Error(w, "Tailscale required", http.StatusForbidden)
|
|
return
|
|
}
|
|
target := "https://" + r.Host + r.URL.RequestURI()
|
|
http.Redirect(w, r, target, http.StatusMovedPermanently)
|
|
})),
|
|
}
|
|
log.Printf("tinqs-proxy :80 (ACME + redirect)")
|
|
if err := httpSrv.ListenAndServe(); err != nil {
|
|
log.Fatalf(":80 failed: %v", err)
|
|
}
|
|
}()
|
|
|
|
// :443 — TLS with autocert
|
|
srv := &http.Server{
|
|
Addr: ":443",
|
|
Handler: handler,
|
|
TLSConfig: &tls.Config{
|
|
GetCertificate: mgr.GetCertificate,
|
|
MinVersion: tls.VersionTLS12,
|
|
},
|
|
ReadTimeout: 30 * time.Second,
|
|
WriteTimeout: 30 * time.Minute, // 37 GB repos need time
|
|
IdleTimeout: 5 * time.Minute,
|
|
}
|
|
|
|
log.Printf("tinqs-proxy v%s starting on :443", version)
|
|
for _, d := range domains {
|
|
log.Printf(" %s → %s", d, routes[d])
|
|
}
|
|
log.Fatal(srv.ListenAndServeTLS("", ""))
|
|
}
|