diff options
Diffstat (limited to 'web/source/settings/views/admin')
5 files changed, 624 insertions, 6 deletions
diff --git a/web/source/settings/views/admin/http-header-permissions/create.tsx b/web/source/settings/views/admin/http-header-permissions/create.tsx new file mode 100644 index 000000000..b791ae0a9 --- /dev/null +++ b/web/source/settings/views/admin/http-header-permissions/create.tsx @@ -0,0 +1,143 @@ +/* +	GoToSocial +	Copyright (C) GoToSocial Authors admin@gotosocial.org +	SPDX-License-Identifier: AGPL-3.0-or-later + +	This program is free software: you can redistribute it and/or modify +	it under the terms of the GNU Affero General Public License as published by +	the Free Software Foundation, either version 3 of the License, or +	(at your option) any later version. + +	This program is distributed in the hope that it will be useful, +	but WITHOUT ANY WARRANTY; without even the implied warranty of +	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +	GNU Affero General Public License for more details. + +	You should have received a copy of the GNU Affero General Public License +	along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +import React from "react"; +import { usePostHeaderAllowMutation, usePostHeaderBlockMutation } from "../../../lib/query/admin/http-header-permissions"; +import { useTextInput } from "../../../lib/form"; +import useFormSubmit from "../../../lib/form/submit"; +import { TextInput } from "../../../components/form/inputs"; +import MutationButton from "../../../components/form/mutation-button"; +import { PermType } from "../../../lib/types/perm"; + +export default function HeaderPermCreateForm({ permType }: { permType: PermType }) { +	const form = { +		header: useTextInput("header", { +			validator: (val: string) => { +				// Technically invalid but avoid +				// showing red outline when user +				// hasn't entered anything yet. +				if (val.length === 0) { +					return ""; +				} + +				// Only requirement is that header +				// must be less than 1024 chars. +				if (val.length > 1024) { +					return "header must be less than 1024 characters"; +				} + +				return ""; +			} +		}), +		regex: useTextInput("regex", { +			validator: (val: string) => { +				// Technically invalid but avoid +				// showing red outline when user +				// hasn't entered anything yet. +				if (val.length === 0) { +					return ""; +				} + +				// Ensure regex compiles. +				try { +					new RegExp(val); +				} catch (e) { +					return e; +				} + +				return ""; +			} +		}), +	}; + +	// Use appropriate mutation for given permType. +	const [ postAllowTrigger, postAllowResult ] = usePostHeaderAllowMutation(); +	const [ postBlockTrigger, postBlockResult ] = usePostHeaderBlockMutation(); + +	let mutationTrigger; +	let mutationResult; + +	if (permType === "block") { +		mutationTrigger = postBlockTrigger; +		mutationResult = postBlockResult; +	} else { +		mutationTrigger = postAllowTrigger; +		mutationResult = postAllowResult; +	} + +	const [formSubmit, result] = useFormSubmit( +		form, +		[mutationTrigger, mutationResult], +		{ +			changedOnly: false, +			onFinish: ({ _data }) => { +				form.header.reset(); +				form.regex.reset(); +			}, +		}); + +	return ( +		<form onSubmit={formSubmit}> +			<h2>Create new HTTP header {permType}</h2> +			<TextInput +				field={form.header} +				label={ +					<> +						HTTP Header Name  +						 <a +							href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" +							target="_blank" +							className="docslink" +							rel="noreferrer" +						> +							Learn more about HTTP request headers (opens in a new tab) +						</a> +					</> +				} +				placeholder={"User-Agent"} +			/> +			<TextInput +				field={form.regex} +				label={ +					<> +						HTTP Header Value Regex  +						<a +							href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions" +							target="_blank" +							className="docslink" +							rel="noreferrer" +						> +							Learn more about regular expressions (opens in a new tab) +						</a> +					</> +				} +				placeholder={"^.*Some-User-Agent.*$"} +				{...{className: "monospace"}} +			/> +			<MutationButton +				label="Save" +				result={result} +				disabled={ +					(!form.header.value || !form.regex.value) || +					(!form.header.valid || !form.regex.valid) +				} +			/> +		</form> +	); +} diff --git a/web/source/settings/views/admin/http-header-permissions/detail.tsx b/web/source/settings/views/admin/http-header-permissions/detail.tsx new file mode 100644 index 000000000..db92dd0eb --- /dev/null +++ b/web/source/settings/views/admin/http-header-permissions/detail.tsx @@ -0,0 +1,246 @@ +/* +	GoToSocial +	Copyright (C) GoToSocial Authors admin@gotosocial.org +	SPDX-License-Identifier: AGPL-3.0-or-later + +	This program is free software: you can redistribute it and/or modify +	it under the terms of the GNU Affero General Public License as published by +	the Free Software Foundation, either version 3 of the License, or +	(at your option) any later version. + +	This program is distributed in the hope that it will be useful, +	but WITHOUT ANY WARRANTY; without even the implied warranty of +	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +	GNU Affero General Public License for more details. + +	You should have received a copy of the GNU Affero General Public License +	along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +import React, { useEffect, useMemo } from "react"; +import { useLocation, useParams } from "wouter"; +import { PermType } from "../../../lib/types/perm"; +import { useDeleteHeaderAllowMutation, useDeleteHeaderBlockMutation, useGetHeaderAllowQuery, useGetHeaderBlockQuery } from "../../../lib/query/admin/http-header-permissions"; +import { HeaderPermission } from "../../../lib/types/http-header-permissions"; +import { FetchBaseQueryError } from "@reduxjs/toolkit/query"; +import { SerializedError } from "@reduxjs/toolkit"; +import Loading from "../../../components/loading"; +import { Error } from "../../../components/error"; +import { useLazyGetAccountQuery } from "../../../lib/query/admin"; +import Username from "../../../components/username"; +import { useBaseUrl } from "../../../lib/navigation/util"; +import BackButton from "../../../components/back-button"; +import MutationButton from "../../../components/form/mutation-button"; + +const testString = `/* To test this properly, set "flavor" to "Golang", as that's the language GoToSocial uses for regular expressions */ + +/* Amazon crawler User-Agent example */ +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/600.2.5 (KHTML\\, like Gecko) Version/8.0.2 Safari/600.2.5 (Amazonbot/0.1; +https://developer.amazon.com/support/amazonbot) + +/* Some other test strings */ +Some Test Value +Another Test Value`; + +export default function HeaderPermDetail() { +	let params = useParams(); +	if (params.permType !== "blocks" && params.permType !== "allows") { +		throw "unrecognized perm type " + params.permType; +	} +	const permType = useMemo(() => { +		return params.permType?.slice(0, -1) as PermType; +	}, [params]); + +	let permID = params.permId as string | undefined; +	if (!permID) { +		throw "no perm ID"; +	} + +	if (permType === "block") { +		return <BlockDetail id={permID} />; +	} else { +		return <AllowDetail id={permID} />; +	} +} + +function BlockDetail({ id }: { id: string }) { +	return ( +		<PermDeets +			permType={"Block"} +			{...useGetHeaderBlockQuery(id)} +		/> +	); +} + +function AllowDetail({ id }: { id: string }) { +	return ( +		<PermDeets +			permType={"Allow"} +			{...useGetHeaderAllowQuery(id)} +		/> +	); +} + +interface PermDeetsProps { +	permType: string; +	data?: HeaderPermission; +	isLoading: boolean; +	isFetching: boolean; +	isError: boolean; +	error?: FetchBaseQueryError | SerializedError; +} + +function PermDeets({ +	permType, +	data: perm, +	isLoading: isLoadingPerm, +	isFetching: isFetchingPerm, +	isError: isErrorPerm, +	error: errorPerm, +}: PermDeetsProps) { +	const [ location ] = useLocation(); +	const baseUrl = useBaseUrl(); +	 +	// Once we've loaded the perm, trigger +	// getting the account that created it. +	const [ getAccount, getAccountRes ] = useLazyGetAccountQuery(); +	useEffect(() => { +		if (!perm) { +			return; +		} +		getAccount(perm.created_by, true); +	}, [getAccount, perm]); + +	// Load the createdByAccount if possible, +	// returning a username lozenge with +	// a link to the account. +	const createdByAccount = useMemo(() => { +		const { +			data: account, +			isLoading: isLoadingAccount, +			isFetching: isFetchingAccount, +			isError: isErrorAccount, +		} = getAccountRes; +		 +		// Wait for query to finish, returning +		// loading spinner in the meantime. +		if (isLoadingAccount || isFetchingAccount || !perm) { +			return <Loading />; +		} else if (isErrorAccount || account === undefined) { +			// Fall back to account ID. +			return perm?.created_by; +		} + +		return ( +			<Username +				account={account} +				linkTo={`~/settings/moderation/accounts/${account.id}`} +				backLocation={`~${baseUrl}${location}`} +			/> +		); +	}, [getAccountRes, perm, baseUrl, location]); + +	// Now wait til the perm itself is loaded. +	if (isLoadingPerm || isFetchingPerm) { +		return <Loading />; +	} else if (isErrorPerm) { +		return <Error error={errorPerm} />; +	} else if (perm === undefined) { +		throw "perm undefined"; +	} + +	const created = new Date(perm.created_at).toDateString();	 +	 +	// Create parameters to link to regex101 +	// with this regular expression prepopulated. +	const testParams = new URLSearchParams(); +	testParams.set("regex", perm.regex); +	testParams.set("flags", "g"); +	testParams.set("testString", testString); +	const regexLink = `https://regex101.com/?${testParams.toString()}`;	 + +	return ( +		<div className="http-header-permission-details"> +			<h1><BackButton to={`~${baseUrl}/${permType.toLowerCase()}s`} /> HTTP Header {permType} Detail</h1> +			<dl className="info-list"> +				<div className="info-list-entry"> +					<dt>ID</dt> +					<dd>{perm.id}</dd> +				</div> +				<div className="info-list-entry"> +					<dt>Created</dt> +					<dd><time dateTime={perm.created_at}>{created}</time></dd> +				</div> +				<div className="info-list-entry"> +					<dt>Created By</dt> +					<dd>{createdByAccount}</dd> +				</div> +				<div className="info-list-entry"> +					<dt>Header Name</dt> +					<dd>{perm.header}</dd> +				</div> +				<div className="info-list-entry"> +					<dt>Header Value Regex</dt> +					<dd className="monospace">{perm.regex}</dd> +				</div> +				<div className="info-list-entry"> +					<dt>Test This Regex</dt> +					<dd> +						<a +							href={regexLink} +							target="_blank" +							rel="noreferrer" +						> +							<i className="fa fa-fw fa-external-link" aria-hidden="true"></i> Link to Regex101 (opens in a new tab) +						</a> +					</dd> +				</div> +			</dl> +			{ permType === "Block" +				? <DeleteBlock id={perm.id} /> +				: <DeleteAllow id={perm.id} /> +			} +		</div> +	); +} + +function DeleteBlock({ id }: { id: string }) { +	const [ _location, setLocation ] = useLocation(); +	const baseUrl = useBaseUrl(); +	const [ removeTrigger, removeResult ] = useDeleteHeaderBlockMutation(); +	 +	return ( +		<MutationButton +			type="button" +			onClick={() => { +				removeTrigger(id); +				setLocation(`~${baseUrl}/blocks`); +			}} +			label="Remove this block" +			result={removeResult} +			className="button danger" +			showError={false} +			disabled={false} +		/> +	); +} + +function DeleteAllow({ id }: { id: string }) { +	const [ _location, setLocation ] = useLocation(); +	const baseUrl = useBaseUrl(); +	const [ removeTrigger, removeResult ] = useDeleteHeaderAllowMutation(); +	 +	return ( +		<MutationButton +			type="button" +			onClick={() => { +				removeTrigger(id); +				setLocation(`~${baseUrl}/allows`); +			}} +			label="Remove this allow" +			result={removeResult} +			className="button danger" +			showError={false} +			disabled={false} +		/> +	); +} diff --git a/web/source/settings/views/admin/http-header-permissions/overview.tsx b/web/source/settings/views/admin/http-header-permissions/overview.tsx new file mode 100644 index 000000000..7735e624e --- /dev/null +++ b/web/source/settings/views/admin/http-header-permissions/overview.tsx @@ -0,0 +1,169 @@ +/* +	GoToSocial +	Copyright (C) GoToSocial Authors admin@gotosocial.org +	SPDX-License-Identifier: AGPL-3.0-or-later + +	This program is free software: you can redistribute it and/or modify +	it under the terms of the GNU Affero General Public License as published by +	the Free Software Foundation, either version 3 of the License, or +	(at your option) any later version. + +	This program is distributed in the hope that it will be useful, +	but WITHOUT ANY WARRANTY; without even the implied warranty of +	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +	GNU Affero General Public License for more details. + +	You should have received a copy of the GNU Affero General Public License +	along with this program.  If not, see <http://www.gnu.org/licenses/>. +*/ + +import React, { useMemo } from "react"; +import { useGetHeaderAllowsQuery, useGetHeaderBlocksQuery } from "../../../lib/query/admin/http-header-permissions"; +import { NoArg } from "../../../lib/types/query"; +import { PageableList } from "../../../components/pageable-list"; +import { HeaderPermission } from "../../../lib/types/http-header-permissions"; +import { useLocation, useParams } from "wouter"; +import { PermType } from "../../../lib/types/perm"; +import { FetchBaseQueryError } from "@reduxjs/toolkit/query"; +import { SerializedError } from "@reduxjs/toolkit"; +import HeaderPermCreateForm from "./create"; + +export default function HeaderPermsOverview() { +	const [ location, setLocation ] = useLocation(); +	 +	// Parse perm type from routing params. +	let params = useParams(); +	if (params.permType !== "blocks" && params.permType !== "allows") { +		throw "unrecognized perm type " + params.permType; +	} +	const permType = useMemo(() => { +		return params.permType?.slice(0, -1) as PermType; +	}, [params]); + +	// Uppercase first letter of given permType. +	const permTypeUpper = useMemo(() => { +		return permType.charAt(0).toUpperCase() + permType.slice(1);  +	}, [permType]); +	 +	// Fetch desired perms, skipping +	// the ones we don't want. +	const { +		data: blocks, +		isLoading: isLoadingBlocks, +		isFetching: isFetchingBlocks, +		isSuccess: isSuccessBlocks, +		isError: isErrorBlocks, +		error: errorBlocks +	} = useGetHeaderBlocksQuery(NoArg, { skip: permType !== "block" }); + +	const { +		data: allows, +		isLoading: isLoadingAllows, +		isFetching: isFetchingAllows, +		isSuccess: isSuccessAllows, +		isError: isErrorAllows, +		error: errorAllows +	} = useGetHeaderAllowsQuery(NoArg, { skip: permType !== "allow" }); + +	const itemToEntry = (perm: HeaderPermission) => { +		return ( +			<dl +				key={perm.id} +				className="entry spanlink" +				onClick={() => { +					// When clicking on a header perm, +					// go to the detail view for perm. +					setLocation(`/${permType}s/${perm.id}`, { +						// Store the back location in +						// history so the detail view +						// can use it to return here. +						state: { backLocation: location } +					}); +				}} +				role="link" +				tabIndex={0} +			> +				<dt>{perm.header}</dt> +				<dd>{perm.regex}</dd> +			</dl> +		); +	}; + +	const emptyMessage = ( +		<div className="info"> +			<i className="fa fa-fw fa-info-circle" aria-hidden="true"></i> +			<b> +				No HTTP header {permType}s exist yet. +				You can create one using the form below. +			</b> +		</div> +	); + +	let isLoading: boolean; +	let isFetching: boolean; +	let isSuccess: boolean;  +	let isError: boolean; +	let error: FetchBaseQueryError | SerializedError | undefined; +	let items: HeaderPermission[] | undefined; + +	if (permType === "block") { +		isLoading = isLoadingBlocks; +		isFetching = isFetchingBlocks; +		isSuccess = isSuccessBlocks; +		isError = isErrorBlocks; +		error = errorBlocks; +		items = blocks; +	} else { +		isLoading = isLoadingAllows; +		isFetching = isFetchingAllows; +		isSuccess = isSuccessAllows; +		isError = isErrorAllows; +		error = errorAllows; +		items = allows; +	} + +	return ( +		<div className="http-header-permissions"> +			<div className="form-section-docs"> +				<h1>HTTP Header {permTypeUpper}s</h1> +				<p> +					On this page, you can view, create, and remove HTTP header {permType} entries, +					<br/> +					Blocks and allows have different effects depending on the value you've set +					for <code>advanced-header-filter-mode</code> in your instance configuration. +					<br/> +					{ permType === "block" && <> +						<strong> +							When running in <code>block</code> mode, be very careful when creating +							your value regexes, as a too-broad match can cause your instance to +							deny all requests, locking you out of this settings panel. +						</strong> +						<br/> +						If you do this by accident, you can fix it by stopping your instance, +						changing <code>advanced-header-filter-mode</code> to an empty string +						(disabled), starting your instance again, and removing the block. +					</> } +				</p> +				<a +					href="https://docs.gotosocial.org/en/latest/admin/request_filtering_modes/" +					target="_blank" +					className="docslink" +					rel="noreferrer" +				> +					Learn more about HTTP request filtering (opens in a new tab) +				</a> +			</div> +			<PageableList +				isLoading={isLoading} +				isFetching={isFetching} +				isSuccess={isSuccess} +				isError={isError} +				error={error} +				items={items} +				itemToEntry={itemToEntry} +				emptyMessage={emptyMessage} +			/> +			<HeaderPermCreateForm permType={permType} /> +		</div> +	); +} diff --git a/web/source/settings/views/admin/menu.tsx b/web/source/settings/views/admin/menu.tsx index 2cf5a35c2..481f51a4d 100644 --- a/web/source/settings/views/admin/menu.tsx +++ b/web/source/settings/views/admin/menu.tsx @@ -36,6 +36,10 @@ import { useHasPermission } from "../../lib/navigation/util";   * - /settings/admin/actions   * - /settings/admin/actions/media   * - /settings/admin/actions/keys + * - /settings/admin/http-header-permissions/blocks + * - /settings/admin/http-header-permissions/blocks/:blockId\ + * - /settings/admin/http-header-permissions/allows + * - /settings/admin/http-header-permissions/allows/:allowId   */  export default function AdminMenu() {	  	const permissions = ["admin"]; @@ -54,6 +58,7 @@ export default function AdminMenu() {  			<AdminInstanceMenu />  			<AdminEmojisMenu />  			<AdminActionsMenu /> +			<AdminHTTPHeaderPermissionsMenu />  		</MenuItem>  	);  } @@ -127,3 +132,25 @@ function AdminEmojisMenu() {  		</MenuItem>  	);  } + +function AdminHTTPHeaderPermissionsMenu() { +	return ( +		<MenuItem +			name="HTTP Header Permissions" +			itemUrl="http-header-permissions" +			defaultChild="blocks" +			icon="fa-hubzilla" +		> +			<MenuItem +				name="Blocks" +				itemUrl="blocks" +				icon="fa-close" +			/> +			<MenuItem +				name="Allows" +				itemUrl="allows" +				icon="fa-check" +			/> +		</MenuItem> +	); +} diff --git a/web/source/settings/views/admin/router.tsx b/web/source/settings/views/admin/router.tsx index 95d146510..68c4a5ef3 100644 --- a/web/source/settings/views/admin/router.tsx +++ b/web/source/settings/views/admin/router.tsx @@ -29,15 +29,17 @@ import Keys from "./actions/keys";  import EmojiOverview from "./emoji/local/overview";  import EmojiDetail from "./emoji/local/detail";  import RemoteEmoji from "./emoji/remote"; +import HeaderPermsOverview from "./http-header-permissions/overview"; +import HeaderPermDetail from "./http-header-permissions/detail";  /*  	EXPORTED COMPONENTS  */  /** - * - /settings/instance/settings - * - /settings/instance/rules - * - /settings/instance/rules/:ruleId + * - /settings/admin/instance/settings + * - /settings/admin/instance/rules + * - /settings/admin/instance/rules/:ruleId   * - /settings/admin/emojis   * - /settings/admin/emojis/local   * - /settings/admin/emojis/local/:emojiId @@ -45,6 +47,10 @@ import RemoteEmoji from "./emoji/remote";   * - /settings/admin/actions   * - /settings/admin/actions/media   * - /settings/admin/actions/keys + * - /settings/admin/http-header-permissions/allows + * - /settings/admin/http-header-permissions/allows/:allowId + * - /settings/admin/http-header-permissions/blocks + * - /settings/admin/http-header-permissions/blocks/:blockId   */  export default function AdminRouter() {  	const parentUrl = useBaseUrl(); @@ -57,6 +63,7 @@ export default function AdminRouter() {  				<AdminInstanceRouter />  				<AdminEmojisRouter />  				<AdminActionsRouter /> +				<AdminHTTPHeaderPermissionsRouter />  			</Router>  		</BaseUrlContext.Provider>  	); @@ -125,9 +132,9 @@ function AdminActionsRouter() {  }  /** - * - /settings/instance/settings - * - /settings/instance/rules - * - /settings/instance/rules/:ruleId + * - /settings/admin/instance/settings + * - /settings/admin/instance/rules + * - /settings/admin/instance/rules/:ruleId   */  function AdminInstanceRouter() {  	const parentUrl = useBaseUrl(); @@ -149,3 +156,29 @@ function AdminInstanceRouter() {  		</BaseUrlContext.Provider>  	);  } + +/** + * - /settings/admin/http-header-permissions/blocks + * - /settings/admin/http-header-permissions/blocks/:blockId + * - /settings/admin/http-header-permissions/allows + * - /settings/admin/http-header-permissions/allows/:allowId + */ +function AdminHTTPHeaderPermissionsRouter() { +	const parentUrl = useBaseUrl(); +	const thisBase = "/http-header-permissions"; +	const absBase = parentUrl + thisBase; + +	return ( +		<BaseUrlContext.Provider value={absBase}> +			<Router base={thisBase}> +				<ErrorBoundary> +					<Switch> +						<Route path="/:permType" component={HeaderPermsOverview} /> +						<Route path="/:permType/:permId" component={HeaderPermDetail} /> +						<Route><Redirect to="/blocks" /></Route> +					</Switch> +				</ErrorBoundary> +			</Router> +		</BaseUrlContext.Provider> +	); +}  | 
