package safemode import ( "context" "crypto/tls" "fmt" "html" "net" "net/http" "strings" "time" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" ) var exampleAPIKeys = map[string]struct{}{ "your-api-key-1": {}, "your-api-key-2": {}, "your-api-key-3": {}, } // ExampleAPIKeys returns configured top-level API keys that still use template values. func ExampleAPIKeys(keys []string) []string { if len(keys) == 0 { return nil } matches := make([]string, 0, len(keys)) seen := make(map[string]struct{}, len(exampleAPIKeys)) for _, key := range keys { trimmed := strings.TrimSpace(key) if _, ok := exampleAPIKeys[trimmed]; !ok { continue } if _, exists := seen[trimmed]; exists { continue } seen[trimmed] = struct{}{} matches = append(matches, trimmed) } if len(matches) == 0 { return nil } return matches } // HasExampleAPIKeys reports whether any configured top-level API key is a template value. func HasExampleAPIKeys(keys []string) bool { return len(ExampleAPIKeys(keys)) > 0 } // WarningServerURL returns a local-friendly URL for the warning-only server. func WarningServerURL(cfg *config.Config) string { scheme := "http" host := "127.0.0.1" port := 0 if cfg != nil { port = cfg.Port if cfg.TLS.Enable { scheme = "https" } if trimmed := strings.TrimSpace(cfg.Host); trimmed != "" { host = trimmed } } if strings.Contains(host, ":") && !strings.HasPrefix(host, "[") { host = "[" + host + "]" } return fmt.Sprintf("%s://%s:%d/", scheme, host, port) } // NewExampleAPIKeyWarningHandler serves a setup warning page and leaves all other routes unregistered. func NewExampleAPIKeyWarningHandler(configPath string, keys []string) http.Handler { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL == nil || (r.URL.Path != "/" && r.URL.Path != "/management.html") { http.NotFound(w, r) return } if r.Method != http.MethodGet && r.Method != http.MethodHead { w.Header().Set("Allow", "GET, HEAD") http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store") if r.Method == http.MethodHead { w.WriteHeader(http.StatusOK) return } _, _ = fmt.Fprint(w, warningPageHTML(configPath, keys)) }) return mux } // StartExampleAPIKeyWarningServer starts the warning-only HTTP(S) server and blocks until it stops. func StartExampleAPIKeyWarningServer(ctx context.Context, cfg *config.Config, configPath string, keys []string) error { if cfg == nil { cfg = &config.Config{} } if ctx == nil { ctx = context.Background() } var tlsConfig *tls.Config if cfg.TLS.Enable { certPath := strings.TrimSpace(cfg.TLS.Cert) keyPath := strings.TrimSpace(cfg.TLS.Key) if certPath == "" || keyPath == "" { return fmt.Errorf("failed to start HTTPS warning server: tls.cert or tls.key is empty") } certPair, errLoad := tls.LoadX509KeyPair(certPath, keyPath) if errLoad != nil { return fmt.Errorf("failed to start HTTPS warning server: %w", errLoad) } tlsConfig = &tls.Config{ Certificates: []tls.Certificate{certPair}, MinVersion: tls.VersionTLS12, } } addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) listener, errListen := net.Listen("tcp", addr) if errListen != nil { return fmt.Errorf("failed to start warning server: %w", errListen) } if tlsConfig != nil { listener = tls.NewListener(listener, tlsConfig) } server := &http.Server{ Addr: addr, Handler: NewExampleAPIKeyWarningHandler(configPath, keys), } errCh := make(chan error, 1) go func() { errCh <- server.Serve(listener) }() select { case errServe := <-errCh: if errServe == nil || errServe == http.ErrServerClosed { return nil } return errServe case <-ctx.Done(): shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() errShutdown := server.Shutdown(shutdownCtx) errServe := <-errCh if errShutdown != nil { return errShutdown } if errServe != nil && errServe != http.ErrServerClosed { return errServe } return ctx.Err() } } func warningPageHTML(configPath string, keys []string) string { var b strings.Builder b.WriteString(`Example API key detected

Example API key detected

The normal API server was not started because the top-level api-keys configuration still contains template values.

`) if len(keys) > 0 { b.WriteString(`

Replace these values before using the proxy:

`) } if strings.TrimSpace(configPath) != "" { b.WriteString(`

Edit `) b.WriteString(html.EscapeString(configPath)) b.WriteString(`, set strong random API keys, then restart CLIProxyAPI.

`) } else { b.WriteString(`

Edit your config file, set strong random API keys, then restart CLIProxyAPI.

`) } b.WriteString(`
`) return b.String() }