Update
This commit is contained in:
6
vendor/tailscale.com/ipn/localapi/cert.go
generated
vendored
6
vendor/tailscale.com/ipn/localapi/cert.go
generated
vendored
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ios && !android && !js
|
||||
//go:build !ios && !android && !js && !ts_omit_acme
|
||||
|
||||
package localapi
|
||||
|
||||
@@ -14,6 +14,10 @@ import (
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("cert/", (*Handler).serveCert)
|
||||
}
|
||||
|
||||
func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite && !h.PermitCert {
|
||||
http.Error(w, "cert access denied", http.StatusForbidden)
|
||||
|
||||
495
vendor/tailscale.com/ipn/localapi/debug.go
generated
vendored
Normal file
495
vendor/tailscale.com/ipn/localapi/debug.go
generated
vendored
Normal file
@@ -0,0 +1,495 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_debug
|
||||
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/util/httpm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("component-debug-logging", (*Handler).serveComponentDebugLogging)
|
||||
Register("debug", (*Handler).serveDebug)
|
||||
Register("debug-rotate-disco-key", (*Handler).serveDebugRotateDiscoKey)
|
||||
Register("dev-set-state-store", (*Handler).serveDevSetStateStore)
|
||||
Register("debug-bus-events", (*Handler).serveDebugBusEvents)
|
||||
Register("debug-bus-graph", (*Handler).serveEventBusGraph)
|
||||
Register("debug-derp-region", (*Handler).serveDebugDERPRegion)
|
||||
Register("debug-dial-types", (*Handler).serveDebugDialTypes)
|
||||
Register("debug-log", (*Handler).serveDebugLog)
|
||||
Register("debug-packet-filter-matches", (*Handler).serveDebugPacketFilterMatches)
|
||||
Register("debug-packet-filter-rules", (*Handler).serveDebugPacketFilterRules)
|
||||
Register("debug-peer-endpoint-changes", (*Handler).serveDebugPeerEndpointChanges)
|
||||
Register("debug-optional-features", (*Handler).serveDebugOptionalFeatures)
|
||||
}
|
||||
|
||||
func (h *Handler) serveDebugPeerEndpointChanges(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "status access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
ipStr := r.FormValue("ip")
|
||||
if ipStr == "" {
|
||||
http.Error(w, "missing 'ip' parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ip, err := netip.ParseAddr(ipStr)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid IP", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
chs, err := h.b.GetPeerEndpointChanges(r.Context(), ip)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
e := json.NewEncoder(w)
|
||||
e.SetIndent("", "\t")
|
||||
e.Encode(chs)
|
||||
}
|
||||
|
||||
func (h *Handler) serveComponentDebugLogging(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
component := r.FormValue("component")
|
||||
secs, _ := strconv.Atoi(r.FormValue("secs"))
|
||||
err := h.b.SetComponentDebugLogging(component, h.clock.Now().Add(time.Duration(secs)*time.Second))
|
||||
var res struct {
|
||||
Error string
|
||||
}
|
||||
if err != nil {
|
||||
res.Error = err.Error()
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func (h *Handler) serveDebugDialTypes(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "debug-dial-types access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
ip := r.FormValue("ip")
|
||||
port := r.FormValue("port")
|
||||
network := r.FormValue("network")
|
||||
|
||||
addr := ip + ":" + port
|
||||
if _, err := netip.ParseAddrPort(addr); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, "invalid address %q: %v", addr, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var bareDialer net.Dialer
|
||||
|
||||
dialer := h.b.Dialer()
|
||||
|
||||
var peerDialer net.Dialer
|
||||
peerDialer.Control = dialer.PeerDialControlFunc()
|
||||
|
||||
// Kick off a dial with each available dialer in parallel.
|
||||
dialers := []struct {
|
||||
name string
|
||||
dial func(context.Context, string, string) (net.Conn, error)
|
||||
}{
|
||||
{"SystemDial", dialer.SystemDial},
|
||||
{"UserDial", dialer.UserDial},
|
||||
{"PeerDial", peerDialer.DialContext},
|
||||
{"BareDial", bareDialer.DialContext},
|
||||
}
|
||||
type result struct {
|
||||
name string
|
||||
conn net.Conn
|
||||
err error
|
||||
}
|
||||
results := make(chan result, len(dialers))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, dialer := range dialers {
|
||||
dialer := dialer // loop capture
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
conn, err := dialer.dial(ctx, network, addr)
|
||||
results <- result{dialer.name, conn, err}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
for range len(dialers) {
|
||||
res := <-results
|
||||
fmt.Fprintf(w, "[%s] connected=%v err=%v\n", res.name, res.conn != nil, res.err)
|
||||
if res.conn != nil {
|
||||
res.conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
|
||||
if !buildfeatures.HasDebug {
|
||||
http.Error(w, "debug not supported in this build", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
// The action is normally in a POST form parameter, but
|
||||
// some actions (like "notify") want a full JSON body, so
|
||||
// permit some to have their action in a header.
|
||||
var action string
|
||||
switch v := r.Header.Get("Debug-Action"); v {
|
||||
case "notify":
|
||||
action = v
|
||||
default:
|
||||
action = r.FormValue("action")
|
||||
}
|
||||
var err error
|
||||
switch action {
|
||||
case "derp-set-homeless":
|
||||
h.b.MagicConn().SetHomeless(true)
|
||||
case "derp-unset-homeless":
|
||||
h.b.MagicConn().SetHomeless(false)
|
||||
case "rebind":
|
||||
err = h.b.DebugRebind()
|
||||
case "restun":
|
||||
err = h.b.DebugReSTUN()
|
||||
case "notify":
|
||||
var n ipn.Notify
|
||||
err = json.NewDecoder(r.Body).Decode(&n)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
h.b.DebugNotify(n)
|
||||
case "notify-last-netmap":
|
||||
h.b.DebugNotifyLastNetMap()
|
||||
case "break-tcp-conns":
|
||||
err = h.b.DebugBreakTCPConns()
|
||||
case "break-derp-conns":
|
||||
err = h.b.DebugBreakDERPConns()
|
||||
case "force-netmap-update":
|
||||
h.b.DebugForceNetmapUpdate()
|
||||
case "control-knobs":
|
||||
k := h.b.ControlKnobs()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(k.AsDebugJSON())
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
case "pick-new-derp":
|
||||
err = h.b.DebugPickNewDERP()
|
||||
case "force-prefer-derp":
|
||||
var n int
|
||||
err = json.NewDecoder(r.Body).Decode(&n)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
h.b.DebugForcePreferDERP(n)
|
||||
case "peer-relay-servers":
|
||||
servers := h.b.DebugPeerRelayServers().Slice()
|
||||
slices.SortFunc(servers, func(a, b netip.Addr) int {
|
||||
return a.Compare(b)
|
||||
})
|
||||
err = json.NewEncoder(w).Encode(servers)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
case "rotate-disco-key":
|
||||
err = h.b.DebugRotateDiscoKey()
|
||||
case "":
|
||||
err = fmt.Errorf("missing parameter 'action'")
|
||||
default:
|
||||
err = fmt.Errorf("unknown action %q", action)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
io.WriteString(w, "done\n")
|
||||
}
|
||||
|
||||
func (h *Handler) serveDevSetStateStore(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if err := h.b.SetDevStateStore(r.FormValue("key"), r.FormValue("value")); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
io.WriteString(w, "done\n")
|
||||
}
|
||||
|
||||
func (h *Handler) serveDebugPacketFilterRules(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
nm := h.b.NetMap()
|
||||
if nm == nil {
|
||||
http.Error(w, "no netmap", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", "\t")
|
||||
enc.Encode(nm.PacketFilterRules)
|
||||
}
|
||||
|
||||
func (h *Handler) serveDebugPacketFilterMatches(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
nm := h.b.NetMap()
|
||||
if nm == nil {
|
||||
http.Error(w, "no netmap", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", "\t")
|
||||
enc.Encode(nm.PacketFilter)
|
||||
}
|
||||
|
||||
// debugEventError provides the JSON encoding of internal errors from event processing.
|
||||
type debugEventError struct {
|
||||
Error string
|
||||
}
|
||||
|
||||
// serveDebugBusEvents taps into the tailscaled/utils/eventbus and streams
|
||||
// events to the client.
|
||||
func (h *Handler) serveDebugBusEvents(w http.ResponseWriter, r *http.Request) {
|
||||
// Require write access (~root) as the logs could contain something
|
||||
// sensitive.
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "event bus access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.GET {
|
||||
http.Error(w, "GET required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
bus, ok := h.LocalBackend().Sys().Bus.GetOK()
|
||||
if !ok {
|
||||
http.Error(w, "event bus not running", http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
f, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
io.WriteString(w, `{"Event":"[event listener connected]\n"}`+"\n")
|
||||
f.Flush()
|
||||
|
||||
mon := bus.Debugger().WatchBus()
|
||||
defer mon.Close()
|
||||
|
||||
i := 0
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
fmt.Fprintf(w, `{"Event":"[event listener closed]\n"}`)
|
||||
return
|
||||
case <-mon.Done():
|
||||
return
|
||||
case event := <-mon.Events():
|
||||
data := eventbus.DebugEvent{
|
||||
Count: i,
|
||||
Type: reflect.TypeOf(event.Event).String(),
|
||||
Event: event.Event,
|
||||
From: event.From.Name(),
|
||||
}
|
||||
for _, client := range event.To {
|
||||
data.To = append(data.To, client.Name())
|
||||
}
|
||||
|
||||
if msg, err := json.Marshal(data); err != nil {
|
||||
data.Event = debugEventError{Error: fmt.Sprintf(
|
||||
"failed to marshal JSON for %T", event.Event,
|
||||
)}
|
||||
if errMsg, err := json.Marshal(data); err != nil {
|
||||
fmt.Fprintf(w,
|
||||
`{"Count": %d, "Event":"[ERROR] failed to marshal JSON for %T\n"}`,
|
||||
i, event.Event)
|
||||
} else {
|
||||
w.Write(errMsg)
|
||||
}
|
||||
} else {
|
||||
w.Write(msg)
|
||||
}
|
||||
f.Flush()
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// serveEventBusGraph taps into the event bus and dumps out the active graph of
|
||||
// publishers and subscribers. It does not represent anything about the messages
|
||||
// exchanged.
|
||||
func (h *Handler) serveEventBusGraph(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != httpm.GET {
|
||||
http.Error(w, "GET required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
bus, ok := h.LocalBackend().Sys().Bus.GetOK()
|
||||
if !ok {
|
||||
http.Error(w, "event bus not running", http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
|
||||
debugger := bus.Debugger()
|
||||
clients := debugger.Clients()
|
||||
|
||||
graph := map[string]eventbus.DebugTopic{}
|
||||
|
||||
for _, client := range clients {
|
||||
for _, pub := range debugger.PublishTypes(client) {
|
||||
topic, ok := graph[pub.Name()]
|
||||
if !ok {
|
||||
topic = eventbus.DebugTopic{Name: pub.Name()}
|
||||
}
|
||||
topic.Publisher = client.Name()
|
||||
graph[pub.Name()] = topic
|
||||
}
|
||||
for _, sub := range debugger.SubscribeTypes(client) {
|
||||
topic, ok := graph[sub.Name()]
|
||||
if !ok {
|
||||
topic = eventbus.DebugTopic{Name: sub.Name()}
|
||||
}
|
||||
topic.Subscribers = append(topic.Subscribers, client.Name())
|
||||
graph[sub.Name()] = topic
|
||||
}
|
||||
}
|
||||
|
||||
// The top level map is not really needed for the client, convert to a list.
|
||||
topics := eventbus.DebugTopics{}
|
||||
for _, v := range graph {
|
||||
topics.Topics = append(topics.Topics, v)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(topics)
|
||||
}
|
||||
|
||||
func (h *Handler) serveDebugLog(w http.ResponseWriter, r *http.Request) {
|
||||
if !buildfeatures.HasLogTail {
|
||||
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "debug-log access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
defer h.b.TryFlushLogs() // kick off upload after we're done logging
|
||||
|
||||
type logRequestJSON struct {
|
||||
Lines []string
|
||||
Prefix string
|
||||
}
|
||||
|
||||
var logRequest logRequestJSON
|
||||
if err := json.NewDecoder(r.Body).Decode(&logRequest); err != nil {
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
prefix := logRequest.Prefix
|
||||
if prefix == "" {
|
||||
prefix = "debug-log"
|
||||
}
|
||||
logf := logger.WithPrefix(h.logf, prefix+": ")
|
||||
|
||||
// We can write logs too fast for logtail to handle, even when
|
||||
// opting-out of rate limits. Limit ourselves to at most one message
|
||||
// per 20ms and a burst of 60 log lines, which should be fast enough to
|
||||
// not block for too long but slow enough that we can upload all lines.
|
||||
logf = logger.SlowLoggerWithClock(r.Context(), logf, 20*time.Millisecond, 60, h.clock.Now)
|
||||
|
||||
for _, line := range logRequest.Lines {
|
||||
logf("%s", line)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *Handler) serveDebugOptionalFeatures(w http.ResponseWriter, r *http.Request) {
|
||||
of := &apitype.OptionalFeatures{
|
||||
Features: feature.Registered(),
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(of)
|
||||
}
|
||||
|
||||
func (h *Handler) serveDebugRotateDiscoKey(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if err := h.b.DebugRotateDiscoKey(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
io.WriteString(w, "done\n")
|
||||
}
|
||||
92
vendor/tailscale.com/ipn/localapi/debugderp.go
generated
vendored
92
vendor/tailscale.com/ipn/localapi/debugderp.go
generated
vendored
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_debug
|
||||
|
||||
package localapi
|
||||
|
||||
import (
|
||||
@@ -228,55 +230,59 @@ func (h *Handler) serveDebugDERPRegion(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Start by checking whether we can establish a HTTP connection
|
||||
for _, derpNode := range reg.Nodes {
|
||||
connSuccess := checkConn(derpNode)
|
||||
if !derpNode.STUNOnly {
|
||||
connSuccess := checkConn(derpNode)
|
||||
|
||||
// Verify that the /generate_204 endpoint works
|
||||
captivePortalURL := fmt.Sprintf("http://%s/generate_204?t=%d", derpNode.HostName, time.Now().Unix())
|
||||
req, err := http.NewRequest("GET", captivePortalURL, nil)
|
||||
if err != nil {
|
||||
st.Warnings = append(st.Warnings, fmt.Sprintf("Internal error creating request for captive portal check: %v", err))
|
||||
continue
|
||||
}
|
||||
req.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate, no-transform, max-age=0")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
st.Warnings = append(st.Warnings, fmt.Sprintf("Error making request to the captive portal check %q; is port 80 blocked?", captivePortalURL))
|
||||
} else {
|
||||
resp.Body.Close()
|
||||
}
|
||||
// Verify that the /generate_204 endpoint works
|
||||
captivePortalURL := fmt.Sprintf("http://%s/generate_204?t=%d", derpNode.HostName, time.Now().Unix())
|
||||
req, err := http.NewRequest("GET", captivePortalURL, nil)
|
||||
if err != nil {
|
||||
st.Warnings = append(st.Warnings, fmt.Sprintf("Internal error creating request for captive portal check: %v", err))
|
||||
continue
|
||||
}
|
||||
req.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate, no-transform, max-age=0")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
st.Warnings = append(st.Warnings, fmt.Sprintf("Error making request to the captive portal check %q; is port 80 blocked?", captivePortalURL))
|
||||
} else {
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
if !connSuccess {
|
||||
continue
|
||||
}
|
||||
if !connSuccess {
|
||||
continue
|
||||
}
|
||||
|
||||
fakePrivKey := key.NewNode()
|
||||
fakePrivKey := key.NewNode()
|
||||
|
||||
// Next, repeatedly get the server key to see if the node is
|
||||
// behind a load balancer (incorrectly).
|
||||
serverPubKeys := make(map[key.NodePublic]bool)
|
||||
for i := range 5 {
|
||||
func() {
|
||||
rc := derphttp.NewRegionClient(fakePrivKey, h.logf, h.b.NetMon(), func() *tailcfg.DERPRegion {
|
||||
return &tailcfg.DERPRegion{
|
||||
RegionID: reg.RegionID,
|
||||
RegionCode: reg.RegionCode,
|
||||
RegionName: reg.RegionName,
|
||||
Nodes: []*tailcfg.DERPNode{derpNode},
|
||||
// Next, repeatedly get the server key to see if the node is
|
||||
// behind a load balancer (incorrectly).
|
||||
serverPubKeys := make(map[key.NodePublic]bool)
|
||||
for i := range 5 {
|
||||
func() {
|
||||
rc := derphttp.NewRegionClient(fakePrivKey, h.logf, h.b.NetMon(), func() *tailcfg.DERPRegion {
|
||||
return &tailcfg.DERPRegion{
|
||||
RegionID: reg.RegionID,
|
||||
RegionCode: reg.RegionCode,
|
||||
RegionName: reg.RegionName,
|
||||
Nodes: []*tailcfg.DERPNode{derpNode},
|
||||
}
|
||||
})
|
||||
if err := rc.Connect(ctx); err != nil {
|
||||
st.Errors = append(st.Errors, fmt.Sprintf("Error connecting to node %q @ try %d: %v", derpNode.HostName, i, err))
|
||||
return
|
||||
}
|
||||
})
|
||||
if err := rc.Connect(ctx); err != nil {
|
||||
st.Errors = append(st.Errors, fmt.Sprintf("Error connecting to node %q @ try %d: %v", derpNode.HostName, i, err))
|
||||
return
|
||||
}
|
||||
|
||||
if len(serverPubKeys) == 0 {
|
||||
st.Info = append(st.Info, fmt.Sprintf("Successfully established a DERP connection with node %q", derpNode.HostName))
|
||||
}
|
||||
serverPubKeys[rc.ServerPublicKey()] = true
|
||||
}()
|
||||
}
|
||||
if len(serverPubKeys) > 1 {
|
||||
st.Errors = append(st.Errors, fmt.Sprintf("Received multiple server public keys (%d); is the DERP server behind a load balancer?", len(serverPubKeys)))
|
||||
if len(serverPubKeys) == 0 {
|
||||
st.Info = append(st.Info, fmt.Sprintf("Successfully established a DERP connection with node %q", derpNode.HostName))
|
||||
}
|
||||
serverPubKeys[rc.ServerPublicKey()] = true
|
||||
}()
|
||||
}
|
||||
if len(serverPubKeys) > 1 {
|
||||
st.Errors = append(st.Errors, fmt.Sprintf("Received multiple server public keys (%d); is the DERP server behind a load balancer?", len(serverPubKeys)))
|
||||
}
|
||||
} else {
|
||||
st.Info = append(st.Info, fmt.Sprintf("Node %q is marked STUNOnly; skipped non-STUN checks", derpNode.HostName))
|
||||
}
|
||||
|
||||
// Send a STUN query to this node to verify whether or not it
|
||||
|
||||
2013
vendor/tailscale.com/ipn/localapi/localapi.go
generated
vendored
2013
vendor/tailscale.com/ipn/localapi/localapi.go
generated
vendored
File diff suppressed because it is too large
Load Diff
141
vendor/tailscale.com/ipn/localapi/localapi_drive.go
generated
vendored
Normal file
141
vendor/tailscale.com/ipn/localapi/localapi_drive.go
generated
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_drive
|
||||
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/util/httpm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("drive/fileserver-address", (*Handler).serveDriveServerAddr)
|
||||
Register("drive/shares", (*Handler).serveShares)
|
||||
}
|
||||
|
||||
// serveDriveServerAddr handles updates of the Taildrive file server address.
|
||||
func (h *Handler) serveDriveServerAddr(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != httpm.PUT {
|
||||
http.Error(w, "only PUT allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
h.b.DriveSetServerAddr(string(b))
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
// serveShares handles the management of Taildrive shares.
|
||||
//
|
||||
// PUT - adds or updates an existing share
|
||||
// DELETE - removes a share
|
||||
// GET - gets a list of all shares, sorted by name
|
||||
// POST - renames an existing share
|
||||
func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.b.DriveSharingEnabled() {
|
||||
http.Error(w, `taildrive sharing not enabled, please add the attribute "drive:share" to this node in your ACLs' "nodeAttrs" section`, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case httpm.PUT:
|
||||
var share drive.Share
|
||||
err := json.NewDecoder(r.Body).Decode(&share)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
share.Path = path.Clean(share.Path)
|
||||
fi, err := os.Stat(share.Path)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
http.Error(w, "not a directory", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if drive.AllowShareAs() {
|
||||
// share as the connected user
|
||||
username, err := h.Actor.Username()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
share.As = username
|
||||
}
|
||||
err = h.b.DriveSetShare(&share)
|
||||
if err != nil {
|
||||
if errors.Is(err, drive.ErrInvalidShareName) {
|
||||
http.Error(w, "invalid share name", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
case httpm.DELETE:
|
||||
b, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = h.b.DriveRemoveShare(string(b))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
http.Error(w, "share not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case httpm.POST:
|
||||
var names [2]string
|
||||
err := json.NewDecoder(r.Body).Decode(&names)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = h.b.DriveRenameShare(names[0], names[1])
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
http.Error(w, "share not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if os.IsExist(err) {
|
||||
http.Error(w, "share name already used", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, drive.ErrInvalidShareName) {
|
||||
http.Error(w, "invalid share name", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case httpm.GET:
|
||||
shares := h.b.DriveGetShares()
|
||||
err := json.NewEncoder(w).Encode(shares)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
default:
|
||||
http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
2
vendor/tailscale.com/ipn/localapi/pprof.go
generated
vendored
2
vendor/tailscale.com/ipn/localapi/pprof.go
generated
vendored
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ios && !android && !js
|
||||
//go:build !ios && !android && !js && !ts_omit_debug
|
||||
|
||||
// We don't include it on mobile where we're more memory constrained and
|
||||
// there's no CLI to get at the results anyway.
|
||||
|
||||
108
vendor/tailscale.com/ipn/localapi/serve.go
generated
vendored
Normal file
108
vendor/tailscale.com/ipn/localapi/serve.go
generated
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_serve
|
||||
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/util/httpm"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("serve-config", (*Handler).serveServeConfig)
|
||||
}
|
||||
|
||||
func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case httpm.GET:
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "serve config denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
config, etag, err := h.b.ServeConfigETag()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
bts, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
http.Error(w, "error encoding config: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Etag", etag)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(bts)
|
||||
case httpm.POST:
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "serve config denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
configIn := new(ipn.ServeConfig)
|
||||
if err := json.NewDecoder(r.Body).Decode(configIn); err != nil {
|
||||
WriteErrorJSON(w, fmt.Errorf("decoding config: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
// require a local admin when setting a path handler
|
||||
// TODO: roll-up this Windows-specific check into either PermitWrite
|
||||
// or a global admin escalation check.
|
||||
if err := authorizeServeConfigForGOOSAndUserContext(runtime.GOOS, configIn, h); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
etag := r.Header.Get("If-Match")
|
||||
if err := h.b.SetServeConfig(configIn, etag); err != nil {
|
||||
if errors.Is(err, ipnlocal.ErrETagMismatch) {
|
||||
http.Error(w, err.Error(), http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
WriteErrorJSON(w, fmt.Errorf("updating config: %w", err))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
default:
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func authorizeServeConfigForGOOSAndUserContext(goos string, configIn *ipn.ServeConfig, h *Handler) error {
|
||||
switch goos {
|
||||
case "windows", "linux", "darwin", "illumos", "solaris":
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
// Only check for local admin on tailscaled-on-mac (based on "sudo"
|
||||
// permissions). On sandboxed variants (MacSys and AppStore), tailscaled
|
||||
// cannot serve files outside of the sandbox and this check is not
|
||||
// relevant.
|
||||
if goos == "darwin" && version.IsSandboxedMacOS() {
|
||||
return nil
|
||||
}
|
||||
if !configIn.HasPathHandler() {
|
||||
return nil
|
||||
}
|
||||
if h.Actor.IsLocalAdmin(h.b.OperatorUserID()) {
|
||||
return nil
|
||||
}
|
||||
switch goos {
|
||||
case "windows":
|
||||
return errors.New("must be a Windows local admin to serve a path")
|
||||
case "linux", "darwin", "illumos", "solaris":
|
||||
return errors.New("must be root, or be an operator and able to run 'sudo tailscale' to serve a path")
|
||||
default:
|
||||
// We filter goos at the start of the func, this default case
|
||||
// should never happen.
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
68
vendor/tailscale.com/ipn/localapi/syspolicy_api.go
generated
vendored
Normal file
68
vendor/tailscale.com/ipn/localapi/syspolicy_api.go
generated
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_syspolicy
|
||||
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/util/httpm"
|
||||
"tailscale.com/util/syspolicy/rsop"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("policy/", (*Handler).servePolicy)
|
||||
}
|
||||
|
||||
func (h *Handler) servePolicy(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "policy access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
suffix, ok := strings.CutPrefix(r.URL.EscapedPath(), "/localapi/v0/policy/")
|
||||
if !ok {
|
||||
http.Error(w, "misconfigured", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var scope setting.PolicyScope
|
||||
if suffix == "" {
|
||||
scope = setting.DefaultScope()
|
||||
} else if err := scope.UnmarshalText([]byte(suffix)); err != nil {
|
||||
http.Error(w, fmt.Sprintf("%q is not a valid scope", suffix), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
policy, err := rsop.PolicyFor(scope)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var effectivePolicy *setting.Snapshot
|
||||
switch r.Method {
|
||||
case httpm.GET:
|
||||
effectivePolicy = policy.Get()
|
||||
case httpm.POST:
|
||||
effectivePolicy, err = policy.Reload()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
default:
|
||||
http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
e := json.NewEncoder(w)
|
||||
e.SetIndent("", "\t")
|
||||
e.Encode(effectivePolicy)
|
||||
}
|
||||
413
vendor/tailscale.com/ipn/localapi/tailnetlock.go
generated
vendored
Normal file
413
vendor/tailscale.com/ipn/localapi/tailnetlock.go
generated
vendored
Normal file
@@ -0,0 +1,413 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_tailnetlock
|
||||
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/tkatype"
|
||||
"tailscale.com/util/httpm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("tka/affected-sigs", (*Handler).serveTKAAffectedSigs)
|
||||
Register("tka/cosign-recovery-aum", (*Handler).serveTKACosignRecoveryAUM)
|
||||
Register("tka/disable", (*Handler).serveTKADisable)
|
||||
Register("tka/force-local-disable", (*Handler).serveTKALocalDisable)
|
||||
Register("tka/generate-recovery-aum", (*Handler).serveTKAGenerateRecoveryAUM)
|
||||
Register("tka/init", (*Handler).serveTKAInit)
|
||||
Register("tka/log", (*Handler).serveTKALog)
|
||||
Register("tka/modify", (*Handler).serveTKAModify)
|
||||
Register("tka/sign", (*Handler).serveTKASign)
|
||||
Register("tka/status", (*Handler).serveTKAStatus)
|
||||
Register("tka/submit-recovery-aum", (*Handler).serveTKASubmitRecoveryAUM)
|
||||
Register("tka/verify-deeplink", (*Handler).serveTKAVerifySigningDeeplink)
|
||||
Register("tka/wrap-preauth-key", (*Handler).serveTKAWrapPreauthKey)
|
||||
}
|
||||
|
||||
func (h *Handler) serveTKAStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "lock status access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.GET {
|
||||
http.Error(w, "use GET", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(j)
|
||||
}
|
||||
|
||||
func (h *Handler) serveTKASign(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "lock sign access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
type signRequest struct {
|
||||
NodeKey key.NodePublic
|
||||
RotationPublic []byte
|
||||
}
|
||||
var req signRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.b.NetworkLockSign(req.NodeKey, req.RotationPublic); err != nil {
|
||||
http.Error(w, "signing failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "lock init access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
type initRequest struct {
|
||||
Keys []tka.Key
|
||||
DisablementValues [][]byte
|
||||
SupportDisablement []byte
|
||||
}
|
||||
var req initRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !h.b.NetworkLockAllowed() {
|
||||
http.Error(w, "Tailnet Lock is not supported on your pricing plan", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.b.NetworkLockInit(req.Keys, req.DisablementValues, req.SupportDisablement); err != nil {
|
||||
http.Error(w, "initialization failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(j)
|
||||
}
|
||||
|
||||
func (h *Handler) serveTKAModify(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
type modifyRequest struct {
|
||||
AddKeys []tka.Key
|
||||
RemoveKeys []tka.Key
|
||||
}
|
||||
var req modifyRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.b.NetworkLockModify(req.AddKeys, req.RemoveKeys); err != nil {
|
||||
http.Error(w, "network-lock modify failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(204)
|
||||
}
|
||||
|
||||
func (h *Handler) serveTKAWrapPreauthKey(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
type wrapRequest struct {
|
||||
TSKey string
|
||||
TKAKey string // key.NLPrivate.MarshalText
|
||||
}
|
||||
var req wrapRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 12*1024)).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var priv key.NLPrivate
|
||||
if err := priv.UnmarshalText([]byte(req.TKAKey)); err != nil {
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
wrappedKey, err := h.b.NetworkLockWrapPreauthKey(req.TSKey, priv)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(wrappedKey))
|
||||
}
|
||||
|
||||
func (h *Handler) serveTKAVerifySigningDeeplink(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "signing deeplink verification access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
type verifyRequest struct {
|
||||
URL string
|
||||
}
|
||||
var req verifyRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON for verifyRequest body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.b.NetworkLockVerifySigningDeeplink(req.URL)
|
||||
j, err := json.MarshalIndent(res, "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(j)
|
||||
}
|
||||
|
||||
func (h *Handler) serveTKADisable(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
body := io.LimitReader(r.Body, 1024*1024)
|
||||
secret, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
http.Error(w, "reading secret", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.b.NetworkLockDisable(secret); err != nil {
|
||||
http.Error(w, "network-lock disable failed: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *Handler) serveTKALocalDisable(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Require a JSON stanza for the body as an additional CSRF protection.
|
||||
var req struct{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.b.NetworkLockForceLocalDisable(); err != nil {
|
||||
http.Error(w, "network-lock local disable failed: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *Handler) serveTKALog(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != httpm.GET {
|
||||
http.Error(w, "use GET", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
limit := 50
|
||||
if limitStr := r.FormValue("limit"); limitStr != "" {
|
||||
lm, err := strconv.Atoi(limitStr)
|
||||
if err != nil {
|
||||
http.Error(w, "parsing 'limit' parameter: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
limit = int(lm)
|
||||
}
|
||||
|
||||
updates, err := h.b.NetworkLockLog(limit)
|
||||
if err != nil {
|
||||
http.Error(w, "reading log failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
j, err := json.MarshalIndent(updates, "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(j)
|
||||
}
|
||||
|
||||
func (h *Handler) serveTKAAffectedSigs(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
keyID, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 2048))
|
||||
if err != nil {
|
||||
http.Error(w, "reading body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sigs, err := h.b.NetworkLockAffectedSigs(keyID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
j, err := json.MarshalIndent(sigs, "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(j)
|
||||
}
|
||||
|
||||
func (h *Handler) serveTKAGenerateRecoveryAUM(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
type verifyRequest struct {
|
||||
Keys []tkatype.KeyID
|
||||
ForkFrom string
|
||||
}
|
||||
var req verifyRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON for verifyRequest body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var forkFrom tka.AUMHash
|
||||
if req.ForkFrom != "" {
|
||||
if err := forkFrom.UnmarshalText([]byte(req.ForkFrom)); err != nil {
|
||||
http.Error(w, "decoding fork-from: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
res, err := h.b.NetworkLockGenerateRecoveryAUM(req.Keys, forkFrom)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Write(res.Serialize())
|
||||
}
|
||||
|
||||
func (h *Handler) serveTKACosignRecoveryAUM(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
body := io.LimitReader(r.Body, 1024*1024)
|
||||
aumBytes, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
http.Error(w, "reading AUM", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var aum tka.AUM
|
||||
if err := aum.Unserialize(aumBytes); err != nil {
|
||||
http.Error(w, "decoding AUM", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := h.b.NetworkLockCosignRecoveryAUM(&aum)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Write(res.Serialize())
|
||||
}
|
||||
|
||||
func (h *Handler) serveTKASubmitRecoveryAUM(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
body := io.LimitReader(r.Body, 1024*1024)
|
||||
aumBytes, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
http.Error(w, "reading AUM", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var aum tka.AUM
|
||||
if err := aum.Unserialize(aumBytes); err != nil {
|
||||
http.Error(w, "decoding AUM", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.b.NetworkLockSubmitRecoveryAUM(&aum); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
Reference in New Issue
Block a user