hello.go 5.4 KB

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