Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement origin checking #125

Merged
merged 8 commits into from
Jul 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions ADVANCED-USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# advanced usage

This guide highlights advanced usage of giscus through additional
configurations.

## `giscus.json`

Additional configurations can be made by creating a `giscus.json` file at
the root of your repository. You can see an example
[on this repository][giscus.json]. The `giscus.json` file supports the
following keys:

- [`origins`](#origins)
- [`originsRegex`](#originsregex)

### `origins`

You can restrict the domains that can load giscus with your repository's
discussions using the `origins` key. The `origins` key accepts a list of
strings that will be checked against the `window.origin` of the page that loads
giscus. The strings are compared using the following code.

```js
string === window.origin
```

You can combine `origins` with [`originsRegex`](#originsregex). If none of the
strings in `origins` and `originsRegex` match `window.origin`, then giscus will
refuse to load. If `origins` and `originsRegex` are both empty lists (or
undefined), giscus will proceed to load the discussion.

Example `giscus.json`:

```json
{
"origins": ["https://giscus.app"]
}
```

### `originsRegex`

Like [`origins`](#origins), but it accepts a list of regular expression
patterns to test the origin. The test is done using the following code.

```js
new RegExp(pattern).test(window.origin)
```

Example `giscus.json`:

```json
{
"origins": ["https://giscus.app"],
"originsRegex": ["http://localhost:[0-9]+"]
}
```

[giscus.json]: giscus.json
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ To comment, visitors must authorize the [giscus app][giscus-app] to [post on the

If you're using giscus, consider [starring 🌟 giscus on GitHub][repo] and adding the [`giscus`][giscus-topic] topic [to your repository][topic-howto]! 🎉

## advanced usage

You can add additional configurations (e.g. allowing specific origins) by following the [advanced usage guide][advanced-usage].

## migrating

If you've previously used other systems that utilize GitHub Issues (e.g. [utterances][utterances], [gitalk][gitalk]), you can [convert the existing issues into discussions][convert]. After the conversion, just make sure that the mapping between the discussion titles and the pages are correct, then giscus will automatically use the discussions.
Expand All @@ -49,6 +53,7 @@ See [CONTRIBUTING.md][contributing]
[repo]: https://github.com/laymonage/giscus
[giscus-topic]: https://github.com/topics/giscus
[topic-howto]: https://docs.github.com/en/github/administering-a-repository/classifying-your-repository-with-topics
[advanced-usage]: https://github.com/laymonage/giscus/blob/main/ADVANCED-USAGE.md
[utterances]: https://github.com/utterance/utterances
[gitalk]: https://github.com/gitalk/gitalk
[convert]: https://docs.github.com/en/discussions/managing-discussions-for-your-community/moderating-discussions#converting-an-issue-to-a-discussion
Expand Down
10 changes: 10 additions & 0 deletions giscus.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"origins": [
"https://giscus.app",
"https://giscus.vercel.app"
],
"originsRegex": [
"https:\/\/giscus-git-([A-z]|-)*laymonage\\.vercel\\.app",
"http:\/\/localhost:[0-9]+"
]
}
15 changes: 15 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { RepoConfig } from './types/giscus';

export function assertOrigin(origin: string, { origins = [], originsRegex = [] }: RepoConfig) {
if (!origins.length && !originsRegex.length) return true;

for (let i = 0; i < origins.length; i++) {
if (origin === origins[i]) return true;
}

for (let i = 0; i < originsRegex.length; i++) {
if (new RegExp(originsRegex[i]).test(origin)) return true;
}

return false;
}
5 changes: 5 additions & 0 deletions lib/types/giscus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ export interface ITokenRequest {
export interface ITokenResponse {
token: string;
}

export interface RepoConfig {
origins?: string[];
originsRegex?: string[];
}
8 changes: 8 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ export function getTheme(theme: string) {
return theme;
}

export function getOriginHost(origin: string) {
try {
return new URL(origin).origin;
} catch (err) {
return '';
}
}

export function formatDate(dt: string) {
return format(new Date(dt), 'LLL d, y, p O');
}
Expand Down
8 changes: 1 addition & 7 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import '../styles/base.css';
import '../styles/globals.css';

import type { AppProps } from 'next/app';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { ThemeContext } from '../lib/context';
import { getTheme } from '../lib/utils';
Expand All @@ -21,14 +20,9 @@ const meta = {
};

export default function App({ Component, pageProps }: AppProps) {
const router = useRouter();
const [theme, setTheme] = useState(router.query.theme as string);
const [theme, setTheme] = useState('');
const [, rerender] = useState({});

useEffect(() => {
setTheme(router.query.theme as string);
}, [router.query.theme]);

useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const listener = () => rerender({});
Expand Down
28 changes: 12 additions & 16 deletions pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { readFileSync } from 'fs';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { join } from 'path';
import Comment from '../components/Comment';
import { Reactions } from '../lib/reactions';
Expand Down Expand Up @@ -41,7 +40,6 @@ type DirectConfigHandler = ComponentProps<typeof Configuration>['onDirectConfigC

export default function Home({ contentBefore, contentAfter }: HomeProps) {
const isMounted = useIsMounted();
const router = useRouter();
const { theme, setTheme } = useContext(ThemeContext);
const [directConfig, setDirectConfig] = useState<DirectConfig>({
theme: 'light',
Expand Down Expand Up @@ -98,20 +96,18 @@ export default function Home({ contentBefore, contentAfter }: HomeProps) {
</Comment>

<div className="w-full my-8 giscus color-bg-canvas" />
{router.isReady ? (
<Head>
<script
src="/client.js"
data-repo="laymonage/giscus"
data-repo-id="MDEwOlJlcG9zaXRvcnkzNTE5NTgwNTM="
data-category-id="MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyNzk2NTc1"
data-mapping="specific"
data-term="Welcome to giscus!"
data-theme={directConfig.theme}
data-reactions-enabled={`${+directConfig.reactionsEnabled}`}
/>
</Head>
) : null}
<Head>
<script
src="/client.js"
data-repo="laymonage/giscus"
data-repo-id="MDEwOlJlcG9zaXRvcnkzNTE5NTgwNTM="
data-category-id="MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyNzk2NTc1"
data-mapping="specific"
data-term="Welcome to giscus!"
data-theme={directConfig.theme}
data-reactions-enabled={`${+directConfig.reactionsEnabled}`}
/>
</Head>
</>
) : null}
</div>
Expand Down
132 changes: 92 additions & 40 deletions pages/widget.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,112 @@
import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useContext, useEffect, useState } from 'react';
import { useContext, useEffect } from 'react';
import Widget from '../components/Widget';
import { assertOrigin } from '../lib/config';
import { ThemeContext } from '../lib/context';
import { useIsMounted } from '../lib/hooks';
import { decodeState } from '../lib/oauth/state';
import { getOriginHost } from '../lib/utils';
import { env } from '../lib/variables';
import { getAppAccessToken } from '../services/github/getAppAccessToken';
import { getRepoConfig } from '../services/github/getConfig';

export default function WidgetPage() {
const router = useRouter();
const isMounted = useIsMounted();
const { theme } = useContext(ThemeContext);
const [session, setSession] = useState('');
type ErrorType = 'ORIGIN' | null;

const origin = (router.query.origin as string) || (isMounted ? location.href : '');
export async function getServerSideProps({ query }: GetServerSidePropsContext) {
const origin = (query.origin as string) || '';
const session = (query.session as string) || '';
const repo = (query.repo as string) || '';
const term = (query.term as string) || '';
const category = (query.category as string) || '';
const number = +query.number || 0;
const repoId = (query.repoId as string) || '';
const categoryId = (query.categoryId as string) || '';
const description = (query.description as string) || '';
const reactionsEnabled = Boolean(+query.reactionsEnabled);
const theme = (query.theme as string) || '';
const originHost = getOriginHost(origin);
let error: ErrorType = null;

useEffect(() => {
if (router.query.session && !session) {
const { session: querySession, ...query } = router.query;
setSession(querySession as string);
const { encryption_password } = env;
const token = await decodeState(session, encryption_password)
.catch(() => getAppAccessToken(repo))
.catch(() => '');

const url = { pathname: router.pathname, query };
const options = { scroll: false, shallow: true };
const repoConfig = await getRepoConfig(repo, token);
if (!assertOrigin(originHost, repoConfig)) {
error = 'ORIGIN';
}

router.replace(url, undefined, options);
}
}, [router, session]);
return {
props: {
origin,
session,
repo,
term,
category,
number,
repoId,
categoryId,
description,
reactionsEnabled,
theme,
originHost,
error,
},
};
}

if (!router.isReady || (router.query.session && !session)) return null;
export default function WidgetPage({
origin,
session,
repo,
term,
number,
category,
repoId,
categoryId,
description,
reactionsEnabled,
theme,
originHost,
error,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
const resolvedOrigin = origin || (typeof location === 'undefined' ? '' : location.href);
const { theme: resolvedTheme, setTheme } = useContext(ThemeContext);

const repo = router.query.repo as string;
const term = router.query.term as string;
const category = router.query.category as string;
const number = +router.query.number;
const repoId = router.query.repoId as string;
const categoryId = router.query.categoryId as string;
const description = router.query.description as string;
const reactionsEnabled = Boolean(+router.query.reactionsEnabled);
useEffect(() => setTheme(theme), [setTheme, theme]);

return (
<>
<Head>
<base target="_top" />
</Head>

<main className="w-full mx-auto" data-theme={theme}>
<Widget
origin={origin}
session={session}
repo={repo}
term={term}
number={number}
category={category}
repoId={repoId}
categoryId={categoryId}
description={description}
reactionsEnabled={reactionsEnabled}
/>
<main className="w-full mx-auto" data-theme={resolvedTheme}>
{error ? (
<div className="px-4 py-5 text-sm border rounded-md flash-error">
Origin <code>{originHost}</code> is not allowed by{' '}
<span className="font-semibold">{repo}</span>. If you own the repository, include{' '}
<code>{originHost}</code> in the allowed origins list in{' '}
<code>
<a href={`https://github.com/${repo}/blob/HEAD/giscus.json`}>giscus.json</a>
</code>
.
</div>
) : (
<Widget
origin={resolvedOrigin}
session={session}
repo={repo}
term={term}
number={number}
category={category}
repoId={repoId}
categoryId={categoryId}
description={description}
reactionsEnabled={reactionsEnabled}
/>
)}
</main>

<script
Expand Down
2 changes: 2 additions & 0 deletions services/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export const GITHUB_GRAPHQL_API_URL = `${GITHUB_API_HOST}/graphql`;

export const GITHUB_MARKDOWN_API_URL = `${GITHUB_API_HOST}/markdown`;

export const GITHUB_REPOS_API_URL = `${GITHUB_API_HOST}/repos`;

export const GITHUB_INSTALLATIONS_URL = `${GITHUB_API_HOST}/app/installations`;

export const GITHUB_REPO_INSTALLATION_URL = (repoWithOwner: string) =>
Expand Down
10 changes: 10 additions & 0 deletions services/github/getConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { RepoConfig } from '../../lib/types/giscus';
import { getJSONFile } from './getFile';

export async function getRepoConfig(repoWithOwner: string, token?: string) {
try {
return await getJSONFile<RepoConfig>(repoWithOwner, 'giscus.json', token);
} catch (err) {
return {};
}
}
Loading