diff options
author | Marius David Wieschollek <passwords.public@mdns.eu> | 2020-05-02 16:14:45 +0300 |
---|---|---|
committer | Marius David Wieschollek <passwords.public@mdns.eu> | 2020-05-02 16:14:45 +0300 |
commit | dffd79816c53406658d302a2295e4512808e45d7 (patch) | |
tree | 243ce2268a806e75c1bbd5e2389f2debd097cc3c | |
parent | b358b4b224bd1b9b01bc3392b4ffd523ed49104a (diff) |
Added passlink connect action
Signed-off-by: Marius David Wieschollek <passwords.public@mdns.eu>
-rw-r--r-- | package-lock.json | 5 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | src/Exception/PassLink/InvalidLink.js | 3 | ||||
-rw-r--r-- | src/Exception/PassLink/UnknownAction.js | 3 | ||||
-rw-r--r-- | src/Network/ApiResponse.js | 60 | ||||
-rw-r--r-- | src/Network/HttpRequest.js | 194 | ||||
-rw-r--r-- | src/Network/HttpResponse.js | 60 | ||||
-rw-r--r-- | src/PassLink/Action/Connect.js | 129 | ||||
-rw-r--r-- | src/PassLink/Action/PassLinkAction.js | 6 | ||||
-rw-r--r-- | src/PassLink/PassLink.js | 42 | ||||
-rw-r--r-- | src/main.js | 3 |
11 files changed, 447 insertions, 59 deletions
diff --git a/package-lock.json b/package-lock.json index 7d922a3..ce5812c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,11 @@ "libsodium": "0.7.6" } }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "querystringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", diff --git a/package.json b/package.json index 1812812..ae9c881 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dependencies": { "eventemitter3": "^4.0.0", "libsodium-wrappers": "^0.7.6", + "pako": "^1.0.11", "url-parse": "^1.4.7", "uuidv4": "^4.0.0" } diff --git a/src/Exception/PassLink/InvalidLink.js b/src/Exception/PassLink/InvalidLink.js new file mode 100644 index 0000000..42f70e7 --- /dev/null +++ b/src/Exception/PassLink/InvalidLink.js @@ -0,0 +1,3 @@ +export default class InvalidLink extends Error { + constructor() {super('Not a valid passlink');} +}
\ No newline at end of file diff --git a/src/Exception/PassLink/UnknownAction.js b/src/Exception/PassLink/UnknownAction.js new file mode 100644 index 0000000..18eb576 --- /dev/null +++ b/src/Exception/PassLink/UnknownAction.js @@ -0,0 +1,3 @@ +export default class InvalidLink extends Error { + constructor(action) {super(`Unknown action ${action}`);} +}
\ No newline at end of file diff --git a/src/Network/ApiResponse.js b/src/Network/ApiResponse.js index 56af03e..dffb55b 100644 --- a/src/Network/ApiResponse.js +++ b/src/Network/ApiResponse.js @@ -1,60 +1,4 @@ -export default class ApiResponse { - - constructor() { - this._data = null; - this._headers = []; - this._contentType = 'text/plain'; - this._httpStatus = 0; - this._response = {}; - } - - getData() { - return this._data; - } - - setData(value) { - this._data = value; - - return this; - } - - getHeaders() { - return this._headers; - } - - setHeaders(value) { - this._headers = value; - - return this; - } - - getContentType() { - return this._contentType; - } - - setContentType(value) { - this._contentType = value; - - return this; - } - - getHttpStatus() { - return this._httpStatus; - } - - setHttpStatus(value) { - this._httpStatus = value; - - return this; - } - - getHttpResponse() { - return this._response; - } - - setHttpResponse(value) { - this._response = value; - return this; - } +import HttpResponse from './HttpResponse'; +export default class ApiResponse extends HttpResponse { }
\ No newline at end of file diff --git a/src/Network/HttpRequest.js b/src/Network/HttpRequest.js new file mode 100644 index 0000000..ed5f88d --- /dev/null +++ b/src/Network/HttpRequest.js @@ -0,0 +1,194 @@ +import ResponseDecodingError from '../Exception/ResponseDecodingError'; +import NetworkError from '../Exception/NetworkError'; +import HttpError from '../Exception/Http/HttpError'; +import ResponseContentTypeError from '../Exception/ResponseContentTypeError'; +import HttpResponse from './HttpResponse'; + +export default class HttpRequest { + + /** + * + * @param {String} [url=null] + */ + constructor(url = null) { + this._url = url; + this._data = null; + this._userAgent = null; + this._responseType = 'application/json'; + } + + /** + * + * @returns {(String|null)} + */ + getUrl() { + return this._url; + } + + /** + * + * @param {Session} value + * @returns {HttpRequest} + */ + setUrl(value) { + this._url = value; + + return this; + } + + /** + * + * @param {Object} value + * @return {HttpRequest} + */ + setData(value) { + this._data = value; + + return this; + } + + /** + * + * @param {String} value + * @return {HttpRequest} + */ + setUserAgent(value) { + this._userAgent = value; + + return this; + } + + /** + * + * @returns {Promise<HttpResponse>} + */ + async send() { + let options = this._getRequestOptions(); + let httpResponse = await this._executeRequest(this._url, options); + let expectedContentType = options.headers.get('content-type'); + let contentType = httpResponse.headers.get('content-type'); + + let response = new HttpResponse() + .setContentType(contentType) + .setHeaders(httpResponse.headers) + .setHttpStatus(httpResponse.status) + .setHttpResponse(httpResponse); + + if(expectedContentType !== null && contentType && contentType.indexOf(expectedContentType) === -1) { + throw new ResponseContentTypeError(expectedContentType, contentType, httpResponse); + } else if(contentType && contentType.indexOf('application/json') !== -1) { + await this._processJsonResponse(httpResponse, response); + } else { + await this._processBinaryResponse(httpResponse, response); + } + + return response; + } + + /** + * + * @return {{redirect: string, headers: Headers, method: string, credentials: string}} + * @private + */ + _getRequestOptions() { + let headers = this._getRequestHeaders(); + let method = 'GET'; + let options = {method, headers, credentials: 'omit', redirect: 'error'}; + if(this._data !== null) { + options.body = JSON.stringify(this._data); + method = 'POST'; + } + options.method = method; + + return options; + } + + /** + * + * @return {Headers} + * @private + */ + _getRequestHeaders() { + let headers = new Headers(); + + headers.append('accept', this._responseType); + + if(this._data !== null) { + headers.append('content-type', 'application/json'); + } + + if(this._userAgent !== null) { + headers.append('user-agent', this._userAgent); + } + + return headers; + } + + /** + * + * @param {String} url + * @param {Object} options + * @returns {Promise<Response>} + * @private + */ + async _executeRequest(url, options) { + try { + let request = new Request(url, options); + + return await fetch(request); + } catch(e) { + throw e; + } + } + + /** + * + * @param {Response} httpResponse + * @param {HttpResponse} response + * @private + */ + async _processJsonResponse(httpResponse, response) { + if(!httpResponse.ok) { + throw this._getHttpError(httpResponse); + } + + try { + let json = await httpResponse.json(); + response.setData(json); + } catch(e) { + throw new ResponseDecodingError(response, e); + } + } + + /** + * + * @param {Response} httpResponse + * @param {HttpResponse} response + * @private + */ + async _processBinaryResponse(httpResponse, response) { + if(!httpResponse.ok) { + throw this._getHttpError(httpResponse); + } + + try { + let blob = await httpResponse.blob(); + response.setData(blob); + } catch(e) { + throw new ResponseDecodingError(response, e); + } + } + + /** + * + * @param {Response} response + * @private + */ + _getHttpError(response) { + if(response.status > 99) { + return new HttpError(response); + } + + return new NetworkError(response); + } +}
\ No newline at end of file diff --git a/src/Network/HttpResponse.js b/src/Network/HttpResponse.js new file mode 100644 index 0000000..7758045 --- /dev/null +++ b/src/Network/HttpResponse.js @@ -0,0 +1,60 @@ +export default class HttpResponse { + + constructor() { + this._data = null; + this._headers = []; + this._contentType = 'text/plain'; + this._httpStatus = 0; + this._response = {}; + } + + getData() { + return this._data; + } + + setData(value) { + this._data = value; + + return this; + } + + getHeaders() { + return this._headers; + } + + setHeaders(value) { + this._headers = value; + + return this; + } + + getContentType() { + return this._contentType; + } + + setContentType(value) { + this._contentType = value; + + return this; + } + + getHttpStatus() { + return this._httpStatus; + } + + setHttpStatus(value) { + this._httpStatus = value; + + return this; + } + + getHttpResponse() { + return this._response; + } + + setHttpResponse(value) { + this._response = value; + return this; + } + +}
\ No newline at end of file diff --git a/src/PassLink/Action/Connect.js b/src/PassLink/Action/Connect.js new file mode 100644 index 0000000..681d745 --- /dev/null +++ b/src/PassLink/Action/Connect.js @@ -0,0 +1,129 @@ +import PassLinkAction from './PassLinkAction'; +import HttpRequest from '../../Network/HttpRequest'; + +export default class Connect extends PassLinkAction { + + /** + * + * @param {Object} parameters + */ + constructor(parameters) { + super(parameters); + this._codes = null; + this._clientLabel = null; + this._theme = null; + } + + /** + * Get the suggested name of the client + * + * @return {(null|String)} + */ + getClientLabel() { + return this._clientLabel; + } + + /** + * Set the suggested name of the client + * + * @param {String} value + * @return {Connect} + */ + setClientLabel(value) { + this._clientLabel = value; + + return this; + } + + /** + * Get the theme from the link + * + * @return {Promise<null|object>|null} + */ + getTheme() { + if(this._theme !== null) return this._theme; + + return this._decodeTheme(); + } + + /** + * + * @return {[]} + */ + getCodes() { + if(this._codes !== null) return this._codes; + + let codes = [], + spec = new RegExp('^(?=\\S*[a-z])(?=\\S*[A-Z])(?=\\S*[\\d])\\S*$'); + + while(codes.length < 4) { + let code = Array(4) + .fill('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz') + .map(x => x[Math.floor(crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1) * x.length)]) + .join(''); + + if(spec.test(code)) codes.push(code); + } + + this._codes = codes; + return codes; + } + + /** + * Apply for the registration + * + * @return {Promise<void>} + */ + async apply() { + let url = `${this._parameters.baseUrl}index.php/apps/passwords/link/connect/apply`, + request = new HttpRequest(url); + + let data = { + id : this._parameters.id, + codes: this.getCodes() + }; + + if(this._clientLabel !== null) data.label = this._clientLabel; + + let response = await request.setData(data).send(); + + return response.getData(); + } + + /** + * + * @private + */ + async _decodeTheme() { + if(!this._parameters.hasOwnProperty('theme')) return null; + + let pako = await import(/* webpackChunkName: "pako" */ 'pako'), + base64 = this._parameters.theme, + zipped = atob(base64), + json = pako.inflate(zipped, {to: 'string'}), + theme = JSON.parse(json); + + if(theme.hasOwnProperty('color')) { + theme['color.primary'] = theme.color; + delete theme.color; + } + if(theme.hasOwnProperty('txtColor')) { + theme['color.text'] = theme.txtColor; + delete theme.txtColor; + } + if(theme.hasOwnProperty('bgColor')) { + theme['color.background'] = theme.bgColor; + delete theme.bgColor; + } + if(theme.hasOwnProperty('background')) { + theme.background = this._parameters.baseUrl + theme.background; + } + if(theme.hasOwnProperty('logo')) { + theme.logo = this._parameters.baseUrl + theme.logo; + } + + this._theme = theme; + + return theme; + } +}
\ No newline at end of file diff --git a/src/PassLink/Action/PassLinkAction.js b/src/PassLink/Action/PassLinkAction.js new file mode 100644 index 0000000..d73cf6c --- /dev/null +++ b/src/PassLink/Action/PassLinkAction.js @@ -0,0 +1,6 @@ +export default class PassLinkAction { + constructor(parameters) { + this._parameters = parameters; + } + +}
\ No newline at end of file diff --git a/src/PassLink/PassLink.js b/src/PassLink/PassLink.js new file mode 100644 index 0000000..db89796 --- /dev/null +++ b/src/PassLink/PassLink.js @@ -0,0 +1,42 @@ +import InvalidLink from '../Exception/PassLink/InvalidLink'; +import UnknownAction from '../Exception/PassLink/UnknownAction'; +import Connect from './Action/Connect'; + +class PassLink { + + /** + * + * @param {String} link + * @return {{server: String, action: String, parameters: {}}} + */ + analyzeLink(link) { + let url = new URL(link); + + if(['ext+passlink:', 'web+passlink:', 'passlink:'].indexOf(url.protocol) === -1 || url.pathname.indexOf('/do/') === -1) { + throw new InvalidLink(); + } + let [server, action] = url.pathname.split('do/'); + + let parameters = {}; + for(let key of url.searchParams.keys()) { + parameters[key] = url.searchParams.get(key); + } + parameters.baseUrl = `https://${server}`; + + return {server, action, parameters} + } + + /** + * + * @param {String} action + * @param {Object} parameters + * @return {PassLinkAction} + */ + getAction(action, parameters) { + if(action === 'connect') return new Connect(parameters); + + throw new UnknownAction(action); + } +} + +export default new PassLink();
\ No newline at end of file diff --git a/src/main.js b/src/main.js index 3591088..c7cf752 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,5 @@ import Api from './Api/Api'; +import PassLink from './PassLink/PassLink'; export default Api; -export {Api}; +export {Api, PassLink}; |