Skip to content

Commit

Permalink
feat: add username claim and delete logic to redis and update DNS ser…
Browse files Browse the repository at this point in the history
…ver with it.
  • Loading branch information
zicklag committed Nov 8, 2024
1 parent 6d7066d commit ac3231d
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 8 deletions.
1 change: 1 addition & 0 deletions .typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ extend-exclude = ["src/lib/pow/wasm/*"]

[default.extend-words]
ratatui = "ratatui"
additionals = "additionals"
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@types/cookie": "^0.6.0",
"@types/dns-packet": "^5.6.5",
"@types/eslint": "^8.56.12",
"@types/node": "^20.16.11",
"@types/nodemailer": "^6.4.16",
Expand All @@ -38,7 +39,9 @@
"codemirror": "^6.0.1",
"cookie": "^0.6.0",
"date-fns": "^3.6.0",
"dinodns": "^0.0.9",
"discord.js": "^14.16.3",
"dns-packet": "^5.6.1",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.44.1",
Expand All @@ -63,6 +66,7 @@
"prosemirror-state": "^1.4.3",
"prosemirror-view": "^1.34.3",
"proxy-agent": "^6.4.0",
"redis": "^4.7.0",
"sanitize-html": "^2.13.1",
"slugify": "^1.6.6",
"svelte": "5.0.0-next.239",
Expand All @@ -75,11 +79,7 @@
"vite-plugin-top-level-await": "^1.4.4",
"vite-plugin-wasm": "^3.3.0",
"ws": "^8.18.0",
"zod": "^3.23.8",
"@types/dns-packet": "^5.6.5",
"dinodns": "^0.0.9",
"dns-packet": "^5.6.1",
"redis": "^4.7.0"
"zod": "^3.23.8"
},
"dependencies": {
"@atproto/api": "^0.13.12",
Expand Down
10 changes: 8 additions & 2 deletions src/lib/dns/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { z } from 'zod';
import { RCode } from 'dinodns/common/core/utils';
import { redis } from '$lib/redis';

const REDIS_USER_PREFIX = 'weird:users:';
const REDIS_USER_PREFIX = 'weird:users:names:';
const REDIS_DNS_RECORD_PREFIX = 'weird:dns:records:';

const redisDnsRecordSchema = z.array(
Expand Down Expand Up @@ -100,6 +100,12 @@ export async function startDnsServer() {
]
});

// TODO: handle AXFR requests so we can setup a secondary DNS server.

// TODO: add glue records for nameservers ( I think ):
//
// https://serverfault.com/questions/309622/what-is-a-glue-record

// Set all answers to authoritative by default
s.use(async (_req, res, next) => {
if (res.finished) return next();
Expand Down Expand Up @@ -312,7 +318,7 @@ export async function startDnsServer() {
case 'TXT':
const txtUsername = name.match(WEIRD_HOST_TXT_RECORD_REGEX)?.[1];
if (!txtUsername) return returnAnswers(null);
const pubkey = await redis.get(REDIS_USER_PREFIX + txtUsername);
const pubkey = await redis.hGet(REDIS_USER_PREFIX + txtUsername, 'subspace');
if (!pubkey) return returnAnswers(null);
returnAnswers([
{
Expand Down
43 changes: 42 additions & 1 deletion src/lib/redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,47 @@ import { env } from '$env/dynamic/private';
import { createClient } from 'redis';

/** The global redis client used by the Weird server. */
export const redis = await createClient({ url: env.REDIS_URL })
export const redis = await createClient({
url: env.REDIS_URL
// EXPERIMENTAL: Currently using transactions, aka MULTI, instead.
// scripts: {
// /** This is meant to claim a new weird username and associate 3 things to each-other:
// * rauthy user ID, iroh namespace pubkey, the actual username */
// claimUsername: defineScript({
// SCRIPT: `
// local usernameKey = KEYS[1]
// local rauthyIdKey = KEYS[2]
// local subspaceKey = KEYS[3]
// local username = ARGV[1]
// local rauthyId = ARGV[2]
// local subspace = ARGV[3]

// if redis.call("EXISTS", usernameKey) > 0 then
// return redis.error_reply(string.format("USER_EXISTS The username '%s' already exists so it cannot be claimed.", username))
// end

// redis.call("HSET", usernameKey, "subspace", subspace)
// redis.call("HSET", usernameKey, "rauthyId", rauthyId)
// redis.call("HSET", rauthyIdKey, "username", username)
// redis.call("HSET", rauthyIdKey, "subspace", subspace)
// redis.call("HSET", subspaceKey, "username", username)
// redis.call("HSET", subspaceKey, "rauthyId", rauthyId)
// redis.call("HSET", "testing", "dummy", "something")
// `,
// NUMBER_OF_KEYS: 3,
// FIRST_KEY_INDEX: 1,
// transformArguments(username: string, rauthyId: string, subspace: string) {
// return [
// 'weird:users:names:' + username,
// 'weird:users:rauthyIds:' + rauthyId,
// 'weird:users:subspaces:' + subspace,
// username,
// rauthyId,
// subspace
// ];
// }
// })
// }
})
.on('error', (err) => console.error('Redis client error', err))
.connect();
68 changes: 68 additions & 0 deletions src/lib/usernames.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { redis } from '$lib/redis';
import { validUsernameRegex } from './utils/username';

const USER_NAMES_PREFIX = 'weird:users:names:';
const USER_RAUTHY_IDS_PREFIX = 'weird:users:rauthyIds:';
const USER_SUBSPACES_PREFIX = 'weird:users:subspaces:';

export async function claimUsername(username: string, rauthyId: string, subspace: string) {
if (!username.match(validUsernameRegex)) {
throw 'Username does not pass valid username check.';
}

const usernameKey = USER_NAMES_PREFIX + username;
const rauthyIdKey = USER_RAUTHY_IDS_PREFIX + rauthyId;
const subspaceKey = USER_SUBSPACES_PREFIX + subspace;

const TRIES = 3;
const failures = 0;
while (failures <= TRIES) {
redis.watch([usernameKey, rauthyIdKey, subspaceKey]);

if (await redis.exists(usernameKey)) {
await redis.unwatch();
throw `Cannot claim username "${username}": username already claimed.`;
}

const multi = redis.multi();

multi.del(usernameKey);
multi.hSet(usernameKey, 'subspace', subspace);
multi.hSet(usernameKey, 'rauthyId', rauthyId);
multi.del(rauthyIdKey);
multi.hSet(rauthyIdKey, 'username', username);
multi.hSet(rauthyIdKey, 'subspace', subspace);
multi.del(subspaceKey);
multi.hSet(subspaceKey, 'username', username);
multi.hSet(subspaceKey, 'rauthyId', rauthyId);

try {
await multi.exec();
return;
} catch (e) {
console.warn(
`Initial attempt to claim username ${username} failed will try ${TRIES - failures} more times: ${e}`
);
}
}
}

export async function deleteUsername(username: string) {
const usernameKey = USER_NAMES_PREFIX + username;

await redis.watch(usernameKey);

const user = await redis.hGetAll(usernameKey);

const multi = redis.multi();

await multi.del(usernameKey);
if (user.subspace) {
await multi.del(USER_SUBSPACES_PREFIX + user.subspace);
}
if (user.rauthyId) {
await multi.del(USER_RAUTHY_IDS_PREFIX + user.rauthyId);
}

await multi.exec();
}
2 changes: 2 additions & 0 deletions src/lib/utils/username.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { env } from '$env/dynamic/public';

export const validUsernameRegex = /^([a-z0-9][_-]?){3,32}$/;

export interface Username {
name: string;
domain?: string;
Expand Down
1 change: 1 addition & 0 deletions src/routes/(internal)/__internal__/admin/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
<li><a href="/__internal__/admin/explorer">Data Explorer</a></li>
<li><a href="/__internal__/admin/dns/resolve">DNS Resolver</a></li>
<li><a href="/__internal__/admin/dns/set">DNS Record Set</a></li>
<li><a href="/__internal__/admin/usernames">Username Manager</a></li>
</ul>
33 changes: 33 additions & 0 deletions src/routes/(internal)/__internal__/admin/usernames/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { claimUsername, deleteUsername } from '$lib/usernames';
import type { Actions } from './$types';

export const actions = {
claimUsername: async ({ request }) => {
const formData = await request.formData();
const username = formData.get('username')?.toString();
const rauthyId = formData.get('rauthyId')?.toString();
const subspace = formData.get('subspace')?.toString();
if (!(username && rauthyId && subspace)) return { error: 'You must fill in all fields' };

try {
await claimUsername(username, rauthyId, subspace);
} catch (e) {
return { error: `Error claiming username: ${e}` };
}

return { success: `Username ${username} claimed.` };
},
deleteUsername: async ({ request }) => {
const formData = await request.formData();
const username = formData.get('username')?.toString();
if (!username) return { error: 'You must fill in all fields' };

try {
await deleteUsername(username);
} catch (e) {
return { error: `Error deleting username: ${e}` };
}

return { success: `Username ${username} deleted.` };
}
} satisfies Actions;
37 changes: 37 additions & 0 deletions src/routes/(internal)/__internal__/admin/usernames/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script lang="ts">
import type { ActionData } from './$types';
const { form }: { form: ActionData } = $props();
</script>

{#if form?.error}
<article class="pico-background-red-550">
{form.error}
</article>
{:else if form?.success}
<article class="pico-background-green-550">
{form.success}
</article>
{/if}

<h2>Claim Username</h2>

<form method="post" action="?/claimUsername">
<!-- svelte-ignore a11y_no_redundant_roles -->
<fieldset role="group">
<input name="username" placeholder="username" />
<input name="rauthyId" placeholder="rauthyId" />
<input name="subspace" placeholder="subspace" />
<button>Claim</button>
</fieldset>
</form>

<h2>Delete Username</h2>

<form method="post" action="?/deleteUsername">
<!-- svelte-ignore a11y_no_redundant_roles -->
<fieldset role="group">
<input name="username" placeholder="username" />
<button>Delete</button>
</fieldset>
</form>

0 comments on commit ac3231d

Please sign in to comment.