๐Ÿฆ‹๐Ÿ”‘ PAM provider for ATProto
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

so far...

since someone else is working on something similar, though i'd slap this mess up finally to compare notes

Ducky d76e501a

+214
+8
deno.json
··· 1 + { 2 + "tasks": { 3 + "dev": "deno run --watch src/main.ts" 4 + }, 5 + "imports": { 6 + "@std/assert": "jsr:@std/assert@1" 7 + } 8 + }
+36
native/src/nss_atpam.c
··· 1 + 2 + /* libnss_atpam โ€“ synthesise passwd entries for at(plc|web)-* names */ 3 + #define _GNU_SOURCE 4 + #include <errno.h> 5 + #include <nss.h> 6 + #include <pwd.h> 7 + #include <string.h> 8 + #include <stdlib.h> 9 + #include <stdio.h> 10 + #include <sys/types.h> 11 + 12 + static uid_t uid_for(const char *name) 13 + { 14 + return 2000 + (name[7]*31 + name[8]) % 30000; 15 + } 16 + 17 + enum nss_status _nss_atpam_getpwnam_r(const char *name, struct passwd *pw, 18 + char *buf, size_t buflen, int *err){ 19 + if (strncmp(name,"didplc-",6) && strncmp(name,"didweb-",6)) 20 + return NSS_STATUS_NOTFOUND; 21 + 22 + pw->pw_name = (char*)name; 23 + //pw->pw_uid = uid_for(name); 24 + pw->pw_uid = 60051; 25 + pw->pw_gid = 60050; /* must exist */ 26 + pw->pw_gecos= (char*)"atpam user"; 27 + pw->pw_dir = buf; 28 + pw->pw_shell= (char*)"/bin/bash"; 29 + int needed = snprintf(buf,buflen,"/home/%s",name); 30 + if ((size_t)needed>=buflen){ *err=ERANGE; return NSS_STATUS_TRYAGAIN;} 31 + return NSS_STATUS_SUCCESS; 32 + } 33 + enum nss_status _nss_atpam_getpwuid_r(uid_t uid, struct passwd *pw, 34 + char *buf, size_t buflen, int *err){ 35 + return NSS_STATUS_NOTFOUND; 36 + }
+68
native/src/pam_atpam.c
··· 1 + /* pam_atpam.c โ€“ talk to the atpam Unix-socket daemon (DEBUG build) */ 2 + #define PAM_SM_AUTH 3 + #include <security/pam_modules.h> 4 + #include <security/pam_ext.h> 5 + #include <stdio.h> 6 + #include <string.h> 7 + #include <unistd.h> 8 + #include <sys/socket.h> 9 + #include <sys/un.h> 10 + 11 + #define SOCK "/run/atpam.sock" 12 + 13 + PAM_EXTERN int 14 + pam_sm_authenticate(pam_handle_t *pamh, int flags, 15 + int argc, const char **argv) 16 + { 17 + const char *user = NULL, *pass = NULL; 18 + pam_get_user(pamh, &user, NULL); 19 + pam_get_authtok(pamh, PAM_AUTHTOK, &pass, NULL); 20 + 21 + fprintf(stderr, "DEBUG pam_atpam: user=[%s] pass-len=%zu\n", 22 + user ? user : "(null)", pass ? strlen(pass) : 0); 23 + 24 + const char *field, *value; 25 + if (strchr(user, ':')) { 26 + field = "did"; value = user; 27 + } else if (strncmp(user, "didplc-", 7) == 0 || 28 + strncmp(user, "didweb-", 7) == 0) { 29 + field = "unix"; value = user; 30 + } else { 31 + field = "handle"; value = user; 32 + } 33 + fprintf(stderr, "DEBUG pam_atpam: chosen field=[%s] value=[%s]\n", field, value); 34 + 35 + char json[512]; 36 + int n = snprintf(json, sizeof(json), 37 + "{\"%s\":\"%s\",\"password\":\"%s\"}", 38 + field, value, pass ? pass : ""); 39 + fprintf(stderr, "DEBUG pam_atpam: sending %.*s\n", n, json); 40 + 41 + int fd = socket(AF_UNIX, SOCK_STREAM, 0); 42 + if (fd < 0) { perror("socket"); return PAM_AUTH_ERR; } 43 + 44 + struct sockaddr_un addr = { .sun_family = AF_UNIX }; 45 + strncpy(addr.sun_path, SOCK, sizeof(addr.sun_path) - 1); 46 + 47 + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { 48 + perror("connect"); 49 + close(fd); 50 + return PAM_AUTH_ERR; 51 + } 52 + fprintf(stderr, "DEBUG pam_atpam: connected to %s\n", SOCK); 53 + 54 + if (write(fd, json, n) != n) { perror("write"); close(fd); return PAM_AUTH_ERR; } 55 + 56 + n = read(fd, json, sizeof(json) - 1); 57 + close(fd); 58 + if (n <= 0) { perror("read"); return PAM_AUTH_ERR; } 59 + json[n] = '\0'; 60 + 61 + fprintf(stderr, "DEBUG pam_atpam: daemon replied [%s]\n", json); 62 + return strstr(json, "\"ok\":true") ? PAM_SUCCESS : PAM_AUTH_ERR; 63 + } 64 + 65 + PAM_EXTERN int 66 + pam_sm_setcred(pam_handle_t *pamh, int flags, 67 + int argc, const char **argv) 68 + { return PAM_SUCCESS; }
+102
src/main.ts
··· 1 + import { AtpAgent } from "https://esm.sh/@atproto/api@0.14?target=deno"; 2 + 3 + const SOCK = "/run/atpam.sock"; 4 + const PID = "/run/atpam.pid"; 5 + const DIDMAP = "/etc/atpam/dids.json"; 6 + const AT_ENDPOINT = "https://zio.blue"; // NOTE: hardcoded because lazy rn 7 + 8 + function exists(path: string): boolean { 9 + try { Deno.statSync(path); return true; } 10 + catch (e: any) { 11 + if (e instanceof Deno.errors.NotFound) return false; 12 + throw e; 13 + } 14 + } 15 + const loadMap = () => exists(DIDMAP) ? JSON.parse(Deno.readTextFileSync(DIDMAP)) : {}; 16 + const saveMap = (m: Record<string,string>) => 17 + Deno.writeTextFileSync(DIDMAP, JSON.stringify(m, null, 2)); 18 + 19 + const didToUnix = (did: string) => 20 + did.replace(/^did:/, "").replace(/:/g, "-").replace(/\./g, "-"); 21 + 22 + async function resolveCurrentHandle(did: string): Promise<string> { 23 + const [meth, id] = did.replace(/^did:/, "").split(":"); 24 + if (meth === "plc") { 25 + const doc = await fetch(`https://plc.directory/${did}`).then(r => r.json()); 26 + const aka = doc.alsoKnownAs?.find((s: string) => s.startsWith("at://")); 27 + return aka ? aka.slice(5) : "handle.invalid"; 28 + } else if (meth === "web") { 29 + const doc = await fetch(`https://${id}/.well-known/did.json`).then(r => r.json()); 30 + const aka = doc.alsoKnownAs?.find((s: string) => s.startsWith("at://")); 31 + return aka ? aka.slice(5) : "handle.invalid"; 32 + } 33 + return "handle.invalid"; 34 + } 35 + 36 + async function authenticate( 37 + cred: {handle?:string; did?:string; unix?:string; password:string} 38 + ): Promise<{ok:boolean; unixUser?:string; error?:string}> { 39 + const map = loadMap(); 40 + let did: string; 41 + 42 + if (cred.did) { 43 + did = cred.did; 44 + } else if (cred.unix) { 45 + const expand = (u: string): string | null => { 46 + if (u.startsWith("didplc-")) return "did:plc:" + u.slice(7); 47 + if (u.startsWith("didweb-")) return "did:web:" + u.slice(7).replace(/-/g, "."); 48 + return null; 49 + }; 50 + const exp = expand(cred.unix); 51 + if (!exp) return {ok:false, error:"Bad unix name"}; 52 + did = exp; 53 + } else if (cred.handle) { 54 + let cached = map[cred.handle]; 55 + if (!cached) { 56 + const u = new URL(`${AT_ENDPOINT}/xrpc/com.atproto.identity.resolveHandle`); 57 + u.searchParams.set("handle", cred.handle); 58 + const {did: resolved} = await fetch(u).then(r => r.json()); 59 + if (!resolved) return {ok:false, error:"Handle not found"}; 60 + cached = resolved; 61 + map[cred.handle] = cached; 62 + saveMap(map); 63 + } 64 + did = cached; 65 + } else { 66 + return {ok:false, error:"No identity provided"}; 67 + } 68 + 69 + const current = await resolveCurrentHandle(did); 70 + if (current === "handle.invalid") 71 + return {ok:false, error:"DID has no valid handle"}; 72 + 73 + const agent = new AtpAgent({service: AT_ENDPOINT}); 74 + //try { 75 + await agent.login({identifier:current, password:cred.password}); 76 + //} catch { 77 + // return {ok:false, error:"Auth failed"}; 78 + //} 79 + return {ok:true, unixUser: didToUnix(did)}; 80 + } 81 + 82 + /* ---------- socket server ---------- */ 83 + try { await Deno.remove(SOCK); } catch {} 84 + const listener = Deno.listen({ path: SOCK, transport: "unix" }); 85 + Deno.writeTextFileSync(PID, String(Deno.pid)); 86 + console.log(`atpam listening on ${SOCK} (PID ${Deno.pid})`); 87 + 88 + for await (const conn of listener) { 89 + (async () => { 90 + const buf = new Uint8Array(4096); 91 + const n = await conn.read(buf); 92 + if (!n) return conn.close(); 93 + //try { 94 + const cred = JSON.parse(new TextDecoder().decode(buf.slice(0, n))); 95 + const res = await authenticate(cred); 96 + await conn.write(new TextEncoder().encode(JSON.stringify(res))); 97 + //} catch { 98 + // await conn.write(new TextEncoder().encode(JSON.stringify({ok:false}))); 99 + //} 100 + conn.close(); 101 + })(); 102 + }