This commit is contained in:
Gustaf Räntilä 2017-09-29 22:43:25 +02:00
Родитель 6ef703d0f9
Коммит 4e263f703f
12 изменённых файлов: 1808 добавлений и 3 удалений

3
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,3 @@
node_modules
test

Просмотреть файл

@ -23,7 +23,7 @@ function getBackend( Promise )
if ( isSafari )
// Safari doesn't support U2F, and the Safari-FIDO-U2F
// extension lacks full support (Multi-factet apps), so we
// extension lacks full support (Multi-facet apps), so we
// block it until proper support.
return notSupported( );
@ -43,6 +43,10 @@ function getBackend( Promise )
// U2F isn't supported over http, only https
return notSupported( );
if ( typeof MessageChannel === 'undefined' )
// Unsupported browser, the chrome hack would throw
return notSupported( );
// Test for google extension support
chromeApi.isSupported( function( ok )
{

1253
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -8,6 +8,12 @@
"type": "git",
"url": "https://github.com/grantila/u2f-api.git"
},
"scripts": {
"test": "scripts/test.sh"
},
"pre-commit": [
"test"
],
"keywords": [
"u2f",
"api",
@ -18,5 +24,14 @@
"yubikey",
"promise"
],
"types": "./index.d.ts"
"types": "./index.d.ts",
"devDependencies": {
"@types/mocha": "^2.2.43",
"already": "^0.6.0",
"chai": "^4.1.2",
"jsdom": "^11.2.0",
"mocha": "^3.5.3",
"pre-commit": "^1.2.2",
"typescript": "^2.5.3"
}
}

8
scripts/test.sh Executable file
Просмотреть файл

@ -0,0 +1,8 @@
#!/bin/bash
rm -rf test
cp -r test.in test
node_modules/.bin/tsc -p test
node_modules/.bin/mocha

1
test.in/compiled/index.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1 @@
import 'mocha';

226
test.in/compiled/index.js Normal file
Просмотреть файл

@ -0,0 +1,226 @@
'use strict';
var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
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) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t;
return { next: verb(0), "throw": verb(1), "return": verb(2) };
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [0, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var _this = this;
Object.defineProperty(exports, "__esModule", { value: true });
require("mocha");
var chai_1 = require("chai");
var already_1 = require("already");
var jsdom = require("jsdom");
var _1 = require("../../");
var JSDOM = jsdom.JSDOM;
var appId = "https://example.org/";
var MonkeyPatcher = (function () {
function MonkeyPatcher(obj) {
this._object = obj;
this.clear();
}
MonkeyPatcher.prototype.patch = function (obj, overwrite) {
var _this = this;
if (overwrite === void 0) { overwrite = false; }
Object.keys(obj).forEach(function (key) {
var ownProp = _this._object.hasOwnProperty(key);
if (!ownProp || overwrite) {
var value = obj[key];
if (!ownProp)
_this._values.push(key);
else
_this._overwrittenValues.push({ key: key, value: _this._object[key] });
_this._object[key] = value;
}
});
};
MonkeyPatcher.prototype.restore = function () {
var _this = this;
this._values.forEach(function (key) {
delete _this._object[key];
});
this._overwrittenValues.forEach(function (_a) {
var key = _a.key, value = _a.value;
_this._object[key] = value;
});
this.clear();
};
MonkeyPatcher.prototype.clear = function () {
this._values = [];
this._overwrittenValues = [];
};
return MonkeyPatcher;
}());
var GlobalMonkeyPatcher = (function (_super) {
__extends(GlobalMonkeyPatcher, _super);
function GlobalMonkeyPatcher() {
return _super.call(this, global) || this;
}
return GlobalMonkeyPatcher;
}(MonkeyPatcher));
function deleteModule(moduleName) {
try {
var solvedName = require.resolve(moduleName);
var nodeModule = require.cache[solvedName];
if (nodeModule) {
for (var i = 0; i < nodeModule.children.length; ++i) {
var child = nodeModule.children[i];
deleteModule(child.filename);
}
delete require.cache[solvedName];
}
}
catch (err) { }
}
function getNewU2FApi() {
deleteModule('../../');
return require('../../');
}
function handleTimeout(delay, timeout, fn) {
var timeoutPromise = timeout
? delay(timeout).then(function () {
return ({ errorCode: _1.ErrorCodesEnum.TIMEOUT });
})
: null;
var flowPromise = delay(delay || 0).then(fn);
return Promise.race([
timeoutPromise,
flowPromise,
]
.filter(function (exists) { return exists; }));
}
function u2fMock(props) {
if (props === void 0) { props = {}; }
var store = {};
return {
sign: function (appId, challenge, signRequests, cbNative, timeout) {
handleTimeout(props.delay, timeout, function () {
if (props.appId && props.appId !== appId)
return { errorCode: _1.ErrorCodesEnum.BAD_REQUEST };
})
.then(cbNative);
},
register: function (appId, registerRequests, signRequests, cbNative, timeout) {
handleTimeout(props.delay, timeout, function () {
if (props.appId && props.appId !== appId)
return { errorCode: _1.ErrorCodesEnum.BAD_REQUEST };
var secret = Math.random();
registerRequests.forEach(function (req) {
});
})
.then(function (value) { return value || {}; })
.then(cbNative);
},
};
}
function wrappedTest(props, fn) {
return function () {
var dom = new JSDOM("", Object.assign({
url: appId,
userAgent: "FakeBrowser/1",
}, props));
if (!props || !props.mock || !props.mock.disable) {
var mock = (props || {}).mock || {};
dom.window.u2f = u2fMock();
}
var gmp = new GlobalMonkeyPatcher();
gmp.patch(dom.window);
var api = getNewU2FApi();
return (_a = already_1.Try(function () { return fn(api); })).then.apply(_a, already_1.Finally(function () {
gmp.restore();
}));
var _a;
};
}
describe('general', function () {
it('isSupported should be false for unsupported browsers', wrappedTest({ mock: { disable: true } }, function (api) { return __awaiter(_this, void 0, void 0, function () {
var supported;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, api.isSupported()];
case 1:
supported = _a.sent();
chai_1.expect(supported).to.be.false;
return [2 /*return*/];
}
});
}); }));
it('isSupported should be false for Safari', wrappedTest({ userAgent: "Safari/10" }, function (api) { return __awaiter(_this, void 0, void 0, function () {
var supported;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, api.isSupported()];
case 1:
supported = _a.sent();
chai_1.expect(supported).to.be.false;
return [2 /*return*/];
}
});
}); }));
it('isSupported should be true with fake window.u2f', wrappedTest({}, function (api) { return __awaiter(_this, void 0, void 0, function () {
var supported;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, api.isSupported()];
case 1:
supported = _a.sent();
chai_1.expect(supported).to.be.true;
return [2 /*return*/];
}
});
}); }));
it('isSupported should be true with fake window.u2f', wrappedTest({}, function (api) { return __awaiter(_this, void 0, void 0, function () {
var request;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, api.ensureSupport()];
case 1:
_a.sent();
request = "req";
api.register({ appId: appId, request: request });
chai_1.expect(supported).to.be.true;
return [2 /*return*/];
}
});
}); }));
});
//# sourceMappingURL=index.js.map

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

1
test.in/mocha.opts Normal file
Просмотреть файл

@ -0,0 +1 @@
--recursive

4
test.in/setup.js Normal file
Просмотреть файл

@ -0,0 +1,4 @@
var chai = require( 'chai' );
global.expect = chai.expect;

15
test.in/tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,15 @@
{
"compilerOptions": {
"declaration": true,
"lib": [ "es2015", "es2017", "dom" ],
"moduleResolution": "node",
"outDir": "compiled",
"pretty": true,
"sourceMap": true,
"target": "es5",
"allowSyntheticDefaultImports": true
},
"include": [
"u2f-api"
]
}

276
test.in/u2f-api/index.ts Normal file
Просмотреть файл

@ -0,0 +1,276 @@
'use strict';
import 'mocha';
import { expect } from 'chai';
import { Finally, Try, delay } from 'already';
import * as jsdom from "jsdom";
import { API } from '../../';
const ErrorCodesEnum = {
CANCELLED: -1,
OK: 0,
OTHER_ERROR: 1,
BAD_REQUEST: 2,
CONFIGURATION_UNSUPPORTED: 3,
DEVICE_INELIGIBLE: 4,
TIMEOUT: 5,
};
const { JSDOM } = jsdom;
const appId = "https://example.org/";
interface KeyValue
{
key: string;
value: any;
}
type KeyValues = Array< KeyValue >;
class MonkeyPatcher
{
_object: any;
_values: Array< string >;
_overwrittenValues: KeyValues;
constructor( obj: any )
{
this._object = obj;
this.clear( );
}
patch( obj: any, overwrite = false )
{
Object.keys( obj ).forEach( key =>
{
const ownProp = this._object.hasOwnProperty( key );
if ( !ownProp || overwrite )
{
const value = obj[ key ];
if ( !ownProp )
this._values.push( key );
else
this._overwrittenValues.push(
{ key, value: this._object[ key ] } );
this._object[ key ] = value;
}
} );
}
restore( )
{
this._values.forEach( key =>
{
delete this._object[ key ];
} );
this._overwrittenValues.forEach( ( { key, value } ) =>
{
this._object[ key ] = value;
} );
this.clear( );
}
clear( )
{
this._values = [ ];
this._overwrittenValues = [ ];
}
}
class GlobalMonkeyPatcher extends MonkeyPatcher
{
constructor( )
{
super( global );
}
}
function deleteModule( moduleName: string ): void
{
try
{
const solvedName = require.resolve( moduleName );
const nodeModule = require.cache[ solvedName ];
if ( nodeModule )
{
for ( let i = 0; i < nodeModule.children.length; ++i )
{
const child = nodeModule.children[ i ];
deleteModule( child.filename );
}
delete require.cache[ solvedName ];
}
}
catch ( err ) { }
}
function getNewU2FApi( ): API
{
deleteModule( '../../' );
return require( '../../' );
}
interface MockProps
{
appId?: string;
delay?: number;
}
function handleTimeout(
props: MockProps,
timeout,
fn: ( ) => any
)
: Promise< any >
{
const timeoutPromise = timeout
? delay( timeout ).then( ( ) =>
( { errorCode: ErrorCodesEnum.TIMEOUT } ) )
: null;
const flowPromise = delay( props.delay || 0 ).then( fn );
return Promise.race(
[
timeoutPromise,
flowPromise,
]
.filter( exists => exists )
);
}
type FakeRequest = { request: string; appId: string; };
function u2fMock( props: MockProps = { } )
{
const store: Array< FakeRequest > = [ ];
return {
sign(
appId,
challenge,
signRequests: Array< FakeRequest >,
cbNative,
timeout
)
{
return handleTimeout( props, timeout, ( ) =>
{
if ( props.appId && props.appId !== appId )
return { errorCode: ErrorCodesEnum.BAD_REQUEST };
const found = signRequests.some( req =>
store.some( storeReq =>
storeReq.request === req.request
&&
storeReq.appId === req.appId
)
);
if ( !found )
return { errorCode: ErrorCodesEnum.BAD_REQUEST };
} )
.then( value => value || { } )
.then( cbNative );
},
register(
appId,
registerRequests: Array< FakeRequest >,
signRequests: Array< FakeRequest >,
cbNative,
timeout
)
{
return handleTimeout( props, timeout, ( ) =>
{
if ( props.appId && props.appId !== appId )
return { errorCode: ErrorCodesEnum.BAD_REQUEST };
registerRequests.forEach( req =>
{
store.push( req );
} );
} )
.then( value => value || { } )
.then( cbNative );
},
};
}
function wrappedTest(
props: any,
fn: ( api: API ) => Promise< void >
)
: ( ) => Promise< void >
{
return ( ): Promise< void > =>
{
const dom = new JSDOM(
"",
Object.assign( {
url: appId,
userAgent: "FakeBrowser/1",
}, props )
);
if ( !props || !props.mock || !props.mock.disable )
{
const mock = ( props || { } ).mock || { };
dom.window.u2f = u2fMock( );
}
const gmp = new GlobalMonkeyPatcher( );
gmp.patch( dom.window );
const api = getNewU2FApi( );
return Try( ( ) => fn( api ) )
.then( ...Finally( ( ) =>
{
gmp.restore( );
} ) );
};
}
describe( 'general', ( ) =>
{
it( 'isSupported should be false for unsupported browsers',
wrappedTest( { mock: { disable: true } }, async api =>
{
const supported = await api.isSupported( );
expect( supported ).to.be.false;
} ) );
it( 'isSupported should be false for Safari',
wrappedTest( { userAgent: "Safari/10" }, async api =>
{
const supported = await api.isSupported( );
expect( supported ).to.be.false;
} ) );
it( 'isSupported should be true with fake window.u2f',
wrappedTest( { }, async api =>
{
const supported = await api.isSupported( );
expect( supported ).to.be.true;
} ) );
it( 'the flow of register + sign should run through',
wrappedTest( { }, async api =>
{
await api.ensureSupport( );
const request = "req";
await api.register( { appId, request } );
await api.sign( { appId, request } );
} ) );
} );