|
|
@@ -113,6 +113,7 @@ var handler = map[string]localAPIHandler{
|
|
|
"upload-client-metrics": (*Handler).serveUploadClientMetrics,
|
|
|
"watch-ipn-bus": (*Handler).serveWatchIPNBus,
|
|
|
"whois": (*Handler).serveWhoIs,
|
|
|
+ "query-feature": (*Handler).serveQueryFeature,
|
|
|
}
|
|
|
|
|
|
func randHex(n int) string {
|
|
|
@@ -1932,6 +1933,66 @@ func (h *Handler) serveProfiles(w http.ResponseWriter, r *http.Request) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// serveQueryFeature makes a request to the "/machine/feature/query"
|
|
|
+// Noise endpoint to get instructions on how to enable a feature, such as
|
|
|
+// Funnel, for the node's tailnet.
|
|
|
+//
|
|
|
+// This request itself does not directly enable the feature on behalf of
|
|
|
+// the node, but rather returns information that can be presented to the
|
|
|
+// acting user about where/how to enable the feature. If relevant, this
|
|
|
+// includes a control URL the user can visit to explicitly consent to
|
|
|
+// using the feature.
|
|
|
+//
|
|
|
+// See tailcfg.QueryFeatureResponse for full response structure.
|
|
|
+func (h *Handler) serveQueryFeature(w http.ResponseWriter, r *http.Request) {
|
|
|
+ feature := r.FormValue("feature")
|
|
|
+ switch {
|
|
|
+ case !h.PermitRead:
|
|
|
+ http.Error(w, "access denied", http.StatusForbidden)
|
|
|
+ return
|
|
|
+ case r.Method != httpm.POST:
|
|
|
+ http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
|
|
+ return
|
|
|
+ case feature == "":
|
|
|
+ http.Error(w, "missing feature", http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ nm := h.b.NetMap()
|
|
|
+ if nm == nil {
|
|
|
+ http.Error(w, "no netmap", http.StatusServiceUnavailable)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ b, err := json.Marshal(&tailcfg.QueryFeatureRequest{
|
|
|
+ NodeKey: nm.NodeKey,
|
|
|
+ Feature: feature,
|
|
|
+ })
|
|
|
+ if err != nil {
|
|
|
+ http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ req, err := http.NewRequestWithContext(r.Context(),
|
|
|
+ "POST", "https://unused/machine/feature/query", bytes.NewReader(b))
|
|
|
+ if err != nil {
|
|
|
+ http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ resp, err := h.b.DoNoiseRequest(req)
|
|
|
+ if err != nil {
|
|
|
+ http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ defer resp.Body.Close()
|
|
|
+ w.Header().Set("Content-Type", "application/json")
|
|
|
+ w.WriteHeader(resp.StatusCode)
|
|
|
+ if _, err := io.Copy(w, resp.Body); err != nil {
|
|
|
+ http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
+ return
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
func defBool(a string, def bool) bool {
|
|
|
if a == "" {
|
|
|
return def
|