package console import ( "context" "encoding/json" "fmt" "io" "net/http" "net/http/httputil" "net/url" "strings" "time" ) // Runtime is the resolver's admin API as the console sees it. The console never // reads the resolver's state any other way: everything it shows that the // resolver owns (statistics, query log, cache, health) comes through here. type Runtime struct { base *url.URL key string hc *http.Client } // NewRuntime points at a resolver's admin API, e.g. http://fancy-dns.dns:9053. // key is sent as X-API-Key; the console's own users never see it. func NewRuntime(rawURL, key string) (*Runtime, error) { u, err := url.Parse(rawURL) if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { return nil, fmt.Errorf("runtime URL %q must be http(s)://host[:port]", rawURL) } u.Path = strings.TrimSuffix(u.Path, "/") return &Runtime{base: u, key: key, hc: &http.Client{Timeout: 10 * time.Second}}, nil } // Get fetches path (e.g. "/api/zones") and decodes the JSON answer into v. func (r *Runtime) Get(ctx context.Context, path string, v any) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, r.base.String()+path, nil) if err != nil { return err } if r.key != "" { req.Header.Set("X-API-Key", r.key) } res, err := r.hc.Do(req) if err != nil { return fmt.Errorf("resolver unreachable: %w", err) } defer res.Body.Close() body, _ := io.ReadAll(io.LimitReader(res.Body, 32<<20)) if res.StatusCode != http.StatusOK { return fmt.Errorf("resolver answered %d: %s", res.StatusCode, strings.TrimSpace(string(body))) } return json.Unmarshal(body, v) } // Proxy forwards a request to the resolver unchanged except for credentials: // whatever the browser sent is dropped and the console's own key is used. // Streaming responses (the live query log) are flushed as they arrive. func (r *Runtime) Proxy() http.Handler { p := &httputil.ReverseProxy{ Rewrite: func(pr *httputil.ProxyRequest) { pr.SetURL(r.base) h := pr.Out.Header for _, k := range []string{"Cookie", "Authorization", "X-Api-Key", "X-Forwarded-For", "X-Forwarded-Host", "X-Forwarded-Proto", "Forwarded", "Origin", "Referer"} { h.Del(k) } if r.key != "" { h.Set("X-API-Key", r.key) } }, FlushInterval: -1, ErrorHandler: func(w http.ResponseWriter, _ *http.Request, err error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadGateway) _ = json.NewEncoder(w).Encode(map[string]string{"error": "resolver unreachable: " + err.Error()}) }, } return p }