Skip to content

Commit

Permalink
Merge branch 'tailscale:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
Asutorufa authored Oct 11, 2023
2 parents 5ae531e + 05a1f5b commit c3c293f
Show file tree
Hide file tree
Showing 164 changed files with 6,902 additions and 2,437 deletions.
4 changes: 0 additions & 4 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,6 @@ You can also [list all devices in the tailnet](#list-tailnet-devices) to get the
"192.68.0.21:59128"
],

// derp (string) is the IP:port of the DERP server currently being used.
// Learn about DERP servers at https://tailscale.com/kb/1232/.
"derp":"",

// mappingVariesByDestIP (boolean) is 'true' if the host's NAT mappings
// vary based on the destination IP.
"mappingVariesByDestIP":false,
Expand Down
16 changes: 9 additions & 7 deletions client/web/qnap.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,23 @@ import (
"net/url"
)

// authorizeQNAP authenticates the logged-in QNAP user and verifies
// that they are authorized to use the web client. It returns true if the
// request was handled and no further processing is required.
func authorizeQNAP(w http.ResponseWriter, r *http.Request) (handled bool) {
// authorizeQNAP authenticates the logged-in QNAP user and verifies that they
// are authorized to use the web client.
// It reports true if the request is authorized to continue, and false otherwise.
// authorizeQNAP manages writing out any relevant authorization errors to the
// ResponseWriter itself.
func authorizeQNAP(w http.ResponseWriter, r *http.Request) (ok bool) {
_, resp, err := qnapAuthn(r)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return true
return false
}
if resp.IsAdmin == 0 {
http.Error(w, "user is not an admin", http.StatusForbidden)
return true
return false
}

return false
return true
}

type qnapAuthResponse struct {
Expand Down
127 changes: 110 additions & 17 deletions client/web/src/components/app.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,123 @@
import React from "react"
import { Footer, Header, IP, State } from "src/components/legacy"
import useNodeData from "src/hooks/node-data"
import useNodeData, { NodeData } from "src/hooks/node-data"
import { ReactComponent as ConnectedDeviceIcon } from "src/icons/connected-device.svg"
import { ReactComponent as TailscaleIcon } from "src/icons/tailscale-icon.svg"
import { ReactComponent as TailscaleLogo } from "src/icons/tailscale-logo.svg"

export default function App() {
// TODO(sonia): use isPosting value from useNodeData
// to fill loading states.
const { data, refreshData, updateNode } = useNodeData()

return (
if (!data) {
// TODO(sonia): add a loading view
return <div className="text-center py-14">Loading...</div>
}

const needsLogin = data?.Status === "NeedsLogin" || data?.Status === "NoState"

return !needsLogin &&
(data.DebugMode === "login" || data.DebugMode === "full") ? (
<div className="flex flex-col items-center min-w-sm max-w-lg mx-auto py-10">
{data.DebugMode === "login" ? (
<LoginView {...data} />
) : (
<ManageView {...data} />
)}
<Footer className="mt-20" licensesURL={data.LicensesURL} />
</div>
) : (
// Legacy client UI
<div className="py-14">
{!data ? (
// TODO(sonia): add a loading view
<div className="text-center">Loading...</div>
<main className="container max-w-lg mx-auto mb-8 py-6 px-8 bg-white rounded-md shadow-2xl">
<Header data={data} refreshData={refreshData} updateNode={updateNode} />
<IP data={data} />
<State data={data} updateNode={updateNode} />
</main>
<Footer licensesURL={data.LicensesURL} />
</div>
)
}

function LoginView(props: NodeData) {
return (
<>
<div className="pb-52 mx-auto">
<TailscaleLogo />
</div>
<div className="w-full p-4 bg-stone-50 rounded-3xl border border-gray-200 flex flex-col gap-4">
<div className="flex gap-2.5">
<ProfilePic url={props.Profile.ProfilePicURL} />
<div className="font-medium">
<div className="text-neutral-500 text-xs uppercase tracking-wide">
Owned by
</div>
<div className="text-neutral-800 text-sm leading-tight">
{/* TODO(sonia): support tagged node profile view more eloquently */}
{props.Profile.LoginName}
</div>
</div>
</div>
<div className="px-5 py-4 bg-white rounded-lg border border-gray-200 justify-between items-center flex">
<div className="flex gap-3">
<ConnectedDeviceIcon />
<div className="text-neutral-800">
<div className="text-lg font-medium leading-[25.20px]">
{props.DeviceName}
</div>
<div className="text-sm leading-tight">{props.IP}</div>
</div>
</div>
<button className="button button-blue ml-6">Access</button>
</div>
</div>
</>
)
}

function ManageView(props: NodeData) {
return (
<div className="px-5">
<div className="flex justify-between mb-12">
<TailscaleIcon />
<div className="flex">
<p className="mr-2">{props.Profile.LoginName}</p>
{/* TODO(sonia): support tagged node profile view more eloquently */}
<ProfilePic url={props.Profile.ProfilePicURL} />
</div>
</div>
<p className="tracking-wide uppercase text-gray-600 pb-3">This device</p>
<div className="-mx-5 border rounded-md px-5 py-4 bg-white">
<div className="flex justify-between items-center text-lg">
<div className="flex items-center">
<ConnectedDeviceIcon />
<p className="font-medium ml-3">{props.DeviceName}</p>
</div>
<p className="tracking-widest">{props.IP}</p>
</div>
</div>
<p className="text-gray-500 pt-2">
Tailscale is up and running. You can connect to this device from devices
in your tailnet by using its name or IP address.
</p>
</div>
)
}

function ProfilePic({ url }: { url: string }) {
return (
<div className="relative flex-shrink-0 w-8 h-8 rounded-full overflow-hidden">
{url ? (
<div
className="w-8 h-8 flex pointer-events-none rounded-full bg-gray-200"
style={{
backgroundImage: `url(${url})`,
backgroundSize: "cover",
}}
/>
) : (
<>
<main className="container max-w-lg mx-auto mb-8 py-6 px-8 bg-white rounded-md shadow-2xl">
<Header
data={data}
refreshData={refreshData}
updateNode={updateNode}
/>
<IP data={data} />
<State data={data} updateNode={updateNode} />
</main>
<Footer data={data} />
</>
<div className="w-8 h-8 flex pointer-events-none rounded-full border border-gray-400 border-dashed" />
)}
</div>
)
Expand Down
10 changes: 5 additions & 5 deletions client/web/src/components/legacy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,14 +282,14 @@ export function State({
}
}

export function Footer(props: { data: NodeData }) {
const { data } = props

export function Footer(props: { licensesURL: string; className?: string }) {
return (
<footer className="container max-w-lg mx-auto text-center">
<footer
className={cx("container max-w-lg mx-auto text-center", props.className)}
>
<a
className="text-xs text-gray-500 hover:text-gray-600"
href={data.LicensesURL}
href={props.licensesURL}
>
Open Source Licenses
</a>
Expand Down
2 changes: 2 additions & 0 deletions client/web/src/hooks/node-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export type NodeData = {
IsUnraid: boolean
UnraidToken: string
IPNVersion: string

DebugMode: "" | "login" | "full" // empty when not running in any debug mode
}

export type UserProfile = {
Expand Down
15 changes: 15 additions & 0 deletions client/web/src/icons/connected-device.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions client/web/src/icons/tailscale-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions client/web/src/icons/tailscale-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 10 additions & 8 deletions client/web/synology.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,36 @@ import (
)

// authorizeSynology authenticates the logged-in Synology user and verifies
// that they are authorized to use the web client. It returns true if the
// request was handled and no further processing is required.
func authorizeSynology(w http.ResponseWriter, r *http.Request) (handled bool) {
// that they are authorized to use the web client.
// It reports true if the request is authorized to continue, and false otherwise.
// authorizeSynology manages writing out any relevant authorization errors to the
// ResponseWriter itself.
func authorizeSynology(w http.ResponseWriter, r *http.Request) (ok bool) {
if synoTokenRedirect(w, r) {
return true
return false
}

// authenticate the Synology user
cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi")
out, err := cmd.CombinedOutput()
if err != nil {
http.Error(w, fmt.Sprintf("auth: %v: %s", err, out), http.StatusUnauthorized)
return true
return false
}
user := strings.TrimSpace(string(out))

// check if the user is in the administrators group
isAdmin, err := groupmember.IsMemberOfGroup("administrators", user)
if err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return true
return false
}
if !isAdmin {
http.Error(w, "not a member of administrators group", http.StatusForbidden)
return true
return false
}

return false
return true
}

func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
Expand Down
1 change: 1 addition & 0 deletions client/web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"jsx": "react",
"types": ["vite-plugin-svgr/client", "vite/client"]
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
Expand Down
Loading

0 comments on commit c3c293f

Please sign in to comment.