hello.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. // The hello binary runs hello.ts.net.
  4. package main // import "tailscale.com/cmd/hello"
  5. import (
  6. "context"
  7. "crypto/tls"
  8. _ "embed"
  9. "encoding/json"
  10. "errors"
  11. "flag"
  12. "html/template"
  13. "log"
  14. "net/http"
  15. "os"
  16. "strings"
  17. "time"
  18. "tailscale.com/client/tailscale"
  19. "tailscale.com/client/tailscale/apitype"
  20. )
  21. var (
  22. httpAddr = flag.String("http", ":80", "address to run an HTTP server on, or empty for none")
  23. httpsAddr = flag.String("https", ":443", "address to run an HTTPS server on, or empty for none")
  24. testIP = flag.String("test-ip", "", "if non-empty, look up IP and exit before running a server")
  25. )
  26. //go:embed hello.tmpl.html
  27. var embeddedTemplate string
  28. var localClient tailscale.LocalClient
  29. func main() {
  30. flag.Parse()
  31. if *testIP != "" {
  32. res, err := localClient.WhoIs(context.Background(), *testIP)
  33. if err != nil {
  34. log.Fatal(err)
  35. }
  36. e := json.NewEncoder(os.Stdout)
  37. e.SetIndent("", "\t")
  38. e.Encode(res)
  39. return
  40. }
  41. if devMode() {
  42. // Parse it optimistically
  43. var err error
  44. tmpl, err = template.New("home").Parse(embeddedTemplate)
  45. if err != nil {
  46. log.Printf("ignoring template error in dev mode: %v", err)
  47. }
  48. } else {
  49. if embeddedTemplate == "" {
  50. log.Fatalf("embeddedTemplate is empty; must be build with Go 1.16+")
  51. }
  52. tmpl = template.Must(template.New("home").Parse(embeddedTemplate))
  53. }
  54. http.HandleFunc("/", root)
  55. log.Printf("Starting hello server.")
  56. errc := make(chan error, 1)
  57. if *httpAddr != "" {
  58. log.Printf("running HTTP server on %s", *httpAddr)
  59. go func() {
  60. errc <- http.ListenAndServe(*httpAddr, nil)
  61. }()
  62. }
  63. if *httpsAddr != "" {
  64. log.Printf("running HTTPS server on %s", *httpsAddr)
  65. go func() {
  66. hs := &http.Server{
  67. Addr: *httpsAddr,
  68. TLSConfig: &tls.Config{
  69. GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
  70. switch hi.ServerName {
  71. case "hello.ts.net":
  72. return localClient.GetCertificate(hi)
  73. case "hello.ipn.dev":
  74. c, err := tls.LoadX509KeyPair(
  75. "/etc/hello/hello.ipn.dev.crt",
  76. "/etc/hello/hello.ipn.dev.key",
  77. )
  78. if err != nil {
  79. return nil, err
  80. }
  81. return &c, nil
  82. }
  83. return nil, errors.New("invalid SNI name")
  84. },
  85. },
  86. IdleTimeout: 30 * time.Second,
  87. ReadHeaderTimeout: 20 * time.Second,
  88. MaxHeaderBytes: 10 << 10,
  89. }
  90. errc <- hs.ListenAndServeTLS("", "")
  91. }()
  92. }
  93. log.Fatal(<-errc)
  94. }
  95. func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
  96. func getTmpl() (*template.Template, error) {
  97. if devMode() {
  98. tmplData, err := os.ReadFile("hello.tmpl.html")
  99. if os.IsNotExist(err) {
  100. log.Printf("using baked-in template in dev mode; can't find hello.tmpl.html in current directory")
  101. return tmpl, nil
  102. }
  103. return template.New("home").Parse(string(tmplData))
  104. }
  105. return tmpl, nil
  106. }
  107. // tmpl is the template used in prod mode.
  108. // In dev mode it's only used if the template file doesn't exist on disk.
  109. // It's initialized by main after flag parsing.
  110. var tmpl *template.Template
  111. type tmplData struct {
  112. DisplayName string // "Foo Barberson"
  113. LoginName string // "[email protected]"
  114. ProfilePicURL string // "https://..."
  115. MachineName string // "imac5k"
  116. MachineOS string // "Linux"
  117. IP string // "100.2.3.4"
  118. }
  119. func tailscaleIP(who *apitype.WhoIsResponse) string {
  120. if who == nil {
  121. return ""
  122. }
  123. for _, nodeIP := range who.Node.Addresses {
  124. if nodeIP.Addr().Is4() && nodeIP.IsSingleIP() {
  125. return nodeIP.Addr().String()
  126. }
  127. }
  128. for _, nodeIP := range who.Node.Addresses {
  129. if nodeIP.IsSingleIP() {
  130. return nodeIP.Addr().String()
  131. }
  132. }
  133. return ""
  134. }
  135. func root(w http.ResponseWriter, r *http.Request) {
  136. if r.TLS == nil && *httpsAddr != "" {
  137. host := r.Host
  138. if strings.Contains(r.Host, "100.101.102.103") ||
  139. strings.Contains(r.Host, "hello.ipn.dev") {
  140. host = "hello.ts.net"
  141. }
  142. http.Redirect(w, r, "https://"+host, http.StatusFound)
  143. return
  144. }
  145. if r.RequestURI != "/" {
  146. http.Redirect(w, r, "/", http.StatusFound)
  147. return
  148. }
  149. if r.TLS != nil && *httpsAddr != "" && strings.Contains(r.Host, "hello.ipn.dev") {
  150. http.Redirect(w, r, "https://hello.ts.net", http.StatusFound)
  151. return
  152. }
  153. tmpl, err := getTmpl()
  154. if err != nil {
  155. w.Header().Set("Content-Type", "text/plain")
  156. http.Error(w, "template error: "+err.Error(), 500)
  157. return
  158. }
  159. who, err := localClient.WhoIs(r.Context(), r.RemoteAddr)
  160. var data tmplData
  161. if err != nil {
  162. if devMode() {
  163. log.Printf("warning: using fake data in dev mode due to whois lookup error: %v", err)
  164. data = tmplData{
  165. DisplayName: "Taily Scalerson",
  166. LoginName: "[email protected]",
  167. ProfilePicURL: "https://placekitten.com/200/200",
  168. MachineName: "scaled",
  169. MachineOS: "Linux",
  170. IP: "100.1.2.3",
  171. }
  172. } else {
  173. log.Printf("whois(%q) error: %v", r.RemoteAddr, err)
  174. http.Error(w, "Your Tailscale works, but we failed to look you up.", 500)
  175. return
  176. }
  177. } else {
  178. data = tmplData{
  179. DisplayName: who.UserProfile.DisplayName,
  180. LoginName: who.UserProfile.LoginName,
  181. ProfilePicURL: who.UserProfile.ProfilePicURL,
  182. MachineName: firstLabel(who.Node.ComputedName),
  183. MachineOS: who.Node.Hostinfo.OS(),
  184. IP: tailscaleIP(who),
  185. }
  186. }
  187. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  188. tmpl.Execute(w, data)
  189. }
  190. // firstLabel s up until the first period, if any.
  191. func firstLabel(s string) string {
  192. s, _, _ = strings.Cut(s, ".")
  193. return s
  194. }