Removed NodeInfo and WebFinger endpoints

This commit is contained in:
Natty 2023-04-22 12:47:12 +02:00
parent 86c4804f9b
commit ddf7e07481
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
6 changed files with 163 additions and 551 deletions

View File

@ -1,10 +1,7 @@
root = true root = true
[*] [*]
indent_style = tab
indent_size = 2
charset = utf-8
insert_final_newline = true
[*.yml]
indent_style = space indent_style = space
indent_size = 4
charset = utf-8
insert_final_newline = true

View File

@ -1,72 +0,0 @@
# Replace example.tld with your domain
# For WebSocket
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;
server {
listen 80;
listen [::]:80;
server_name example.tld;
# For SSL domain validation
root /var/www/html;
location /.well-known/acme-challenge/ { allow all; }
location /.well-known/pki-validation/ { allow all; }
location / { return 301 https://$server_name$request_uri; }
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.tld;
ssl_session_timeout 1d;
ssl_session_cache shared:ssl_session_cache:10m;
ssl_session_tickets off;
# To use Let's Encrypt certificate
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
# To use Debian/Ubuntu's self-signed certificate (For testing or before issuing a certificate)
#ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
#ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
# SSL protocol settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_stapling on;
ssl_stapling_verify on;
# Change to your upload limit
client_max_body_size 80m;
# Proxy to Node
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_redirect off;
# If it's behind another reverse proxy or CDN, remove the following.
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
# For WebSocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Cache settings
proxy_cache cache1;
proxy_cache_lock on;
proxy_cache_use_stale updating;
add_header X-Cache $upstream_cache_status;
}
}

View File

@ -1,103 +0,0 @@
# configuration file for git-cliff (0.1.0)
[changelog]
# changelog header
header = """
# Changelog\n
All changes from v13.0.0 onwards, for a full list of differences read CALCKEY.md\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = false
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))"},
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^add", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^prevent", group = "Bug Fixes"},
{ message = "^doc", group = "Documentation"},
{ message = "^perf", group = "Performance"},
{ message = "^🎨", group = "Refactor"},
{ message = "^enhance", group = "Refactor"},
{ message = "^⚡️", group = "Refactor"},
{ message = "^🔥", group = "Features"},
{ message = "^🐛", group = "Bug Fixes"},
{ message = "^🚑️", group = "Bug Fixes"},
{ message = "^block", group = "Bug Fixes"},
{ message = "^✨", group = "Features"},
{ message = "^📝", group = "Documentation"},
{ message = "^🚀", group = "Features"},
{ message = "^💄", group = "Styling"},
{ message = "^✅", group = "Testing"},
{ message = "^🔒️", group = "Security"},
{ message = "^🚨", group = "Testing"},
{ message = "^💚", group = "CI"},
{ message = "^👷", group = "CI"},
{ message = "^⬇️", group = "Miscellaneous Tasks"},
{ message = "^⬆️", group = "Miscellaneous Tasks"},
{ message = "^📌", group = "Miscellaneous Tasks"},
{ message = "^", group = "Miscellaneous Tasks"},
{ message = "^", group = "Miscellaneous Tasks"},
{ message = "^♻️", group = "Refactor"},
{ message = "^🔧", group = "CI"},
{ message = "^🔨", group = "CI"},
{ message = "^🌐", group = "Localization"},
{ message = "^✏️", group = "Localization"},
{ message = "^👽️", group = "Bug Fixes"},
{ message = "^🍱", group = "Styling"},
{ message = "^♿️", group = "Styling"},
{ message = "^🩹", group = "Bug Fixes"},
{ message = "^refactor", group = "Refactor"},
{ message = "^style", group = "Styling"},
{ message = "^test", group = "Testing"},
{ message = "^chore\\(release\\): prepare for", skip = true},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ message = "^update", group = "Miscellaneous Tasks"},
{ body = ".*security", group = "Security"},
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
# regex for ignoring tags
ignore_tags = ""
# sort the tags chronologically
date_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
# limit the number of commits included in the changelog.
# limit_commits = 42

View File

@ -21,7 +21,6 @@ import { publishMainStream } from "@/services/stream.js";
import * as Acct from "@/misc/acct.js"; import * as Acct from "@/misc/acct.js";
import { envOption } from "@/env.js"; import { envOption } from "@/env.js";
import activityPub from "./activitypub.js"; import activityPub from "./activitypub.js";
import nodeinfo from "./nodeinfo.js";
import wellKnown from "./well-known.js"; import wellKnown from "./well-known.js";
import apiServer from "./api/index.js"; import apiServer from "./api/index.js";
import fileServer from "./file/index.js"; import fileServer from "./file/index.js";
@ -36,30 +35,30 @@ const app = new Koa();
app.proxy = true; app.proxy = true;
if (!["production", "test"].includes(process.env.NODE_ENV || "")) { if (!["production", "test"].includes(process.env.NODE_ENV || "")) {
// Logger // Logger
app.use( app.use(
koaLogger((str) => { koaLogger((str) => {
serverLogger.info(str); serverLogger.info(str);
}), })
); );
// Delay // Delay
if (envOption.slow) { if (envOption.slow) {
app.use( app.use(
slow({ slow({
delay: 3000, delay: 3000,
}), })
); );
} }
} }
// HSTS // HSTS
// 6months (15552000sec) // 6months (15552000sec)
if (config.url.startsWith("https") && !config.disableHsts) { if (config.url.startsWith("https") && !config.disableHsts) {
app.use(async (ctx, next) => { app.use(async (ctx, next) => {
ctx.set("strict-transport-security", "max-age=15552000; preload"); ctx.set("strict-transport-security", "max-age=15552000; preload");
await next(); await next();
}); });
} }
app.use(mount("/api", apiServer)); app.use(mount("/api", apiServer));
@ -71,66 +70,65 @@ const router = new Router();
// Routing // Routing
router.use(activityPub.routes()); router.use(activityPub.routes());
router.use(nodeinfo.routes());
router.use(wellKnown.routes()); router.use(wellKnown.routes());
router.get("/avatar/@:acct", async (ctx) => { router.get("/avatar/@:acct", async (ctx) => {
const { username, host } = Acct.parse(ctx.params.acct); const { username, host } = Acct.parse(ctx.params.acct);
const user = await Users.findOne({ const user = await Users.findOne({
where: { where: {
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
host: host == null || host === config.host ? IsNull() : host, host: host == null || host === config.host ? IsNull() : host,
isSuspended: false, isSuspended: false,
}, },
relations: ["avatar"], relations: ["avatar"],
}); });
if (user) { if (user) {
ctx.redirect(Users.getAvatarUrlSync(user)); ctx.redirect(Users.getAvatarUrlSync(user));
} else { } else {
ctx.redirect("/static-assets/user-unknown.png"); ctx.redirect("/static-assets/user-unknown.png");
} }
}); });
router.get("/identicon/:x", async (ctx) => { router.get("/identicon/:x", async (ctx) => {
const [temp, cleanup] = await createTemp(); const [temp, cleanup] = await createTemp();
await genIdenticon(ctx.params.x, fs.createWriteStream(temp)); await genIdenticon(ctx.params.x, fs.createWriteStream(temp));
ctx.set("Content-Type", "image/png"); ctx.set("Content-Type", "image/png");
ctx.body = fs.createReadStream(temp).on("close", () => cleanup()); ctx.body = fs.createReadStream(temp).on("close", () => cleanup());
}); });
router.get("/verify-email/:code", async (ctx) => { router.get("/verify-email/:code", async (ctx) => {
const profile = await UserProfiles.findOneBy({ const profile = await UserProfiles.findOneBy({
emailVerifyCode: ctx.params.code, emailVerifyCode: ctx.params.code,
}); });
if (profile != null) { if (profile != null) {
ctx.body = "Verify succeeded!"; ctx.body = "Verify succeeded!";
ctx.status = 200; ctx.status = 200;
await UserProfiles.update( await UserProfiles.update(
{ userId: profile.userId }, { userId: profile.userId },
{ {
emailVerified: true, emailVerified: true,
emailVerifyCode: null, emailVerifyCode: null,
}, }
); );
publishMainStream( publishMainStream(
profile.userId, profile.userId,
"meUpdated", "meUpdated",
await Users.pack( await Users.pack(
profile.userId, profile.userId,
{ id: profile.userId }, { id: profile.userId },
{ {
detail: true, detail: true,
includeSecrets: true, includeSecrets: true,
}, }
), )
); );
} else { } else {
ctx.status = 404; ctx.status = 404;
} }
}); });
// Register router // Register router
@ -139,51 +137,51 @@ app.use(router.routes());
app.use(mount(webServer)); app.use(mount(webServer));
function createServer() { function createServer() {
return http.createServer(app.callback()); return http.createServer(app.callback());
} }
// For testing // For testing
export const startServer = () => { export const startServer = () => {
const server = createServer(); const server = createServer();
initializeStreamingServer(server); initializeStreamingServer(server);
server.listen(config.port); server.listen(config.port);
return server; return server;
}; };
export default () => export default () =>
new Promise((resolve) => { new Promise((resolve) => {
const server = createServer(); const server = createServer();
initializeStreamingServer(server); initializeStreamingServer(server);
server.on("error", (e) => { server.on("error", (e) => {
switch ((e as any).code) { switch ((e as any).code) {
case "EACCES": case "EACCES":
serverLogger.error( serverLogger.error(
`You do not have permission to listen on port ${config.port}.`, `You do not have permission to listen on port ${config.port}.`
); );
break; break;
case "EADDRINUSE": case "EADDRINUSE":
serverLogger.error( serverLogger.error(
`Port ${config.port} is already in use by another process.`, `Port ${config.port} is already in use by another process.`
); );
break; break;
default: default:
serverLogger.error(e); serverLogger.error(e);
break; break;
} }
if (cluster.isWorker) { if (cluster.isWorker) {
process.send!("listenFailed"); process.send!("listenFailed");
} else { } else {
// disableClustering // disableClustering
process.exit(1); process.exit(1);
} }
}); });
// @ts-ignore // @ts-ignore
server.listen(config.port, resolve); server.listen(config.port, resolve);
}); });

View File

@ -1,119 +0,0 @@
import Router from "@koa/router";
import config from "@/config/index.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { Users, Notes } from "@/models/index.js";
import { IsNull, MoreThan } from "typeorm";
import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js";
import { Cache } from "@/misc/cache.js";
const router = new Router();
const nodeinfo2_1path = "/nodeinfo/2.1";
const nodeinfo2_0path = "/nodeinfo/2.0";
// to cleo: leave this http or bonks
export const links = [
{
rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
href: config.url + nodeinfo2_1path,
},
{
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",
href: config.url + nodeinfo2_0path,
},
];
const nodeinfo2 = async () => {
const now = Date.now();
const [meta, total, activeHalfyear, activeMonth, localPosts] =
await Promise.all([
fetchMeta(true),
Users.count({ where: { host: IsNull() } }),
Users.count({
where: {
host: IsNull(),
lastActiveDate: MoreThan(new Date(now - 15552000000)),
},
}),
Users.count({
where: {
host: IsNull(),
lastActiveDate: MoreThan(new Date(now - 2592000000)),
},
}),
Notes.count({ where: { userHost: IsNull() } }),
]);
const proxyAccount = meta.proxyAccountId
? await Users.pack(meta.proxyAccountId).catch(() => null)
: null;
return {
software: {
name: "calckey",
version: config.version,
repository: meta.repositoryUrl,
homepage: "https://calckey.cloud",
},
protocols: ["activitypub"],
services: {
inbound: [] as string[],
outbound: ["atom1.0", "rss2.0"],
},
openRegistrations: !meta.disableRegistration,
usage: {
users: { total, activeHalfyear, activeMonth },
localPosts,
localComments: 0,
},
metadata: {
nodeName: meta.name,
nodeDescription: meta.description,
maintainer: {
name: meta.maintainerName,
email: meta.maintainerEmail,
},
langs: meta.langs,
tosUrl: meta.ToSUrl,
repositoryUrl: meta.repositoryUrl,
feedbackUrl: meta.feedbackUrl,
disableRegistration: meta.disableRegistration,
disableLocalTimeline: meta.disableLocalTimeline,
disableRecommendedTimeline: meta.disableRecommendedTimeline,
disableGlobalTimeline: meta.disableGlobalTimeline,
emailRequiredForSignup: meta.emailRequiredForSignup,
enableHcaptcha: meta.enableHcaptcha,
enableRecaptcha: meta.enableRecaptcha,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
maxCaptionTextLength: MAX_CAPTION_TEXT_LENGTH,
enableTwitterIntegration: meta.enableTwitterIntegration,
enableGithubIntegration: meta.enableGithubIntegration,
enableDiscordIntegration: meta.enableDiscordIntegration,
enableEmail: meta.enableEmail,
enableServiceWorker: meta.enableServiceWorker,
proxyAccountName: proxyAccount ? proxyAccount.username : null,
themeColor: meta.themeColor || "#31748f",
},
};
};
const cache = new Cache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
router.get(nodeinfo2_1path, async (ctx) => {
const base = await cache.fetch(null, () => nodeinfo2());
ctx.body = { version: "2.1", ...base };
ctx.set("Cache-Control", "public, max-age=600");
});
router.get(nodeinfo2_0path, async (ctx) => {
const base = await cache.fetch(null, () => nodeinfo2());
// @ts-ignore
base.software.repository = undefined;
ctx.body = { version: "2.0", ...base };
ctx.set("Cache-Control", "public, max-age=600");
});
export default router;

View File

@ -1,34 +1,33 @@
import Router from "@koa/router"; import Router from "@koa/router";
import config from "@/config/index.js"; import config from "@/config/index.js";
import * as Acct from "@/misc/acct.js"; import {escapeAttribute, escapeValue} from "@/prelude/xml.js";
import { links } from "./nodeinfo.js";
import { escapeAttribute, escapeValue } from "@/prelude/xml.js";
import { Users } from "@/models/index.js";
import type { User } from "@/models/entities/user.js";
import type { FindOptionsWhere } from "typeorm";
import { IsNull } from "typeorm";
// Init router // Init router
const router = new Router(); const router = new Router();
const XRD = ( const XRD = (
...x: { ...x: {
element: string; element: string;
value?: string; value?: string;
attributes?: Record<string, string>; attributes?: Record<string, string>;
}[] }[]
) => ) =>
`<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">${x `<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">${x
.map( .map(
({ element, value, attributes }) => ({ element, value, attributes }) =>
`<${Object.entries( `<${Object.entries(
(typeof attributes === "object" && attributes) || {}, (typeof attributes === "object" && attributes) || {}
).reduce((a, [k, v]) => `${a} ${k}="${escapeAttribute(v)}"`, element)}${ ).reduce(
typeof value === "string" ? `>${escapeValue(value)}</${element}` : "/" (a, [k, v]) => `${a} ${k}="${escapeAttribute(v)}"`,
}>`, element
) )}${
.reduce((a, c) => a + c, "")}</XRD>`; typeof value === "string"
? `>${escapeValue(value)}</${element}`
: "/"
}>`
)
.reduce((a, c) => a + c, "")}</XRD>`;
const allPath = "/.well-known/(.*)"; const allPath = "/.well-known/(.*)";
const webFingerPath = "/.well-known/webfinger"; const webFingerPath = "/.well-known/webfinger";
@ -36,156 +35,68 @@ const jrd = "application/jrd+json";
const xrd = "application/xrd+xml"; const xrd = "application/xrd+xml";
router.use(allPath, async (ctx, next) => { router.use(allPath, async (ctx, next) => {
ctx.set({ ctx.set({
"Access-Control-Allow-Headers": "Accept", "Access-Control-Allow-Headers": "Accept",
"Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
"Access-Control-Expose-Headers": "Vary", "Access-Control-Expose-Headers": "Vary",
}); });
await next(); await next();
}); });
router.options(allPath, async (ctx) => { router.options(allPath, async (ctx) => {
ctx.status = 204; ctx.status = 204;
}); });
router.get("/.well-known/host-meta", async (ctx) => { router.get("/.well-known/host-meta", async (ctx) => {
ctx.set("Content-Type", xrd); ctx.set("Content-Type", xrd);
ctx.body = XRD({ ctx.body = XRD({
element: "Link", element: "Link",
attributes: { attributes: {
rel: "lrdd", rel: "lrdd",
type: xrd, type: xrd,
template: `${config.url}${webFingerPath}?resource={uri}`, template: `${config.url}${webFingerPath}?resource={uri}`,
}, },
}); });
}); });
router.get("/.well-known/host-meta.json", async (ctx) => { router.get("/.well-known/host-meta.json", async (ctx) => {
ctx.set("Content-Type", jrd); ctx.set("Content-Type", jrd);
ctx.body = { ctx.body = {
links: [ links: [
{ {
rel: "lrdd", rel: "lrdd",
type: jrd, type: jrd,
template: `${config.url}${webFingerPath}?resource={uri}`, template: `${config.url}${webFingerPath}?resource={uri}`,
}, },
], ],
}; };
}); });
if (config.twa != null) { if (config.twa != null) {
router.get("/.well-known/assetlinks.json", async (ctx) => { router.get("/.well-known/assetlinks.json", async (ctx) => {
ctx.set("Content-Type", "application/json"); ctx.set("Content-Type", "application/json");
ctx.body = [ ctx.body = [
{ {
relation: ["delegate_permission/common.handle_all_urls"], relation: ["delegate_permission/common.handle_all_urls"],
target: { target: {
namespace: config.twa.nameSpace, namespace: config.twa.nameSpace,
package_name: config.twa.packageName, package_name: config.twa.packageName,
sha256_cert_fingerprints: config.twa.sha256CertFingerprints, sha256_cert_fingerprints: config.twa.sha256CertFingerprints,
}, },
}, },
]; ];
}); });
} }
router.get("/.well-known/nodeinfo", async (ctx) => {
ctx.body = { links };
});
/* TODO /* TODO
router.get('/.well-known/change-password', async ctx => { router.get('/.well-known/change-password', async ctx => {
}); });
*/ */
router.get(webFingerPath, async (ctx) => {
const fromId = (id: User["id"]): FindOptionsWhere<User> => ({
id,
host: IsNull(),
isSuspended: false,
});
const generateQuery = (resource: string): FindOptionsWhere<User> | number =>
resource.startsWith(`${config.url.toLowerCase()}/users/`)
? fromId(resource.split("/").pop()!)
: fromAcct(
Acct.parse(
resource.startsWith(`${config.url.toLowerCase()}/@`)
? resource.split("/").pop()!
: resource.startsWith("acct:")
? resource.slice("acct:".length)
: resource,
),
);
const fromAcct = (acct: Acct.Acct): FindOptionsWhere<User> | number =>
!acct.host || acct.host === config.host.toLowerCase()
? {
usernameLower: acct.username,
host: IsNull(),
isSuspended: false,
}
: 422;
if (typeof ctx.query.resource !== "string") {
ctx.status = 400;
return;
}
const query = generateQuery(ctx.query.resource.toLowerCase());
if (typeof query === "number") {
ctx.status = query;
return;
}
const user = await Users.findOneBy(query);
if (user == null) {
ctx.status = 404;
return;
}
const subject = `acct:${user.username}@${config.host}`;
const self = {
rel: "self",
type: "application/activity+json",
href: `${config.url}/users/${user.id}`,
};
const profilePage = {
rel: "http://webfinger.net/rel/profile-page",
type: "text/html",
href: `${config.url}/@${user.username}`,
};
const subscribe = {
rel: "http://ostatus.org/schema/1.0/subscribe",
template: `${config.url}/authorize-follow?acct={uri}`,
};
if (ctx.accepts(jrd, xrd) === xrd) {
ctx.body = XRD(
{ element: "Subject", value: subject },
{ element: "Link", attributes: self },
{ element: "Link", attributes: profilePage },
{ element: "Link", attributes: subscribe },
);
ctx.type = xrd;
} else {
ctx.body = {
subject,
links: [self, profilePage, subscribe],
};
ctx.type = jrd;
}
ctx.vary("Accept");
ctx.set("Cache-Control", "public, max-age=180");
});
// Return 404 for other .well-known // Return 404 for other .well-known
router.all(allPath, async (ctx) => { router.all(allPath, async (ctx) => {
ctx.status = 404; ctx.status = 404;
}); });
export default router; export default router;