/* Copyright 2012 Mozilla Foundation and Mozilla contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* Copyright © 2013, Deutsche Telekom, Inc. */ "use strict"; const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); let NFC = {}; Cu.import("resource://gre/modules/nfc_consts.js", NFC); Cu.import("resource://gre/modules/systemlibs.js"); const NFC_ENABLED = libcutils.property_get("ro.moz.nfc.enabled", "false") === "true"; // set to true in nfc_consts.js to see debug messages let DEBUG = NFC.DEBUG_NFC; let debug; if (DEBUG) { debug = function (s) { dump("-*- Nfc: " + s + "\n"); }; } else { debug = function (s) {}; } const NFC_CONTRACTID = "@mozilla.org/nfc;1"; const NFC_CID = Components.ID("{2ff24790-5e74-11e1-b86c-0800200c9a66}"); const NFC_IPC_MSG_NAMES = [ "NFC:SetSessionToken", "NFC:ReadNDEF", "NFC:WriteNDEF", "NFC:GetDetailsNDEF", "NFC:MakeReadOnlyNDEF", "NFC:Connect", "NFC:Close", "NFC:SendFile" ]; const NFC_IPC_PEER_MSG_NAMES = [ "NFC:RegisterPeerTarget", "NFC:UnregisterPeerTarget", "NFC:CheckP2PRegistration", "NFC:NotifyUserAcceptedP2P" ]; XPCOMUtils.defineLazyServiceGetter(this, "ppmm", "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster"); XPCOMUtils.defineLazyServiceGetter(this, "gSystemMessenger", "@mozilla.org/system-message-internal;1", "nsISystemMessagesInternal"); XPCOMUtils.defineLazyServiceGetter(this, "gSystemWorkerManager", "@mozilla.org/telephony/system-worker-manager;1", "nsISystemWorkerManager"); XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService", "@mozilla.org/settingsService;1", "nsISettingsService"); XPCOMUtils.defineLazyServiceGetter(this, "UUIDGenerator", "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"); XPCOMUtils.defineLazyGetter(this, "gMessageManager", function () { return { QueryInterface: XPCOMUtils.generateQI([Ci.nsIMessageListener, Ci.nsIObserver]), nfc: null, // Manage message targets in terms of sessionToken. Only the authorized and // registered contents can receive related messages. targetsBySessionTokens: {}, sessionTokens: [], // Manage registered Peer Targets peerTargetsMap: {}, currentPeerAppId: null, init: function init(nfc) { this.nfc = nfc; Services.obs.addObserver(this, "xpcom-shutdown", false); this._registerMessageListeners(); }, _shutdown: function _shutdown() { this.nfc = null; Services.obs.removeObserver(this, "xpcom-shutdown"); this._unregisterMessageListeners(); }, _registerMessageListeners: function _registerMessageListeners() { ppmm.addMessageListener("child-process-shutdown", this); for (let msgname of NFC_IPC_MSG_NAMES) { ppmm.addMessageListener(msgname, this); } for (let msgname of NFC_IPC_PEER_MSG_NAMES) { ppmm.addMessageListener(msgname, this); } }, _unregisterMessageListeners: function _unregisterMessageListeners() { ppmm.removeMessageListener("child-process-shutdown", this); for (let msgname of NFC_IPC_MSG_NAMES) { ppmm.removeMessageListener(msgname, this); } for (let msgname of NFC_IPC_PEER_MSG_NAMES) { ppmm.removeMessageListener(msgname, this); } ppmm = null; }, _registerMessageTarget: function _registerMessageTarget(sessionToken, target) { let targets = this.targetsBySessionTokens[sessionToken]; if (!targets) { targets = this.targetsBySessionTokens[sessionToken] = []; let list = this.sessionTokens; if (list.indexOf(sessionToken) == -1) { list.push(sessionToken); } } if (targets.indexOf(target) != -1) { debug("Already registered this target!"); return; } targets.push(target); debug("Registered :" + sessionToken + " target: " + target); }, _unregisterMessageTarget: function _unregisterMessageTarget(sessionToken, target) { if (sessionToken == null) { // Unregister the target for every sessionToken when no sessionToken is specified. for (let session of this.sessionTokens) { this._unregisterMessageTarget(session, target); } return; } // Unregister the target for a specified sessionToken. let targets = this.targetsBySessionTokens[sessionToken]; if (!targets) { return; } if (target == null) { debug("Unregistered all targets for the " + sessionToken + " targets: " + targets); targets = []; let list = this.sessionTokens; if (sessionToken !== null) { let index = list.indexOf(sessionToken); if (index > -1) { list.splice(index, 1); } } return; } let index = targets.indexOf(target); if (index != -1) { targets.splice(index, 1); } }, _sendTargetMessage: function _sendTargetMessage(sessionToken, message, options) { let targets = this.targetsBySessionTokens[sessionToken]; if (!targets) { return; } for (let target of targets) { target.sendAsyncMessage(message, options); } }, registerPeerTarget: function registerPeerTarget(msg) { let appInfo = msg.json; // Sanity check on PeerEvent if (!this.isValidPeerEvent(appInfo.event)) { return; } let targets = this.peerTargetsMap; let targetInfo = targets[appInfo.appId]; // If the application Id is already registered if (targetInfo) { // If the event is not registered if (targetInfo.event !== appInfo.event) { // Update the event field ONLY targetInfo.event |= appInfo.event; } // Otherwise event is already registered, return! return; } // Target not registered yet! Add to the target map // Registered targetInfo target consists of 2 fields (values) // target : Target to notify the right content for peer notifications // event : NFC_PEER_EVENT_READY (0x01) Or NFC_PEER_EVENT_LOST (0x02) let newTargetInfo = { target : msg.target, event : appInfo.event }; targets[appInfo.appId] = newTargetInfo; }, unregisterPeerTarget: function unregisterPeerTarget(msg) { let appInfo = msg.json; // Sanity check on PeerEvent if (!this.isValidPeerEvent(appInfo.event)) { return; } let targets = this.peerTargetsMap; let targetInfo = targets[appInfo.appId]; if (targetInfo) { // Application Id registered and the event exactly matches. if (targetInfo.event === appInfo.event) { // Remove the target from the list of registered targets delete targets[appInfo.appId] } else { // Otherwise, update the event field ONLY, by removing the event flag targetInfo.event &= ~appInfo.event; } } }, removePeerTarget: function removePeerTarget(target) { let targets = this.peerTargetsMap; Object.keys(targets).forEach((appId) => { let targetInfo = targets[appId]; if (targetInfo && targetInfo.target === target) { // Remove the target from the list of registered targets delete targets[appId]; } }); }, isRegisteredP2PTarget: function isRegisteredP2PTarget(appId, event) { let targetInfo = this.peerTargetsMap[appId]; // Check if it is a registered target for the 'event' return ((targetInfo != null) && (targetInfo.event & event !== 0)); }, notifyPeerEvent: function notifyPeerEvent(appId, event) { let targetInfo = this.peerTargetsMap[appId]; // Check if the application id is a registeredP2PTarget if (this.isRegisteredP2PTarget(appId, event)) { targetInfo.target.sendAsyncMessage("NFC:PeerEvent", { event: event, sessionToken: this.nfc.sessionTokenMap[this.nfc._currentSessionId] }); return; } debug("Application ID : " + appId + " is not a registered target" + "for the event " + event + " notification"); }, isValidPeerEvent: function isValidPeerEvent(event) { // Valid values : 0x01, 0x02 Or 0x03 return ((event === NFC.NFC_PEER_EVENT_READY) || (event === NFC.NFC_PEER_EVENT_LOST) || (event === (NFC.NFC_PEER_EVENT_READY | NFC.NFC_PEER_EVENT_LOST))); }, /** * nsIMessageListener interface methods. */ receiveMessage: function receiveMessage(msg) { debug("Received '" + msg.name + "' message from content process"); if (msg.name == "child-process-shutdown") { // By the time we receive child-process-shutdown, the child process has // already forgotten its permissions so we need to unregister the target // for every permission. this._unregisterMessageTarget(null, msg.target); this.removePeerTarget(msg.target); return null; } if (NFC_IPC_MSG_NAMES.indexOf(msg.name) != -1) { if (!msg.target.assertPermission("nfc-read")) { debug("Nfc message " + msg.name + " from a content process with no 'nfc-read' privileges."); return null; } } else if (NFC_IPC_PEER_MSG_NAMES.indexOf(msg.name) != -1) { if (!msg.target.assertPermission("nfc-write")) { debug("Nfc Peer message " + msg.name + " from a content process with no 'nfc-write' privileges."); return null; } // Add extra permission check for below IPC Peer events: // 'NFC:CheckP2PRegistration' , 'NFC:NotifyUserAcceptedP2P' if ((msg.name == "NFC:CheckP2PRegistration") || (msg.name == "NFC:NotifyUserAcceptedP2P")) { // ONLY privileged Content can send these events if (!msg.target.assertPermission("nfc-manager")) { debug("NFC message " + message.name + " from a content process with no 'nfc-manager' privileges."); return null; } } } else { debug("Ignoring unknown message type: " + msg.name); return null; } switch (msg.name) { case "NFC:SetSessionToken": this._registerMessageTarget(this.nfc.sessionTokenMap[this.nfc._currentSessionId], msg.target); debug("Registering target for this SessionToken : " + this.nfc.sessionTokenMap[this.nfc._currentSessionId]); break; case "NFC:RegisterPeerTarget": this.registerPeerTarget(msg); break; case "NFC:UnregisterPeerTarget": this.unregisterPeerTarget(msg); break; case "NFC:CheckP2PRegistration": // Check if the application id is a valid registered target. // (It should have registered for NFC_PEER_EVENT_READY). let isRegistered = this.isRegisteredP2PTarget(msg.json.appId, NFC.NFC_PEER_EVENT_READY); // Remember the current AppId if registered. this.currentPeerAppId = (isRegistered) ? msg.json.appId : null; let status = (isRegistered) ? NFC.GECKO_NFC_ERROR_SUCCESS : NFC.GECKO_NFC_ERROR_GENERIC_FAILURE; // Notify the content process immediately of the status msg.target.sendAsyncMessage(msg.name + "Response", { status: status, requestId: msg.json.requestId }); break; case "NFC:NotifyUserAcceptedP2P": // Notify the 'NFC_PEER_EVENT_READY' since user has acknowledged this.notifyPeerEvent(msg.json.appId, NFC.NFC_PEER_EVENT_READY); break; } return null; }, /** * nsIObserver interface methods. */ observe: function observe(subject, topic, data) { switch (topic) { case "xpcom-shutdown": this._shutdown(); break; } }, sendNfcResponseMessage: function sendNfcResponseMessage(message, data) { this._sendTargetMessage(this.nfc.sessionTokenMap[this.nfc._currentSessionId], message, data); }, }; }); function Nfc() { debug("Starting Worker"); this.worker = new ChromeWorker("resource://gre/modules/nfc_worker.js"); this.worker.onerror = this.onerror.bind(this); this.worker.onmessage = this.onmessage.bind(this); for each (let msgname in NFC_IPC_MSG_NAMES) { ppmm.addMessageListener(msgname, this); } Services.obs.addObserver(this, NFC.TOPIC_MOZSETTINGS_CHANGED, false); Services.obs.addObserver(this, NFC.TOPIC_XPCOM_SHUTDOWN, false); gMessageManager.init(this); let lock = gSettingsService.createLock(); lock.get(NFC.SETTING_NFC_POWER_LEVEL, this); lock.get(NFC.SETTING_NFC_ENABLED, this); // Maps sessionId (that are generated from nfcd) with a unique guid : 'SessionToken' this.sessionTokenMap = {}; gSystemWorkerManager.registerNfcWorker(this.worker); } Nfc.prototype = { classID: NFC_CID, classInfo: XPCOMUtils.generateCI({classID: NFC_CID, classDescription: "Nfc", interfaces: [Ci.nsIWorkerHolder]}), QueryInterface: XPCOMUtils.generateQI([Ci.nsIWorkerHolder, Ci.nsIObserver, Ci.nsISettingsServiceCallback]), _currentSessionId: null, _enabled: false, onerror: function onerror(event) { debug("Got an error: " + event.filename + ":" + event.lineno + ": " + event.message + "\n"); event.preventDefault(); }, /** * Send arbitrary message to worker. * * @param nfcMessageType * A text message type. * @param message [optional] * An optional message object to send. */ sendToWorker: function sendToWorker(nfcMessageType, message) { message = message || {}; message.type = nfcMessageType; this.worker.postMessage(message); }, /** * Send Error response to content. * * @param message * An nsIMessageListener's message parameter. */ sendNfcErrorResponse: function sendNfcErrorResponse(message) { if (!message.target) { return; } let nfcMsgType = message.name + "Response"; message.target.sendAsyncMessage(nfcMsgType, { sessionId: message.json.sessionToken, requestId: message.json.requestId, status: NFC.GECKO_NFC_ERROR_GENERIC_FAILURE }); }, /** * Process the incoming message from the NFC worker */ onmessage: function onmessage(event) { let message = event.data; debug("Received message from NFC worker: " + JSON.stringify(message)); switch (message.type) { case "techDiscovered": this._currentSessionId = message.sessionId; // Check if the session token already exists. If exists, continue to use the same one. // If not, generate a new token. if (!this.sessionTokenMap[this._currentSessionId]) { this.sessionTokenMap[this._currentSessionId] = UUIDGenerator.generateUUID().toString(); } // Update the upper layers with a session token (alias) message.sessionToken = this.sessionTokenMap[this._currentSessionId]; // Do not expose the actual session to the content delete message.sessionId; gSystemMessenger.broadcastMessage("nfc-manager-tech-discovered", message); break; case "techLost": gMessageManager._unregisterMessageTarget(this.sessionTokenMap[this._currentSessionId], null); // Update the upper layers with a session token (alias) message.sessionToken = this.sessionTokenMap[this._currentSessionId]; // Do not expose the actual session to the content delete message.sessionId; gSystemMessenger.broadcastMessage("nfc-manager-tech-lost", message); // Notify 'PeerLost' to appropriate registered target, if any gMessageManager.notifyPeerEvent(this.currentPeerAppId, NFC.NFC_PEER_EVENT_LOST); delete this.sessionTokenMap[this._currentSessionId]; this._currentSessionId = null; this.currentPeerAppId = null; break; case "ConfigResponse": gSystemMessenger.broadcastMessage("nfc-powerlevel-change", message); break; case "ConnectResponse": // Fall through. case "CloseResponse": case "GetDetailsNDEFResponse": case "ReadNDEFResponse": case "MakeReadOnlyNDEFResponse": case "WriteNDEFResponse": message.sessionToken = this.sessionTokenMap[this._currentSessionId]; // Do not expose the actual session to the content delete message.sessionId; gMessageManager.sendNfcResponseMessage("NFC:" + message.type, message); break; default: throw new Error("Don't know about this message type: " + message.type); } }, // nsINfcWorker worker: null, powerLevel: NFC.NFC_POWER_LEVEL_DISABLED, sessionTokenMap: null, /** * Process a message from the content process. */ receiveMessage: function receiveMessage(message) { debug("Received '" + JSON.stringify(message) + "' message from content process"); if (!this._enabled) { debug("NFC is not enabled."); this.sendNfcErrorResponse(message); return null; } // Enforce bare minimums for NFC permissions switch (message.name) { case "NFC:Connect": // Fall through case "NFC:Close": case "NFC:GetDetailsNDEF": case "NFC:ReadNDEF": if (!message.target.assertPermission("nfc-read")) { debug("NFC message " + message.name + " from a content process with no 'nfc-read' privileges."); this.sendNfcErrorResponse(message); return null; } break; case "NFC:WriteNDEF": // Fall through case "NFC:MakeReadOnlyNDEF": case "NFC:SendFile": if (!message.target.assertPermission("nfc-write")) { debug("NFC message " + message.name + " from a content process with no 'nfc-write' privileges."); this.sendNfcErrorResponse(message); return null; } break; case "NFC:SetSessionToken": //Do nothing here. No need to process this message further return null; } // Sanity check on sessionId if (message.json.sessionToken !== this.sessionTokenMap[this._currentSessionId]) { debug("Invalid Session Token: " + message.json.sessionToken + " Expected Session Token: " + this.sessionTokenMap[this._currentSessionId]); this.sendNfcErrorResponse(message); return null; } // Update the current sessionId before sending to the worker message.sessionId = this._currentSessionId; switch (message.name) { case "NFC:GetDetailsNDEF": this.sendToWorker("getDetailsNDEF", message.json); break; case "NFC:ReadNDEF": this.sendToWorker("readNDEF", message.json); break; case "NFC:WriteNDEF": this.sendToWorker("writeNDEF", message.json); break; case "NFC:MakeReadOnlyNDEF": this.sendToWorker("makeReadOnlyNDEF", message.json); break; case "NFC:Connect": this.sendToWorker("connect", message.json); break; case "NFC:Close": this.sendToWorker("close", message.json); break; case "NFC:SendFile": // Chrome process is the arbitrator / mediator between // system app (content process) that issued nfc 'sendFile' operation // and system app that handles the system message : // 'nfc-manager-send-file'. System app subsequently handover's // the data to alternate carrier's (BT / WiFi) 'sendFile' interface. // Notify system app to initiate BT send file operation gSystemMessenger.broadcastMessage("nfc-manager-send-file", message.json); break; default: debug("UnSupported : Message Name " + message.name); return null; } }, /** * nsISettingsServiceCallback */ handle: function handle(aName, aResult) { switch(aName) { case NFC.SETTING_NFC_ENABLED: debug("'nfc.enabled' is now " + aResult); this._enabled = aResult; // General power setting let powerLevel = this._enabled ? NFC.NFC_POWER_LEVEL_ENABLED : NFC.NFC_POWER_LEVEL_DISABLED; // Only if the value changes, set the power config and persist if (powerLevel !== this.powerLevel) { debug("New Power Level " + powerLevel); this.setConfig({powerLevel: powerLevel}); this.powerLevel = powerLevel; } break; } }, /** * nsIObserver */ observe: function observe(subject, topic, data) { switch (topic) { case NFC.TOPIC_XPCOM_SHUTDOWN: for each (let msgname in NFC_IPC_MSG_NAMES) { ppmm.removeMessageListener(msgname, this); } ppmm = null; Services.obs.removeObserver(this, NFC.TOPIC_XPCOM_SHUTDOWN); break; case NFC.TOPIC_MOZSETTINGS_CHANGED: let setting = JSON.parse(data); if (setting) { let setting = JSON.parse(data); this.handle(setting.key, setting.value); } break; } }, setConfig: function setConfig(prop) { this.sendToWorker("config", prop); } }; if (NFC_ENABLED) { this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Nfc]); }