summaryrefslogtreecommitdiffhomepage
path: root/cmd/tsconnect/src/app/app.tsx
blob: 8d25b227437dd08b1e2ca61c1c7b36391ea81464 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

import { render, Component } from "preact"
import { URLDisplay } from "./url-display"
import { Header } from "./header"
import { GoPanicDisplay } from "./go-panic-display"
import { SSH } from "./ssh"

type AppState = {
  ipn?: IPN
  ipnState: IPNState
  netMap?: IPNNetMap
  browseToURL?: string
  goPanicError?: string
}

class App extends Component<{}, AppState> {
  state: AppState = { ipnState: "NoState" }
  #goPanicTimeout?: number

  render() {
    const { ipn, ipnState, goPanicError, netMap, browseToURL } = this.state

    let goPanicDisplay
    if (goPanicError) {
      goPanicDisplay = (
        <GoPanicDisplay error={goPanicError} dismiss={this.clearGoPanic} />
      )
    }

    let urlDisplay
    if (browseToURL) {
      urlDisplay = <URLDisplay url={browseToURL} />
    }

    let machineAuthInstructions
    if (ipnState === "NeedsMachineAuth") {
      machineAuthInstructions = (
        <div class="container mx-auto px-4 text-center">
          An administrator needs to approve this device.
        </div>
      )
    }

    const lockedOut = netMap?.lockedOut
    let lockedOutInstructions
    if (lockedOut) {
      lockedOutInstructions = (
        <div class="container mx-auto px-4 text-center space-y-4">
          <p>This instance of Tailscale Connect needs to be signed, due to
            {" "}<a href="https://tailscale.com/kb/1226/tailnet-lock/" class="link">tailnet lock</a>{" "}
            being enabled on this domain.
          </p>

          <p>
            Run the following command on a device with a trusted tailnet lock key:
            <pre>tailscale lock sign {netMap.self.nodeKey}</pre>
          </p>
        </div>
      )
    }

    let ssh
    if (ipn && ipnState === "Running" && netMap && !lockedOut) {
      ssh = <SSH netMap={netMap} ipn={ipn} />
    }

    return (
      <>
        <Header state={ipnState} ipn={ipn} />
        {goPanicDisplay}
        <div class="flex-grow flex flex-col justify-center overflow-hidden">
          {urlDisplay}
          {machineAuthInstructions}
          {lockedOutInstructions}
          {ssh}
        </div>
      </>
    )
  }

  runWithIPN(ipn: IPN) {
    this.setState({ ipn }, () => {
      ipn.run({
        notifyState: this.handleIPNState,
        notifyNetMap: this.handleNetMap,
        notifyBrowseToURL: this.handleBrowseToURL,
        notifyPanicRecover: this.handleGoPanic,
      })
    })
  }

  handleIPNState = (state: IPNState) => {
    const { ipn } = this.state
    this.setState({ ipnState: state })
    if (state === "NeedsLogin") {
      ipn?.login()
    } else if (["Running", "NeedsMachineAuth"].includes(state)) {
      this.setState({ browseToURL: undefined })
    }
  }

  handleNetMap = (netMapStr: string) => {
    const netMap = JSON.parse(netMapStr) as IPNNetMap
    if (DEBUG) {
      console.log("Received net map: " + JSON.stringify(netMap, null, 2))
    }
    this.setState({ netMap })
  }

  handleBrowseToURL = (url: string) => {
    if (this.state.ipnState === "Running") {
      // Ignore URL requests if we're already running -- it's most likely an
      // SSH check mode trigger and we already linkify the displayed URL
      // in the terminal.
      return
    }
    this.setState({ browseToURL: url })
  }

  handleGoPanic = (error: string) => {
    if (DEBUG) {
      console.error("Go panic", error)
    }
    this.setState({ goPanicError: error })
    if (this.#goPanicTimeout) {
      window.clearTimeout(this.#goPanicTimeout)
    }
    this.#goPanicTimeout = window.setTimeout(this.clearGoPanic, 10000)
  }

  clearGoPanic = () => {
    window.clearTimeout(this.#goPanicTimeout)
    this.#goPanicTimeout = undefined
    this.setState({ goPanicError: undefined })
  }
}

export function renderApp(): Promise<App> {
  return new Promise((resolve) => {
    render(
      <App ref={(app) => (app ? resolve(app) : undefined)} />,
      document.body
    )
  })
}