diff --git a/.dockerignore b/.dockerignore index d144621..d51a1b8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,6 +8,9 @@ dist/ config.example.toml config.toml docker-compose.yml +docker-compose.dev.yml LICENSE README.md yarn-error.log +drawpile-config.ini +testkey.pem diff --git a/.gitignore b/.gitignore index b1725ce..3a16d9c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ node_modules/ yarn-error.log /config.toml +testkey.pem +drawpile-config.ini diff --git a/config.example.toml b/config.example.toml index e6e90d0..94638db 100644 --- a/config.example.toml +++ b/config.example.toml @@ -1,11 +1,17 @@ +# Base64-encoded Ed25519 private key +signingKey = "" + [ldap] url = "ldap://localhost:389" bindDN = "cn=admin,dc=example,dc=com" bindPW = "admin" userDN = "ou=users,dc=example,dc=com" userSearchFilter = "(uid=%u)" - -# Uncomment if you only want members of a specific group to be able to -# authenticate -#groupDN = "ou=groups,dc=example,dc=com" -#groupName = "drawpile" +# DN which contains your LDAP groups +groupDN = "ou=drawpile,ou=groups,dc=example,dc=com" +# The name of the group the user must belong to in order to join sessions at all +#requiredGroup = "drawpile" +groupObjectClass = "groupOfNames" +memberAttribute = "member" +# LDAP group representing your moderators; will set mod flag if the user is in this group +# modGroup = "moderator" diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index f4a0973..a62c470 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -12,6 +12,20 @@ services: networks: - default - ciderandsaddle + drawpile: + image: callaa/drawpile-srv:2.1 + volumes: + - drawpile:/home/drawpile + - ./drawpile-config.ini:/home/drawpile/drawpile-config.ini:ro + command: --sessions /home/drawpile/sessions --config /home/drawpile/drawpile-config.ini --extauth http://127.0.0.1:8081 + ports: + - 27750:27750 + networks: + - default + - ciderandsaddle + +volumes: + drawpile: networks: ciderandsaddle: diff --git a/src/index.ts b/src/index.ts index d9ee46a..92795bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import crypto from 'crypto'; +import crypto, { sign } from 'crypto'; import express from 'express'; import * as ldapts from 'ldapts'; import winston from 'winston'; @@ -6,21 +6,6 @@ import winston from 'winston'; require('source-map-support').install(); require('toml-require').install(); -const config = require('../config.toml') as ServerConfig; - -const log = winston.createLogger({ - level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', - format: winston.format.combine( - winston.format.timestamp(), - winston.format.printf(info => `[${info.timestamp}] ${info.level}: ${info.message}`), - ), - transports: [ - new winston.transports.Console(), - ], -}); - -const DEFUALT_PORT = 8081; - interface ServerConfig { port?: number; signingKey: string; @@ -30,9 +15,10 @@ interface ServerConfig { bindPW: string; userDN: string; userSearchFilter: string; - groupDN?: string; - groupName?: string; - memberOfAttribute: string; + groupDN: string; + groupObjectClass: string; + memberAttribute: string; + modGroup?: string; }; } @@ -58,25 +44,47 @@ interface LDAPUser { [key: string]: string | string[] | Buffer | Buffer[]; } +const config = require('../config.toml') as ServerConfig; +const DEFUALT_PORT = 8081; +const signingKey = crypto.createPrivateKey({ + key: Buffer.from(config.signingKey, 'base64'), + format: 'der', + type: 'pkcs8', +}); + +const log = winston.createLogger({ + level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.printf(info => `[${info.timestamp}] ${info.level}: ${info.message}`), + ), + transports: [ + new winston.transports.Console(), + ], +}); + + const ldapClient = new ldapts.Client(config.ldap); const app = express(); app.use(express.json()); +const btoa = (data: string) => Buffer.from(data, 'utf8').toString('base64'); + function createDrawpileAuthToken(payload: DrawpileAuthPayload, avatar?: string | Buffer) { const payloadJSON = JSON.stringify(payload); - const signingKey = `-----BEGIN PRIVATE KEY----- -${config.signingKey} ------END PRIVATE KEY-----`; - const signature = crypto.sign(null, Buffer.from(payloadJSON, 'utf8'), signingKey).toString('base64'); // version.payload.avatar?.signature const components = [ avatar ? '2': '1', - Buffer.from(payloadJSON).toString('base64'), - signature + btoa(payloadJSON), ]; - if (avatar) components.splice(2, 0, typeof avatar === 'string' ? avatar : avatar.toString('base64')); + + const signature = crypto.sign(null, Buffer.from(components.join('.'), 'utf8'), signingKey).toString('base64'); + + if (avatar) components.push(typeof avatar === 'string' ? avatar : avatar.toString('base64')); + components.push(signature); + return components.join('.'); } @@ -114,32 +122,49 @@ async function loginUser(username: string, password: string): Promise { +app.use((req, res, next) => { log.info('Request received from ' + req.ip); + log.debug('Request method: ' + req.method); + log.debug('Request URI: ' + req.url); + log.debug('Request body: ' + JSON.stringify(req.body)); + + next(); +}); +app.post('/', async (req, res) => { if (!req.body.username) return res.status(400).send('Bad request'); const authResponse: AuthResponse = { status: 'auth', - ingroup: config.ldap.groupName, + ingroup: req.body.group, }; - if (!req.body.password) { // auth server request + if (!req.body.password) { // server request + log.info(`Checking if username ${req.body.username} is available for guest access`); + if (!config.allowGuests) { + log.info(`Guest login disabled`); return res.json(authResponse); } const user = await findUser(req.body.username); if (user) { + log.info(`Username ${req.body.username} is taken by a registered user`); return res.json(authResponse); // auth required, username taken } + log.info(`Username ${req.body.username} is available`); authResponse.status = 'guest'; return res.json(authResponse); } // since password is set, this is a login request + // missing nonce, invalid request + if (!req.body.nonce) return res.status(400).send('Bad request'); + + log.info(`Attempting to authenticate user ${req.body.username}`); + let user: LDAPUser | null = null; try { @@ -163,14 +188,14 @@ app.get('/', async (req, res) => { log.info(`Username ${req.body.username} successfully authenticated`); - const authPayload: DrawpileAuthPayload = { + authResponse.token = createDrawpileAuthToken({ username: req.body.username, iat: Date.now(), uid: user.entryUUID, - nonce: crypto.randomBytes(8).toString('hex'), - }; - - authResponse.token = createDrawpileAuthToken(authPayload); + group: req.body.group, + nonce: req.body.nonce, + }); + log.debug('Auth response: ' + JSON.stringify(authResponse)); return res.json(authResponse); });