client/web: pipe unraid csrf token through apiFetch

Ensures that we're sending back the csrf token for all requests
made back to unraid clients.

Updates tailscale/corp#13775

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
Sonia Appasamy 2023-08-29 18:02:01 -04:00 committed by Sonia Appasamy
parent 1cd03bc0a1
commit e952564b59
3 changed files with 38 additions and 34 deletions

View File

@ -1,4 +1,5 @@
let csrfToken: string let csrfToken: string
let unraidCsrfToken: string | undefined // required for unraid POST requests (#8062)
// apiFetch wraps the standard JS fetch function with csrf header // apiFetch wraps the standard JS fetch function with csrf header
// management and param additions specific to the web client. // management and param additions specific to the web client.
@ -8,11 +9,12 @@ let csrfToken: string
// (i.e. provide `/data` rather than `api/data`). // (i.e. provide `/data` rather than `api/data`).
export function apiFetch( export function apiFetch(
endpoint: string, endpoint: string,
init?: RequestInit | undefined, method: "GET" | "POST",
addURLParams?: Record<string, string> body?: any,
params?: Record<string, string>
): Promise<Response> { ): Promise<Response> {
const urlParams = new URLSearchParams(window.location.search) const urlParams = new URLSearchParams(window.location.search)
const nextParams = new URLSearchParams(addURLParams) const nextParams = new URLSearchParams(params)
const token = urlParams.get("SynoToken") const token = urlParams.get("SynoToken")
if (token) { if (token) {
nextParams.set("SynoToken", token) nextParams.set("SynoToken", token)
@ -20,9 +22,28 @@ export function apiFetch(
const search = nextParams.toString() const search = nextParams.toString()
const url = `api${endpoint}${search ? `?${search}` : ""}` const url = `api${endpoint}${search ? `?${search}` : ""}`
var contentType: string
if (unraidCsrfToken) {
const params = new URLSearchParams()
params.append("csrf_token", unraidCsrfToken)
if (body) {
params.append("ts_data", JSON.stringify(body))
}
body = params.toString()
contentType = "application/x-www-form-urlencoded;charset=UTF-8"
} else {
body = body ? JSON.stringify(body) : undefined
contentType = "application/json"
}
return fetch(url, { return fetch(url, {
...init, method: method,
headers: withCsrfToken(init?.headers), headers: {
Accept: "application/json",
"Content-Type": contentType,
"X-CSRF-Token": csrfToken,
},
body,
}).then((r) => { }).then((r) => {
updateCsrfToken(r) updateCsrfToken(r)
if (!r.ok) { if (!r.ok) {
@ -34,13 +55,13 @@ export function apiFetch(
}) })
} }
function withCsrfToken(h?: HeadersInit): HeadersInit {
return { ...h, "X-CSRF-Token": csrfToken }
}
function updateCsrfToken(r: Response) { function updateCsrfToken(r: Response) {
const tok = r.headers.get("X-CSRF-Token") const tok = r.headers.get("X-CSRF-Token")
if (tok) { if (tok) {
csrfToken = tok csrfToken = tok
} }
} }
export function setUnraidCsrfToken(token?: string) {
unraidCsrfToken = token
}

View File

@ -93,7 +93,7 @@ export function Header({
|{" "} |{" "}
<button <button
onClick={() => onClick={() =>
apiFetch("/local/v0/logout", { method: "POST" }) apiFetch("/local/v0/logout", "POST")
.then(refreshData) .then(refreshData)
.catch((err) => alert("Logout failed: " + err.message)) .catch((err) => alert("Logout failed: " + err.message))
} }

View File

@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useState } from "react"
import { apiFetch } from "src/api" import { apiFetch, setUnraidCsrfToken } from "src/api"
export type NodeData = { export type NodeData = {
Profile: UserProfile Profile: UserProfile
@ -37,9 +37,12 @@ export default function useNodeData() {
const refreshData = useCallback( const refreshData = useCallback(
() => () =>
apiFetch("/data") apiFetch("/data", "GET")
.then((r) => r.json()) .then((r) => r.json())
.then((d) => setData(d)) .then((d: NodeData) => {
setData(d)
setUnraidCsrfToken(d.IsUnraid ? d.UnraidToken : undefined)
})
.catch((error) => console.error(error)), .catch((error) => console.error(error)),
[setData] [setData]
) )
@ -70,27 +73,7 @@ export default function useNodeData() {
: data.AdvertiseExitNode, : data.AdvertiseExitNode,
} }
var body, contentType: string apiFetch("/data", "POST", update, { up: "true" })
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(
"/data",
{
method: "POST",
headers: { Accept: "application/json", "Content-Type": contentType },
body: body,
},
{ up: "true" }
)
.then((r) => r.json()) .then((r) => r.json())
.then((r) => { .then((r) => {
setIsPosting(false) setIsPosting(false)