import { DOMParser } from '@xmldom/xmldom';import { select, SelectedValue } from 'xpath';import { uniq, last, zipObject, notEmpty } from './utility';import camelCase from 'camelcase';const dom = DOMParser;
interface ExtractorField { key: string; localPath: string[] | string[][]; attributes: string[]; index?: string[]; attributePath?: string[]; context?: boolean;}
export type ExtractorFields = ExtractorField[];
function buildAbsoluteXPath(paths) { return paths.reduce((currentPath, name) => { let appendedPath = currentPath; const isWildcard = name.startsWith('~'); if (isWildcard) { const pathName = name.replace('~', ''); appendedPath = currentPath + `/*[contains(local-name(), '${pathName}')]`; } if (!isWildcard) { appendedPath = currentPath + `/*[local-name(.)='${name}']`; } return appendedPath; }, '');}
function buildAttributeXPath(attributes) { if (attributes.length === 0) { return '/text()'; } if (attributes.length === 1) { return `/@${attributes[0]}`; } const filters = attributes.map(attribute => `name()='${attribute}'`).join(' or '); return `/@*[${filters}]`;}
export const loginRequestFields: ExtractorFields = [ { key: 'request', localPath: ['AuthnRequest'], attributes: ['ID', 'IssueInstant', 'Destination', 'AssertionConsumerServiceURL'] }, { key: 'issuer', localPath: ['AuthnRequest', 'Issuer'], attributes: [] }, { key: 'nameIDPolicy', localPath: ['AuthnRequest', 'NameIDPolicy'], attributes: ['Format', 'AllowCreate'] }, { key: 'authnContextClassRef', localPath: ['AuthnRequest', 'AuthnContextClassRef'], attributes: [] }, { key: 'signature', localPath: ['AuthnRequest', 'Signature'], attributes: [], context: true }];
export const loginResponseStatusFields = [ { key: 'top', localPath: ['Response', 'Status', 'StatusCode'], attributes: ['Value'], }, { key: 'second', localPath: ['Response', 'Status', 'StatusCode', 'StatusCode'], attributes: ['Value'], }];
export const logoutResponseStatusFields = [ { key: 'top', localPath: ['LogoutResponse', 'Status', 'StatusCode'], attributes: ['Value'] }, { key: 'second', localPath: ['LogoutResponse', 'Status', 'StatusCode', 'StatusCode'], attributes: ['Value'], }];
export const loginResponseFields: ((assertion: any) => ExtractorFields) = assertion => [ { key: 'conditions', localPath: ['Assertion', 'Conditions'], attributes: ['NotBefore', 'NotOnOrAfter'], shortcut: assertion }, { key: 'response', localPath: ['Response'], attributes: ['ID', 'IssueInstant', 'Destination', 'InResponseTo'], }, { key: 'audience', localPath: ['Assertion', 'Conditions', 'AudienceRestriction', 'Audience'], attributes: [], shortcut: assertion }, { key: 'issuer', localPath: ['Assertion', 'Issuer'], attributes: [], shortcut: assertion }, { key: 'nameID', localPath: ['Assertion', 'Subject', 'NameID'], attributes: [], shortcut: assertion }, { key: 'sessionIndex', localPath: ['Assertion', 'AuthnStatement'], attributes: ['AuthnInstant', 'SessionNotOnOrAfter', 'SessionIndex'], shortcut: assertion }, { key: 'attributes', localPath: ['Assertion', 'AttributeStatement', 'Attribute'], index: ['Name'], attributePath: ['AttributeValue'], attributes: [], shortcut: assertion }];
export const logoutRequestFields: ExtractorFields = [ { key: 'request', localPath: ['LogoutRequest'], attributes: ['ID', 'IssueInstant', 'Destination'] }, { key: 'issuer', localPath: ['LogoutRequest', 'Issuer'], attributes: [] }, { key: 'nameID', localPath: ['LogoutRequest', 'NameID'], attributes: [] }, { key: 'signature', localPath: ['LogoutRequest', 'Signature'], attributes: [], context: true }];
export const logoutResponseFields: ExtractorFields = [ { key: 'response', localPath: ['LogoutResponse'], attributes: ['ID', 'Destination', 'InResponseTo'] }, { key: 'issuer', localPath: ['LogoutResponse', 'Issuer'], attributes: [] }, { key: 'signature', localPath: ['LogoutResponse', 'Signature'], attributes: [], context: true }];
export function extract(context: string, fields) {
const rootDoc = new dom().parseFromString(context);
return fields.reduce((result: any, field) => { const key = field.key; const localPath = field.localPath; const attributes = field.attributes; const isEntire = field.context; const shortcut = field.shortcut; const index = field.index; const attributePath = field.attributePath;
let targetDoc = rootDoc;
if (shortcut) { targetDoc = new dom().parseFromString(shortcut); }
if (localPath.every(path => Array.isArray(path))) { const multiXPaths = localPath .map(path => { return `${buildAbsoluteXPath(path)}/text()`; }) .join(' | ');
return { ...result, [key]: uniq(select(multiXPaths, targetDoc).map((n: Node) => n.nodeValue).filter(notEmpty)) }; }
const baseXPath = buildAbsoluteXPath(localPath); const attributeXPath = buildAttributeXPath(attributes);
if (index && attributePath) { const indexPath = buildAttributeXPath(index); const fullLocalXPath = `${baseXPath}${indexPath}`; const parentNodes = select(baseXPath, targetDoc); const parentAttributes = select(fullLocalXPath, targetDoc).map((n: Attr) => n.value); const childXPath = buildAbsoluteXPath([last(localPath)].concat(attributePath)); const childAttributeXPath = buildAttributeXPath(attributes); const fullChildXPath = `${childXPath}${childAttributeXPath}`; const childAttributes = parentNodes.map(node => { const nodeDoc = new dom().parseFromString(node.toString()); if (attributes.length === 0) { const childValues = select(fullChildXPath, nodeDoc).map((n: Node) => n.nodeValue); if (childValues.length === 1) { return childValues[0]; } return childValues; } if (attributes.length > 0) { const childValues = select(fullChildXPath, nodeDoc).map((n: Attr) => n.value); if (childValues.length === 1) { return childValues[0]; } return childValues; } return null; }); const obj = zipObject(parentAttributes, childAttributes, false); return { ...result, [key]: obj };
} if (isEntire) { const node = select(baseXPath, targetDoc); let value: string | string[] | null = null; if (node.length === 1) { value = node[0].toString(); } if (node.length > 1) { value = node.map(n => n.toString()); } return { ...result, [key]: value }; }
if (attributes.length > 1) { const baseNode = select(baseXPath, targetDoc).map(n => n.toString()); const childXPath = `${buildAbsoluteXPath([last(localPath)])}${attributeXPath}`; const attributeValues = baseNode.map((node: string) => { const nodeDoc = new dom().parseFromString(node); const values = select(childXPath, nodeDoc).reduce((r: any, n: Attr) => { r[camelCase(n.name)] = n.value; return r; }, {}); return values; }); return { ...result, [key]: attributeValues.length === 1 ? attributeValues[0] : attributeValues }; } if (attributes.length === 1) { const fullPath = `${baseXPath}${attributeXPath}`; const attributeValues = select(fullPath, targetDoc).map((n: Attr) => n.value); return { ...result, [key]: attributeValues[0] }; } if (attributes.length === 0) { let attributeValue: SelectedValue[] | (string | null)[] | null = null; const node = select(baseXPath, targetDoc); if (node.length === 1) { const fullPath = `string(${baseXPath}${attributeXPath})`; attributeValue = select(fullPath, targetDoc); } if (node.length > 1) { attributeValue = node.filter((n: Node) => n.firstChild) .map((n: Node) => n.firstChild!.nodeValue); } return { ...result, [key]: attributeValue }; }
return result; }, {});
}