Compare commits

...

No commits in common. "main" and "main2" have entirely different histories.
main ... main2

50 changed files with 5773 additions and 957 deletions

View file

@ -1,15 +0,0 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{yaml,yml}]
indent_size = 2

View file

@ -1,16 +1,19 @@
{
"env": {
"browser": false,
"commonjs": true,
"es2021": true,
"node": true
},
"extends": "eslint:recommended",
"overrides": [
],
"parserOptions": {
"ecmaVersion": "latest"
},
"extends": [
// "eslint:recommended",
// "airbnb-base"
],
"plugins": [
"import"
],
"rules": {
"indent": [
"error",
@ -22,33 +25,70 @@
],
"quotes": [
"error",
"single"
"double"
],
"semi": [
"error",
"always"
],
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_"
}
],
"no-undef": "error",
"no-useless-escape": "error",
"no-var": "error",
"prefer-const": "error",
"quote-props": [
"error",
"consistent-as-needed"
"as-needed"
],
"padded-blocks": [
"dot-notation": [
"error",
{
"allowKeywords": false
}
],
"eol-last": "error",
"comma-dangle": [
"error",
"always-multiline"
],
"no-multi-spaces": "error",
"space-in-parens": [
"error",
"never"
],
"import/order": "error",
"no-return-await": "error",
"no-trailing-spaces": "error",
"padded-blocks": [
"error",
"never",
{
"allowSingleLineBlocks": false
}
],
"space-infix-ops": "error",
"radix": [
"error",
"always"
],
"object-curly-spacing": [
"error",
"never"
],
"space-before-function-paren": [
"error",
"always"
],
"one-var": [
"error",
"never"
],
"function-call-argument-newline": [
"one-var-declaration-per-line": [
"error",
"consistent"
"always"
],
"template-curly-spacing": [
"error",
"never"
]
}
}

135
.gitignore vendored
View file

@ -1,132 +1,5 @@
# ---> Node
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
brainz-social.db
brainz-social.db-shm
brainz-social.db-wal
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.idea/

View file

@ -1,14 +0,0 @@
---
allowedLicenses:
- (MIT AND CC-BY-3.0)
- (MIT OR CC0-1.0)
- Apache-2.0
- BSD-2-Clause
- BSD-3-Clause
- BlueOak-1.0.0
- CC0-1.0
- CC-BY-3.0
- ISC
- MIT
- Python-2.0
- WTFPL

View file

@ -1,16 +0,0 @@
---
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: check-added-large-files
- id: check-merge-conflict
- id: check-yaml
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.36.0
hooks:
- id: eslint
- repo: https://github.com/kontrolilo/kontrolilo
rev: v2.1.0
hooks:
- id: license-check-npm

11
LICENSE
View file

@ -1,11 +0,0 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

View file

@ -1,2 +1,7 @@
# haxsocial
# BRAINZ SOCIAL
## Creating your first account
```node ./bin/create_account.js USERNAME EMAIL_ADDRESS $(mkpasswd -m bcrypt)```
For the provided password prompt, enter your chosen password.

77
app.js
View file

@ -1,55 +1,82 @@
'use strict';
"use strict";
const express = require('express');
const glob = require('glob');
const { match: createPathMatch } = require('path-to-regexp');
const express = require("express");
const {glob} = require("glob");
const {match: createPathMatch} = require("path-to-regexp");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const qs = require("qs");
const databaseHandler = require("./lib/database-handler");
(async () => {
const app = express();
const routes = await glob('**/*.js', {
cwd: './routes',
app.set("query parser", "extended");
app.use(bodyParser.json({
type: "application/*+json",
verify (req, _res, buf) {
req.rawBody = buf;
},
}));
app.use(bodyParser.json({
verify (req, _res, buf) {
req.rawBody = buf;
},
}));
app.use(bodyParser.urlencoded({
extended: false,
verify (req, _res, buf) {
req.rawBody = buf;
},
}));
app.use(cookieParser());
const routes = await glob("**/*.js", {
cwd: "./routes",
dot: true,
});
const pathMatches = [];
app.use((req, res, next) => {
const requestUrl = new URL(req.url, 'https://example.com/');
let candidateUrl = '';
let secondCandidateUrl = '';
for ( const pathMatch in pathMatches ) {
if ( pathMatches[pathMatch](requestUrl.pathname) ) {
app.use((req, _res, next) => {
console.log(`${req.path}${Object.keys(req.query).length ? "?" : ""}${qs.stringify(req.query)}`);
const requestUrl = new URL(req.url, "https://example.com/");
let candidateUrl = "";
let secondCandidateUrl = "";
for (const pathMatch in pathMatches) {
if (pathMatches[pathMatch](requestUrl.pathname)) {
// If we get an exact match, we don't need to process further.
return next();
} else if ( requestUrl.pathname.endsWith('/') && pathMatches[pathMatch](`${requestUrl.pathname}index`) ) {
} if (requestUrl.pathname.endsWith("/") && pathMatches[pathMatch](`${requestUrl.pathname}index`)) {
// If we end with a /, and the index path matches, lets do the index path, but prioritize the non-index path.
const secondRequestUrl = new URL(requestUrl);
secondRequestUrl.pathname = `${requestUrl.pathname}index`;
candidateUrl = secondRequestUrl.toString().substring(19);
} else if ( pathMatches[pathMatch](`${requestUrl.pathname}/index`) ) {
} else if (pathMatches[pathMatch](`${requestUrl.pathname}/index`)) {
// If we don't end with a /, and the /index path matches, lets do the /index path, but prioritize paths checked previously.
const secondRequestUrl = new URL(requestUrl);
secondRequestUrl.pathname = `${requestUrl.pathname}/index`;
secondCandidateUrl = secondRequestUrl.toString().substring(19);
}
}
if ( candidateUrl !== '' ) {
if (candidateUrl !== "") {
req.url = candidateUrl;
console.log(candidateUrl);
return next();
}
if ( secondCandidateUrl !== '' ) {
if (secondCandidateUrl !== "") {
req.url = secondCandidateUrl;
return next();
}
return next();
} );
for ( const routeScript in routes ) {
const route = routes[routeScript].replace(/\.js$/, '');
console.log(route);
pathMatches.push( createPathMatch(`/${route}`));
});
for (const routeScript in routes) {
const route = routes[routeScript].replace(/\.js$/, "");
pathMatches.push(createPathMatch(`/${route}`));
const routeObj = require(`./routes/${route}`);
if ( routeObj.get ) {
app.get(`/${route}`, routeObj.get);
if (routeObj.route) {
routeObj.route(app.route(`/${route}`));
}
}
app.listen(process.env.PORT || 3000);
const server = app.listen(process.env.PORT || 3000, () => {
process.on("SIGINT", () => {
databaseHandler.db.close();
server.close();
});
});
})();

3
bin/create_account.js Normal file
View file

@ -0,0 +1,3 @@
const database_handler = require("../lib/database-handler");
database_handler.createAccount(process.argv[2], process.argv[3], process.argv[4]);

6
database.json Normal file
View file

@ -0,0 +1,6 @@
{
"dev": {
"driver": "sqlite3",
"filename": "brainz-social.db"
}
}

6
jsconfig.json Normal file
View file

@ -0,0 +1,6 @@
{
"compilerOptions": {
"module": "CommonJS"
},
"exclude": ["node_modules"]
}

34
lib/activitypub.js Normal file
View file

@ -0,0 +1,34 @@
const jsonld = require("jsonld");
const databaseHandler = require("./database-handler");
module.exports = {
jsonldCustomLoader: async (url, options) => {
const cache = databaseHandler.getJsonldSchemaCache(url);
if (cache && cache.schema) {
return {
contextUrl: null,
document: JSON.parse(cache.schema),
documentUrl: url,
};
}
// TODO: Write HTTP client handler.
const retData = await jsonld.documentLoaders.node()(url);
databaseHandler.storeJsonldSchemaCache(url, JSON.stringify(retData.document), (Math.floor(Date.now()/1000) + 86400));
return retData;
},
compactedForm: async (urlOrObj) => {
if (typeof urlOrObj === "string") {
try {
const url = new URL(urlOrObj);
return jsonld.compact(await jsonld.expand(jsonld.documentLoaders.node()(url)), {});
} catch (e) {
return jsonld.compact(await jsonld.expand(JSON.parse(urlOrObj)), {});
}
} else {
return jsonld.compact(await jsonld.expand(urlOrObj), {});
}
},
};
jsonld.documentLoader = module.exports.jsonldCustomLoader;

166
lib/database-handler.js Normal file
View file

@ -0,0 +1,166 @@
const db = require("better-sqlite3")("brainz-social.db");
db.pragma("journal_mode = WAL");
module.exports = {
db,
application: new Proxy({}, {
get (target, key) {
if (typeof(key) === "Number") {
return db.prepare("SELECT * FROM applications WHERE id = ?").get(key);
} else {
return db.prepare("SELECT * FROM applications WHERE client_id = ?").get(key);
}
},
}),
config: {
getStatement: "SELECT * FROM config",
getByKey: (key) => {
return db.prepare(module.exports.config.getStatement + " WHERE key = ?").get(key)?.value ?? null;
},
},
getConfig: (key) => {
return module.exports.config.getByKey(key);
},
setConfig: (key, value) => {
db.prepare("INSERT OR REPLACE INTO config (key, value) VALUES(?, ?);").run(key, value);
},
createApplication: (client_name, redirect_uri, scopes, website, client_id, client_secret) => {
db.prepare("INSERT INTO applications (client_name, redirect_uri, scopes, website, client_id, client_secret) VALUES (?, ?, ?, ?, ?, ?);").run(client_name, redirect_uri, scopes, website, client_id, client_secret);
},
createToken: (token, scope, application_id, user_id, created_at) => {
db.prepare("INSERT INTO oauth_tokens (token, scopes, application_id, user_id, created_at, revoked) VALUES (?, ?, ?, ?, ?, false)").run(token, scope, application_id, user_id, created_at);
},
getTokenData: (token) => {
return db.prepare("SELECT application_id, user_id, created_at, revoked, scopes, token FROM oauth_tokens WHERE token = ?").get(token);
},
revokeToken: (token) => {
db.prepare("UPDATE oauth_tokens SET revoked = true WHERE token = ?").run(token);
},
getAccountByUsername: (username) => {
return db.prepare("SELECT id, username, email, password_hash, account_tier, created_at FROM accounts WHERE username = ?").get(username);
},
createAccount: (username, email, password_hash) => {
db.prepare("INSERT INTO accounts (username, email, password_hash, account_tier, created_at) VALUES (?, ?, ?, 0)").run(username, email, password_hash, Date.now());
},
checkAuthCookie: (cookie_value) => {
return db.prepare("SELECT id, cookie_value, created_at, user_id, revoked FROM cookies WHERE cookie_value = ?").get(cookie_value);
},
revokeAuthCookie: (cookie_value) => {
db.prepare("UPDATE cookies SET revoked = true WHERE cookie_value = ?").run(cookie_value);
},
createAuthCookie: (cookie_value, created_at, user_id) => {
db.prepare("INSERT INTO cookies (cookie_value, created_at, user_id, revoked) VALUES (?, ?, ?, false)").run(cookie_value, created_at, user_id);
},
createCsrfToken: (url, created_at, cookie_value) => {
const db_row_id = db.prepare("INSERT INTO csrf_token (url, created_at, cookie_value) VALUES (?, ?, ?)").run(url, created_at, cookie_value).lastInsertRowid;
return db.prepare("SELECT id FROM csrf_token WHERE rowid = ?").get(db_row_id);
},
createCsrfTokenAssociation: (...ids) => {
for (const source_id in ids) {
if (Number.parseInt(ids[source_id], 10) === ids[source_id]) {
for (const destination_id in ids) {
if (Number.parseInt(ids[destination_id], 10) === ids[destination_id]) {
db.prepare("INSERT INTO csrf_token_relations (source_id, destination_id) VALUES (?, ?)").run(ids[source_id], ids[destination_id]);
}
}
}
}
},
removeAssociatedCsrfTokens: (id) => {
db.prepare("DELETE FROM csrf_token WHERE id IN (SELECT destination_id AS id FROM csrf_token_relations WHERE source_id = ?)").run(id);
},
getCsrfToken: (url) => {
return db.prepare("SELECT id, url, created_at, cookie_value FROM csrf_token WHERE url = ?").get(url);
},
createOauthCode: (code, application_id, user_id, scopes, created_at) => {
db.prepare("INSERT INTO oauth_code (code, application_id, user_id, scopes, created_at, revoked) VALUES (?, ?, ?, ?, ?, false)").run(code, application_id, user_id, scopes, created_at);
},
getOauthCode: (code) => {
return db.prepare("SELECT code, application_id, user_id, scopes, created_at, revoked FROM oauth_code WHERE code = ?").get(code);
},
revokeOauthCode: (code) => {
db.prepare("UPDATE oauth_code SET revoked = true WHERE code = ?").run(code);
},
selectApplicationByAuthToken: (token) => {
return db.prepare("SELECT id, client_id, client_secret, redirect_uri, scopes, website FROM applications WHERE id in (SELECT application_id as id FROM oauth_tokens WHERE token = ?);").get(token);
},
getVapidKey: () => {
const vapidPublic = db.prepare("SELECT value FROM config WHERE key = vapid_key_public").get();
const vapidPrivate = db.prepare("SELECT value FROM config WHERE key = vapid_key_private").get();
if (vapidPublic.value && vapidPrivate.value) {
return {public: vapidPublic, private: vapidPrivate};
}
return null;
},
setVapidKey: (publicKey, privateKey) => {
db.prepare("INSERT INTO config (key, value) VALUES (vapid_key_public, ?)").run(publicKey);
db.prepare("INSERT INTO config (key, value) VALUES (vapid_key_private, ?)").run(privateKey);
},
getJsonldSchemaCache: (url) => {
return db.prepare("SELECT schema FROM jsonld_schema_cache WHERE schema_uri = ? AND expires > ?").get(url, Math.floor(Date.now() / 1000));
},
storeJsonldSchemaCache: (url, schema, expiry) => {
db.prepare("INSERT INTO jsonld_schema_cache (schema, schema_uri, expires) VALUES (?, ?, ?)").run(schema, url, expiry);
},
getAccountByToken: (token) => {
return db.prepare("SELECT id, username, email, password_hash, account_tier, created_at FROM accounts WHERE id IN (SELECT user_id FROM oauth_tokens WHERE token = ?)").get(token);
},
getAccountActivityByAccount: (user_id) => {
return db.prepare("SELECT id, object, type, local, uri_id, owner FROM activity_objects WHERE (type = 'https://www.w3.org/ns/activitystreams#Person' OR type = 'https://www.w3.org/ns/activitystreams#Service' OR type = 'https://www.w3.org/ns/activitystreams#Group') AND local = true AND owner in (SELECT username FROM accounts WHERE id = ?)").get(user_id);
},
addActivity: (object, type, local, uri_id, owner, created_at) => {
db.prepare("INSERT INTO activity_objects (object, type, local, uri_id, owner, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(object, type, local, uri_id, owner, created_at);
},
activity: new Proxy({}, {
get: (target, key) => {
return db.prepare("SELECT * FROM activity_objects WHERE uri_id = ?").get(key);
},
set: (target, key, value) => {
db.prepare("INSERT INTO activity_objects (object, type, local, uri_id, owner, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(value.object, value.type, value.local.toString(), key, value.owner, value.created_at);
},
}),
getLastStatus: (owner) => {
return db.prepare("SELECT created_at FROM activity_objects WHERE type = 'https://www.w3.org/ns/activitystreams#Note' AND owner = ? ORDER BY created_at DESC").get(owner);
},
getStatusCount: (owner) => {
return db.prepare("SELECT COUNT(*) AS count FROM activity_objects WHERE type = 'https://www.w3.org/ns/activitystreams#Note' AND owner = ?").get(owner);
},
storeW3idSecurityKey: (key_uri, publicKey, privateKey, expires) => {
db.prepare("INSERT INTO w3id_security_keys (key_uri, public_key, private_key, expires) VALUES (?, ?, ?, ?)").run(key_uri, publicKey, privateKey, expires);
},
};

56
lib/input_validate.js Normal file
View file

@ -0,0 +1,56 @@
module.exports = {
validate_exists: (body, fields) => {
if (!body) {
return {error: "Validation failed, no body provided."};
}
for (const field in fields) {
if (!body[fields[field]]) {
return {error: `Validation failed, field ${field} missing from request.`};
}
}
return true;
},
validate_appropriate_scopes: (max_scope, scope_requested) => {
const max_scope_array_temp = max_scope.split(/[\s+]+/);
const max_scope_array = [];
for (const scope in max_scope_array_temp) {
if (max_scope_array_temp[scope].match(/[a-zA-Z0-9:]/)) {
max_scope_array.push(max_scope_array_temp[scope]);
if (max_scope_array_temp[scope] === "read") {
max_scope_array.push("read:accounts", "read:blocks", "read:bookmarks", "read:favorites", "read:filters", "read:follows", "read:lists", "read:mutes", "read:notifications", "read:search", "read:statuses");
}
if (max_scope_array_temp[scope] === "write") {
max_scope_array.push("write:accounts", "write:blocks", "write:bookmarks", "write:conversations", "write:favourites", "write:filters", "write:follows", "write:lists", "write:media", "write:mutes", "write:notifications", "write:reports", "write:statuses");
}
if (max_scope_array_temp[scope] === "follow") {
max_scope_array.push("read:blocks", "write:blocks", "read:follows", "write:follows", "read:mutes", "write:mutes");
}
if (max_scope_array_temp[scope] === "admin:read") {
max_scope_array.push("admin:read:accounts", "admin:read:reports", "admin:read:domain_allows", "admin:read:domain_blocks", "admin:read:ip_blocks", "admin:read:email_domain_blocks", "admin:read:canonical_email_blocks");
}
if (max_scope_array_temp[scope] === "admin:write") {
max_scope_array.push("admin:write:accounts", "admin:write:reports", "admin:write:domain_allows", "admin:write:domain_blocks", "admin:write:ip_blocks", "admin:write:email_domain_blocks", "admin:write:canonical_email_blocks");
}
}
}
const scope_requested_array = scope_requested.split(/[\s+]+/);
for (const scope in scope_requested_array) {
if (max_scope_array.includes(scope_requested_array[scope])) {
continue;
}
return false;
}
return true;
},
};

53
middleware/auth.js Normal file
View file

@ -0,0 +1,53 @@
const databaseHandler = require("../lib/database-handler");
const input_validate = require("../lib/input_validate");
module.exports = {
auth_token: (needs_user, need_scopes) => {
return (req, res, next) => {
const token = databaseHandler.getTokenData(req.header("authorization").split(/\s+/)[1]);
if (!token) {
res.status(401);
res.json({
error: "UNAUTHENTICATED",
});
res.end();
return;
}
if (token.revoked) {
res.status(401);
res.json({
error: "UNAUTHENTICATED",
});
res.end();
return;
}
if (needs_user && token.user_id === 0) {
res.status(401);
res.json({
error: "INSUFFICIENT_AUTHORIZATION",
});
res.end();
return;
}
if (need_scopes && !input_validate.validate_appropriate_scopes(token.scopes, need_scopes)) {
res.status(401);
res.json({
error: "INSUFFICIENT_SCOPE",
});
res.end();
return;
}
if (!req.brainz) {
req.brainz = {};
}
req.brainz.token = token;
next();
};
},
};

View file

@ -0,0 +1,43 @@
"use strict";
const async = require("async");
let dbm;
let type;
let seed;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};
exports.up = function (db, callback) {
async.series([
db.createTable.bind(db, "applications", {
id: {type: "int", primaryKey: true, autoIncrement: true},
client_name: "string",
redirect_uri: "string",
scopes: "string",
website: "string",
client_id: {type: "string", unique: true},
client_secret: "string",
}),
db.addIndex.bind(db, "applications", "clientIdIndex", ["client_id"], true),
], callback);
};
exports.down = function (db, callback) {
async.series([
db.removeIndex.bind(db, "applications", "clientIdIndex"),
db.dropTable.bind(db, "applications"),
], callback);
};
exports._meta = {
version: 1,
};

View file

@ -0,0 +1,31 @@
"use strict";
let dbm;
let type;
let seed;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};
exports.up = function (db) {
return db.createTable("config", {
id: {type: "int", primaryKey: true, autoIncrement: true},
key: {type: "string", unique: true},
value: "string",
});
};
exports.down = function (db) {
return db.dropTable("config");
};
exports._meta = {
version: 1,
};

View file

@ -0,0 +1,45 @@
"use strict";
const async = require("async");
let dbm;
let type;
let seed;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};
exports.up = function (db, callback) {
async.series([
db.createTable.bind(db, "oauth_tokens", {
id: {type: "int", primaryKey: true, autoIncrements: true},
token: {type: "string", unique: true},
application_id: "int",
scopes: "string",
user_id: "int",
revoked: "boolean",
created_at: "int",
}),
db.addIndex.bind(db, "oauth_tokens", "oauth_token_index", ["token"]),
db.addIndex.bind(db, "oauth_tokens", "oauth_token_user_id_index", ["user_id"]),
], callback);
};
exports.down = function (db, callback) {
async.series([
db.removeIndex.bind(db, "oauth_tokens", "oauth_token_user_id_index"),
db.removeIndex.bind(db, "oauth_tokens", "oauth_token_index"),
db.dropTable("oauth_tokens"),
], callback);
};
exports._meta = {
version: 1,
};

View file

@ -0,0 +1,39 @@
"use strict";
let dbm;
let type;
let seed;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};
exports.up = function (db, callback) {
db.createTable("accounts", {
id: {type: "int", primaryKey: true, autoIncrement: true},
username: {type: "string", unique: true},
email: "string",
password_hash: "string",
account_tier: "int",
}, (result) => {
if (result) {
callback(result);
}
db.runSql("INSERT INTO \"accounts\" (\"username\",\"email\",\"password_hash\",\"account_tier\") VALUES (\"guest\",\"null@null.null\",\"purposely_invalid_password\",0);", callback);
});
};
exports.down = function (db) {
return db.dropTable("accounts");
};
exports._meta = {
version: 1,
};

View file

@ -0,0 +1,41 @@
"use strict";
const async = require("async");
let dbm;
let type;
let seed;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};
exports.up = function (db, callback) {
async.series([
db.createTable.bind(db, "cookies", {
id: {type: "int", primaryKey: true, autoIncrement: true},
cookie_value: {type: "string", unique: true},
created_at: "int",
user_id: "int",
revoked: "boolean",
}),
db.addIndex.bind(db, "cookies", "cookies_cookie_value_index", ["cookie_value"]),
], callback);
};
exports.down = function (db, callback) {
async.series([
db.removeIndex.bind(db, "cookies", "cookies_cookie_value_index"),
db.dropTable.bind(db, "cookies"),
], callback);
};
exports._meta = {
version: 1,
};

View file

@ -0,0 +1,31 @@
"use strict";
let dbm;
let type;
let seed;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};
exports.up = function (db) {
return db.createTable("csrf_token", {
id: {type: "int", primaryKey: true, autoIncrement: true},
url: "string",
created_at: "int",
});
};
exports.down = function (db) {
return db.dropTable("csrf_token");
};
exports._meta = {
version: 1,
};

View file

@ -0,0 +1,31 @@
"use strict";
let dbm;
let type;
let seed;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};
exports.up = function (db) {
return db.createTable("csrf_token_relations", {
id: {type: "int", primaryKey: true, autoIncrement: true},
source_id: "int",
destination_id: "int",
});
};
exports.down = function (db) {
return db.dropTable("csrf_token_relations");
};
exports._meta = {
version: 1,
};

View file

@ -0,0 +1,27 @@
"use strict";
let dbm;
let type;
let seed;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};
exports.up = function (db) {
return db.addColumn("csrf_token", "cookie_value", "string");
};
exports.down = function (db) {
return db.removeColumn("csrf_token", "cookie_value");
};
exports._meta = {
version: 1,
};

View file

@ -0,0 +1,42 @@
"use strict";
const async = require("async");
let dbm;
let type;
let seed;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};
exports.up = function (db, callback) {
async.series([
db.createTable.bind(db, "oauth_code", {
id: {type: "int", primaryKey: true, autoIncrements: true},
code: {type: "string", unique: true},
application_id: "int",
scopes: "string",
user_id: "int",
created_at: "int",
}),
db.addIndex.bind(db, "oauth_code", "oauth_code_index", ["code"]),
], callback);
};
exports.down = function (db, callback) {
async.series([
db.removeIndex.bind(db, "oauth_code", "oauth_code_index"),
db.dropTable.bind(db, "oauth_code"),
], callback);
};
exports._meta = {
version: 1,
};

View file

@ -0,0 +1,27 @@
"use strict";
let dbm;
let type;
let seed;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};
exports.up = function (db) {
return db.addColumn("oauth_code", "revoked", "boolean");
};
exports.down = function (db) {
return db.removeColumn("oauth_code", "revoked");
};
exports._meta = {
version: 1,
};

View file

@ -0,0 +1,32 @@
"use strict";
let dbm;
let type;
let seed;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};
exports.up = function (db) {
return db.createTable("jsonld_schema_cache", {
id: {type: "int", primaryKey: true, autoIncrements: true},
schema_uri: "string",
schema: "string",
expires: "int",
});
};
exports.down = function (db) {
return db.dropTable("jsonld_schema_cache");
};
exports._meta = {
version: 1,
};

View file

@ -0,0 +1,34 @@
"use strict";
let dbm;
let type;
let seed;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};
exports.up = function (db) {
return db.createTable("activity_objects", {
id: {type: "int", primaryKey: true, autoIncrements: true},
object: "string",
type: "string",
local: "boolean",
uri_id: "string",
owner: "string",
});
};
exports.down = function (db) {
return db.dropTable("activity_objects");
};
exports._meta = {
version: 1,
};

View file

@ -0,0 +1,27 @@
"use strict";
let dbm;
let type;
let seed;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};
exports.up = function (db) {
return db.addColumn("activity_objects", "created_at", "string");
};
exports.down = function (db) {
return db.removeColumn("activity_objects", "created_at");
};
exports._meta = {
version: 1,
};

View file

@ -0,0 +1,33 @@
"use strict";
let dbm;
let type;
let seed;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};
exports.up = function (db) {
return db.createTable("w3id_security_keys", {
id: {type: "int", primaryKey: true, autoIncrements: true},
key_uri: "string",
public_key: "string",
private_key: "string",
expires: "int",
});
};
exports.down = function (db) {
return db.dropTable("w3id_security_keys");
};
exports._meta = {
version: 1,
};

View file

@ -0,0 +1,30 @@
"use strict";
let dbm;
let type;
let seed;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};
exports.up = function (db) {
return db.addColumn("accounts", "created_at", {
type: "string",
default: Date.now(),
});
};
exports.down = function (db) {
return db.removeColumn("accounts", "created_at");
};
exports._meta = {
version: 1,
};

4632
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,24 +1,33 @@
{
"name": "haxsocial",
"name": "brainz-social",
"version": "0.0.1",
"description": "",
"description": "Brainz Social",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://forgejo.hax.social/rallias/haxsocial.git"
},
"author": "Andrew Pietila <a.pietila@protonmail.com>",
"license": "WTFPL",
"dependencies": {
"async": "^3.2.4",
"bcrypt": "^5.1.1",
"better-sqlite3": "^8.6.0",
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.6",
"db-migrate": "^0.11.14",
"db-migrate-sqlite3": "^0.5.0",
"express": "^4.18.2",
"glob": "^9.3.0",
"path-to-regexp": "^6.2.1"
"glob": "^10.3.10",
"jsonld": "^8.3.1",
"path-to-regexp": "^6.2.1",
"qs": "^6.11.2",
"sanitize-html": "^2.11.0",
"web-push": "^3.6.6"
},
"type": "commonjs",
"devDependencies": {
"eslint": "^8.36.0",
"license-checker": "^25.0.1"
"eslint": "^8.51.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.28.1"
}
}

48
routes/api/v1/accounts.js Normal file
View file

@ -0,0 +1,48 @@
const crypto = require("crypto");
const bcrypt = require("bcrypt");
const databaseHandler = require("../../../lib/database-handler");
const input_validate = require("../../../lib/input_validate");
const auth = require("../../../middleware/auth");
module.exports = {
route: ((routeObj) => {
routeObj.post(auth.auth_token(false, "write:accounts"), (req, res) => {
const authToken = req.header("Authorization").split(/\s+/)[1];
const validation_result = input_validate.validate_exists(req.body, ["username", "email", "password", "agreement", "locale"]);
if (validation_result !== true) {
res.status(422);
res.json(validation_result);
return;
}
const username = req.body.username;
if (databaseHandler.getAccountByUsername(username)) {
res.status(422);
res.json({error: "Validation failed, username taken.", details: {username: {error: "ERR_TAKEN", description: "Username taken."}}});
return;
}
// TODO: We're just taking emails at face value for now.
const email = req.body.email;
const password = req.body.password;
const password_hash = bcrypt.hashSync(password, bcrypt.genSaltSync());
databaseHandler.createAccount(username, email, password_hash);
const userObject = databaseHandler.getAccountByUsername(username);
const userToken = crypto.randomBytes(32).toString("base64");
const created_at = Math.floor(Date.now() / 1000);
const application = databaseHandler.application[authToken.application_id];
databaseHandler.createToken(userToken, application.scopes, application.id, userObject.id, created_at);
res.status(200);
res.json({
access_token: userToken,
token_type: "Bearer",
scope: application.scopes,
created_at,
});
});
}),
};

View file

@ -0,0 +1,125 @@
const crypto = require("crypto");
const databaseHandler = require("../../../../lib/database-handler");
const auth = require("../../../../middleware/auth");
module.exports = {
route: (routeObj) => {
routeObj.get(auth.auth_token(true, "read:accounts"), (req, res) => {
const token = req.brainz.token;
const account = databaseHandler.getAccountByToken(token.token);
const accountActivityRow = databaseHandler.getAccountActivityByAccount(account.id);
let accountActivity = {};
if (accountActivityRow) {
accountActivity = JSON.parse(accountActivityRow.object);
} else {
const keyPair = crypto.generateKeyPairSync("rsa", {
modulusLength: 4096,
publicKeyEncoding: {
type: "spki",
format: "pem",
},
privateKeyEncoding: {
type: "pkcs8",
format: "pem",
},
});
databaseHandler.storeW3idSecurityKey(`https://${req.headers.host}/users/${account.username}#main-key`, keyPair.publicKey, keyPair.privateKey, (Math.floor(Date.now() / 1000) + (86400 * 90)));
accountActivity = {
"@id": `https://${req.headers.host}/users/${account.username}`,
"@type": "https://www.w3.org/ns/activitystreams#Person",
"http://joinmastodon.org/ns#devices": {
"@id": `https://${req.headers.host}/users/${account.username}/collections/devices`,
},
"http://joinmastodon.org/ns#discoverable": true,
"http://joinmastodon.org/ns#featured": {
"@id": `https://${req.headers.host}/users/${account.username}/collections/featured`,
},
"http://joinmastodon.org/ns#featuredTags": {
"@id": `https://${req.headers.host}/users/${account.username}/collections/tags`,
},
"http://joinmastodon.org/ns#indexable": true,
"http://joinmastodon.org/ns#memorial": false,
"http://www.w3.org/ns/ldp#inbox": {
"@id": `https://${req.headers.host}/users/${account.username}/inbox`,
},
"https://w3id.org/security#publicKey": {
"@id": `https://${req.headers.host}/users/${account.username}#main-key`,
"https://w3id.org/security#owner": {
"@id": `https://${req.headers.host}/users/${account.username}`,
},
"https://w3id.org/security#publicKeyPem": keyPair.publicKey,
},
"https://www.w3.org/ns/activitystreams#endpoints": {
"https://www.w3.org/ns/activitystreams#sharedInbox": {
"@id": `https://${req.headers.host}/inbox`,
},
},
"https://www.w3.org/ns/activitystreams#followers": {
"@id": `https://${req.headers.host}/users/${account.username}/followers`,
},
"https://www.w3.org/ns/activitystreams#following": {
"@id": `https://${req.headers.host}/users/${account.username}/following`,
},
"https://www.w3.org/ns/activitystreams#icon": {
"@type": "https://www.w3.org/ns/activitystreams#Image",
"https://www.w3.org/ns/activitystreams#mediaType": "image/png",
"https://www.w3.org/ns/activitystreams#url": {
"@id": `https://${req.headers.host}/res/avatar_not_found.png`,
},
},
"https://www.w3.org/ns/activitystreams#manuallyApprovesFollowers": false,
"https://www.w3.org/ns/activitystreams#name": account.username,
"https://www.w3.org/ns/activitystreams#outbox": {
"@id": `https://${req.headers.host}/users/${account.username}/outbox`,
},
"https://www.w3.org/ns/activitystreams#preferredUsername": account.username,
"https://www.w3.org/ns/activitystreams#published": {
"@type": "http://www.w3.org/2001/XMLSchema#dateTime",
"@value": new Date(Date.now()).toISOString(),
},
"https://www.w3.org/ns/activitystreams#summary": "",
"https://www.w3.org/ns/activitystreams#tag": [],
"https://www.w3.org/ns/activitystreams#url": {
"@id": `https://${req.headers.host}/@${account.username}`,
},
};
databaseHandler.addActivity(JSON.stringify(accountActivity), accountActivity["@type"], "true", accountActivity["@id"], account.username, (new Date(Number(account.created_at))).toISOString());
}
const last_status = databaseHandler.getLastStatus(account.username);
res.status(200);
res.json({
id: accountActivity["@id"],
username: account.username,
acct: account.username,
url: accountActivity["https://www.w3.org/ns/activitystreams#url"]["@id"],
display_name: accountActivity["https://www.w3.org/ns/activitystreams#preferredUsername"],
avatar: accountActivity["https://www.w3.org/ns/activitystreams#icon"]["https://www.w3.org/ns/activitystreams#url"]["@id"],
avatar_static: accountActivity["https://www.w3.org/ns/activitystreams#icon"]["https://www.w3.org/ns/activitystreams#url"]["@id"],
header: accountActivity["https://www.w3.org/ns/activitystreams#icon"]["https://www.w3.org/ns/activitystreams#url"]["@id"],
header_static: accountActivity["https://www.w3.org/ns/activitystreams#icon"]["https://www.w3.org/ns/activitystreams#url"]["@id"],
locked: accountActivity["https://www.w3.org/ns/activitystreams#manuallyApprovesFollowers"],
emojis: [],
bot: accountActivity["@type"] === "https://www.w3.org/ns/activitystreams#Service",
group: accountActivity["@type"] === "https://www.w3.org/ns/activitystreams#Group",
discoverable: accountActivity["http://joinmastodon.org/ns#discoverable"],
created_at: accountActivity["https://www.w3.org/ns/activitystreams#published"]["@value"],
last_status_at: last_status ? last_status.created_at : null,
statuses_count: databaseHandler.getStatusCount(account.username).count,
// TODO: Proper followers and following count.
followers_count: -1,
following_count: -1,
source: {
note: "",
fields: [],
privacy: "public",
sensitive: false,
language: "en",
},
});
return;
});
},
};

79
routes/api/v1/apps.js Normal file
View file

@ -0,0 +1,79 @@
const crypto = require("crypto");
const webpush = require("web-push");
const database_handler = require("../../../lib/database-handler");
const input_validate = require("../../../lib/input_validate");
module.exports = {
route: ((routeObj) => {
routeObj.post((req, res) => {
const validation_result = input_validate.validate_exists(req.body, ["client_name", "redirect_uris"]);
if (validation_result !== true) {
res.status(422);
res.json(validation_result);
return;
}
let scopes;
let website;
const client_name = req.body.client_name;
const redirect_uris = req.body.redirect_uris;
if (redirect_uris !== "urn:ietf:wg:oauth:2.0:oob") {
let url;
try {
url = new URL(redirect_uris);
} catch (e) {
res.status(422);
res.json({error: "Validation failed, redirect_uris not parsable as absolute URL."});
return;
}
// if ( url.protocol.toLower === "http" ) {
// res.status(422);
// res.json({error: "Validation failed, http redirect_uris not allowed."});
// }
}
scopes = "read";
if (req.body.scopes) {
scopes = req.body.scopes;
}
website = null;
if (req.body.website) {
website = req.body.website;
}
let vapid_key;
// Not that anything is implemented yet regarding Web Push/VAPID/whatever,
// this is a required item per the Mastodon API documentation.
vapid_key = database_handler.getConfig("vapid_key_public");
if (!vapid_key) {
const vapidKeys = webpush.generateVAPIDKeys();
database_handler.setConfig("vapid_key_public", vapidKeys.publicKey);
database_handler.setConfig("vapid_key_private", vapidKeys.privateKey);
vapid_key = vapidKeys.publicKey;
}
const client_id = crypto.randomBytes(32).toString("base64");
const client_secret = crypto.randomBytes(32).toString("base64");
database_handler.createApplication(client_name, redirect_uris, scopes, website, client_id, client_secret);
res.status(200);
res.json({
name: client_name,
website,
vapid_key,
client_id,
client_secret,
});
return;
});
}),
};

View file

@ -0,0 +1,30 @@
const webpush = require("web-push");
const databaseHandler = require("../../../../lib/database-handler");
const auth_middleware = require("../../../../middleware/auth");
module.exports = {
route: (routeObj) => {
routeObj.get(auth_middleware.auth_token(false, ""), (req, res) => {
const token = req.header("Authorization").split(/\s+/)[1];
const application = databaseHandler.getApplicationByToken(token);
let vapid_key = databaseHandler.getVapidKey();
if (vapid_key) {
vapid_key = vapid_key["public"];
}
if (!vapid_key) {
const vapidKeys = webpush.generateVAPIDKeys();
databaseHandler.setVapidKey(vapidKeys.publicKey, vapidKeys.privateKey);
vapid_key = vapidKeys.publicKey;
}
res.status(200);
res.json({
name: application.name,
website: application.website,
vapid_key: vapid_key["public"],
});
return;
});
},
};

9
routes/api/v1/filters.js Normal file
View file

@ -0,0 +1,9 @@
const auth = require("../../../middleware/auth");
module.exports = {
route: (routeObj) => {
routeObj.get(auth.auth_token(true, "read:filters"), (req, res) => {
return res.json([]);
});
},
};

55
routes/api/v1/instance.js Normal file
View file

@ -0,0 +1,55 @@
module.exports = {
route: (routeObj) => {
routeObj.get((req, res) => {
res.status(200);
res.json({
uri: req.headers.host,
title: "Brainz Social",
short_description: "",
description: "",
email: "a.pietila@protonmail.com",
version: "4.2.0",
urls: {},
stats: {
user_count: -1,
status_count: -1,
domain_count: -1
},
thumbnail: null,
languages: [
"en"
],
registrations: true,
approval_required: false,
invites_enabled: false,
configuration: {
accounts: {
max_featured_tags: 4,
},
statuses: {
max_characters: 500,
max_media_attachments: 0,
characters_reserved_per_url: 25
},
media_attachments: {
supported_mime_types: [],
image_size_limit: 0,
image_matrix_limit: 0,
video_size_limit: 0,
video_frame_rate_limit: 0,
video_matrix_limit: 0,
},
polls: {
max_options: 0,
max_characters_per_option: 0,
min_expiration: Number.MAX_SAFE_INTEGER,
max_expiration: Number.MAX_SAFE_INTEGER,
},
contact_account: {},
rules: [],
},
});
return;
});
},
};

View file

@ -0,0 +1,9 @@
const auth = require("../../../middleware/auth");
module.exports = {
route: (routeObj) => {
routeObj.get(auth.auth_token(true, "read:statuses"), (req, res) => {
return res.json([]);
});
},
};

View file

107
routes/api/v1/statuses.js Normal file
View file

@ -0,0 +1,107 @@
const sanitizeHtml = require("sanitize-html");
const auth = require("../../../middleware/auth");
const databaseHandler = require("../../../lib/database-handler");
module.exports = {
route: (routeObj) => {
routeObj.post(auth.auth_token(true, "write:statuses"), (req, res) => {
const id = process.hrtime.bigint().toString(10);
const token = req.brainz.token;
const account = databaseHandler.getAccountByToken(token.token);
const created_at = Date.now();
const statusText = sanitizeHtml(req.body.status, {
allowedTags: [],
allowedAttributes: {},
});
const status = {
"@id": `https://${req.header.host}/users/${account.username}/statuses/${id}`,
"@type": "https://www.w3.org/ns/activitystreams#Note",
"http://ostatus.org#atomUri": `https://${req.header.host}/users/${account.username}/statuses/${id}`,
"http://ostatus.org#conversation": null, // TODO: wut
"http://ostatus.org#inReplyToAtomUri": null,
"https://www.w3.org/ns/activitystreams#attachment": [],
"https://www.w3.org/ns/activitystreams#attributedTo": {
"@id": `https://${req.headers.host}/users/${account.username}`,
},
"https://www.w3.org/ns/activitystreams#cc": {
"@id": `https://${req.headers.host}/users/${account.username}/followers`,
},
"https://www.w3.org/ns/activitystreams#content": [
`<p>${statusText.split("\n").join("</p><p>")}</p>`,
],
"https://www.w3.org/ns/activitystreams#inReplyTo": null,
"https://www.w3.org/ns/activitystreams#published": {
"@type": "http://www.w3.org/2001/XMLSchema#dateTime",
"@value": new Date(created_at).toISOString(),
},
"https://www.w3.org/ns/activitystreams#replies": {
"@id": `https://${req.headers.host}/users/${account.username}/statuses/${id}/replies`,
"@type": "https://www.w3.org/ns/activitystreams#Collection",
"https://www.w3.org/ns/activitystreams#first": {
"@type": "https://www.w3.org/ns/activitystreams#CollectionPage",
"https://www.w3.org/ns/activitystreams#items": [],
"https://www.w3.org/ns/activitystreams#next": {
"@id": `https://${req.headers.host}/users/${account.username}/statuses/${id}/replies?only_other_accounts=true&page=true`,
},
"https://www.w3.org/ns/activitystreams#partOf": {
"@id": `https://${req.headers.host}/users/${account.username}/statuses/${id}/replies`,
},
},
},
"https://www.w3.org/ns/activitystreams#tag": [],
"https://www.w3.org/ns/activitystreams#to": {
"@id": "https://www.w3.org/ns/activitystreams#Public",
},
"https://www.w3.org/ns/activitystreams#url": {
"@id": `https://${req.headers.host}/@${account.username}/${id}`,
},
};
status["https://www.w3.org/ns/activitystreams#summary"] = req.body.spoiler_text ?? "";
status["https://www.w3.org/ns/activitystreams#sensitive"] = req.body.spoiler_text ?? req.body.spoiler_text ? true : false;
databaseHandler.activity[`https://${req.header.host}/users/${account.username}/statuses/${id}`] = {
object: JSON.stringify(status),
type: status["@type"],
local: true,
owner: account.username,
created_at,
};
// TODO: Distribution queue.
const retObj = {
id: status["@id"],
created_at: status["https://www.w3.org/ns/activitystreams#published"]["@value"],
in_reply_to_id: null,
in_reply_to_account_id: null,
sensitive: status["https://www.w3.org/ns/activitystreams#sensitive"],
spoiler_text: status["https://www.w3.org/ns/activitystreams#summary"],
visibility: "public", // TODO: Private posting capability.
language: "en", // TODO: Other languages.
uri: status["@id"],
url: status["@id"],
replies_count: 0, // Future copy paste jobs - These counts should be accurate.
reblogs_count: 0,
favourites_count: 0,
favourited: false,
reblogged: false,
muted: false,
bookmarked: false,
content: status["https://www.w3.org/ns/activitystreams#content"][0],
reblog: null,
account: {},
media_attachments: [], // TODO: Support media.
mentions: [],
tags: [],
emojis: [],
card: null,
poll: null,
};
res.json(retObj);
return;
});
},
};

View file

@ -0,0 +1,9 @@
const auth = require("../../../../middleware/auth");
module.exports = {
route: (routeObj) => {
routeObj.get(auth.auth_token(true, "read:statuses"), (req, res) => {
return res.json([]);
});
},
};

View file

@ -0,0 +1,9 @@
const auth = require("../../../../middleware/auth");
module.exports = {
route: (routeObj) => {
routeObj.get(auth.auth_token(false, ""), (req, res) => {
return res.json([]);
});
},
};

94
routes/login.js Normal file
View file

@ -0,0 +1,94 @@
const crypto = require("crypto");
const bcrypt = require("bcrypt");
const databaseHandler = require("../lib/database-handler");
const input_validate = require("../lib/input_validate");
module.exports = {
route: (routeObj) => {
routeObj.get((req, res) => {
if (req.cookies && req.cookies.auth !== undefined) {
const cookie_db = databaseHandler.checkAuthCookie(req.cookies.auth);
if (cookie_db && !cookie_db.revoked && cookie_db.created_at + 86400 > Math.floor(Date.now() / 1000)) {
const new_cookie_value = crypto.randomBytes(32).toString("base64");
databaseHandler.revokeAuthCookie(req.cookies.auth);
databaseHandler.createAuthCookie(new_cookie_value, Math.floor(Date.now() / 1000), cookie_db.user_id);
// TODO: Set correct redirect status.
res.status(302);
res.cookie("auth", new_cookie_value, {maxAge: 86400000, httpOnly: true});
if (req.query && req.query.redirect_to) {
// TODO: This should be validated so as to not go off-site without warning the user.
res.redirect(req.query.redirect_to);
} else {
res.redirect("/");
}
res.end();
return;
}
}
res.status(200);
res.send(`<html><body><form action="/login" method="POST"><label for="username" >Username: </label><input type="text" id="username" name="username" /><br /><label for="password" >Password: </label><input type="password" id="password" name="password" /><br />${req.query.redirect_to ? `<input type="hidden" id="redirect_to" name="redirect_to" value="${req.query.redirect_to}" />` : ""}<input type="submit" /></form></body></html>`);
res.end();
});
routeObj.post((req, res) => {
if (req.cookies && req.cookies.auth) {
const cookie_db = databaseHandler.checkAuthCookie(req.cookies.auth);
if (cookie_db && !cookie_db.revoked && cookie_db.created_at + 86400 > Math.floor(Date.now() / 1000)) {
const new_cookie_value = crypto.randomBytes(32).toString("base64");
databaseHandler.revokeAuthCookie(req.cookies.auth);
databaseHandler.createAuthCookie(new_cookie_value, Math.floor(Date.now() / 1000), cookie_db.user_id);
// TODO: Set correct redirect status.
res.status(302);
res.cookie("auth", new_cookie_value, {maxAge: 86400000, httpOnly: true});
if (req.body && req.body.redirect_to) {
// TODO: External site warning.
res.redirect(req.body.redirect_to);
} else {
res.redirect("/");
}
res.end();
return;
}
}
const validation_result = input_validate.validate_exists(req.body, ["username", "password"]);
if (validation_result !== true) {
res.status(422);
res.json(validation_result);
return;
}
const user_obj = databaseHandler.getAccountByUsername(req.body.username);
if (!user_obj) {
res.status(401);
// TODO: redirect_url
res.send("<html><body>AUTH FAILURE<br /><form action=\"/login\" method=\"POST\"><label for=\"username\" >Username: </label><input type=\"text\" id=\"username\" name=\"username\" /><br /><label for=\"password\" >Password: </label><input type=\"password\" id=\"password\" name=\"password\" /><br /><input type=\"submit\" /></form></body></html>");
res.end();
return;
}
if (bcrypt.compareSync(req.body.password, user_obj.password_hash)) {
const new_cookie_value = crypto.randomBytes(32).toString("base64");
databaseHandler.createAuthCookie(new_cookie_value, Math.floor(Date.now() / 1000), user_obj.id);
// TODO: Set correct redirect status.
res.status(302);
res.cookie("auth", new_cookie_value, {maxAge: 86400000, httpOnly: true});
if (req.body && req.body.redirect_to) {
// TODO: External site warning.
res.redirect(req.body.redirect_to);
} else {
res.redirect("/");
}
res.end();
return;
}
res.status(401);
// TODO: redirect_url
res.send("<html><body>AUTH FAILURE<br /><form action=\"/login\" method=\"POST\"><label for=\"username\" >Username: </label><input type=\"text\" id=\"username\" name=\"username\" /><br /><label for=\"password\" >Password: </label><input type=\"password\" id=\"password\" name=\"password\" /><br /><input type=\"submit\" /></form></body></html>");
res.end();
return;
});
},
};

27
routes/nodeinfo/2.0.js Normal file
View file

@ -0,0 +1,27 @@
module.exports = {
route: (routeObj) => {
routeObj.get((req, res) => {
res.status(200);
res.json({
version: 2.0,
software: {
name: "Brainz Social",
version: "0.0.1",
},
protocols: [],
services: [],
openRegistrations: true,
usage: {
users: {
total: 0,
activeHalfYear: 0,
activeMonth: 0,
},
localPosts: 0,
localComments: 0,
},
metadata: {},
});
});
},
};

83
routes/oauth/authorize.js Normal file
View file

@ -0,0 +1,83 @@
const crypto = require("crypto");
const qs = require("qs");
const databaseHandler = require("../../lib/database-handler");
const input_validate = require("../../lib/input_validate");
module.exports = {
route: (routeObj) => {
routeObj.get((req, res) => {
const validation_result = input_validate.validate_exists(req.query, ["response_type", "client_id", "redirect_uri"]);
if (validation_result !== true) {
res.status(422);
res.send(`<html><body>Someone done goof'ed with your oauth request.<br />${validation_result.error}</body></html>`);
res.end();
return;
}
if (!req.cookies || !req.cookies.auth) {
const new_redirect_url = new URL("https://example.com");
new_redirect_url.host = req.headers.host;
new_redirect_url.pathname = req.path;
new_redirect_url.search = qs.stringify(req.query);
const redirecting_to = new URL("https://example.com");
redirecting_to.host = req.headers.host;
redirecting_to.pathname = "/login";
redirecting_to.searchParams.set("redirect_to", new_redirect_url.toString());
res.status(302);
res.redirect(redirecting_to.toString());
res.end();
} else {
const cookie_db = databaseHandler.checkAuthCookie(req.cookies.auth);
if (cookie_db != null && !cookie_db.revoked && (cookie_db.created_at + 86400) > Math.floor(Date.now() / 1000)) {
const new_cookie_value = crypto.randomBytes(32).toString("base64");
databaseHandler.revokeAuthCookie(req.cookies.auth);
databaseHandler.createAuthCookie(new_cookie_value, Math.floor(Date.now() / 1000), cookie_db.user_id);
const application_obj = databaseHandler.application[req.query.client_id];
let scope = "read";
if (req.query.scope) {
scope = req.query.scope;
}
if (!input_validate.validate_appropriate_scopes(application_obj.scopes, scope)) {
res.status(422);
res.cookie("auth", new_cookie_value);
// TODO: XSS
res.send(`<html><body>Error: invalid scopes requested. Contact the author of ${application_obj.client_name} to correct their scope request.</body></html>`);
res.end();
return;
}
res.status(200);
res.cookie("auth", new_cookie_value);
const approve_url = new URL("https://example.com");
approve_url.host = req.headers.host;
approve_url.pathname = "/oauth/authorize/yes";
approve_url.searchParams.set("response_type", req.query.response_type);
approve_url.searchParams.set("client_id", req.query.client_id);
approve_url.searchParams.set("redirect_uri", req.query.redirect_uri);
approve_url.searchParams.set("scope", scope);
const approve_csrf = crypto.randomBytes(32).toString("base64");
approve_url.searchParams.set("csrf_token", approve_csrf);
const deny_url = new URL(approve_url);
deny_url.pathname = "/oauth/authorize/no";
const deny_csrf = crypto.randomBytes(32).toString("base64");
deny_url.searchParams.set("csrf_token", deny_csrf);
const approve_csrf_id = databaseHandler.createCsrfToken(approve_url.toString(), Math.floor(Date.now() / 1000), new_cookie_value);
const deny_csrf_id = databaseHandler.createCsrfToken(deny_url.toString(), Math.floor(Date.now() / 1000), new_cookie_value);
databaseHandler.createCsrfTokenAssociation(approve_csrf_id, deny_csrf_id);
res.send(`<html><body>The ${application_obj.client_name} application has requested the following scopes:<br /><ul><li>${scope.split(/(\s|\+)+/).join("</li><li>")}</li></ul><br /><a href="${approve_url.toString()}" >Approve</a><br /><a href="${deny_url.toString()}" >Deny</a></body></html>`);
res.end();
return;
}
const new_redirect_url = new URL("https://example.com");
new_redirect_url.host = req.headers.host;
new_redirect_url.pathname = req.path;
new_redirect_url.search = qs.stringify(req.query);
const redirecting_to = new URL("https://example.com");
redirecting_to.host = req.headers.host;
redirecting_to.pathname = "/login";
redirecting_to.searchParams.set("redirect_to", new_redirect_url.toString());
res.status(302);
res.redirect(redirecting_to.toString());
res.end();
}
});
},
};

View file

@ -0,0 +1,57 @@
const crypto = require("crypto");
const qs = require("qs");
const databaseHandler = require("../../../lib/database-handler");
module.exports = {
route: (routeObj) => {
routeObj.get((req, res) => {
const request_url = new URL("https://example.com");
request_url.host = req.headers.host;
request_url.pathname = "/oauth/authorize/yes";
request_url.search = new URLSearchParams(qs.stringify(req.query));
const cookie_object = databaseHandler.checkAuthCookie(req.cookies.auth);
if (cookie_object.revoked || (cookie_object.created_at + 86400) < Math.floor(Date.now() / 1000)) {
res.status(302);
const redirect_to = new URL("https://example.com");
redirect_to.host = req.headers.host;
redirect_to.pathname = "/login";
redirect_to.searchParams.set("redirect_to", request_url.toString());
res.redirect(redirect_to.toString());
return;
}
databaseHandler.revokeAuthCookie(cookie_object.cookie_value);
const new_cookie = crypto.randomBytes(32).toString("base64");
databaseHandler.createAuthCookie(new_cookie, Math.floor(Date.now() / 1000), cookie_object.user_id);
const csrf_token = databaseHandler.getCsrfToken(request_url.toString());
if (csrf_token.cookie_value === req.cookies.auth && (Math.floor(Date.now() / 1000) < (csrf_token.created_at + 300))) {
const response_code = crypto.randomBytes(32).toString("base64");
const application_object = databaseHandler.application[req.query.client_id];
databaseHandler.createOauthCode(response_code, application_object.id, cookie_object.user_id, req.query.scopes, Math.floor(Date.now() / 1000));
if (req.query.redirect_uri === "urn:ietf:wg:oauth:2.0:oob") {
res.status(200);
res.cookie("auth", new_cookie);
res.send(`<html><body>Your code: &lt; ${response_code} &gt;. Valid for 300 seconds.</body></html>`);
res.end();
return;
}
// TODO: Proper redirect code.
res.status(302);
const redirect_to = new URL(req.query.redirect_uri);
redirect_to.searchParams.set("code", response_code);
res.cookie("auth", new_cookie);
res.redirect(redirect_to.toString());
res.end();
return;
}
const redirect_to = new URL("http://example.com");
redirect_to.host = req.headers.host;
redirect_to.pathname = "/oauth/authorize";
redirect_to.search = qs.stringify(req.query);
res.status(302);
res.redirect(redirect_to.toString());
res.end();
return;
});
},
};

34
routes/oauth/revoke.js Normal file
View file

@ -0,0 +1,34 @@
const databaseHandler = require("../../lib/database-handler");
const input_validate = require("../../lib/input_validate");
module.exports = {
route: (routeObj) => {
routeObj.post((req, res) => {
const validation_result = input_validate(req.body, ["client_id", "client_secret", "token"]);
if (validation_result !== true) {
res.status(422);
res.json(validation_result);
return;
}
const tokenData = databaseHandler.getTokenData(req.body.token);
const application = databaseHandler.application[req.body.client_id];
if (application.client_secret !== req.body.client_secret) {
res.status(401);
res.json({error: "unauthorized_client", error_description: "You are not authorized to revoke this token."});
return;
}
if (tokenData.application_id !== application.id) {
res.status(401);
res.json({error: "unauthorized_client", error_description: "You are not authorized to revoke this token."});
return;
}
databaseHandler.revokeToken(req.body.token);
res.status(200);
res.json({});
});
},
};

99
routes/oauth/token.js Normal file
View file

@ -0,0 +1,99 @@
const crypto = require("crypto");
const databaseHandler = require("../../lib/database-handler");
const input_validate = require("../../lib/input_validate");
module.exports = {
route: (routeObj) => {
routeObj.post((req, res) => {
let scope;
const validation_result = input_validate.validate_exists(req.body, ["grant_type", "client_id", "client_secret", "redirect_uri"]);
if (validation_result !== true) {
res.status(422);
res.json(validation_result);
return;
}
const grant_type = req.body.grant_type;
const client_id = req.body.client_id;
const client_secret = req.body.client_secret;
const redirect_uri = req.body.redirect_uri;
// TODO: Validate redirect_uri for application.
scope = "read";
if (req.body.scope != undefined) {
scope = req.body.scope.replace(/\+/, " ");
}
const application = databaseHandler.application[client_id];
if (!application.client_id) {
res.status(422);
res.json({error: "UNREGISTERED_APPLICATION"});
res.end();
return;
}
let application_scope = "read";
if (application.scopes != undefined) {
application_scope = application.scopes;
}
if (client_secret !== application.client_secret) {
res.status(401);
res.json({error: "invalid_client", error_description: "client_id not known or does not match client_secret."});
return;
}
if (!input_validate.validate_appropriate_scopes(application_scope, scope)) {
res.status(422);
res.json({error: "Requested scopes in transaction application did not previously declare."});
return;
}
if (grant_type === "authorization_code") {
const code = databaseHandler.getOauthCode(req.body.code);
databaseHandler.revokeOauthCode(req.query.code);
if (code.revoked) {
res.status(401);
res.json({error: "used_code", error_description: "Oauth codes are single use."});
return;
}
if ((code.created_at + 60) < Math.floor(Date.now() / 1000)) {
res.status(401);
res.json({error: "expired_code", error_description: "Oauth codes must be used within 60 seconds of issuance."});
return;
}
const token = crypto.randomBytes(32).toString("base64");
const created_at = Math.floor(Date.now() / 1000);
databaseHandler.createToken(token, scope, application.id, code.user_id, created_at);
res.status(200);
res.json({
access_token: token,
token_type: "Bearer",
scope: req.body.scope,
created_at,
});
return;
} if (grant_type !== "client_credentials") {
res.status(422);
res.json({error: "Validation failed, unrecognized grant_type"});
return;
}
const token = crypto.randomBytes(32).toString("base64");
const created_at = Math.floor(Date.now() / 1000);
databaseHandler.createToken(token, scope, application.id, 0, created_at);
res.status(200);
res.json({
access_token: token,
token_type: "Bearer",
scope,
created_at,
});
});
},
};

View file

@ -0,0 +1,13 @@
const databaseHandler = require("../../../../lib/database-handler");
module.exports = {
route: (routeObj) => {
routeObj.get((req, res) => {
res.header("content-type", "application/activity+json");
console.log(`Attempting object https://${req.header.host}/users/${req.params.username}/statuses/${req.params.statusId}`);
console.log(databaseHandler.activity[`https://${req.header.host}/users/${req.params.username}/statuses/${req.params.statusId}`]);
res.send(databaseHandler.activity[`https://${req.header.host}/users/${req.params.username}/statuses/${req.params.statusId}`].object);
res.end();
});
},
};