diff --git a/prosody-modules/mod_http_peertubelivechat_manage_users/mod_http_peertubelivechat_manage_users.lua b/prosody-modules/mod_http_peertubelivechat_manage_users/mod_http_peertubelivechat_manage_users.lua index 52cf7cbc..32ee4446 100644 --- a/prosody-modules/mod_http_peertubelivechat_manage_users/mod_http_peertubelivechat_manage_users.lua +++ b/prosody-modules/mod_http_peertubelivechat_manage_users/mod_http_peertubelivechat_manage_users.lua @@ -1,11 +1,14 @@ local json = require "util.json"; local jid_split = require"util.jid".split; local usermanager = require "core.usermanager"; +local st = require "util.stanza" module:depends"http"; local module_host = module:get_host(); -- this module is not global +local vcards = module:open_store("vcard"); + function check_auth(routes) local function check_request_auth(event) local apikey = module:get_option_string("peertubelivechat_manage_users_apikey", "") @@ -31,6 +34,26 @@ function check_auth(routes) end +local function update_vcard(username, avatar) + if not avatar then + module:log("debug", "No avatar for user %s, deleting vcard", username); + vcards:set(username, nil) + return + end + + module:log("debug", "There is a avatar for user %s, storing the relevant vcard.", username); + local vcard_temp = st.stanza("vCard", { xmlns = "vcard-temp" }); + vcard_temp:tag("PHOTO"); + vcard_temp:text_tag("TYPE", avatar.mimetype); + -- avatar.base64 is already base64 encoded + vcard_temp:text_tag("BINVAL", avatar.base64); + vcard_temp:up(); + if not vcards:set(username, st.preserialize(vcard_temp)) then + module:log("error", "Failed to store the vcard for user %s", username); + end +end + + local function ensure_user(event) local request, response = event.request, event.response; event.response.headers["Content-Type"] = "application/json"; @@ -42,29 +65,30 @@ local function ensure_user(event) }); end - module:log("debug", "Calling ensure_user", config.jid); + module:log("debug", "Calling ensure_user for %s", config.jid); local username, host = jid_split(config.jid); if module_host ~= host then - module:log("error", "Wrong host", host); + module:log("error", "Wrong host %s", host); return json.encode({ result = "failed"; message = "Wrong host" }); end - -- TODO: handle avatars. - -- if user exists, just update. if usermanager.user_exists(username, host) then - module:log("debug", "User already exists, updating", filename); + module:log("debug", "User already exists, updating"); if not usermanager.set_password(username, config.password, host, nil) then - module:log("error", "Failed to update the password", host); + module:log("error", "Failed to update the password"); return json.encode({ result = "failed"; message = "Failed to update the password" }); end + + update_vcard(username, config.avatar); + return json.encode({ result = "ok"; message = "User updated" @@ -72,14 +96,17 @@ local function ensure_user(event) end -- we must create the user. - module:log("debug", "User does not exists, creating", filename); + module:log("debug", "User does not exists, creating"); if (not usermanager.create_user(username, config.password, host)) then - module:log("error", "Failed to create the user", host); + module:log("error", "Failed to create the user"); return json.encode({ result = "failed"; message = "Failed to create the user" }); end + + update_vcard(username, config.avatar); + return json.encode({ result = "ok"; message = "User created" diff --git a/server/lib/external-auth/oidc.ts b/server/lib/external-auth/oidc.ts index 84ff5fa8..ec2273e6 100644 --- a/server/lib/external-auth/oidc.ts +++ b/server/lib/external-auth/oidc.ts @@ -1,6 +1,6 @@ import type { RegisterServerOptions } from '@peertube/peertube-types' import type { Request, Response, CookieOptions } from 'express' -import type { ExternalAccountInfos } from './types' +import type { ExternalAccountInfos, AcceptableAvatarMimeType } from './types' import { ExternalAuthenticationError } from './error' import { getBaseRouterRoute } from '../helpers' import { canonicalizePluginUri } from '../uri/canonicalize' @@ -10,7 +10,38 @@ import { Issuer, BaseClient, generators, UnknownObject } from 'openid-client' import { JID } from '@xmpp/jid' import { URL } from 'url' -type UserInfoField = 'username' | 'last_name' | 'first_name' | 'nickname' +const got = require('got') + +function getMimeTypeFromArrayBuffer (arrayBuffer: ArrayBuffer): AcceptableAvatarMimeType | null { + const uint8arr = new Uint8Array(arrayBuffer) + + const len = 4 + if (uint8arr.length >= len) { + const signatureArr = new Array(len) + for (let i = 0; i < len; i++) { + signatureArr[i] = (new Uint8Array(arrayBuffer))[i].toString(16) + } + const signature = signatureArr.join('').toUpperCase() + + switch (signature) { + case '89504E47': + return 'image/png' + case '47494638': + return 'image/gif' + case 'FFD8FFDB': + case 'FFD8FFE0': + return 'image/jpeg' + case '52494646': + case '57454250': + return 'image/webp' + default: + return null + } + } + return null +} + +type UserInfoField = 'username' | 'last_name' | 'first_name' | 'nickname' | 'picture' interface UnserializedToken { jid: string @@ -354,11 +385,14 @@ class ExternalAuthOIDC { } const token = await this.encrypt(JSON.stringify(tokenContent)) + const avatar = await this.readUserInfoPicture(userInfo) + return { jid, nickname, password, - token + token, + avatar } } @@ -458,6 +492,7 @@ class ExternalAuthOIDC { guesses.push('given_name') break case 'nickname': + guesses.push('preferred_username') guesses.push('name') break } @@ -471,6 +506,39 @@ class ExternalAuthOIDC { return undefined } + /** + * Read and get the avatar for the remote user (if exists). + * @param userInfos userInfos returned by the remote OIDC Provider. + */ + private async readUserInfoPicture (userInfos: UnknownObject): Promise { + // According to "Standard Claims" section https://openid.net/specs/openid-connect-core-1_0.html, + // there should be a `picture` field containing an URL to the avatar + const picture = this.readUserInfoField(userInfos, 'picture') + if (!picture) { return undefined } + + try { + const url = new URL(picture) + const buf = await got(url.toString(), { + method: 'GET', + headers: {}, + responseType: 'buffer' + }).buffer() + + const mimeType = await getMimeTypeFromArrayBuffer(buf) + if (!mimeType) { + throw new Error('Failed to get the avatar file type') + } + + return { + mimetype: mimeType, + base64: buf.toString('base64') + } + } catch (err) { + this.logger.error(`Failed to get the user avatar: ${err as string}`) + return undefined + } + } + /** * Compute the JID to use for this remote account. * Format will be: "username+remote.domain.tld@external.instance.tld" diff --git a/server/lib/external-auth/types.ts b/server/lib/external-auth/types.ts index 449d3885..c22e1da0 100644 --- a/server/lib/external-auth/types.ts +++ b/server/lib/external-auth/types.ts @@ -1,11 +1,17 @@ +type AcceptableAvatarMimeType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp' + interface ExternalAccountInfos { nickname: string jid: string password: string token: string - // TODO: avatar + avatar?: { + mimetype: AcceptableAvatarMimeType + base64: string + } } export { - ExternalAccountInfos + ExternalAccountInfos, + AcceptableAvatarMimeType } diff --git a/server/lib/prosody/api/manage-users.ts b/server/lib/prosody/api/manage-users.ts index b62d0daa..bb60181f 100644 --- a/server/lib/prosody/api/manage-users.ts +++ b/server/lib/prosody/api/manage-users.ts @@ -34,7 +34,8 @@ async function ensureUser (options: RegisterServerOptions, infos: ExternalAccoun const apiData = { jid: infos.jid, nickname: infos.nickname, - password: infos.password + password: infos.password, + avatar: infos.avatar } try { logger.debug('Calling ensure-user API on url: ' + apiUrl) diff --git a/server/lib/prosody/config/content.ts b/server/lib/prosody/config/content.ts index d6b5814c..ec4c5d09 100644 --- a/server/lib/prosody/config/content.ts +++ b/server/lib/prosody/config/content.ts @@ -231,6 +231,7 @@ class ProsodyConfigContent { this.external.set('modules_enabled', [ 'ping', 'http', + 'vcard', 'http_peertubelivechat_manage_users' ]) this.external.set('peertubelivechat_manage_users_apikey', apikey) diff --git a/server/lib/routers/oidc.ts b/server/lib/routers/oidc.ts index 02022c14..4deb161c 100644 --- a/server/lib/routers/oidc.ts +++ b/server/lib/routers/oidc.ts @@ -72,7 +72,10 @@ async function initOIDCRouter (options: RegisterServerOptions): Promise externalAccountInfos, { password: '**removed**', // removing the password from logs! - token: '**removed**' // same as password + token: '**removed**', // same as password + avatar: externalAccountInfos.avatar + ? `**removed** ${externalAccountInfos.avatar.mimetype} avatar` + : undefined } ) ))