mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-04 23:45:34 +00:00
db5e269463
They were scattered/duplicated in misc places before. It can't be in the client package itself for circular dep reasons. This new package is basically tailcfg but for localhost communications, instead of to control. Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
186 lines
4.7 KiB
Go
186 lines
4.7 KiB
Go
// 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.
|
|
|
|
// The hello binary runs hello.ipn.dev.
|
|
package main // import "tailscale.com/cmd/hello"
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"flag"
|
|
"html/template"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"tailscale.com/client/tailscale"
|
|
"tailscale.com/client/tailscale/apitype"
|
|
)
|
|
|
|
var (
|
|
httpAddr = flag.String("http", ":80", "address to run an HTTP server on, or empty for none")
|
|
httpsAddr = flag.String("https", ":443", "address to run an HTTPS server on, or empty for none")
|
|
testIP = flag.String("test-ip", "", "if non-empty, look up IP and exit before running a server")
|
|
)
|
|
|
|
//go:embed hello.tmpl.html
|
|
var embeddedTemplate string
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
if *testIP != "" {
|
|
res, err := tailscale.WhoIs(context.Background(), *testIP)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
e := json.NewEncoder(os.Stdout)
|
|
e.SetIndent("", "\t")
|
|
e.Encode(res)
|
|
return
|
|
}
|
|
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 == "" {
|
|
log.Fatalf("embeddedTemplate is empty; must be build with Go 1.16+")
|
|
}
|
|
tmpl = template.Must(template.New("home").Parse(embeddedTemplate))
|
|
}
|
|
|
|
http.HandleFunc("/", root)
|
|
log.Printf("Starting hello server.")
|
|
|
|
errc := make(chan error, 1)
|
|
if *httpAddr != "" {
|
|
log.Printf("running HTTP server on %s", *httpAddr)
|
|
go func() {
|
|
errc <- http.ListenAndServe(*httpAddr, nil)
|
|
}()
|
|
}
|
|
if *httpsAddr != "" {
|
|
log.Printf("running HTTPS server on %s", *httpsAddr)
|
|
go func() {
|
|
errc <- http.ListenAndServeTLS(*httpsAddr,
|
|
"/etc/hello/hello.ipn.dev.crt",
|
|
"/etc/hello/hello.ipn.dev.key",
|
|
nil,
|
|
)
|
|
}()
|
|
}
|
|
log.Fatal(<-errc)
|
|
}
|
|
|
|
func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
|
|
|
|
func getTmpl() (*template.Template, error) {
|
|
if devMode() {
|
|
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
|
|
}
|
|
|
|
// 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 {
|
|
DisplayName string // "Foo Barberson"
|
|
LoginName string // "foo@bar.com"
|
|
ProfilePicURL string // "https://..."
|
|
MachineName string // "imac5k"
|
|
MachineOS string // "Linux"
|
|
IP string // "100.2.3.4"
|
|
}
|
|
|
|
func tailscaleIP(who *apitype.WhoIsResponse) string {
|
|
if who == nil {
|
|
return ""
|
|
}
|
|
for _, nodeIP := range who.Node.Addresses {
|
|
if nodeIP.IP.Is4() && nodeIP.IsSingleIP() {
|
|
return nodeIP.IP.String()
|
|
}
|
|
}
|
|
for _, nodeIP := range who.Node.Addresses {
|
|
if nodeIP.IsSingleIP() {
|
|
return nodeIP.IP.String()
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func root(w http.ResponseWriter, r *http.Request) {
|
|
if r.TLS == nil && *httpsAddr != "" {
|
|
host := r.Host
|
|
if strings.Contains(r.Host, "100.101.102.103") {
|
|
host = "hello.ipn.dev"
|
|
}
|
|
http.Redirect(w, r, "https://"+host, http.StatusFound)
|
|
return
|
|
}
|
|
if r.RequestURI != "/" {
|
|
http.Redirect(w, r, "/", http.StatusFound)
|
|
return
|
|
}
|
|
tmpl, err := getTmpl()
|
|
if err != nil {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
http.Error(w, "template error: "+err.Error(), 500)
|
|
return
|
|
}
|
|
|
|
who, err := tailscale.WhoIs(r.Context(), r.RemoteAddr)
|
|
var data tmplData
|
|
if err != nil {
|
|
if devMode() {
|
|
log.Printf("warning: using fake data in dev mode due to whois lookup error: %v", err)
|
|
data = tmplData{
|
|
DisplayName: "Taily Scalerson",
|
|
LoginName: "taily@scaler.son",
|
|
ProfilePicURL: "https://placekitten.com/200/200",
|
|
MachineName: "scaled",
|
|
MachineOS: "Linux",
|
|
IP: "100.1.2.3",
|
|
}
|
|
} else {
|
|
log.Printf("whois(%q) error: %v", r.RemoteAddr, err)
|
|
http.Error(w, "Your Tailscale works, but we failed to look you up.", 500)
|
|
return
|
|
}
|
|
} else {
|
|
data = tmplData{
|
|
DisplayName: who.UserProfile.DisplayName,
|
|
LoginName: who.UserProfile.LoginName,
|
|
ProfilePicURL: who.UserProfile.ProfilePicURL,
|
|
MachineName: firstLabel(who.Node.ComputedName),
|
|
MachineOS: who.Node.Hostinfo.OS,
|
|
IP: tailscaleIP(who),
|
|
}
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
tmpl.Execute(w, data)
|
|
}
|
|
|
|
// firstLabel s up until the first period, if any.
|
|
func firstLabel(s string) string {
|
|
if i := strings.Index(s, "."); i != -1 {
|
|
return s[:i]
|
|
}
|
|
return s
|
|
}
|