ipn/ipnserver, ipn/ipnlocal: move whois handler to new localapi package

This commit is contained in:
Brad Fitzpatrick 2021-02-15 10:41:52 -08:00
parent 36189e2704
commit fdac0387a7
3 changed files with 119 additions and 46 deletions

View File

@ -72,9 +72,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/disco from tailscale.com/derp+ tailscale.com/disco from tailscale.com/derp+
tailscale.com/internal/deepprint from tailscale.com/ipn/ipnlocal+ tailscale.com/internal/deepprint from tailscale.com/ipn/ipnlocal+
tailscale.com/ipn from tailscale.com/ipn/ipnserver+ tailscale.com/ipn from tailscale.com/ipn/ipnserver+
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver+
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
tailscale.com/ipn/ipnstate from tailscale.com/ipn+ tailscale.com/ipn/ipnstate from tailscale.com/ipn+
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
tailscale.com/log/logheap from tailscale.com/control/controlclient tailscale.com/log/logheap from tailscale.com/control/controlclient

View File

@ -7,7 +7,6 @@
import ( import (
"bufio" "bufio"
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -25,16 +24,17 @@
"syscall" "syscall"
"time" "time"
"go4.org/mem"
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/control/controlclient" "tailscale.com/control/controlclient"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal" "tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/localapi"
"tailscale.com/log/filelogger" "tailscale.com/log/filelogger"
"tailscale.com/logtail/backoff" "tailscale.com/logtail/backoff"
"tailscale.com/net/netstat" "tailscale.com/net/netstat"
"tailscale.com/safesocket" "tailscale.com/safesocket"
"tailscale.com/smallzstd" "tailscale.com/smallzstd"
"tailscale.com/tailcfg"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/pidowner" "tailscale.com/util/pidowner"
"tailscale.com/util/systemd" "tailscale.com/util/systemd"
@ -222,13 +222,22 @@ func (s *server) blockWhileInUse(conn io.Reader, ci connIdentity) {
} }
} }
// bufferHasHTTPRequest reports whether br looks like it has an HTTP
// request in it, without reading any bytes from it.
func bufferHasHTTPRequest(br *bufio.Reader) bool {
peek, _ := br.Peek(br.Buffered())
return mem.HasPrefix(mem.B(peek), mem.S("GET ")) ||
mem.HasPrefix(mem.B(peek), mem.S("POST ")) ||
mem.Contains(mem.B(peek), mem.S(" HTTP/"))
}
func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) { func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
// First see if it's an HTTP request. // First see if it's an HTTP request.
br := bufio.NewReader(c) br := bufio.NewReader(c)
c.SetReadDeadline(time.Now().Add(time.Second)) c.SetReadDeadline(time.Now().Add(time.Second))
peek, _ := br.Peek(4) br.Peek(4)
c.SetReadDeadline(time.Time{}) c.SetReadDeadline(time.Time{})
isHTTPReq := string(peek) == "GET " isHTTPReq := bufferHasHTTPRequest(br)
ci, err := s.addConn(c, isHTTPReq) ci, err := s.addConn(c, isHTTPReq)
if err != nil { if err != nil {
@ -255,7 +264,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
s.b.SetCurrentUserID(ci.UserID) s.b.SetCurrentUserID(ci.UserID)
if isHTTPReq { if isHTTPReq {
httpServer := http.Server{ httpServer := &http.Server{
// Localhost connections are cheap; so only do // Localhost connections are cheap; so only do
// keep-alives for a short period of time, as these // keep-alives for a short period of time, as these
// active connections lock the server into only serving // active connections lock the server into only serving
@ -626,7 +635,9 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) { opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) {
serveHTMLStatus(w, b) serveHTMLStatus(w, b)
}) })
opts.DebugMux.Handle("/localapi/v0/whois", whoIsHandler{b}) h := localapi.NewHandler(b)
h.PermitRead = true
opts.DebugMux.Handle("/localapi/", h)
} }
server.b = b server.b = b
@ -867,8 +878,11 @@ func (psc *protoSwitchConn) Close() error {
func (s *server) localhostHandler(ci connIdentity) http.Handler { func (s *server) localhostHandler(ci connIdentity) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if ci.IsUnixSock && r.URL.Path == "/localapi/v0/whois" { if ci.IsUnixSock && strings.HasPrefix(r.URL.Path, "/localapi/") {
whoIsHandler{s.b}.ServeHTTP(w, r) h := localapi.NewHandler(s.b)
h.PermitRead = true
h.PermitWrite = false // TODO: flesh out connIdentity on more platforms then set this
h.ServeHTTP(w, r)
return return
} }
if ci.Unknown { if ci.Unknown {
@ -894,40 +908,3 @@ func peerPid(entries []netstat.Entry, la, ra netaddr.IPPort) int {
} }
return 0 return 0
} }
// whoIsHandler is the debug server's /debug?ip=$IP HTTP handler.
type whoIsHandler struct {
b *ipnlocal.LocalBackend
}
func (h whoIsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
b := h.b
var ip netaddr.IP
if v := r.FormValue("ip"); v != "" {
var err error
ip, err = netaddr.ParseIP(r.FormValue("ip"))
if err != nil {
http.Error(w, "invalid 'ip' parameter", 400)
return
}
} else {
http.Error(w, "missing 'ip' parameter", 400)
return
}
n, u, ok := b.WhoIs(ip)
if !ok {
http.Error(w, "no match for IP", 404)
return
}
res := &tailcfg.WhoIsResponse{
Node: n,
UserProfile: &u,
}
j, err := json.MarshalIndent(res, "", "\t")
if err != nil {
http.Error(w, "JSON encoding error", 500)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(j)
}

95
ipn/localapi/localapi.go Normal file
View File

@ -0,0 +1,95 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package localapi contains the HTTP server handlers for tailscaled's API server.
package localapi
import (
"encoding/json"
"io"
"net/http"
"inet.af/netaddr"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/tailcfg"
)
func NewHandler(b *ipnlocal.LocalBackend) *Handler {
return &Handler{b: b}
}
type Handler struct {
// RequiredPassword, if non-empty, forces all HTTP
// requests to have HTTP basic auth with this password.
// It's used by the sandboxed macOS sameuserproof GUI auth mechanism.
RequiredPassword string
// PermitRead is whether read-only HTTP handlers are allowed.
PermitRead bool
// PermitWrite is whether mutating HTTP handlers are allowed.
PermitWrite bool
b *ipnlocal.LocalBackend
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h.b == nil {
http.Error(w, "server has no local backend", http.StatusInternalServerError)
return
}
if h.RequiredPassword != "" {
_, pass, ok := r.BasicAuth()
if !ok {
http.Error(w, "auth required", http.StatusUnauthorized)
return
}
if pass != h.RequiredPassword {
http.Error(w, "bad password", http.StatusForbidden)
return
}
}
switch r.URL.Path {
case "/localapi/v0/whois":
h.serveWhoIs(w, r)
default:
io.WriteString(w, "tailscaled\n")
}
}
func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "whois access denied", http.StatusForbidden)
return
}
b := h.b
var ip netaddr.IP
if v := r.FormValue("ip"); v != "" {
var err error
ip, err = netaddr.ParseIP(r.FormValue("ip"))
if err != nil {
http.Error(w, "invalid 'ip' parameter", 400)
return
}
} else {
http.Error(w, "missing 'ip' parameter", 400)
return
}
n, u, ok := b.WhoIs(ip)
if !ok {
http.Error(w, "no match for IP", 404)
return
}
res := &tailcfg.WhoIsResponse{
Node: n,
UserProfile: &u,
}
j, err := json.MarshalIndent(res, "", "\t")
if err != nil {
http.Error(w, "JSON encoding error", 500)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(j)
}