diff options
Diffstat (limited to 'content/includes/network.js')
-rw-r--r-- | content/includes/network.js | 431 |
1 files changed, 431 insertions, 0 deletions
diff --git a/content/includes/network.js b/content/includes/network.js new file mode 100644 index 0000000..3f94c30 --- /dev/null +++ b/content/includes/network.js @@ -0,0 +1,431 @@ +/* + * This file is part of DAV-4-TbSync. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +var { HttpRequest } = ChromeUtils.import("chrome://tbsync/content/HttpRequest.jsm"); +var { OAuth2 } = ChromeUtils.import("resource:///modules/OAuth2.jsm"); +const { DNS } = ChromeUtils.import("resource:///modules/DNS.jsm"); + +var network = { + + getAuthData: function(accountData) { + let connection = { + get host() { + return "TbSync#" + accountData.accountID; + }, + + get username() { + return accountData.getAccountProperty("user"); + }, + + get password() { + // try new host first + let pw = TbSync.passwordManager.getLoginInfo(this.host, "TbSync/DAV", this.username); + if (pw) { + return pw; + } + + // try old host as fallback + let oldHost = accountData.getAccountProperty("calDavHost") ? accountData.getAccountProperty("calDavHost") : accountData.getAccountProperty("cardDavHost"); + if (oldHost.startsWith("http://")) oldHost = oldHost.substr(7); + if (oldHost.startsWith("https://")) oldHost = oldHost.substr(8); + pw = TbSync.passwordManager.getLoginInfo(oldHost, "TbSync/DAV", this.username); + if (pw) { + //migrate + this.updateLoginData(this.username, pw); + } + return pw; + }, + + updateLoginData: function(newUsername, newPassword) { + let oldUsername = this.username; + TbSync.passwordManager.updateLoginInfo(this.host, "TbSync/DAV", oldUsername, newUsername, newPassword); + // Also update the username of this account. + accountData.setAccountProperty("user", newUsername); + }, + + removeLoginData: function() { + TbSync.passwordManager.removeLoginInfos(this.host, "TbSync/DAV"); + } + }; + return connection; + }, + + ConnectionData: class { + constructor(data) { + this._password = ""; + this._username = ""; + this._https = ""; + this._type = ""; + this._fqdn = ""; + this._timeout = dav.Base.getConnectionTimeout(); + + //for error logging + this._eventLogInfo = null; + + //typof syncdata? + let folderData = null; + let accountData = null; + + if (data instanceof TbSync.SyncData) { + folderData = data.currentFolderData; + accountData = data.accountData; + this._eventLogInfo = data.eventLogInfo; + } else if (data instanceof TbSync.FolderData) { + folderData = data; + accountData = data.accountData; + this._eventLogInfo = new TbSync.EventLogInfo( + accountData.getAccountProperty("provider"), + accountData.getAccountProperty("accountname"), + accountData.accountID, + folderData.getFolderProperty("foldername")); + } else if (data instanceof TbSync.AccountData) { + accountData = data; + this._eventLogInfo = new TbSync.EventLogInfo( + accountData.getAccountProperty("provider"), + accountData.getAccountProperty("accountname"), + accountData.accountID, + ""); + } + + if (accountData) { + let authData = dav.network.getAuthData(accountData); + this._password = authData.password; + this._username = authData.username; + + this._accountname = accountData.getAccountProperty("accountname"); + if (folderData) { + this._fqdn = folderData.getFolderProperty("fqdn"); + this._https = folderData.getFolderProperty("https"); + } + this.accountData = accountData; + } + } + + + set password(v) {this._password = v;} + set username(v) {this._username = v;} + set timeout(v) {this._timeout = v;} + set https(v) {this._https = v;} + set fqdn(v) {this._fqdn = v;} + set eventLogInfo(v) {this._eventLogInfo = v;} + + get password() {return this._password;} + get username() {return this._username;} + get timeout() {return this._timeout;} + get https() {return this._https;} + get fqdn() {return this._fqdn;} + get eventLogInfo() {return this._eventLogInfo;} + }, + + + checkForRFC6764Request: async function (path, eventLogInfo) { + function checkDefaultSecPort (sec) { + return sec ? "443" : "80"; + } + + if (!this.isRFC6764Request(path)) { + return path; + } + + let parts = path.toLowerCase().split("6764://"); + let type = parts[0].endsWith("caldav") ? "caldav" : "carddav"; + + // obey preselected security level for DNS lookup + // and only use insecure option if specified + let scheme = parts[0].startsWith("httpca") ? "http" : "https"; //httpcaldav or httpcarddav = httpca = http + let sec = (scheme == "https"); + + let hostPath = parts[1]; + while (hostPath.endsWith("/")) { hostPath = hostPath.slice(0,-1); } + let host = hostPath.split("/")[0]; + + let result = {}; + + //only perform dns lookup, if the provided path does not contain any path information + if (host == hostPath) { + let request = "_" + type + (sec ? "s" : "") + "._tcp." + host; + + // get host from SRV record + let rv = await DNS.srv(request); + if (rv && Array.isArray(rv) && rv.length>0 && rv[0].host) { + result.secure = sec; + result.host = rv[0].host + ((checkDefaultSecPort(sec) == rv[0].port) ? "" : ":" + rv[0].port); + TbSync.eventlog.add("info", eventLogInfo, "RFC6764 DNS request succeeded", "SRV record @ " + request + "\n" + JSON.stringify(rv[0])); + + // Now try to get path from TXT + rv = await DNS.txt(request); + if (rv && Array.isArray(rv) && rv.length>0 && rv[0].data && rv[0].data.toLowerCase().startsWith("path=")) { + result.path = rv[0].data.substring(5); + TbSync.eventlog.add("info", eventLogInfo, "RFC6764 DNS request succeeded", "TXT record @ " + request + "\n" + JSON.stringify(rv[0])); + } else { + result.path = "/.well-known/" + type; + } + + result.url = "http" + (result.secure ? "s" : "") + "://" + result.host + result.path; + return result.url; + } else { + TbSync.eventlog.add("warning", eventLogInfo, "RFC6764 DNS request failed", "SRV record @ " + request); + } + } + + // use the provided hostPath and build standard well-known url + return scheme + "://" + hostPath + "/.well-known/" + type; + }, + + startsWithScheme: function (url) { + return (url.toLowerCase().startsWith("http://") || url.toLowerCase().startsWith("https://") || this.isRFC6764Request(url)); + }, + + isRFC6764Request: function (url) { + let parts = url.split("6764://"); + return (parts.length == 2 && parts[0].endsWith("dav")); + }, + + sendRequest: async function (requestData, path, method, connectionData, headers = {}, options = {}) { + let url = await this.checkForRFC6764Request(path, connectionData.eventLogInfo); + let enforcedPermanentlyRedirectedUrl = (url != path) ? url : null; + + // path could be absolute or relative, we may need to rebuild the full url. + if (url.startsWith("http://") || url.startsWith("https://")) { + // extract segments from url + let uri = Services.io.newURI(url); + connectionData.https = (uri.scheme == "https"); + connectionData.fqdn = uri.hostPort; + } else { + url = "http" + (connectionData.https ? "s" : "") + "://" + connectionData.fqdn + url; + } + + let currentSyncState = connectionData.accountData ? connectionData.accountData.syncData.getSyncState().state : ""; + let accountID = connectionData.accountData ? connectionData.accountData.accountID : ""; + + // Loop: Prompt user for password and retry + const MAX_RETRIES = options.hasOwnProperty("passwordRetries") ? options.passwordRetries+1 : 5; + for (let i=1; i <= MAX_RETRIES; i++) { + TbSync.dump("URL Request #" + i, url); + + connectionData.url = url; + + // Restore original syncstate before open the connection + if (connectionData.accountData && currentSyncState != connectionData.accountData.syncData.getSyncState().state) { + connectionData.accountData.syncData.setSyncState(currentSyncState); + } + + let r = await dav.network.promisifiedHttpRequest(requestData, method, connectionData, headers, options); + if (r && enforcedPermanentlyRedirectedUrl && !r.permanentlyRedirectedUrl) { + r.permanentlyRedirectedUrl = enforcedPermanentlyRedirectedUrl; + } + + if (r && r.passwordPrompt && r.passwordPrompt === true) { + if (i == MAX_RETRIES) { + // If this is the final retry, abort with error. + throw r.passwordError; + } else { + let credentials = null; + let retry = false; + + // Prompt, if connection belongs to an account (and not from the create wizard) + if (connectionData.accountData) { + let promptData = { + windowID: "auth:" + connectionData.accountData.accountID, + accountname: connectionData.accountData.getAccountProperty("accountname"), + usernameLocked: connectionData.accountData.isConnected(), + username: connectionData.username, + } + connectionData.accountData.syncData.setSyncState("passwordprompt"); + + credentials = await TbSync.passwordManager.asyncPasswordPrompt(promptData, dav.openWindows); + if (credentials) { + // update login data + dav.network.getAuthData(connectionData.accountData).updateLoginData(credentials.username, credentials.password); + // update connection data + connectionData.username = credentials.username; + connectionData.password = credentials.password; + retry = true; + } + } + + if (!retry) { + throw r.passwordError; + } + + } + } else { + return r; + } + } + }, + + // Promisified implementation of TbSync's HttpRequest (with XHR interface) + promisifiedHttpRequest: function (requestData, method, connectionData, headers, options) { + let responseData = ""; + + //do not log HEADERS, as it could contain an Authorization header + //TbSync.dump("HEADERS", JSON.stringify(headers)); + if (TbSync.prefs.getIntPref("log.userdatalevel") > 1) TbSync.dump("REQUEST", method + " : " + requestData); + + if (!options.hasOwnProperty("softfail")) { + options.softfail = []; + } + + if (!options.hasOwnProperty("responseType")) { + options.responseType = "xml"; + } + + return new Promise(function(resolve, reject) { + let req = new HttpRequest(); + + req.timeout = connectionData.timeout; + req.mozBackgroundRequest = true; + + req.open(method, connectionData.url, true, connectionData.username, connectionData.password); + + if (options.hasOwnProperty("containerRealm")) req.setContainerRealm(options.containerRealm); + if (options.hasOwnProperty("containerReset") && options.containerReset == true) req.clearContainerCache(); + + if (headers) { + for (let header in headers) { + req.setRequestHeader(header, headers[header]); + } + } + + if (options.responseType == "base64") { + req.responseAsBase64 = true; + } + + req.setRequestHeader("User-Agent", dav.sync.prefSettings.getCharPref("clientID.useragent")); + + req.realmCallback = function(username, realm, host) { + // Store realm, needed later to setup lightning passwords. + TbSync.dump("Found CalDAV authRealm for <"+host+">", realm); + connectionData.realm = realm; + }; + + req.onerror = function () { + let error = TbSync.network.createTCPErrorFromFailedXHR(req); + if (!error) { + return reject(dav.sync.finish("error", "networkerror", "URL:\n" + connectionData.url + " ("+method+")")); //reject/resolve do not terminate control flow + } else { + return reject(dav.sync.finish("error", error, "URL:\n" + connectionData.url + " ("+method+")")); + } + }; + + req.ontimeout = req.onerror; + + req.onredirect = function(flags, uri) { + console.log("Redirect ("+ flags.toString(2) +"): " + uri.spec); + // Update connection settings from current URL + let newHttps = (uri.scheme == "https"); + if (connectionData.https != newHttps) { + TbSync.dump("Updating HTTPS", connectionData.https + " -> " + newHttps); + connectionData.https = newHttps; + } + if (connectionData.fqdn !=uri.hostPort) { + TbSync.dump("Updating FQDN", connectionData.fqdn + " -> " + uri.hostPort); + connectionData.fqdn = uri.hostPort; + } + }; + + req.onload = function() { + if (TbSync.prefs.getIntPref("log.userdatalevel") > 1) TbSync.dump("RESPONSE", req.status + " ("+req.statusText+")" + " : " + req.responseText); + responseData = req.responseText.split("><").join(">\n<"); + + let commLog = "URL:\n" + connectionData.url + " ("+method+")" + "\n\nRequest:\n" + requestData + "\n\nResponse:\n" + responseData; + let aResult = req.responseText; + let responseStatus = req.status; + + switch(responseStatus) { + case 401: //AuthError + { + let response = {}; + response.passwordPrompt = true; + response.passwordError = dav.sync.finish("error", responseStatus, commLog); + return resolve(response); + } + break; + + case 207: //preprocess multiresponse + { + let xml = dav.tools.convertToXML(aResult); + if (xml === null) return reject(dav.sync.finish("warning", "malformed-xml", commLog)); + + let response = {}; + response.davOptions = req.getResponseHeader("dav"); + response.responseURL = req.responseURL; + response.permanentlyRedirectedUrl = req.permanentlyRedirectedUrl; + response.commLog = commLog; + response.node = xml.documentElement; + + let multi = xml.documentElement.getElementsByTagNameNS(dav.sync.ns.d, "response"); + response.multi = []; + for (let i=0; i < multi.length; i++) { + let hrefNode = dav.tools.evaluateNode(multi[i], [["d","href"]]); + let responseStatusNode = dav.tools.evaluateNode(multi[i], [["d", "status"]]); + let propstats = multi[i].getElementsByTagNameNS(dav.sync.ns.d, "propstat"); + if (propstats.length > 0) { + //response contains propstats, push each as single entry + for (let p=0; p < propstats.length; p++) { + let statusNode = dav.tools.evaluateNode(propstats[p], [["d", "status"]]); + + let resp = {}; + resp.node = propstats[p]; + resp.status = statusNode === null ? null : statusNode.textContent.split(" ")[1]; + resp.responsestatus = responseStatusNode === null ? null : responseStatusNode.textContent.split(" ")[1]; + resp.href = hrefNode === null ? null : hrefNode.textContent; + response.multi.push(resp); + } + } else { + //response does not contain any propstats, push raw response + let resp = {}; + resp.node = multi[i]; + resp.status = responseStatusNode === null ? null : responseStatusNode.textContent.split(" ")[1]; + resp.responsestatus = responseStatusNode === null ? null : responseStatusNode.textContent.split(" ")[1]; + resp.href = hrefNode === null ? null : hrefNode.textContent; + response.multi.push(resp); + } + } + + return resolve(response); + } + + + case 200: //returned by DELETE by radicale - watch this !!! + return resolve(aResult); + + case 204: //is returned by DELETE - no data + case 201: //is returned by CREATE - no data + return resolve(null); + break; + + default: + if (options.softfail.includes(responseStatus)) { + let noresponse = {}; + noresponse.softerror = responseStatus; + let xml = dav.tools.convertToXML(aResult); + if (xml !== null) { + let exceptionNode = dav.tools.evaluateNode(xml.documentElement, [["s","exception"]]); + if (exceptionNode !== null) { + noresponse.exception = exceptionNode.textContent; + } + } + //manually log this non-fatal error + TbSync.eventlog.add("info", connectionData.eventLogInfo, "softerror::"+responseStatus, commLog); + return resolve(noresponse); + } else { + return reject(dav.sync.finish("warning", responseStatus, commLog)); + } + break; + + } + }; + + req.send(requestData); + }); + } +} |