client/web: add csrf protection to web client api
Adds csrf protection and hooks up an initial POST request from the React web client. Updates tailscale/corp#13775 Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
parent
77ff705545
commit
077bbb8403
|
@ -0,0 +1,41 @@
|
||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/csrf"
|
||||||
|
"tailscale.com/util/httpm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type api struct {
|
||||||
|
s *Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP serves requests for the web client api.
|
||||||
|
// It should only be called by Server.ServeHTTP, via Server.apiHandler,
|
||||||
|
// which protects the handler using gorilla csrf.
|
||||||
|
func (a *api) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
||||||
|
user, err := authorize(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path := strings.TrimPrefix(r.URL.Path, "/api")
|
||||||
|
switch path {
|
||||||
|
case "/data":
|
||||||
|
switch r.Method {
|
||||||
|
case httpm.GET:
|
||||||
|
a.s.serveGetNodeDataJSON(w, r, user)
|
||||||
|
case httpm.POST:
|
||||||
|
a.s.servePostNodeUpdate(w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, "invalid endpoint", http.StatusNotFound)
|
||||||
|
}
|
|
@ -8,10 +8,12 @@
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"classnames": "^2.3.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/classnames": "^2.2.10",
|
||||||
"@types/react": "^18.0.20",
|
"@types/react": "^18.0.20",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
let csrfToken: string
|
||||||
|
|
||||||
|
// apiFetch wraps the standard JS fetch function
|
||||||
|
// with csrf header management.
|
||||||
|
export function apiFetch(
|
||||||
|
input: RequestInfo | URL,
|
||||||
|
init?: RequestInit | undefined
|
||||||
|
): Promise<Response> {
|
||||||
|
return fetch(input, {
|
||||||
|
...init,
|
||||||
|
headers: withCsrfToken(init?.headers),
|
||||||
|
}).then((r) => {
|
||||||
|
updateCsrfToken(r)
|
||||||
|
if (!r.ok) {
|
||||||
|
return r.text().then((err) => {
|
||||||
|
throw new Error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function withCsrfToken(h?: HeadersInit): HeadersInit {
|
||||||
|
return { ...h, "X-CSRF-Token": csrfToken }
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCsrfToken(r: Response) {
|
||||||
|
const tok = r.headers.get("X-CSRF-Token")
|
||||||
|
if (tok) {
|
||||||
|
csrfToken = tok
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,9 @@ import { Footer, Header, IP, State } from "src/components/legacy"
|
||||||
import useNodeData from "src/hooks/node-data"
|
import useNodeData from "src/hooks/node-data"
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const data = useNodeData()
|
// TODO(sonia): use isPosting value from useNodeData
|
||||||
|
// to fill loading states.
|
||||||
|
const { data, updateNode } = useNodeData()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-14">
|
<div className="py-14">
|
||||||
|
@ -15,7 +17,7 @@ export default function App() {
|
||||||
<main className="container max-w-lg mx-auto mb-8 py-6 px-8 bg-white rounded-md shadow-2xl">
|
<main className="container max-w-lg mx-auto mb-8 py-6 px-8 bg-white rounded-md shadow-2xl">
|
||||||
<Header data={data} />
|
<Header data={data} />
|
||||||
<IP data={data} />
|
<IP data={data} />
|
||||||
<State data={data} />
|
<State data={data} updateNode={updateNode} />
|
||||||
</main>
|
</main>
|
||||||
<Footer data={data} />
|
<Footer data={data} />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
import cx from "classnames"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { NodeData } from "src/hooks/node-data"
|
import { NodeData, NodeUpdate } from "src/hooks/node-data"
|
||||||
|
|
||||||
// TODO(tailscale/corp#13775): legacy.tsx contains a set of components
|
// TODO(tailscale/corp#13775): legacy.tsx contains a set of components
|
||||||
// that (crudely) implement the pre-2023 web client. These are implemented
|
// that (crudely) implement the pre-2023 web client. These are implemented
|
||||||
|
@ -162,9 +163,13 @@ export function IP(props: { data: NodeData }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function State(props: { data: NodeData }) {
|
export function State({
|
||||||
const { data } = props
|
data,
|
||||||
|
updateNode,
|
||||||
|
}: {
|
||||||
|
data: NodeData
|
||||||
|
updateNode: (update: NodeUpdate) => void
|
||||||
|
}) {
|
||||||
switch (data.Status) {
|
switch (data.Status) {
|
||||||
case "NeedsLogin":
|
case "NeedsLogin":
|
||||||
case "NoState":
|
case "NoState":
|
||||||
|
@ -232,25 +237,20 @@ export function State(props: { data: NodeData }) {
|
||||||
device name or IP address above.
|
device name or IP address above.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
|
||||||
<a href="#" className="mb-4 js-advertiseExitNode">
|
|
||||||
{data.AdvertiseExitNode ? (
|
|
||||||
<button
|
<button
|
||||||
className="button button-red button-medium"
|
className={cx("button button-medium mb-4", {
|
||||||
|
"button-red": data.AdvertiseExitNode,
|
||||||
|
"button-blue": !data.AdvertiseExitNode,
|
||||||
|
})}
|
||||||
id="enabled"
|
id="enabled"
|
||||||
|
onClick={() =>
|
||||||
|
updateNode({ AdvertiseExitNode: !data.AdvertiseExitNode })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Stop advertising Exit Node
|
{data.AdvertiseExitNode
|
||||||
|
? "Stop advertising Exit Node"
|
||||||
|
: "Advertise as Exit Node"}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className="button button-blue button-medium"
|
|
||||||
id="enabled"
|
|
||||||
>
|
|
||||||
Advertise as Exit Node
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useEffect, useState } from "react"
|
import { useCallback, useEffect, useState } from "react"
|
||||||
|
import { apiFetch } from "src/api"
|
||||||
|
|
||||||
export type NodeData = {
|
export type NodeData = {
|
||||||
Profile: UserProfile
|
Profile: UserProfile
|
||||||
|
@ -22,16 +23,104 @@ export type UserProfile = {
|
||||||
ProfilePicURL: string
|
ProfilePicURL: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NodeUpdate = {
|
||||||
|
AdvertiseRoutes?: string
|
||||||
|
AdvertiseExitNode?: boolean
|
||||||
|
Reauthenticate?: boolean
|
||||||
|
ForceLogout?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
// useNodeData returns basic data about the current node.
|
// useNodeData returns basic data about the current node.
|
||||||
export default function useNodeData() {
|
export default function useNodeData() {
|
||||||
const [data, setData] = useState<NodeData>()
|
const [data, setData] = useState<NodeData>()
|
||||||
|
const [isPosting, setIsPosting] = useState<boolean>(false)
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchNodeData = useCallback(() => {
|
||||||
fetch("/api/data")
|
apiFetch("/api/data")
|
||||||
.then((response) => response.json())
|
.then((r) => r.json())
|
||||||
.then((json) => setData(json))
|
.then((data) => setData(data))
|
||||||
.catch((error) => console.error(error))
|
.catch((error) => console.error(error))
|
||||||
}, [])
|
}, [setData])
|
||||||
|
|
||||||
return data
|
const updateNode = useCallback(
|
||||||
|
(update: NodeUpdate) => {
|
||||||
|
// The contents of this function are mostly copied over
|
||||||
|
// from the legacy client's web.html file.
|
||||||
|
// It makes all data updates through one API endpoint.
|
||||||
|
// As we build out the web client in React,
|
||||||
|
// this endpoint will eventually be deprecated.
|
||||||
|
|
||||||
|
if (isPosting || !data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setIsPosting(true)
|
||||||
|
|
||||||
|
update = {
|
||||||
|
// Default to current data value for any unset fields.
|
||||||
|
AdvertiseRoutes:
|
||||||
|
update.AdvertiseRoutes !== undefined
|
||||||
|
? update.AdvertiseRoutes
|
||||||
|
: data.AdvertiseRoutes,
|
||||||
|
AdvertiseExitNode:
|
||||||
|
update.AdvertiseExitNode !== undefined
|
||||||
|
? update.AdvertiseExitNode
|
||||||
|
: data.AdvertiseExitNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
|
const nextParams = new URLSearchParams({ up: "true" })
|
||||||
|
const token = urlParams.get("SynoToken")
|
||||||
|
if (token) {
|
||||||
|
nextParams.set("SynoToken", token)
|
||||||
|
}
|
||||||
|
const search = nextParams.toString()
|
||||||
|
const url = `/api/data${search ? `?${search}` : ""}`
|
||||||
|
|
||||||
|
var body, contentType: string
|
||||||
|
|
||||||
|
if (data.IsUnraid) {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.append("csrf_token", data.UnraidToken)
|
||||||
|
params.append("ts_data", JSON.stringify(update))
|
||||||
|
body = params.toString()
|
||||||
|
contentType = "application/x-www-form-urlencoded;charset=UTF-8"
|
||||||
|
} else {
|
||||||
|
body = JSON.stringify(update)
|
||||||
|
contentType = "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
apiFetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Accept: "application/json", "Content-Type": contentType },
|
||||||
|
body: body,
|
||||||
|
})
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((r) => {
|
||||||
|
setIsPosting(false)
|
||||||
|
const err = r["error"]
|
||||||
|
if (err) {
|
||||||
|
throw new Error(err)
|
||||||
|
}
|
||||||
|
const url = r["url"]
|
||||||
|
if (url) {
|
||||||
|
if (data.IsUnraid) {
|
||||||
|
window.open(url, "_blank")
|
||||||
|
} else {
|
||||||
|
document.location.href = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchNodeData()
|
||||||
|
})
|
||||||
|
.catch((err) => alert("Failed operation: " + err.message))
|
||||||
|
},
|
||||||
|
[data]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
fetchNodeData,
|
||||||
|
// Initial data load.
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
return { data, updateNode, isPosting }
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ package web
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
@ -23,6 +24,7 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/csrf"
|
||||||
"tailscale.com/client/tailscale"
|
"tailscale.com/client/tailscale"
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
|
@ -31,7 +33,6 @@ import (
|
||||||
"tailscale.com/net/netutil"
|
"tailscale.com/net/netutil"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/util/groupmember"
|
"tailscale.com/util/groupmember"
|
||||||
"tailscale.com/util/httpm"
|
|
||||||
"tailscale.com/version/distro"
|
"tailscale.com/version/distro"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -52,6 +53,8 @@ type Server struct {
|
||||||
|
|
||||||
devMode bool
|
devMode bool
|
||||||
devProxy *httputil.ReverseProxy // only filled when devMode is on
|
devProxy *httputil.ReverseProxy // only filled when devMode is on
|
||||||
|
|
||||||
|
apiHandler http.Handler // csrf-protected api handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer constructs a new Tailscale web client server.
|
// NewServer constructs a new Tailscale web client server.
|
||||||
|
@ -70,6 +73,11 @@ func NewServer(devMode bool, lc *tailscale.LocalClient) (s *Server, cleanup func
|
||||||
if s.devMode {
|
if s.devMode {
|
||||||
cleanup = s.startDevServer()
|
cleanup = s.startDevServer()
|
||||||
s.addProxyToDevServer()
|
s.addProxyToDevServer()
|
||||||
|
|
||||||
|
// Create new handler for "/api" requests.
|
||||||
|
// And protect with gorilla csrf.
|
||||||
|
csrfProtect := csrf.Protect(csrfKey())
|
||||||
|
s.apiHandler = csrfProtect(&api{s: s})
|
||||||
}
|
}
|
||||||
return s, cleanup
|
return s, cleanup
|
||||||
}
|
}
|
||||||
|
@ -271,19 +279,9 @@ req.send(null);
|
||||||
// ServeHTTP processes all requests for the Tailscale web client.
|
// ServeHTTP processes all requests for the Tailscale web client.
|
||||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.devMode {
|
if s.devMode {
|
||||||
if r.URL.Path == "/api/data" {
|
if strings.HasPrefix(r.URL.Path, "/api/") {
|
||||||
user, err := authorize(w, r)
|
// Pass through to other handlers via CSRF protection.
|
||||||
if err != nil {
|
s.apiHandler.ServeHTTP(w, r)
|
||||||
return
|
|
||||||
}
|
|
||||||
switch r.Method {
|
|
||||||
case httpm.GET:
|
|
||||||
s.serveGetNodeDataJSON(w, r, user)
|
|
||||||
case httpm.POST:
|
|
||||||
s.servePostNodeUpdate(w, r)
|
|
||||||
default:
|
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// When in dev mode, proxy to the Vite dev server.
|
// When in dev mode, proxy to the Vite dev server.
|
||||||
|
@ -527,3 +525,14 @@ func (s *Server) tailscaleUp(ctx context.Context, st *ipnstate.Status, postData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// csrfKey creates a new random csrf token.
|
||||||
|
// If an error surfaces during key creation,
|
||||||
|
// the error is logged and the active process terminated.
|
||||||
|
func csrfKey() []byte {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(key); err != nil {
|
||||||
|
log.Fatal("error generating CSRF key: %w", err)
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
|
@ -543,6 +543,13 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.5.tgz#ae69bcbb1bebb68c4ac0b11e9d8ed04526b3562b"
|
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.5.tgz#ae69bcbb1bebb68c4ac0b11e9d8ed04526b3562b"
|
||||||
integrity sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==
|
integrity sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==
|
||||||
|
|
||||||
|
"@types/classnames@^2.2.10":
|
||||||
|
version "2.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.3.1.tgz#3c2467aa0f1a93f1f021e3b9bcf938bd5dfdc0dd"
|
||||||
|
integrity sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A==
|
||||||
|
dependencies:
|
||||||
|
classnames "*"
|
||||||
|
|
||||||
"@types/estree@^1.0.0":
|
"@types/estree@^1.0.0":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
|
||||||
|
@ -798,6 +805,11 @@ chokidar@^3.5.3:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
|
classnames@*, classnames@^2.3.1:
|
||||||
|
version "2.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
|
||||||
|
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
|
||||||
|
|
||||||
color-convert@^1.9.0:
|
color-convert@^1.9.0:
|
||||||
version "1.9.3"
|
version "1.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||||
|
|
|
@ -22,6 +22,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||||
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
||||||
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
||||||
github.com/google/uuid from tailscale.com/util/quarantine+
|
github.com/google/uuid from tailscale.com/util/quarantine+
|
||||||
|
github.com/gorilla/csrf from tailscale.com/client/web
|
||||||
|
github.com/gorilla/securecookie from github.com/gorilla/csrf
|
||||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
|
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
|
||||||
|
@ -38,6 +40,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||||
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
|
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
|
||||||
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli
|
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli
|
||||||
github.com/peterbourgon/ff/v3/ffcli from tailscale.com/cmd/tailscale/cli
|
github.com/peterbourgon/ff/v3/ffcli from tailscale.com/cmd/tailscale/cli
|
||||||
|
github.com/pkg/errors from github.com/gorilla/csrf
|
||||||
github.com/skip2/go-qrcode from tailscale.com/cmd/tailscale/cli
|
github.com/skip2/go-qrcode from tailscale.com/cmd/tailscale/cli
|
||||||
github.com/skip2/go-qrcode/bitset from github.com/skip2/go-qrcode+
|
github.com/skip2/go-qrcode/bitset from github.com/skip2/go-qrcode+
|
||||||
github.com/skip2/go-qrcode/reedsolomon from github.com/skip2/go-qrcode
|
github.com/skip2/go-qrcode/reedsolomon from github.com/skip2/go-qrcode
|
||||||
|
@ -234,6 +237,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||||
encoding/base32 from tailscale.com/tka+
|
encoding/base32 from tailscale.com/tka+
|
||||||
encoding/base64 from encoding/json+
|
encoding/base64 from encoding/json+
|
||||||
encoding/binary from compress/gzip+
|
encoding/binary from compress/gzip+
|
||||||
|
encoding/gob from github.com/gorilla/securecookie
|
||||||
encoding/hex from crypto/x509+
|
encoding/hex from crypto/x509+
|
||||||
encoding/json from expvar+
|
encoding/json from expvar+
|
||||||
encoding/pem from crypto/tls+
|
encoding/pem from crypto/tls+
|
||||||
|
@ -247,7 +251,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||||
hash/crc32 from compress/gzip+
|
hash/crc32 from compress/gzip+
|
||||||
hash/maphash from go4.org/mem
|
hash/maphash from go4.org/mem
|
||||||
html from tailscale.com/ipn/ipnstate+
|
html from tailscale.com/ipn/ipnstate+
|
||||||
html/template from tailscale.com/client/web
|
html/template from tailscale.com/client/web+
|
||||||
image from github.com/skip2/go-qrcode+
|
image from github.com/skip2/go-qrcode+
|
||||||
image/color from github.com/skip2/go-qrcode+
|
image/color from github.com/skip2/go-qrcode+
|
||||||
image/png from github.com/skip2/go-qrcode
|
image/png from github.com/skip2/go-qrcode
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -100,6 +100,8 @@ require (
|
||||||
software.sslmate.com/src/go-pkcs12 v0.2.0
|
software.sslmate.com/src/go-pkcs12 v0.2.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require github.com/gorilla/securecookie v1.1.1 // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
|
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
|
||||||
4d63.com/gochecknoglobals v0.2.1 // indirect
|
4d63.com/gochecknoglobals v0.2.1 // indirect
|
||||||
|
@ -208,6 +210,7 @@ require (
|
||||||
github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28 // indirect
|
github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28 // indirect
|
||||||
github.com/goreleaser/chglog v0.5.0 // indirect
|
github.com/goreleaser/chglog v0.5.0 // indirect
|
||||||
github.com/goreleaser/fileglob v1.3.0 // indirect
|
github.com/goreleaser/fileglob v1.3.0 // indirect
|
||||||
|
github.com/gorilla/csrf v1.7.1
|
||||||
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
|
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
|
||||||
github.com/gostaticanalysis/comment v1.4.2 // indirect
|
github.com/gostaticanalysis/comment v1.4.2 // indirect
|
||||||
github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect
|
github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -478,6 +478,10 @@ github.com/goreleaser/fileglob v1.3.0 h1:/X6J7U8lbDpQtBvGcwwPS6OpzkNVlVEsFUVRx9+
|
||||||
github.com/goreleaser/fileglob v1.3.0/go.mod h1:Jx6BoXv3mbYkEzwm9THo7xbr5egkAraxkGorbJb4RxU=
|
github.com/goreleaser/fileglob v1.3.0/go.mod h1:Jx6BoXv3mbYkEzwm9THo7xbr5egkAraxkGorbJb4RxU=
|
||||||
github.com/goreleaser/nfpm/v2 v2.32.1-0.20230803123630-24a43c5ad7cf h1:X8rzot0Te1TYSoADyMZfPt95Afhptpj0VqicKPAcmjM=
|
github.com/goreleaser/nfpm/v2 v2.32.1-0.20230803123630-24a43c5ad7cf h1:X8rzot0Te1TYSoADyMZfPt95Afhptpj0VqicKPAcmjM=
|
||||||
github.com/goreleaser/nfpm/v2 v2.32.1-0.20230803123630-24a43c5ad7cf/go.mod h1:Z7rAxucnQGMGfAhpxm/UIrdH0/EcxEt91RW3mmVzx2U=
|
github.com/goreleaser/nfpm/v2 v2.32.1-0.20230803123630-24a43c5ad7cf/go.mod h1:Z7rAxucnQGMGfAhpxm/UIrdH0/EcxEt91RW3mmVzx2U=
|
||||||
|
github.com/gorilla/csrf v1.7.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE=
|
||||||
|
github.com/gorilla/csrf v1.7.1/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
|
||||||
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
|
Loading…
Reference in New Issue