UNPKG

solid-curl

Version:

Command line application mimicking the behaviour of cURL but authenticating using Solid-OIDC

317 lines (316 loc) 13.6 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const solid_client_authn_node_1 = require("@inrupt/solid-client-authn-node"); const process_1 = __importDefault(require("process")); const commander_1 = require("commander"); const fs_1 = require("fs"); const loglevel_1 = __importDefault(require("loglevel")); const keytar_1 = require("keytar"); const readline_sync_1 = require("readline-sync"); ; const n3_1 = require("n3"); const console_table_printer_1 = require("console-table-printer"); const mime_types_1 = require("mime-types"); const { namedNode } = n3_1.DataFactory; const version = '0.1.11'; // Remove draft warning from oidc-client lib process_1.default.emitWarning = (warning, ...args) => { if (args[0] === 'DraftWarning') { return; } return process_1.default.emitWarning(warning, ...args); }; // Command line arguments commander_1.program .version(version, '-V, --version', 'Show version number and quit') .argument('<uri>', 'Target URI') .option('-d, --data <data>', 'HTTP POST data') //.option('-f, --fail', 'Fail silently (no output at all) on HTTP errors') .option('-H, --header <header...>', 'Add header to request') .option('-i, --include', 'Include HTTP response headers in output') .option('-L, --location', 'Follow redirects') //.option('-o, --output <file>', 'Write to file instead of stdout') //.option('-O, --remote-name', 'Write output to a file named as the remote file') .option('-s, --silent', 'Silent mode') //.option('-T, --transfer-file <file>', 'Transfer local FILE to destination') .option('-u, --user <identity>', 'Use stored identity') .option('-u, -- <identity>', 'Use stored identity') //.option('-A, --user-agent <name>', 'Send User-Agent <name> to server') .option('-v, --verbose', 'Make the operation more talkative') .option('-X, --request <method>', 'Specify custom request method') .action(run); commander_1.program .command('register-user') .argument('<uri>', 'WebID') .action(registerUser); commander_1.program .command('delete-user') .argument('<identity>', 'Identity name') .action(deleteUser); commander_1.program .command('list-users') .action(listUsers); commander_1.program.parseAsync(); function run(uri, options) { var _a; return __awaiter(this, void 0, void 0, function* () { if (options.verbose) { loglevel_1.default.setLevel('info'); } if (options.silent) { loglevel_1.default.setLevel('silent'); } const session = new solid_client_authn_node_1.Session(); let headers = { 'accept': 'text/turtle, */*;q=0.8', 'user-agent': 'solid-curl/' + version, 'accept-encoding': 'gzip,deflate', 'connection': 'close', 'host': uri.split('/').slice(2, 3).join() }; let fetchInit = { redirect: options.location ? 'follow' : 'manual' }; // Loading data from file if necessary let data = ((_a = options.data) === null || _a === void 0 ? void 0 : _a.startsWith('@')) ? (0, fs_1.createReadStream)(options.data.substring(1)) : options.data; if (data) { fetchInit['body'] = data; fetchInit['duplex'] = 'half'; // Default method with data is POST fetchInit['method'] = 'POST'; // Determine MIME type (can be overwritten by the user), default text/turtle headers['content-type'] = (0, mime_types_1.lookup)(options.data.substring(1)) || 'text/turtle'; } // Setting method if (options.request) { fetchInit['method'] = options.request; } // Transforming headers into format needed by fetch for (let h of (options === null || options === void 0 ? void 0 : options.header) || []) { let split = h.split(':'); headers[split[0].toLowerCase()] = split.slice(1).join().trim(); } fetchInit['headers'] = headers; const user = options.user; // Do unauthenticated request when no user is provided if (!user) { loglevel_1.default.info('* No user identity given, doing unauthenticated request'); yield doFetch(uri, fetchInit, headers, session, process_1.default.stdout, options === null || options === void 0 ? void 0 : options.include); process_1.default.exit(); } // Get credentials from storage let credentials = yield (0, keytar_1.findCredentials)('solid-curl'); if (!credentials.some(c => c.account === user)) { loglevel_1.default.error('No credentials with name \'' + user + '\' found!'); process_1.default.exit(1); } let creds = JSON.parse(credentials.find(c => c.account === user).password); // Log in let oidcIssuer = creds['oidcIssuer']; loglevel_1.default.info(`* Initiating OIDC login at ${oidcIssuer}`); yield session.login({ oidcIssuer: oidcIssuer, clientId: creds['id'], clientSecret: creds['secret'] }); yield doFetch(uri, fetchInit, headers, session, process_1.default.stdout, options === null || options === void 0 ? void 0 : options.include); process_1.default.exit(); }); } function listUsers() { return __awaiter(this, void 0, void 0, function* () { let credentials = yield (0, keytar_1.findCredentials)('solid-curl'); let prettyCredentials = credentials.map(c => { let creds = JSON.parse(c.password); return { Identity: c.account, WebID: creds['webId'], 'OIDC Issuer': creds['oidcIssuer'], ClientID: creds['id'] }; }); (0, console_table_printer_1.printTable)(prettyCredentials); }); } function registerUser(webId) { return __awaiter(this, void 0, void 0, function* () { let oidcIssuer = yield getOIDCIssuer(webId); let credentials = yield registerApp(oidcIssuer); console.log('Successfully created credentials!'); let identity = (0, readline_sync_1.question)('Identity name: '); (0, keytar_1.setPassword)('solid-curl', identity, JSON.stringify({ webId: webId, oidcIssuer: oidcIssuer, id: credentials.id, secret: credentials.secret })); }); } function deleteUser(identity) { return __awaiter(this, void 0, void 0, function* () { let creds = yield (0, keytar_1.getPassword)('solid-curl', identity); if (creds == null) { throw Error('Identity "' + identity + '" not found!'); } let credsParsed = JSON.parse(creds); let oidcIssuer = credsParsed['oidcIssuer']; let clientId = credsParsed['id']; yield deregisterApp(oidcIssuer, clientId); (0, keytar_1.deletePassword)('solid-curl', identity); }); } function registerApp(oidcIssuer) { return __awaiter(this, void 0, void 0, function* () { if (!oidcIssuer.endsWith('/')) { oidcIssuer += '/'; } // Try Community Solid Server let response = yield fetch(oidcIssuer + 'idp/credentials/'); if (response.status == 405) { console.log('Authenticating with ' + oidcIssuer + ' (Community Solid Server):'); let email = (0, readline_sync_1.question)('E-Mail: '); let password = (0, readline_sync_1.question)('Password: ', { hideEchoBack: true }); response = yield fetch(oidcIssuer + 'idp/credentials/', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: email, password: password, name: 'solid-curl' }) }); if (response.status == 200) { return yield response.json(); } else { throw Error(yield response.text()); } } throw Error('No client registration could be found for the OIDC issuer!'); }); } function deregisterApp(oidcIssuer, clientId) { return __awaiter(this, void 0, void 0, function* () { // Try Community Solid Server let response = yield fetch(oidcIssuer + 'idp/credentials/'); if (response.status == 405) { console.log('Authenticating with ' + oidcIssuer + ' (Community Solid Server):'); let email = (0, readline_sync_1.question)('E-Mail: '); let password = (0, readline_sync_1.question)('Password: ', { hideEchoBack: true }); response = yield fetch(oidcIssuer + 'idp/credentials/', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: email, password: password, delete: clientId }) }); if (response.status == 200) { return yield response.json(); } else { throw Error(yield response.text()); } } throw Error('No client registration could be found for the OIDC issuer!'); }); } function getOIDCIssuer(webId) { return __awaiter(this, void 0, void 0, function* () { let response = yield fetch(webId); let quads = yield parseResponse(response); let store = new n3_1.Store(); store.addQuads(quads); let issuers = store.getObjects(namedNode(webId), namedNode('http://www.w3.org/ns/solid/terms#oidcIssuer'), null); if (issuers.length > 0) { return issuers[0].value; } else { throw Error('No OIDC issuer in Profile Document found!'); } }); } function parseResponse(response) { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { let quads = []; let parser = new n3_1.Parser({ baseIRI: response.url }); let text = yield response.text(); parser.parse(text, (error, quad) => { if (error) { reject(error); } if (quad) { quads.push(quad); } else { resolve(quads); } }); })); }); } function doFetch(uri, fetchInit, headers, session, outStream, include) { return __awaiter(this, void 0, void 0, function* () { // Do actual request loglevel_1.default.info(`> ${fetchInit.method} /${uri.split('/').slice(3).join('/')} HTTP/1.1`); for (let h in headers) { // Make header names upercase for logging loglevel_1.default.info(`> ${h.split('-').map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join('-')}: ${headers[h]}`); } // If request was authenticated, add placeholder for DPoP if (session.info.isLoggedIn) { loglevel_1.default.info(`> Authorization: DPoP [omitted]`); } loglevel_1.default.info('>'); try { let res = yield session.fetch(uri, fetchInit); loglevel_1.default.info(`< HTTP/1.1 ${res.status} ${res.statusText}`); if (include) { outStream.write(`HTTP/1.1 ${res.status} ${res.statusText}\n`); } for (let h of res.headers) { loglevel_1.default.info(`< ${h[0].split('-').map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join('-')}: ${h[1]}`); if (include) { outStream.write(`${'\x1b[1m'}${h[0].split('-').map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join('-')}${'\x1b[0m'}: ${h[1]}\n`); } } if (include) { outStream.write(`\n`); } loglevel_1.default.info('<'); let buffer = Buffer.from(yield res.arrayBuffer()); outStream.write(buffer); } catch (error) { if (error['errno'] === 'ENOTFOUND') { loglevel_1.default.error(`Could not resolve host: ${uri.split('/').slice(2, 3).join()}`); } else if (error['errno'] === 'ECONNREFUSED') { loglevel_1.default.error(`Connection refused: ${uri.split('/').slice(2, 3).join()}`); } else { loglevel_1.default.error(error); } } }); }