|
|
|
@ -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
|
|
|
|
|