1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/payment/Payment.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,391 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; 1.11 + 1.12 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.13 +Cu.import("resource://gre/modules/Services.jsm"); 1.14 + 1.15 +this.EXPORTED_SYMBOLS = []; 1.16 + 1.17 +const PAYMENT_IPC_MSG_NAMES = ["Payment:Pay", 1.18 + "Payment:Success", 1.19 + "Payment:Failed"]; 1.20 + 1.21 +const PREF_PAYMENTPROVIDERS_BRANCH = "dom.payment.provider."; 1.22 +const PREF_PAYMENT_BRANCH = "dom.payment."; 1.23 +const PREF_DEBUG = "dom.payment.debug"; 1.24 + 1.25 +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", 1.26 + "@mozilla.org/parentprocessmessagemanager;1", 1.27 + "nsIMessageListenerManager"); 1.28 + 1.29 +XPCOMUtils.defineLazyServiceGetter(this, "prefService", 1.30 + "@mozilla.org/preferences-service;1", 1.31 + "nsIPrefService"); 1.32 + 1.33 +let PaymentManager = { 1.34 + init: function init() { 1.35 + // Payment providers data are stored as a preference. 1.36 + this.registeredProviders = null; 1.37 + 1.38 + this.messageManagers = {}; 1.39 + 1.40 + // The dom.payment.skipHTTPSCheck pref is supposed to be used only during 1.41 + // development process. This preference should not be active for a 1.42 + // production build. 1.43 + let paymentPrefs = prefService.getBranch(PREF_PAYMENT_BRANCH); 1.44 + this.checkHttps = true; 1.45 + try { 1.46 + if (paymentPrefs.getPrefType("skipHTTPSCheck")) { 1.47 + this.checkHttps = !paymentPrefs.getBoolPref("skipHTTPSCheck"); 1.48 + } 1.49 + } catch(e) {} 1.50 + 1.51 + for each (let msgname in PAYMENT_IPC_MSG_NAMES) { 1.52 + ppmm.addMessageListener(msgname, this); 1.53 + } 1.54 + 1.55 + Services.obs.addObserver(this, "xpcom-shutdown", false); 1.56 + 1.57 + try { 1.58 + this._debug = 1.59 + Services.prefs.getPrefType(PREF_DEBUG) == Ci.nsIPrefBranch.PREF_BOOL 1.60 + && Services.prefs.getBoolPref(PREF_DEBUG); 1.61 + } catch(e) { 1.62 + this._debug = false; 1.63 + } 1.64 + }, 1.65 + 1.66 + /** 1.67 + * Process a message from the content process. 1.68 + */ 1.69 + receiveMessage: function receiveMessage(aMessage) { 1.70 + let name = aMessage.name; 1.71 + let msg = aMessage.json; 1.72 + if (this._debug) { 1.73 + this.LOG("Received '" + name + "' message from content process"); 1.74 + } 1.75 + 1.76 + switch (name) { 1.77 + case "Payment:Pay": { 1.78 + // First of all, we register the payment providers. 1.79 + if (!this.registeredProviders) { 1.80 + this.registeredProviders = {}; 1.81 + this.registerPaymentProviders(); 1.82 + } 1.83 + 1.84 + // We save the message target message manager so we can later dispatch 1.85 + // back messages without broadcasting to all child processes. 1.86 + let requestId = msg.requestId; 1.87 + this.messageManagers[requestId] = aMessage.target; 1.88 + 1.89 + // We check the jwt type and look for a match within the 1.90 + // registered payment providers to get the correct payment request 1.91 + // information. 1.92 + let paymentRequests = []; 1.93 + let jwtTypes = []; 1.94 + for (let i in msg.jwts) { 1.95 + let pr = this.getPaymentRequestInfo(requestId, msg.jwts[i]); 1.96 + if (!pr) { 1.97 + continue; 1.98 + } 1.99 + // We consider jwt type repetition an error. 1.100 + if (jwtTypes[pr.type]) { 1.101 + this.paymentFailed(requestId, 1.102 + "PAY_REQUEST_ERROR_DUPLICATED_JWT_TYPE"); 1.103 + return; 1.104 + } 1.105 + jwtTypes[pr.type] = true; 1.106 + paymentRequests.push(pr); 1.107 + } 1.108 + 1.109 + if (!paymentRequests.length) { 1.110 + this.paymentFailed(requestId, 1.111 + "PAY_REQUEST_ERROR_NO_VALID_REQUEST_FOUND"); 1.112 + return; 1.113 + } 1.114 + 1.115 + // After getting the list of valid payment requests, we ask the user 1.116 + // for confirmation before sending any request to any payment provider. 1.117 + // If there is more than one choice, we also let the user select the one 1.118 + // that he prefers. 1.119 + let glue = Cc["@mozilla.org/payment/ui-glue;1"] 1.120 + .createInstance(Ci.nsIPaymentUIGlue); 1.121 + if (!glue) { 1.122 + if (this._debug) { 1.123 + this.LOG("Could not create nsIPaymentUIGlue instance"); 1.124 + } 1.125 + this.paymentFailed(requestId, 1.126 + "INTERNAL_ERROR_CREATE_PAYMENT_GLUE_FAILED"); 1.127 + return; 1.128 + } 1.129 + 1.130 + let confirmPaymentSuccessCb = function successCb(aRequestId, 1.131 + aResult) { 1.132 + // Get the appropriate payment provider data based on user's choice. 1.133 + let selectedProvider = this.registeredProviders[aResult]; 1.134 + if (!selectedProvider || !selectedProvider.uri) { 1.135 + if (this._debug) { 1.136 + this.LOG("Could not retrieve a valid provider based on user's " + 1.137 + "selection"); 1.138 + } 1.139 + this.paymentFailed(aRequestId, 1.140 + "INTERNAL_ERROR_NO_VALID_SELECTED_PROVIDER"); 1.141 + return; 1.142 + } 1.143 + 1.144 + let jwt; 1.145 + for (let i in paymentRequests) { 1.146 + if (paymentRequests[i].type == aResult) { 1.147 + jwt = paymentRequests[i].jwt; 1.148 + break; 1.149 + } 1.150 + } 1.151 + if (!jwt) { 1.152 + if (this._debug) { 1.153 + this.LOG("The selected request has no JWT information " + 1.154 + "associated"); 1.155 + } 1.156 + this.paymentFailed(aRequestId, 1.157 + "INTERNAL_ERROR_NO_JWT_ASSOCIATED_TO_REQUEST"); 1.158 + return; 1.159 + } 1.160 + 1.161 + this.showPaymentFlow(aRequestId, selectedProvider, jwt); 1.162 + }; 1.163 + 1.164 + let confirmPaymentErrorCb = this.paymentFailed; 1.165 + 1.166 + glue.confirmPaymentRequest(requestId, 1.167 + paymentRequests, 1.168 + confirmPaymentSuccessCb.bind(this), 1.169 + confirmPaymentErrorCb.bind(this)); 1.170 + break; 1.171 + } 1.172 + case "Payment:Success": 1.173 + case "Payment:Failed": { 1.174 + let mm = this.messageManagers[msg.requestId]; 1.175 + mm.sendAsyncMessage(name, { 1.176 + requestId: msg.requestId, 1.177 + result: msg.result, 1.178 + errorMsg: msg.errorMsg 1.179 + }); 1.180 + break; 1.181 + } 1.182 + } 1.183 + }, 1.184 + 1.185 + /** 1.186 + * Helper function to register payment providers stored as preferences. 1.187 + */ 1.188 + registerPaymentProviders: function registerPaymentProviders() { 1.189 + let paymentProviders = prefService 1.190 + .getBranch(PREF_PAYMENTPROVIDERS_BRANCH) 1.191 + .getChildList(""); 1.192 + 1.193 + // First get the numbers of the providers by getting all ###.uri prefs. 1.194 + let nums = []; 1.195 + for (let i in paymentProviders) { 1.196 + let match = /^(\d+)\.uri$/.exec(paymentProviders[i]); 1.197 + if (!match) { 1.198 + continue; 1.199 + } else { 1.200 + nums.push(match[1]); 1.201 + } 1.202 + } 1.203 + 1.204 + // Now register the payment providers. 1.205 + for (let i in nums) { 1.206 + let branch = prefService 1.207 + .getBranch(PREF_PAYMENTPROVIDERS_BRANCH + nums[i] + "."); 1.208 + let vals = branch.getChildList(""); 1.209 + if (vals.length == 0) { 1.210 + return; 1.211 + } 1.212 + try { 1.213 + let type = branch.getCharPref("type"); 1.214 + if (type in this.registeredProviders) { 1.215 + continue; 1.216 + } 1.217 + this.registeredProviders[type] = { 1.218 + name: branch.getCharPref("name"), 1.219 + uri: branch.getCharPref("uri"), 1.220 + description: branch.getCharPref("description"), 1.221 + requestMethod: branch.getCharPref("requestMethod") 1.222 + }; 1.223 + if (this._debug) { 1.224 + this.LOG("Registered Payment Providers: " + 1.225 + JSON.stringify(this.registeredProviders[type])); 1.226 + } 1.227 + } catch (ex) { 1.228 + if (this._debug) { 1.229 + this.LOG("An error ocurred registering a payment provider. " + ex); 1.230 + } 1.231 + } 1.232 + } 1.233 + }, 1.234 + 1.235 + /** 1.236 + * Helper for sending a Payment:Failed message to the parent process. 1.237 + */ 1.238 + paymentFailed: function paymentFailed(aRequestId, aErrorMsg) { 1.239 + let mm = this.messageManagers[aRequestId]; 1.240 + mm.sendAsyncMessage("Payment:Failed", { 1.241 + requestId: aRequestId, 1.242 + errorMsg: aErrorMsg 1.243 + }); 1.244 + }, 1.245 + 1.246 + /** 1.247 + * Helper function to get the payment request info according to the jwt 1.248 + * type. Payment provider's data is stored as a preference. 1.249 + */ 1.250 + getPaymentRequestInfo: function getPaymentRequestInfo(aRequestId, aJwt) { 1.251 + if (!aJwt) { 1.252 + this.paymentFailed(aRequestId, "INTERNAL_ERROR_CALL_WITH_MISSING_JWT"); 1.253 + return true; 1.254 + } 1.255 + 1.256 + // First thing, we check that the jwt type is an allowed type and has a 1.257 + // payment provider flow information associated. 1.258 + 1.259 + // A jwt string consists in three parts separated by period ('.'): header, 1.260 + // payload and signature. 1.261 + let segments = aJwt.split('.'); 1.262 + if (segments.length !== 3) { 1.263 + if (this._debug) { 1.264 + this.LOG("Error getting payment provider's uri. " + 1.265 + "Not enough or too many segments"); 1.266 + } 1.267 + this.paymentFailed(aRequestId, 1.268 + "PAY_REQUEST_ERROR_WRONG_SEGMENTS_COUNT"); 1.269 + return true; 1.270 + } 1.271 + 1.272 + let payloadObject; 1.273 + try { 1.274 + // We only care about the payload segment, which contains the jwt type 1.275 + // that should match with any of the stored payment provider's data and 1.276 + // the payment request information to be shown to the user. 1.277 + // Before decoding the JWT string we need to normalize it to be compliant 1.278 + // with RFC 4648. 1.279 + segments[1] = segments[1].replace("-", "+", "g").replace("_", "/", "g"); 1.280 + let payload = atob(segments[1]); 1.281 + if (this._debug) { 1.282 + this.LOG("Payload " + payload); 1.283 + } 1.284 + if (!payload.length) { 1.285 + this.paymentFailed(aRequestId, "PAY_REQUEST_ERROR_EMPTY_PAYLOAD"); 1.286 + return true; 1.287 + } 1.288 + payloadObject = JSON.parse(payload); 1.289 + if (!payloadObject) { 1.290 + this.paymentFailed(aRequestId, 1.291 + "PAY_REQUEST_ERROR_ERROR_PARSING_JWT_PAYLOAD"); 1.292 + return true; 1.293 + } 1.294 + } catch (e) { 1.295 + this.paymentFailed(aRequestId, 1.296 + "PAY_REQUEST_ERROR_ERROR_DECODING_JWT"); 1.297 + return true; 1.298 + } 1.299 + 1.300 + if (!payloadObject.typ) { 1.301 + this.paymentFailed(aRequestId, 1.302 + "PAY_REQUEST_ERROR_NO_TYP_PARAMETER"); 1.303 + return true; 1.304 + } 1.305 + 1.306 + if (!payloadObject.request) { 1.307 + this.paymentFailed(aRequestId, 1.308 + "PAY_REQUEST_ERROR_NO_REQUEST_PARAMETER"); 1.309 + return true; 1.310 + } 1.311 + 1.312 + // Once we got the jwt 'typ' value we look for a match within the payment 1.313 + // providers stored preferences. If the jwt 'typ' is not recognized as one 1.314 + // of the allowed values for registered payment providers, we skip the jwt 1.315 + // validation but we don't fire any error. This way developers might have 1.316 + // a default set of well formed JWTs that might be used in different B2G 1.317 + // devices with a different set of allowed payment providers. 1.318 + let provider = this.registeredProviders[payloadObject.typ]; 1.319 + if (!provider) { 1.320 + if (this._debug) { 1.321 + this.LOG("Not registered payment provider for jwt type: " + 1.322 + payloadObject.typ); 1.323 + } 1.324 + return false; 1.325 + } 1.326 + 1.327 + if (!provider.uri || !provider.name) { 1.328 + this.paymentFailed(aRequestId, 1.329 + "INTERNAL_ERROR_WRONG_REGISTERED_PAY_PROVIDER"); 1.330 + return true; 1.331 + } 1.332 + 1.333 + // We only allow https for payment providers uris. 1.334 + if (this.checkHttps && !/^https/.exec(provider.uri.toLowerCase())) { 1.335 + // We should never get this far. 1.336 + if (this._debug) { 1.337 + this.LOG("Payment provider uris must be https: " + provider.uri); 1.338 + } 1.339 + this.paymentFailed(aRequestId, 1.340 + "INTERNAL_ERROR_NON_HTTPS_PROVIDER_URI"); 1.341 + return true; 1.342 + } 1.343 + 1.344 + let pldRequest = payloadObject.request; 1.345 + return { jwt: aJwt, type: payloadObject.typ, providerName: provider.name }; 1.346 + }, 1.347 + 1.348 + showPaymentFlow: function showPaymentFlow(aRequestId, 1.349 + aPaymentProvider, 1.350 + aJwt) { 1.351 + let paymentFlowInfo = Cc["@mozilla.org/payment/flow-info;1"] 1.352 + .createInstance(Ci.nsIPaymentFlowInfo); 1.353 + paymentFlowInfo.uri = aPaymentProvider.uri; 1.354 + paymentFlowInfo.requestMethod = aPaymentProvider.requestMethod; 1.355 + paymentFlowInfo.jwt = aJwt; 1.356 + 1.357 + let glue = Cc["@mozilla.org/payment/ui-glue;1"] 1.358 + .createInstance(Ci.nsIPaymentUIGlue); 1.359 + if (!glue) { 1.360 + if (this._debug) { 1.361 + this.LOG("Could not create nsIPaymentUIGlue instance"); 1.362 + } 1.363 + this.paymentFailed(aRequestId, 1.364 + "INTERNAL_ERROR_CREATE_PAYMENT_GLUE_FAILED"); 1.365 + return false; 1.366 + } 1.367 + glue.showPaymentFlow(aRequestId, 1.368 + paymentFlowInfo, 1.369 + this.paymentFailed.bind(this)); 1.370 + }, 1.371 + 1.372 + // nsIObserver 1.373 + 1.374 + observe: function observe(subject, topic, data) { 1.375 + if (topic == "xpcom-shutdown") { 1.376 + for each (let msgname in PAYMENT_IPC_MSG_NAMES) { 1.377 + ppmm.removeMessageListener(msgname, this); 1.378 + } 1.379 + this.registeredProviders = null; 1.380 + this.messageManagers = null; 1.381 + 1.382 + Services.obs.removeObserver(this, "xpcom-shutdown"); 1.383 + } 1.384 + }, 1.385 + 1.386 + LOG: function LOG(s) { 1.387 + if (!this._debug) { 1.388 + return; 1.389 + } 1.390 + dump("-*- PaymentManager: " + s + "\n"); 1.391 + } 1.392 +}; 1.393 + 1.394 +PaymentManager.init();