From 552dfe3ef828e3cb437af7fc5efb2f55c77a343f Mon Sep 17 00:00:00 2001 From: LatoWolf Date: Thu, 14 May 2026 20:14:52 +0800 Subject: [PATCH] Initial Cardless Auth support --- plugins/asphyxia-core.d.ts | 9 +++ src/eamuse/Core/SPPass.ts | 89 +++++++++++++++++++++++++++++ src/eamuse/Core/index.ts | 4 ++ src/eamuse/index.ts | 1 + src/middlewares/EamuseMiddleware.ts | 36 +++++++++--- 5 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 src/eamuse/Core/SPPass.ts diff --git a/plugins/asphyxia-core.d.ts b/plugins/asphyxia-core.d.ts index 2249e28..cc437e8 100644 --- a/plugins/asphyxia-core.d.ts +++ b/plugins/asphyxia-core.d.ts @@ -1049,6 +1049,15 @@ declare namespace U { * @param encoding see [[KEncoding]] */ function DecodeString(buffer: Buffer, encoding: KEncoding): string; + + /** + * Allow to set a token for cardless authentication. + * + * returns a string + * @param token token to set. + * @param refid refid of the profile to set token for. + */ + function SetSPPassToken(token: string, refid: string): void; } /** @ignore */ diff --git a/src/eamuse/Core/SPPass.ts b/src/eamuse/Core/SPPass.ts new file mode 100644 index 0000000..f1c4432 --- /dev/null +++ b/src/eamuse/Core/SPPass.ts @@ -0,0 +1,89 @@ +import { EamuseRouteContainer } from '../EamuseRouteContainer'; +import { get } from 'lodash'; +import { Logger } from '../../utils/Logger'; +import { kitem } from '../../utils/KBinJSON'; +import { FindCardsByRefid } from '../../utils/EamuseIO'; + + +export const sppass = new EamuseRouteContainer(); + +const sppassTokens: { [token: string]: {date: Date, refid: string} } = {}; + +sppass.add('sppass.open', async (info, data, send) => { + cleanUpTokens(); + + const token = randomToken(4); + sppassTokens[token] = {date: new Date(Date.now() + 60000), refid: ""}; // +1 minute expiry + + Logger.debug(`SPPass Opened: ${token}`); + + send.object({ + token: kitem('str', token), + expire_datetime: kitem('str', new Date().toISOString().slice(0, 19).replace('T', ' ')), // Format: 2026-05-14 08:09:35 + url: kitem('str', token), + interval: kitem('s32', 2), + }); +}); + +sppass.add('sppass.lookup', async (info, data, send) => { + Logger.debug(`info: ${JSON.stringify(info)}, data: ${JSON.stringify(data)}`); + + + const token: string = get(data, '@attr.token'); + + let card_id = ""; + let card_type = ""; + if (sppassTokens[token]) { + let card = await FindCardsByRefid(sppassTokens[token].refid); + if (card && card.length > 0) { + card_id = card[0].cid; + card_type = "1"; + } + } + Logger.debug(`SPPass Lookup: ${token} -> ${card_id}`); + + send.object({ + url: kitem('str', token), + interval: kitem('s32', 2), + card_type: kitem('str', card_type), + card_id: kitem('str', card_id), + }); + + // Release token after lookup if card_id is set + if (card_id) { + delete sppassTokens[token]; + Logger.debug(`SPPass Token Released: ${token}`); + } +}); + +export function setSPPassToken(token: string, refid: string) { + Logger.debug(JSON.stringify(sppassTokens, null, 4)); + + if (sppassTokens[token] !== undefined) { + sppassTokens[token].refid = refid; + Logger.debug(`SPPass Token Set: ${token} -> ${refid}`); + } else { + Logger.warn(`SPPass Token Not Found: ${token}`); + } +} + +function cleanUpTokens() { + const now = new Date(); + for (const token in sppassTokens) { + if (sppassTokens[token].date < now) { + delete sppassTokens[token]; + Logger.debug(`SPPass Token Expired: ${token}`); + } + } +} + +function randomToken(length: number): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return result; +} diff --git a/src/eamuse/Core/index.ts b/src/eamuse/Core/index.ts index f5e870c..fbbd46f 100644 --- a/src/eamuse/Core/index.ts +++ b/src/eamuse/Core/index.ts @@ -2,11 +2,14 @@ import { cardmng } from './CardManager'; import { eacoin } from './EamuseCoin'; import { facility } from './Facility'; import { pcbtracker } from './PCBTracker'; +import { sppass} from './SPPass'; + import { kitem } from '../../utils/KBinJSON'; import { EamuseRouteContainer } from '../EamuseRouteContainer'; import { Logger } from '../../utils/Logger'; import { CONFIG } from '../../utils/ArgConfig'; + export const core = new EamuseRouteContainer(); core.add('message.get', async (info, data, send) => { @@ -66,3 +69,4 @@ core.add(eacoin); core.add(facility); core.add(cardmng); core.add(pcbtracker); +core.add(sppass); diff --git a/src/eamuse/index.ts b/src/eamuse/index.ts index a4ca47d..a5e94db 100644 --- a/src/eamuse/index.ts +++ b/src/eamuse/index.ts @@ -49,6 +49,7 @@ export const services = (port: number, plugins: EamusePlugin[]) => { 'netlog', 'sidmgr', 'globby', + 'sppass' ]; /* General Information */ diff --git a/src/middlewares/EamuseMiddleware.ts b/src/middlewares/EamuseMiddleware.ts index ab9f91c..82148ff 100644 --- a/src/middlewares/EamuseMiddleware.ts +++ b/src/middlewares/EamuseMiddleware.ts @@ -113,23 +113,45 @@ export const EamuseMiddleware: RequestHandler = async (req, res, next) => { return; } - const eaModule = findKey( + let eaModule = findKey( get(xml, 'call'), x => has(x, '@attr.method') || has(x, '0.@attr.method') - ); + ) + + if (eaModule === undefined && req.url.includes("fnc")) { + eaModule = "sppass"; + Logger.debug(`${req.url}`) + } if (!eaModule) { res.sendStatus(404); return; } - let moduleObj: any[] = get(xml, `call.${eaModule}`, null); - if (!isArray(moduleObj)) moduleObj = [moduleObj]; + let eaMethod = undefined; + let model = undefined; + let token = undefined; + + if (eaModule !== 'sppass') { + let moduleObj: any[] = get(xml, `call.${eaModule}`, null); + if (!isArray(moduleObj)) moduleObj = [moduleObj]; - const eaMethods: string[] = moduleObj.map(x => get(x, `@attr.method`)); - const eaMethod = eaMethods.join('.'); - const model = get(xml, 'call.@attr.model'); + const eaMethods: string[] = moduleObj.map(x => get(x, `@attr.method`)); + eaMethod = eaMethods.join('.'); + model = get(xml, 'call.@attr.model'); + } else { + const urlParams = new URLSearchParams(req.url.split('?')[1]); + eaMethod = urlParams.get('fnc'); + model = urlParams.get('sic'); + // Since SPPass doesn't send XML data, we need to get token from URL parameters and construct a fake XML data for further processing. + token = { call: { sppass: { "@attr": { token: urlParams.get('token') || "" } } } }; + + // rewind url encoding + model = decodeURIComponent(model || ''); + xml = token; + } + if (!(process as any).pkg) { Logger.debug(`${eaModule}.${eaMethod}\n${dataToXML(xml, false)}`); }