mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
cmd/hello: break out local HTTP client into client/tailscale
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
f647e3daaf
commit
30a37622b4
90
client/tailscale/tailscale.go
Normal file
90
client/tailscale/tailscale.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
// 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 tailscale contains Tailscale client code.
|
||||||
|
package tailscale
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"tailscale.com/safesocket"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// tsClient does HTTP requests to the local Tailscale daemon.
|
||||||
|
var tsClient = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
if addr != "local-tailscaled.sock:80" {
|
||||||
|
return nil, fmt.Errorf("unexpected URL address %q", addr)
|
||||||
|
}
|
||||||
|
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
|
||||||
|
// a TCP server on a random port, find the random port. For HTTP connections,
|
||||||
|
// we don't send the token. It gets added in an HTTP Basic-Auth header.
|
||||||
|
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||||
|
var d net.Dialer
|
||||||
|
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
||||||
|
}
|
||||||
|
return safesocket.ConnectDefault()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon.
|
||||||
|
//
|
||||||
|
// URLs are of the form http://local-tailscaled.sock/localapi/v0/whois?ip=1.2.3.4.
|
||||||
|
//
|
||||||
|
// The hostname must be "local-tailscaled.sock", even though it
|
||||||
|
// doesn't actually do any DNS lookup. The actual means of connecting to and
|
||||||
|
// authenticating to the local Tailscale daemon vary by platform.
|
||||||
|
//
|
||||||
|
// DoLocalRequest may mutate the request to add Authorization headers.
|
||||||
|
func DoLocalRequest(req *http.Request) (*http.Response, error) {
|
||||||
|
if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||||
|
req.SetBasicAuth("", token)
|
||||||
|
}
|
||||||
|
return tsClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
|
||||||
|
func WhoIs(ctx context.Context, remoteAddr string) (*tailcfg.WhoIsResponse, error) {
|
||||||
|
var ip string
|
||||||
|
if net.ParseIP(remoteAddr) != nil {
|
||||||
|
ip = remoteAddr
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
ip, _, err = net.SplitHostPort(remoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid remoteAddr %q", remoteAddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/whois?ip="+url.QueryEscape(ip), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res, err := DoLocalRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
slurp, _ := ioutil.ReadAll(res.Body)
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
return nil, fmt.Errorf("HTTP %s: %s", res.Status, slurp)
|
||||||
|
}
|
||||||
|
r := new(tailcfg.WhoIsResponse)
|
||||||
|
if err := json.Unmarshal(slurp, r); err != nil {
|
||||||
|
if max := 200; len(slurp) > max {
|
||||||
|
slurp = slurp[:max]
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", slurp)
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
@ -10,19 +10,15 @@
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"tailscale.com/safesocket"
|
"tailscale.com/client/tailscale"
|
||||||
"tailscale.com/tailcfg"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -37,7 +33,7 @@
|
|||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if *testIP != "" {
|
if *testIP != "" {
|
||||||
res, err := whoIs(*testIP)
|
res, err := tailscale.WhoIs(context.Background(), *testIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -46,7 +42,14 @@ func main() {
|
|||||||
e.Encode(res)
|
e.Encode(res)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !devMode() {
|
if devMode() {
|
||||||
|
// Parse it optimistically
|
||||||
|
var err error
|
||||||
|
tmpl, err = template.New("home").Parse(embeddedTemplate)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ignoring template error in dev mode: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if embeddedTemplate == "" {
|
if embeddedTemplate == "" {
|
||||||
log.Fatalf("embeddedTemplate is empty; must be build with Go 1.16+")
|
log.Fatalf("embeddedTemplate is empty; must be build with Go 1.16+")
|
||||||
}
|
}
|
||||||
@ -76,24 +79,24 @@ func main() {
|
|||||||
log.Fatal(<-errc)
|
log.Fatal(<-errc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func slurpHTML() string {
|
|
||||||
slurp, err := ioutil.ReadFile("hello.tmpl.html")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
return string(slurp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
|
func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
|
||||||
|
|
||||||
func getTmpl() (*template.Template, error) {
|
func getTmpl() (*template.Template, error) {
|
||||||
if devMode() {
|
if devMode() {
|
||||||
return template.New("home").Parse(slurpHTML())
|
tmplData, err := ioutil.ReadFile("hello.tmpl.html")
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
log.Printf("using baked-in template in dev mode; can't find hello.tmpl.html in current directory")
|
||||||
|
return tmpl, nil
|
||||||
|
}
|
||||||
|
return template.New("home").Parse(string(tmplData))
|
||||||
}
|
}
|
||||||
return tmpl, nil
|
return tmpl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var tmpl *template.Template // not used in dev mode, initialized by main after flag parse
|
// tmpl is the template used in prod mode.
|
||||||
|
// In dev mode it's only used if the template file doesn't exist on disk.
|
||||||
|
// It's initialized by main after flag parsing.
|
||||||
|
var tmpl *template.Template
|
||||||
|
|
||||||
type tmplData struct {
|
type tmplData struct {
|
||||||
DisplayName string // "Foo Barberson"
|
DisplayName string // "Foo Barberson"
|
||||||
@ -117,11 +120,6 @@ func root(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, "/", http.StatusFound)
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "no remote addr", 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tmpl, err := getTmpl()
|
tmpl, err := getTmpl()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
@ -129,7 +127,7 @@ func root(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
who, err := whoIs(ip)
|
who, err := tailscale.WhoIs(r.Context(), r.RemoteAddr)
|
||||||
var data tmplData
|
var data tmplData
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if devMode() {
|
if devMode() {
|
||||||
@ -143,11 +141,12 @@ func root(w http.ResponseWriter, r *http.Request) {
|
|||||||
IP: "100.1.2.3",
|
IP: "100.1.2.3",
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("whois(%q) error: %v", ip, err)
|
log.Printf("whois(%q) error: %v", r.RemoteAddr, err)
|
||||||
http.Error(w, "Your Tailscale works, but we failed to look you up.", 500)
|
http.Error(w, "Your Tailscale works, but we failed to look you up.", 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||||
data = tmplData{
|
data = tmplData{
|
||||||
DisplayName: who.UserProfile.DisplayName,
|
DisplayName: who.UserProfile.DisplayName,
|
||||||
LoginName: who.UserProfile.LoginName,
|
LoginName: who.UserProfile.LoginName,
|
||||||
@ -168,48 +167,3 @@ func firstLabel(s string) string {
|
|||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// tsSockClient does HTTP requests to the local Tailscale daemon.
|
|
||||||
// The hostname in the HTTP request is ignored.
|
|
||||||
var tsSockClient = &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
|
|
||||||
// a TCP server on a random port, find the random port. For HTTP connections,
|
|
||||||
// we don't send the token. It gets added in an HTTP Basic-Auth header.
|
|
||||||
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
|
||||||
var d net.Dialer
|
|
||||||
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
|
||||||
}
|
|
||||||
return safesocket.ConnectDefault()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func whoIs(ip string) (*tailcfg.WhoIsResponse, error) {
|
|
||||||
ctx := context.Background()
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/whois?ip="+url.QueryEscape(ip), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
|
||||||
req.SetBasicAuth("", token)
|
|
||||||
}
|
|
||||||
res, err := tsSockClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
slurp, _ := ioutil.ReadAll(res.Body)
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
return nil, fmt.Errorf("HTTP %s: %s", res.Status, slurp)
|
|
||||||
}
|
|
||||||
r := new(tailcfg.WhoIsResponse)
|
|
||||||
if err := json.Unmarshal(slurp, r); err != nil {
|
|
||||||
if max := 200; len(slurp) > max {
|
|
||||||
slurp = slurp[:max]
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", slurp)
|
|
||||||
}
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user