Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.mdns.eu/nextcloud/passwords-client.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarius David Wieschollek <passwords.public@mdns.eu>2020-05-02 16:14:45 +0300
committerMarius David Wieschollek <passwords.public@mdns.eu>2020-05-02 16:14:45 +0300
commitdffd79816c53406658d302a2295e4512808e45d7 (patch)
tree243ce2268a806e75c1bbd5e2389f2debd097cc3c
parentb358b4b224bd1b9b01bc3392b4ffd523ed49104a (diff)
Added passlink connect action
Signed-off-by: Marius David Wieschollek <passwords.public@mdns.eu>
-rw-r--r--package-lock.json5
-rw-r--r--package.json1
-rw-r--r--src/Exception/PassLink/InvalidLink.js3
-rw-r--r--src/Exception/PassLink/UnknownAction.js3
-rw-r--r--src/Network/ApiResponse.js60
-rw-r--r--src/Network/HttpRequest.js194
-rw-r--r--src/Network/HttpResponse.js60
-rw-r--r--src/PassLink/Action/Connect.js129
-rw-r--r--src/PassLink/Action/PassLinkAction.js6
-rw-r--r--src/PassLink/PassLink.js42
-rw-r--r--src/main.js3
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};