diff options
Diffstat (limited to 'web/source/settings/views/admin/http-header-permissions')
3 files changed, 558 insertions, 0 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> + ); +} |