diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index fe6c7c4e77b..c92156a382a 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,11 +19,11 @@ - + - + @@ -98,7 +98,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index f919b48d085..5bedf541a9a 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + @@ -128,7 +128,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index 73b77055d24..5e8a59e6817 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index fe6c7c4e77b..c92156a382a 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,11 +19,11 @@ - + - + @@ -98,7 +98,7 @@ - + diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml index 60f10ce4022..eb3d067b59d 100644 --- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index d016eb11afb..a7307d0a9e1 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -4,6 +4,6 @@ "remote": "", "branch": "" }, - "revision": "92bddc1a99aa2d80d58fc28749fecedbf5c692f1", + "revision": "333f877ae4029d7cb1a0893f89da484d8e3cc14f", "repo_path": "/integration/gaia-central" } diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index 1255aeaa26a..2049f347b1e 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index 0630faac3d9..5f9fb059b43 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index 3db1f553136..38f931fe707 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index df4e2c9f412..6854c739fab 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/installer/package-manifest.in b/b2g/installer/package-manifest.in index b850c438911..7cf0b3ff240 100644 --- a/b2g/installer/package-manifest.in +++ b/b2g/installer/package-manifest.in @@ -786,8 +786,10 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DLL_SUFFIX@ #ifndef MOZ_WIDGET_GONK #ifdef XP_WIN32 @BINPATH@/xpcshell.exe +@BINPATH@/ssltunnel.exe #else @BINPATH@/xpcshell +@BINPATH@/ssltunnel #endif #endif @BINPATH@/chrome/icons/ diff --git a/browser/devtools/webaudioeditor/test/browser.ini b/browser/devtools/webaudioeditor/test/browser.ini index 9a5d8e178a0..ee07505b793 100644 --- a/browser/devtools/webaudioeditor/test/browser.ini +++ b/browser/devtools/webaudioeditor/test/browser.ini @@ -9,6 +9,7 @@ support-files = doc_media-node-creation.html doc_destroy-nodes.html doc_connect-toggle.html + doc_connect-param.html 440hz_sine.ogg head.js @@ -20,6 +21,7 @@ support-files = [browser_audionode-actor-is-source.js] [browser_webaudio-actor-simple.js] [browser_webaudio-actor-destroy-node.js] +[browser_webaudio-actor-connect-param.js] [browser_wa_destroy-node-01.js] diff --git a/browser/devtools/webaudioeditor/test/browser_webaudio-actor-connect-param.js b/browser/devtools/webaudioeditor/test/browser_webaudio-actor-connect-param.js new file mode 100644 index 00000000000..af407822553 --- /dev/null +++ b/browser/devtools/webaudioeditor/test/browser_webaudio-actor-connect-param.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test the `connect-param` event on the web audio actor. + */ + +function spawnTest () { + let [target, debuggee, front] = yield initBackend(CONNECT_PARAM_URL); + let [_, _, [destNode, carrierNode, modNode, gainNode], _, connectParam] = yield Promise.all([ + front.setup({ reload: true }), + once(front, "start-context"), + getN(front, "create-node", 4), + get2(front, "connect-node"), + once(front, "connect-param") + ]); + + info(connectParam); + + is(connectParam.source.actorID, modNode.actorID, "`connect-param` has correct actor for `source`"); + is(connectParam.dest.actorID, gainNode.actorID, "`connect-param` has correct actor for `dest`"); + is(connectParam.param, "gain", "`connect-param` has correct parameter name for `param`"); + + yield removeTab(target.tab); + finish(); +} diff --git a/browser/devtools/webaudioeditor/test/doc_connect-param.html b/browser/devtools/webaudioeditor/test/doc_connect-param.html new file mode 100644 index 00000000000..9185c0b05d5 --- /dev/null +++ b/browser/devtools/webaudioeditor/test/doc_connect-param.html @@ -0,0 +1,28 @@ + + + + + + + Web Audio Editor test page + + + + + + + + diff --git a/browser/devtools/webaudioeditor/test/head.js b/browser/devtools/webaudioeditor/test/head.js index 22aa1f03799..ec4aa4a305e 100644 --- a/browser/devtools/webaudioeditor/test/head.js +++ b/browser/devtools/webaudioeditor/test/head.js @@ -28,6 +28,7 @@ const MEDIA_NODES_URL = EXAMPLE_URL + "doc_media-node-creation.html"; const BUFFER_AND_ARRAY_URL = EXAMPLE_URL + "doc_buffer-and-array.html"; const DESTROY_NODES_URL = EXAMPLE_URL + "doc_destroy-nodes.html"; const CONNECT_TOGGLE_URL = EXAMPLE_URL + "doc_connect-toggle.html"; +const CONNECT_PARAM_URL = EXAMPLE_URL + "doc_connect-param.html"; // All tests are asynchronous. waitForExplicitFinish(); diff --git a/dom/camera/GonkCameraHwMgr.cpp b/dom/camera/GonkCameraHwMgr.cpp index 96c3628c51a..63e41e64a5c 100644 --- a/dom/camera/GonkCameraHwMgr.cpp +++ b/dom/camera/GonkCameraHwMgr.cpp @@ -132,7 +132,10 @@ GonkCameraHardware::postDataTimestamp(nsecs_t aTimestamp, int32_t aMsgType, cons if (mListener.get()) { DOM_CAMERA_LOGI("Listener registered, posting recording frame!"); - mListener->postDataTimestamp(aTimestamp, aMsgType, aDataPtr); + if (!mListener->postDataTimestamp(aTimestamp, aMsgType, aDataPtr)) { + DOM_CAMERA_LOGW("Listener unable to process. Drop a recording frame."); + mCamera->releaseRecordingFrame(aDataPtr); + } } else { DOM_CAMERA_LOGW("No listener was set. Drop a recording frame."); mCamera->releaseRecordingFrame(aDataPtr); diff --git a/dom/camera/GonkCameraListener.h b/dom/camera/GonkCameraListener.h index 79be63ea758..cd74f7d0b0f 100644 --- a/dom/camera/GonkCameraListener.h +++ b/dom/camera/GonkCameraListener.h @@ -27,9 +27,9 @@ class GonkCameraListener: virtual public RefBase { public: virtual void notify(int32_t msgType, int32_t ext1, int32_t ext2) = 0; - virtual void postData(int32_t msgType, const sp& dataPtr, + virtual bool postData(int32_t msgType, const sp& dataPtr, camera_frame_metadata_t *metadata) = 0; - virtual void postDataTimestamp(nsecs_t timestamp, int32_t msgType, const sp& dataPtr) = 0; + virtual bool postDataTimestamp(nsecs_t timestamp, int32_t msgType, const sp& dataPtr) = 0; }; }; // namespace android diff --git a/dom/camera/GonkCameraSource.cpp b/dom/camera/GonkCameraSource.cpp index 38ff03f76b1..900b63a0aea 100644 --- a/dom/camera/GonkCameraSource.cpp +++ b/dom/camera/GonkCameraSource.cpp @@ -57,10 +57,10 @@ struct GonkCameraSourceListener : public GonkCameraListener { GonkCameraSourceListener(const sp &source); virtual void notify(int32_t msgType, int32_t ext1, int32_t ext2); - virtual void postData(int32_t msgType, const sp &dataPtr, + virtual bool postData(int32_t msgType, const sp &dataPtr, camera_frame_metadata_t *metadata); - virtual void postDataTimestamp( + virtual bool postDataTimestamp( nsecs_t timestamp, int32_t msgType, const sp& dataPtr); protected: @@ -84,7 +84,7 @@ void GonkCameraSourceListener::notify(int32_t msgType, int32_t ext1, int32_t ext CS_LOGV("notify(%d, %d, %d)", msgType, ext1, ext2); } -void GonkCameraSourceListener::postData(int32_t msgType, const sp &dataPtr, +bool GonkCameraSourceListener::postData(int32_t msgType, const sp &dataPtr, camera_frame_metadata_t *metadata) { CS_LOGV("postData(%d, ptr:%p, size:%d)", msgType, dataPtr->pointer(), dataPtr->size()); @@ -92,16 +92,20 @@ void GonkCameraSourceListener::postData(int32_t msgType, const sp &data sp source = mSource.promote(); if (source.get() != NULL) { source->dataCallback(msgType, dataPtr); + return true; } + return false; } -void GonkCameraSourceListener::postDataTimestamp( +bool GonkCameraSourceListener::postDataTimestamp( nsecs_t timestamp, int32_t msgType, const sp& dataPtr) { sp source = mSource.promote(); if (source.get() != NULL) { source->dataCallbackTimestamp(timestamp/1000, msgType, dataPtr); + return true; } + return false; } static int32_t getColorFormat(const char* colorFormat) { diff --git a/dom/mobilemessage/src/MobileMessageCallback.cpp b/dom/mobilemessage/src/MobileMessageCallback.cpp index d6522f6495e..1d7f13f35cd 100644 --- a/dom/mobilemessage/src/MobileMessageCallback.cpp +++ b/dom/mobilemessage/src/MobileMessageCallback.cpp @@ -240,9 +240,12 @@ MobileMessageCallback::NotifyGetSegmentInfoForTextFailed(int32_t aError) NS_IMETHODIMP MobileMessageCallback::NotifyGetSmscAddress(const nsAString& aSmscAddress) { - AutoJSContext cx; - JSString* smsc = JS_NewUCStringCopyN(cx, - static_cast(aSmscAddress.BeginReading()), + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(mDOMRequest->GetOwner()))) { + return NotifyError(nsIMobileMessageCallback::INTERNAL_ERROR); + } + JSContext* cx = jsapi.cx(); + JSString* smsc = JS_NewUCStringCopyN(cx, aSmscAddress.BeginReading(), aSmscAddress.Length()); if (!smsc) { diff --git a/ipc/keystore/KeyStore.cpp b/ipc/keystore/KeyStore.cpp index 513000baaa1..460c483be34 100644 --- a/ipc/keystore/KeyStore.cpp +++ b/ipc/keystore/KeyStore.cpp @@ -27,6 +27,155 @@ #include "ScopedNSSTypes.h" using namespace mozilla::ipc; +#if ANDROID_VERSION >= 18 +// After Android 4.3, it uses binder to access keystore instead of unix socket. +#include +#include +#include +#include +#include +#include + +using namespace android; + +namespace android { +// This class is used to make compiler happy. +class BpKeystoreService : public BpInterface +{ +public: + BpKeystoreService(const sp& impl) + : BpInterface(impl) + { + } + + virtual int32_t get(const String16& name, uint8_t** item, size_t* itemLength) {return 0;} + virtual int32_t test() {return 0;} + virtual int32_t insert(const String16& name, const uint8_t* item, size_t itemLength, int uid, int32_t flags) {return 0;} + virtual int32_t del(const String16& name, int uid) {return 0;} + virtual int32_t exist(const String16& name, int uid) {return 0;} + virtual int32_t saw(const String16& name, int uid, Vector* matches) {return 0;} + virtual int32_t reset() {return 0;} + virtual int32_t password(const String16& password) {return 0;} + virtual int32_t lock() {return 0;} + virtual int32_t unlock(const String16& password) {return 0;} + virtual int32_t zero() {return 0;} + virtual int32_t import(const String16& name, const uint8_t* data, size_t length, int uid, int32_t flags) {return 0;} + virtual int32_t sign(const String16& name, const uint8_t* data, size_t length, uint8_t** out, size_t* outLength) {return 0;} + virtual int32_t verify(const String16& name, const uint8_t* data, size_t dataLength, const uint8_t* signature, size_t signatureLength) {return 0;} + virtual int32_t get_pubkey(const String16& name, uint8_t** pubkey, size_t* pubkeyLength) {return 0;} + virtual int32_t del_key(const String16& name, int uid) {return 0;} + virtual int32_t grant(const String16& name, int32_t granteeUid) {return 0;} + virtual int32_t ungrant(const String16& name, int32_t granteeUid) {return 0;} + virtual int64_t getmtime(const String16& name) {return 0;} + virtual int32_t duplicate(const String16& srcKey, int32_t srcUid, const String16& destKey, int32_t destUid) {return 0;} + virtual int32_t clear_uid(int64_t uid) {return 0;} +#if ANDROID_VERSION == 18 + virtual int32_t generate(const String16& name, int uid, int32_t flags) {return 0;} + virtual int32_t is_hardware_backed() {return 0;} +#else + virtual int32_t generate(const String16& name, int32_t uid, int32_t keyType, int32_t keySize, int32_t flags, Vector >* args) {return 0;} + virtual int32_t is_hardware_backed(const String16& keyType) {return 0;} +#endif +}; + +IMPLEMENT_META_INTERFACE(KeystoreService, "android.security.keystore"); + +// Here comes binder requests. +status_t BnKeystoreService::onTransact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) +{ + switch(code) { + case TEST: { + CHECK_INTERFACE(IKeystoreService, data, reply); + reply->writeNoException(); + reply->writeInt32(test()); + return NO_ERROR; + } break; + case GET: { + CHECK_INTERFACE(IKeystoreService, data, reply); + String16 name = data.readString16(); + String8 tmp(name); + uint8_t* data = NULL; + size_t dataLength = 0; + int32_t ret = get(name, &data, &dataLength); + + reply->writeNoException(); + if (ret == 1) { + reply->writeInt32(dataLength); + void* buf = reply->writeInplace(dataLength); + memcpy(buf, data, dataLength); + free(data); + } else { + reply->writeInt32(-1); + } + return NO_ERROR; + } break; + default: + return NO_ERROR; + } +} + +// Provide service for binder. +class KeyStoreService: public BnKeystoreService +{ +public: + int32_t test() { + uid_t callingUid = IPCThreadState::self()->getCallingUid(); + if (!mozilla::ipc::checkPermission(callingUid)) { + return ::PERMISSION_DENIED; + } + + return ::NO_ERROR; + } + + int32_t get(const String16& name, uint8_t** item, size_t* itemLength) { + uid_t callingUid = IPCThreadState::self()->getCallingUid(); + if (!mozilla::ipc::checkPermission(callingUid)) { + return ::PERMISSION_DENIED; + } + + String8 certName(name); + return mozilla::ipc::getCertificate(certName.string(), (const uint8_t **)item, (int *)itemLength); + } + + int32_t insert(const String16& name, const uint8_t* item, size_t itemLength, int uid, int32_t flags) {return ::UNDEFINED_ACTION;} + int32_t del(const String16& name, int uid) {return ::UNDEFINED_ACTION;} + int32_t exist(const String16& name, int uid) {return ::UNDEFINED_ACTION;} + int32_t saw(const String16& name, int uid, Vector* matches) {return ::UNDEFINED_ACTION;} + int32_t reset() {return ::UNDEFINED_ACTION;} + int32_t password(const String16& password) {return ::UNDEFINED_ACTION;} + int32_t lock() {return ::UNDEFINED_ACTION;} + int32_t unlock(const String16& password) {return ::UNDEFINED_ACTION;} + int32_t zero() {return ::UNDEFINED_ACTION;} + int32_t import(const String16& name, const uint8_t* data, size_t length, int uid, int32_t flags) {return ::UNDEFINED_ACTION;} + int32_t sign(const String16& name, const uint8_t* data, size_t length, uint8_t** out, size_t* outLength) {return ::UNDEFINED_ACTION;} + int32_t verify(const String16& name, const uint8_t* data, size_t dataLength, const uint8_t* signature, size_t signatureLength) {return ::UNDEFINED_ACTION;} + int32_t get_pubkey(const String16& name, uint8_t** pubkey, size_t* pubkeyLength) {return ::UNDEFINED_ACTION;} + int32_t del_key(const String16& name, int uid) {return ::UNDEFINED_ACTION;} + int32_t grant(const String16& name, int32_t granteeUid) {return ::UNDEFINED_ACTION;} + int32_t ungrant(const String16& name, int32_t granteeUid) {return ::UNDEFINED_ACTION;} + int64_t getmtime(const String16& name) {return ::UNDEFINED_ACTION;} + int32_t duplicate(const String16& srcKey, int32_t srcUid, const String16& destKey, int32_t destUid) {return ::UNDEFINED_ACTION;} + int32_t clear_uid(int64_t uid) {return ::UNDEFINED_ACTION;} +#if ANDROID_VERSION == 18 + virtual int32_t generate(const String16& name, int uid, int32_t flags) {return ::UNDEFINED_ACTION;} + virtual int32_t is_hardware_backed() {return ::UNDEFINED_ACTION;} +#else + virtual int32_t generate(const String16& name, int32_t uid, int32_t keyType, int32_t keySize, int32_t flags, Vector >* args) {return ::UNDEFINED_ACTION;} + virtual int32_t is_hardware_backed(const String16& keyType) {return ::UNDEFINED_ACTION;} +#endif +}; + +} // namespace android + +void startKeyStoreService() +{ + android::sp sm = android::defaultServiceManager(); + android::sp keyStoreService = new android::KeyStoreService(); + sm->addService(String16("android.security.keystore"), keyStoreService); +} +#else +void startKeyStoreService() { return; } +#endif namespace mozilla { namespace ipc { @@ -45,6 +194,99 @@ static const char* KEYSTORE_ALLOWED_PREFIXES[] = { NULL }; +// Transform base64 certification data into DER format +void +FormatCaData(const uint8_t *aCaData, int aCaDataLength, + const char *aName, const uint8_t **aFormatData, + int *aFormatDataLength) +{ + int bufSize = strlen(CA_BEGIN) + strlen(CA_END) + strlen(CA_TAILER) * 2 + + strlen(aName) * 2 + aCaDataLength + aCaDataLength/CA_LINE_SIZE + 2; + char *buf = (char *)malloc(bufSize); + + *aFormatDataLength = bufSize; + *aFormatData = (const uint8_t *)buf; + + char *ptr = buf; + int len; + + // Create DER header. + len = snprintf(ptr, bufSize, "%s%s%s", CA_BEGIN, aName, CA_TAILER); + ptr += len; + bufSize -= len; + + // Split base64 data in lines. + int copySize; + while (aCaDataLength > 0) { + copySize = (aCaDataLength > CA_LINE_SIZE) ? CA_LINE_SIZE : aCaDataLength; + + memcpy(ptr, aCaData, copySize); + ptr += copySize; + aCaData += copySize; + aCaDataLength -= copySize; + bufSize -= copySize; + + *ptr = '\n'; + ptr++; + bufSize--; + } + + // Create DEA tailer. + snprintf(ptr, bufSize, "%s%s%s", CA_END, aName, CA_TAILER); +} + +ResponseCode +getCertificate(const char *aCertName, const uint8_t **aCertData, int *aCertDataLength) +{ + // certificate name prefix check. + if (!aCertName) { + return KEY_NOT_FOUND; + } + + const char **prefix = KEYSTORE_ALLOWED_PREFIXES; + for (; *prefix; prefix++ ) { + if (!strncmp(*prefix, aCertName, strlen(*prefix))) { + break; + } + } + if (!(*prefix)) { + return KEY_NOT_FOUND; + } + + // Get cert from NSS by name + ScopedCERTCertificate cert(CERT_FindCertByNickname(CERT_GetDefaultCertDB(), + aCertName)); + + if (!cert) { + return KEY_NOT_FOUND; + } + + char *certDER = PL_Base64Encode((const char *)cert->derCert.data, + cert->derCert.len, nullptr); + if (!certDER) { + return SYSTEM_ERROR; + } + + FormatCaData((const uint8_t *)certDER, strlen(certDER), "CERTIFICATE", + aCertData, aCertDataLength); + PL_strfree(certDER); + + return SUCCESS; +} + +bool +checkPermission(uid_t uid) +{ + struct passwd *userInfo = getpwuid(uid); + for (const char **user = KEYSTORE_ALLOWED_USERS; *user; user++ ) { + if (!strcmp(*user, userInfo->pw_name)) { + return true; + } + } + + return false; +} + int KeyStoreConnector::Create() { @@ -95,14 +337,7 @@ KeyStoreConnector::SetUp(int aFd) return false; } - struct passwd *userInfo = getpwuid(userCred.uid); - for (const char **user = KEYSTORE_ALLOWED_USERS; *user; user++ ) { - if (!strcmp(*user, userInfo->pw_name)) { - return true; - } - } - - return false; + return ::checkPermission(userCred.uid); } bool @@ -124,9 +359,7 @@ KeyStoreConnector::GetSocketAddr(const sockaddr_any& aAddr, KeyStore::KeyStore() { - // Initial NSS - certdb = CERT_GetDefaultCertDB(); - + ::startKeyStoreService(); Listen(); } @@ -257,47 +490,6 @@ KeyStore::ReadData(UnixSocketRawData *aMessage) return SUCCESS; } -// Transform base64 certification data into DER format -void -KeyStore::FormatCaData(const uint8_t *aCaData, int aCaDataLength, - const char *aName, const uint8_t **aFormatData, - int &aFormatDataLength) -{ - int bufSize = strlen(CA_BEGIN) + strlen(CA_END) + strlen(CA_TAILER) * 2 + - strlen(aName) * 2 + aCaDataLength + aCaDataLength/CA_LINE_SIZE + 2; - char *buf = (char *)malloc(bufSize); - - aFormatDataLength = bufSize; - *aFormatData = (const uint8_t *)buf; - - char *ptr = buf; - int len; - - // Create DER header. - len = snprintf(ptr, bufSize, "%s%s%s", CA_BEGIN, aName, CA_TAILER); - ptr += len; - bufSize -= len; - - // Split base64 data in lines. - int copySize; - while (aCaDataLength > 0) { - copySize = (aCaDataLength > CA_LINE_SIZE) ? CA_LINE_SIZE : aCaDataLength; - - memcpy(ptr, aCaData, copySize); - ptr += copySize; - aCaData += copySize; - aCaDataLength -= copySize; - bufSize -= copySize; - - *ptr = '\n'; - ptr++; - bufSize--; - } - - // Create DEA tailer. - snprintf(ptr, bufSize, "%s%s%s", CA_END, aName, CA_TAILER); -} - // Status response void KeyStore::SendResponse(ResponseCode aResponse) @@ -349,39 +541,10 @@ KeyStore::ReceiveSocketData(nsAutoPtr& aMessage) int certDataLength; const char *certName = (const char *)mHandlerInfo.param[0].data; - // certificate name prefix check. - if (!certName) { - result = KEY_NOT_FOUND; + result = getCertificate(certName, &certData, &certDataLength); + if (result != SUCCESS) { break; } - const char **prefix = KEYSTORE_ALLOWED_PREFIXES; - for (; *prefix; prefix++ ) { - if (!strncmp(*prefix, certName, strlen(*prefix))) { - break; - } - } - if (!(*prefix)) { - result = KEY_NOT_FOUND; - break; - } - - // Get cert from NSS by name - ScopedCERTCertificate cert(CERT_FindCertByNickname(certdb, certName)); - if (!cert) { - result = KEY_NOT_FOUND; - break; - } - - char *certDER = PL_Base64Encode((const char *)cert->derCert.data, - cert->derCert.len, nullptr); - if (!certDER) { - result = SYSTEM_ERROR; - break; - } - - FormatCaData((const uint8_t *)certDER, strlen(certDER), "CERTIFICATE", - &certData, certDataLength); - PL_strfree(certDER); SendResponse(SUCCESS); SendData(certData, certDataLength); diff --git a/ipc/keystore/KeyStore.h b/ipc/keystore/KeyStore.h index 32610fee1a2..a11caddc01d 100644 --- a/ipc/keystore/KeyStore.h +++ b/ipc/keystore/KeyStore.h @@ -33,6 +33,15 @@ enum ResponseCode { NO_RESPONSE }; +void FormatCaData(const uint8_t *aCaData, int aCaDataLength, + const char *aName, const uint8_t **aFormatData, + int *aFormatDataLength); + +ResponseCode getCertificate(const char *aCertName, const uint8_t **aCertData, + int *aCertDataLength); + +bool checkPermission(uid_t uid); + static const int MAX_PARAM = 2; static const int KEY_SIZE = ((NAME_MAX - 15) / 2); static const int VALUE_SIZE = 32768; @@ -110,9 +119,6 @@ private: void ResetHandlerInfo(); void Listen(); - void FormatCaData(const uint8_t *aCaData, int aCaDataLength, const char *aName, - const uint8_t **aFormatData, int &aFormatDataLength); - bool CheckSize(UnixSocketRawData *aMessage, size_t aExpectSize); ResponseCode ReadCommand(UnixSocketRawData *aMessage); ResponseCode ReadLength(UnixSocketRawData *aMessage); @@ -121,8 +127,6 @@ private: void SendData(const uint8_t *data, int length); bool mShutdown; - - CERTCertDBHandle *certdb; }; } // namespace ipc diff --git a/mobile/android/base/AndroidManifest.xml.in b/mobile/android/base/AndroidManifest.xml.in index da37375c0b9..c7af00fa20c 100644 --- a/mobile/android/base/AndroidManifest.xml.in +++ b/mobile/android/base/AndroidManifest.xml.in @@ -290,7 +290,9 @@ - + + diff --git a/mobile/android/base/GeckoProfile.java b/mobile/android/base/GeckoProfile.java index 37ac18a91b5..190e6878e11 100644 --- a/mobile/android/base/GeckoProfile.java +++ b/mobile/android/base/GeckoProfile.java @@ -200,6 +200,13 @@ public final class GeckoProfile { getGuestDir(context).mkdir(); GeckoProfile profile = getGuestProfile(context); profile.lock(); + + /* + * Now do the things that createProfileDirectory normally does -- + * right now that's kicking off DB init. + */ + profile.enqueueInitialization(); + return profile; } catch (Exception ex) { Log.e(LOGTAG, "Error creating guest profile", ex); diff --git a/mobile/android/base/ReferrerReceiver.java b/mobile/android/base/ReferrerReceiver.java deleted file mode 100644 index a83e95e0cad..00000000000 --- a/mobile/android/base/ReferrerReceiver.java +++ /dev/null @@ -1,62 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- - * 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/. */ - -package org.mozilla.gecko; - -import org.json.JSONException; -import org.json.JSONObject; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import java.net.URLDecoder; -import java.util.HashMap; - -public class ReferrerReceiver - extends BroadcastReceiver -{ - private static final String LOGTAG = "GeckoReferrerReceiver"; - - public static final String ACTION_INSTALL_REFERRER = "com.android.vending.INSTALL_REFERRER"; - public static final String UTM_SOURCE = "mozilla"; - - @Override - public void onReceive(Context context, Intent intent) { - if (ACTION_INSTALL_REFERRER.equals(intent.getAction())) { - String referrer = intent.getStringExtra("referrer"); - if (referrer == null) - return; - - HashMap values = new HashMap(); - try { - String referrers[] = referrer.split("&"); - for (String referrerValue : referrers) { - String keyValue[] = referrerValue.split("="); - values.put(URLDecoder.decode(keyValue[0]), URLDecoder.decode(keyValue[1])); - } - } catch (Exception e) { - } - - String source = values.get("utm_source"); - String campaign = values.get("utm_campaign"); - - if (source != null && UTM_SOURCE.equals(source) && campaign != null) { - try { - JSONObject data = new JSONObject(); - data.put("id", "playstore"); - data.put("version", campaign); - - // Try to make sure the prefs are written as a group - GeckoEvent event = GeckoEvent.createBroadcastEvent("Campaign:Set", data.toString()); - GeckoAppShell.sendEventToGecko(event); - } catch (JSONException e) { - Log.e(LOGTAG, "Error setting distribution", e); - } - } - } - } -} diff --git a/mobile/android/base/distribution/Distribution.java b/mobile/android/base/distribution/Distribution.java index b6505b27214..bb33075c4c3 100644 --- a/mobile/android/base/distribution/Distribution.java +++ b/mobile/android/base/distribution/Distribution.java @@ -5,12 +5,19 @@ package org.mozilla.gecko.distribution; +import java.io.BufferedInputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.ProtocolException; +import java.net.SocketException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.UnknownHostException; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; @@ -19,34 +26,95 @@ import java.util.Map; import java.util.Queue; import java.util.Scanner; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import javax.net.ssl.SSLException; + +import org.apache.http.protocol.HTTP; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.GeckoSharedPrefs; +import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.mozglue.RobocopTarget; import org.mozilla.gecko.util.ThreadUtils; import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; +import android.os.SystemClock; import android.util.Log; /** * Handles distribution file loading and fetching, * and the corresponding hand-offs to Gecko. */ -public final class Distribution { +@RobocopTarget +public class Distribution { private static final String LOGTAG = "GeckoDistribution"; private static final int STATE_UNKNOWN = 0; private static final int STATE_NONE = 1; private static final int STATE_SET = 2; + private static final String FETCH_PROTOCOL = "https"; + private static final String FETCH_HOSTNAME = "distro-download.cdn.mozilla.net"; + private static final String FETCH_PATH = "/android/1/"; + private static final String FETCH_EXTENSION = ".jar"; + + private static final String EXPECTED_CONTENT_TYPE = "application/java-archive"; + + private static final String DISTRIBUTION_PATH = "distribution/"; + + /** + * Telemetry constants. + */ + private static final String HISTOGRAM_REFERRER_INVALID = "FENNEC_DISTRIBUTION_REFERRER_INVALID"; + private static final String HISTOGRAM_DOWNLOAD_TIME_MS = "FENNEC_DISTRIBUTION_DOWNLOAD_TIME_MS"; + private static final String HISTOGRAM_CODE_CATEGORY = "FENNEC_DISTRIBUTION_CODE_CATEGORY"; + + /** + * Success/failure codes. Don't exceed the maximum listed in Histograms.json. + */ + private static final int CODE_CATEGORY_STATUS_OUT_OF_RANGE = 0; + // HTTP status 'codes' run from 1 to 5. + private static final int CODE_CATEGORY_OFFLINE = 6; + private static final int CODE_CATEGORY_FETCH_EXCEPTION = 7; + + // It's a post-fetch exception if we were able to download, but not + // able to extract. + private static final int CODE_CATEGORY_POST_FETCH_EXCEPTION = 8; + private static final int CODE_CATEGORY_POST_FETCH_SECURITY_EXCEPTION = 9; + + // It's a malformed distribution if we could extract, but couldn't + // process the contents. + private static final int CODE_CATEGORY_MALFORMED_DISTRIBUTION = 10; + + // Specific fetch errors. + private static final int CODE_CATEGORY_FETCH_SOCKET_ERROR = 11; + private static final int CODE_CATEGORY_FETCH_SSL_ERROR = 12; + private static final int CODE_CATEGORY_FETCH_NON_SUCCESS_RESPONSE = 13; + private static final int CODE_CATEGORY_FETCH_INVALID_CONTENT_TYPE = 14; + + // Corresponds to the high value in Histograms.json. + private static final long MAX_DOWNLOAD_TIME_MSEC = 40000; // 40 seconds. + + + + /** + * Used as a drop-off point for ReferrerReceiver. Checked when we process + * first-run distribution. + * + * This is `protected` so that test code can clear it between runs. + */ + @RobocopTarget + protected static volatile ReferrerDescriptor referrer; + private static Distribution instance; private final Context context; @@ -70,6 +138,7 @@ public final class Distribution { return instance; } + @RobocopTarget public static class DistributionDescriptor { public final boolean valid; public final String id; @@ -140,6 +209,7 @@ public final class Distribution { * Use Context.getPackageResourcePath to find an implicit * package path. Reuses the existing Distribution if one exists. */ + @RobocopTarget public static void init(final Context context) { Distribution.init(Distribution.getInstance(context)); } @@ -166,6 +236,17 @@ public final class Distribution { this(context, context.getPackageResourcePath(), null); } + /** + * This method is called by ReferrerReceiver when we receive a post-install + * notification from Google Play. + * + * @param ref a parsed referrer value from the store-supplied intent. + */ + public static void onReceivedReferrer(ReferrerDescriptor ref) { + // Track the referrer object for distribution handling. + referrer = ref; + } + /** * Helper to grab a file in the distribution directory. * @@ -214,9 +295,11 @@ public final class Distribution { } catch (IOException e) { Log.e(LOGTAG, "Error getting distribution descriptor file.", e); + Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); return null; } catch (JSONException e) { Log.e(LOGTAG, "Error parsing preferences.json", e); + Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); return null; } } @@ -232,11 +315,13 @@ public final class Distribution { return new JSONArray(getFileContents(bookmarks)); } catch (IOException e) { Log.e(LOGTAG, "Error getting bookmarks", e); + Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); + return null; } catch (JSONException e) { Log.e(LOGTAG, "Error parsing bookmarks.json", e); + Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION); + return null; } - - return null; } /** @@ -245,9 +330,12 @@ public final class Distribution { * Postcondition: if this returns true, distributionDir will have been * set and populated. * + * This method is *only* protected for use from testDistribution. + * * @return true if we've set a distribution. */ - private boolean doInit() { + @RobocopTarget + protected boolean doInit() { ThreadUtils.assertNotOnUiThread(); // Bail if we've already tried to initialize the distribution, and @@ -274,8 +362,9 @@ public final class Distribution { return true; } - // We try the APK, then the system directory. + // We try the install intent, then the APK, then the system directory. final boolean distributionSet = + checkIntentDistribution() || checkAPKDistribution() || checkSystemDistribution(); @@ -286,6 +375,153 @@ public final class Distribution { return distributionSet; } + /** + * If applicable, download and select the distribution specified in + * the referrer intent. + * + * @return true if a referrer-supplied distribution was selected. + */ + private boolean checkIntentDistribution() { + if (referrer == null) { + return false; + } + + URI uri = getReferredDistribution(referrer); + if (uri == null) { + return false; + } + + long start = SystemClock.uptimeMillis(); + Log.v(LOGTAG, "Downloading referred distribution: " + uri); + + try { + HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection(); + + connection.setRequestProperty(HTTP.USER_AGENT, GeckoAppShell.getGeckoInterface().getDefaultUAString()); + connection.setRequestProperty("Accept", EXPECTED_CONTENT_TYPE); + + try { + final JarInputStream distro; + try { + distro = fetchDistribution(uri, connection); + } catch (Exception e) { + Log.e(LOGTAG, "Error fetching distribution from network.", e); + recordFetchTelemetry(e); + return false; + } + + long end = SystemClock.uptimeMillis(); + final long duration = end - start; + Log.d(LOGTAG, "Distro fetch took " + duration + "ms; result? " + (distro != null)); + Telemetry.HistogramAdd(HISTOGRAM_DOWNLOAD_TIME_MS, clamp(MAX_DOWNLOAD_TIME_MSEC, duration)); + + if (distro == null) { + // Nothing to do. + return false; + } + + // Try to copy distribution files from the fetched stream. + try { + Log.d(LOGTAG, "Copying files from fetched zip."); + if (copyFilesFromStream(distro)) { + // We always copy to the data dir, and we only copy files from + // a 'distribution' subdirectory. Track our dist dir now that + // we know it. + this.distributionDir = new File(getDataDir(), DISTRIBUTION_PATH); + return true; + } + } catch (SecurityException e) { + Log.e(LOGTAG, "Security exception copying files. Corrupt or malicious?", e); + Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_POST_FETCH_SECURITY_EXCEPTION); + } catch (Exception e) { + Log.e(LOGTAG, "Error copying files from distribution.", e); + Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_POST_FETCH_EXCEPTION); + } finally { + distro.close(); + } + } finally { + connection.disconnect(); + } + } catch (IOException e) { + Log.e(LOGTAG, "Error copying distribution files from network.", e); + recordFetchTelemetry(e); + } + + return false; + } + + private static final int clamp(long v, long c) { + return (int) Math.min(c, v); + } + + /** + * Fetch the provided URI, returning a {@link JarInputStream} if the response body + * is appropriate. + * + * Protected to allow for mocking. + * + * @return the entity body as a stream, or null on failure. + */ + @SuppressWarnings("static-method") + @RobocopTarget + protected JarInputStream fetchDistribution(URI uri, HttpURLConnection connection) throws IOException { + final int status = connection.getResponseCode(); + + Log.d(LOGTAG, "Distribution fetch: " + status); + // We record HTTP statuses as 2xx, 3xx, 4xx, 5xx => 2, 3, 4, 5. + final int value; + if (status > 599 || status < 100) { + Log.wtf(LOGTAG, "Unexpected HTTP status code: " + status); + value = CODE_CATEGORY_STATUS_OUT_OF_RANGE; + } else { + value = status / 100; + } + + Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, value); + + if (status != 200) { + Log.w(LOGTAG, "Got status " + status + " fetching distribution."); + Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_NON_SUCCESS_RESPONSE); + return null; + } + + final String contentType = connection.getContentType(); + if (contentType == null || !contentType.startsWith(EXPECTED_CONTENT_TYPE)) { + Log.w(LOGTAG, "Malformed response: invalid Content-Type."); + Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_INVALID_CONTENT_TYPE); + return null; + } + + return new JarInputStream(new BufferedInputStream(connection.getInputStream()), true); + } + + private static void recordFetchTelemetry(final Exception exception) { + if (exception == null) { + // Should never happen. + Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_EXCEPTION); + return; + } + + if (exception instanceof UnknownHostException) { + // Unknown host => we're offline. + Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_OFFLINE); + return; + } + + if (exception instanceof SSLException) { + Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_SSL_ERROR); + return; + } + + if (exception instanceof ProtocolException || + exception instanceof SocketException) { + Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_SOCKET_ERROR); + return; + } + + Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_EXCEPTION); + } + /** * Execute tasks that wanted to run when we were done loading * the distribution. These tasks are expected to call {@link #exists()} @@ -308,7 +544,7 @@ public final class Distribution { // We always copy to the data dir, and we only copy files from // a 'distribution' subdirectory. Track our dist dir now that // we know it. - this.distributionDir = new File(getDataDir(), "distribution/"); + this.distributionDir = new File(getDataDir(), DISTRIBUTION_PATH); return true; } } catch (IOException e) { @@ -330,6 +566,41 @@ public final class Distribution { return false; } + /** + * Unpack distribution files from a downloaded jar stream. + * + * The caller is responsible for closing the provided stream. + */ + private boolean copyFilesFromStream(JarInputStream jar) throws FileNotFoundException, IOException { + final byte[] buffer = new byte[1024]; + boolean distributionSet = false; + JarEntry entry; + while ((entry = jar.getNextJarEntry()) != null) { + final String name = entry.getName(); + + if (entry.isDirectory()) { + // We'll let getDataFile deal with creating the directory hierarchy. + // Yes, we can do better, but it can wait. + continue; + } + + if (!name.startsWith(DISTRIBUTION_PATH)) { + continue; + } + + File outFile = getDataFile(name); + if (outFile == null) { + continue; + } + + distributionSet = true; + + writeStream(jar, outFile, entry.getTime(), buffer); + } + + return distributionSet; + } + /** * Copies the /distribution folder out of the APK and into the app's data directory. * Returns true if distribution files were found and copied. @@ -352,7 +623,7 @@ public final class Distribution { continue; } - if (!name.startsWith("distribution/")) { + if (!name.startsWith(DISTRIBUTION_PATH)) { continue; } @@ -413,6 +684,29 @@ public final class Distribution { return outFile; } + private URI getReferredDistribution(ReferrerDescriptor descriptor) { + final String content = descriptor.content; + if (content == null) { + return null; + } + + // We restrict here to avoid injection attacks. After all, + // we're downloading a distribution payload based on intent input. + if (!content.matches("^[a-zA-Z0-9]+$")) { + Log.e(LOGTAG, "Invalid referrer content: " + content); + Telemetry.HistogramAdd(HISTOGRAM_REFERRER_INVALID, 1); + return null; + } + + try { + return new URI(FETCH_PROTOCOL, FETCH_HOSTNAME, FETCH_PATH + content + FETCH_EXTENSION, null); + } catch (URISyntaxException e) { + // This should never occur. + Log.wtf(LOGTAG, "Invalid URI with content " + content + "!"); + return null; + } + } + /** * After calling this method, either distributionDir * will be set, or there is no distribution in use. @@ -432,7 +726,7 @@ public final class Distribution { // the APK, or it exists in /system/. // Look in each location in turn. // (This could be optimized by caching the path in shared prefs.) - File copied = new File(getDataDir(), "distribution/"); + File copied = new File(getDataDir(), DISTRIBUTION_PATH); if (copied.exists()) { return this.distributionDir = copied; } diff --git a/mobile/android/base/distribution/ReferrerDescriptor.java b/mobile/android/base/distribution/ReferrerDescriptor.java new file mode 100644 index 00000000000..f422810ed14 --- /dev/null +++ b/mobile/android/base/distribution/ReferrerDescriptor.java @@ -0,0 +1,55 @@ +/* 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/. */ + +package org.mozilla.gecko.distribution; + +import org.mozilla.gecko.mozglue.RobocopTarget; + +import android.net.Uri; + +/** + * Encapsulates access to values encoded in the "referrer" extra of an install intent. + * + * This object is immutable. + * + * Example input: + * + * "utm_source=campsource&utm_medium=campmed&utm_term=term%2Bhere&utm_content=content&utm_campaign=name" + */ +@RobocopTarget +public class ReferrerDescriptor { + public final String source; + public final String medium; + public final String term; + public final String content; + public final String campaign; + + public ReferrerDescriptor(final String referrer) { + if (referrer == null) { + source = null; + medium = null; + term = null; + content = null; + campaign = null; + return; + } + + final Uri u = new Uri.Builder() + .scheme("http") + .authority("local") + .path("/") + .encodedQuery(referrer).build(); + + source = u.getQueryParameter("utm_source"); + medium = u.getQueryParameter("utm_medium"); + term = u.getQueryParameter("utm_term"); + content = u.getQueryParameter("utm_content"); + campaign = u.getQueryParameter("utm_campaign"); + } + + @Override + public String toString() { + return "{s: " + source + ", m: " + medium + ", t: " + term + ", c: " + content + ", c: " + campaign + "}"; + } +} diff --git a/mobile/android/base/distribution/ReferrerReceiver.java b/mobile/android/base/distribution/ReferrerReceiver.java new file mode 100644 index 00000000000..9b310a08158 --- /dev/null +++ b/mobile/android/base/distribution/ReferrerReceiver.java @@ -0,0 +1,76 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.distribution; + +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoEvent; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.text.TextUtils; +import android.util.Log; + +public class ReferrerReceiver extends BroadcastReceiver { + private static final String LOGTAG = "GeckoReferrerReceiver"; + + private static final String ACTION_INSTALL_REFERRER = "com.android.vending.INSTALL_REFERRER"; + + /** + * If the install intent has this source, we'll track the campaign ID. + */ + private static final String MOZILLA_UTM_SOURCE = "mozilla"; + + /** + * If the install intent has this campaign, we'll load the specified distribution. + */ + private static final String DISTRIBUTION_UTM_CAMPAIGN = "distribution"; + + @Override + public void onReceive(Context context, Intent intent) { + Log.v(LOGTAG, "Received intent " + intent); + if (!ACTION_INSTALL_REFERRER.equals(intent.getAction())) { + // This should never happen. + return; + } + + ReferrerDescriptor referrer = new ReferrerDescriptor(intent.getStringExtra("referrer")); + + // Track the referrer object for distribution handling. + if (TextUtils.equals(referrer.campaign, DISTRIBUTION_UTM_CAMPAIGN)) { + Distribution.onReceivedReferrer(referrer); + } else { + Log.d(LOGTAG, "Not downloading distribution: non-matching campaign."); + } + + // If this is a Mozilla campaign, pass the campaign along to Gecko. + if (TextUtils.equals(referrer.source, MOZILLA_UTM_SOURCE)) { + propagateMozillaCampaign(referrer); + } + } + + + private void propagateMozillaCampaign(ReferrerDescriptor referrer) { + if (referrer.campaign == null) { + return; + } + + try { + final JSONObject data = new JSONObject(); + data.put("id", "playstore"); + data.put("version", referrer.campaign); + String payload = data.toString(); + + // Try to make sure the prefs are written as a group. + final GeckoEvent event = GeckoEvent.createBroadcastEvent("Campaign:Set", payload); + GeckoAppShell.sendEventToGecko(event); + } catch (JSONException e) { + Log.e(LOGTAG, "Error propagating campaign identifier.", e); + } + } +} diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index 13ea43e4563..f8404c08c8d 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -156,6 +156,8 @@ gbjar.sources += [ 'db/TabsProvider.java', 'db/TopSitesCursorWrapper.java', 'distribution/Distribution.java', + 'distribution/ReferrerDescriptor.java', + 'distribution/ReferrerReceiver.java', 'DoorHangerPopup.java', 'DynamicToolbar.java', 'EditBookmarkDialog.java', @@ -354,7 +356,6 @@ gbjar.sources += [ 'prompts/PromptService.java', 'prompts/TabInput.java', 'ReaderModeUtils.java', - 'ReferrerReceiver.java', 'Restarter.java', 'ScrollAnimator.java', 'ServiceNotificationClient.java', diff --git a/mobile/android/base/tests/testDistribution.java b/mobile/android/base/tests/testDistribution.java index 7d8b7edc2e3..50a75e8191a 100644 --- a/mobile/android/base/tests/testDistribution.java +++ b/mobile/android/base/tests/testDistribution.java @@ -2,19 +2,29 @@ package org.mozilla.gecko.tests; import java.io.File; import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.util.jar.JarInputStream; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.mozilla.gecko.Actions; +import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.distribution.Distribution; +import org.mozilla.gecko.distribution.ReferrerDescriptor; +import org.mozilla.gecko.mozglue.RobocopTarget; import org.mozilla.gecko.util.ThreadUtils; import android.app.Activity; +import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; +import android.util.Log; /** * Tests distribution customization. @@ -28,6 +38,38 @@ import android.content.SharedPreferences; * engine.xml */ public class testDistribution extends ContentProviderTest { + private static final String CLASS_REFERRER_RECEIVER = "org.mozilla.gecko.distribution.ReferrerReceiver"; + private static final String ACTION_INSTALL_REFERRER = "com.android.vending.INSTALL_REFERRER"; + private static final int WAIT_TIMEOUT_MSEC = 10000; + public static final String LOGTAG = "GeckoTestDistribution"; + + public static class TestableDistribution extends Distribution { + @Override + protected JarInputStream fetchDistribution(URI uri, + HttpURLConnection connection) throws IOException { + Log.i(LOGTAG, "Not downloading: this is a test."); + return null; + } + + public TestableDistribution(Context context) { + super(context); + } + + public void go() { + doInit(); + } + + @RobocopTarget + public static void clearReferrerDescriptorForTesting() { + referrer = null; + } + + @RobocopTarget + public static ReferrerDescriptor getReferrerDescriptorForTesting() { + return referrer; + } + } + private static final String MOCK_PACKAGE = "mock-package.zip"; private static final int PREF_REQUEST_ID = 0x7357; @@ -65,7 +107,7 @@ public class testDistribution extends ContentProviderTest { mAsserter.dumpLog("Background task completed. Proceeding."); } - public void testDistribution() { + public void testDistribution() throws Exception { mActivity = getActivity(); String mockPackagePath = getMockPackagePath(); @@ -87,6 +129,90 @@ public class testDistribution extends ContentProviderTest { setTestLocale("es-MX"); initDistribution(mockPackagePath); checkLocalizedPreferences("es-MX"); + + // Test the (stubbed) download interaction. + setTestLocale("en-US"); + clearDistributionPref(); + doTestValidReferrerIntent(); + + clearDistributionPref(); + doTestInvalidReferrerIntent(); + } + + public void doTestValidReferrerIntent() throws Exception { + // Send the faux-download intent. + // Equivalent to + // am broadcast -a com.android.vending.INSTALL_REFERRER \ + // -n org.mozilla.fennec/org.mozilla.gecko.distribution.ReferrerReceiver \ + // --es "referrer" "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=distribution" + final String ref = "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=distribution"; + final Intent intent = new Intent(ACTION_INSTALL_REFERRER); + intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, CLASS_REFERRER_RECEIVER); + intent.putExtra("referrer", ref); + mActivity.sendBroadcast(intent); + + // Wait for the intent to be processed. + final TestableDistribution distribution = new TestableDistribution(mActivity); + + final Object wait = new Object(); + distribution.addOnDistributionReadyCallback(new Runnable() { + @Override + public void run() { + mAsserter.ok(!distribution.exists(), "Not processed.", "No download because we're offline."); + ReferrerDescriptor referrerValue = TestableDistribution.getReferrerDescriptorForTesting(); + mAsserter.dumpLog("Referrer was " + referrerValue); + mAsserter.is(referrerValue.content, "testcontent", "Referrer content"); + mAsserter.is(referrerValue.medium, "testmedium", "Referrer medium"); + mAsserter.is(referrerValue.campaign, "distribution", "Referrer campaign"); + synchronized (wait) { + wait.notifyAll(); + } + } + }); + + distribution.go(); + synchronized (wait) { + wait.wait(WAIT_TIMEOUT_MSEC); + } + } + + /** + * Test processing if the campaign isn't "distribution". The intent shouldn't + * result in a download, and won't be saved as the temporary referrer, + * even if we *do* include it in a Campaign:Set message. + */ + public void doTestInvalidReferrerIntent() throws Exception { + // Send the faux-download intent. + // Equivalent to + // am broadcast -a com.android.vending.INSTALL_REFERRER \ + // -n org.mozilla.fennec/org.mozilla.gecko.distribution.ReferrerReceiver \ + // --es "referrer" "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=testname" + final String ref = "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=testname"; + final Intent intent = new Intent(ACTION_INSTALL_REFERRER); + intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, CLASS_REFERRER_RECEIVER); + intent.putExtra("referrer", ref); + mActivity.sendBroadcast(intent); + + // Wait for the intent to be processed. + final TestableDistribution distribution = new TestableDistribution(mActivity); + + final Object wait = new Object(); + distribution.addOnDistributionReadyCallback(new Runnable() { + @Override + public void run() { + mAsserter.ok(!distribution.exists(), "Not processed.", "No download because campaign was wrong."); + ReferrerDescriptor referrerValue = TestableDistribution.getReferrerDescriptorForTesting(); + mAsserter.is(referrerValue, null, "No referrer."); + synchronized (wait) { + wait.notifyAll(); + } + } + }); + + distribution.go(); + synchronized (wait) { + wait.wait(WAIT_TIMEOUT_MSEC); + } } // Initialize the distribution from the mock package. @@ -288,12 +414,16 @@ public class testDistribution extends ContentProviderTest { return mockPackagePath; } - // Clears the distribution pref to return distribution state to STATE_UNKNOWN + /** + * Clears the distribution pref to return distribution state to STATE_UNKNOWN, + * and wipes the in-memory referrer pigeonhole. + */ private void clearDistributionPref() { mAsserter.dumpLog("Clearing distribution pref."); SharedPreferences settings = mActivity.getSharedPreferences("GeckoApp", Activity.MODE_PRIVATE); String keyName = mActivity.getPackageName() + ".distribution_state"; settings.edit().remove(keyName).commit(); + TestableDistribution.clearReferrerDescriptorForTesting(); } @Override diff --git a/mobile/android/tests/browser/junit3/moz.build b/mobile/android/tests/browser/junit3/moz.build index 7d81516888d..c620954755e 100644 --- a/mobile/android/tests/browser/junit3/moz.build +++ b/mobile/android/tests/browser/junit3/moz.build @@ -11,6 +11,7 @@ jar.sources += [ 'src/harness/BrowserInstrumentationTestRunner.java', 'src/harness/BrowserTestListener.java', 'src/tests/BrowserTestCase.java', + 'src/tests/TestDistribution.java', 'src/tests/TestGeckoSharedPrefs.java', 'src/tests/TestJarReader.java', 'src/tests/TestRawResource.java', diff --git a/mobile/android/tests/browser/junit3/src/tests/TestDistribution.java b/mobile/android/tests/browser/junit3/src/tests/TestDistribution.java new file mode 100644 index 00000000000..dcc2a9fafc7 --- /dev/null +++ b/mobile/android/tests/browser/junit3/src/tests/TestDistribution.java @@ -0,0 +1,36 @@ +/* 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/. */ + +package org.mozilla.gecko.browser.tests; + +import org.mozilla.gecko.distribution.ReferrerDescriptor; + +public class TestDistribution extends BrowserTestCase { + private static final String TEST_REFERRER_STRING = "utm_source=campsource&utm_medium=campmed&utm_term=term%2Bhere&utm_content=content&utm_campaign=name"; + private static final String TEST_MALFORMED_REFERRER_STRING = "utm_source=campsource&utm_medium=campmed&utm_term=term%2"; + + public void testReferrerParsing() { + ReferrerDescriptor good = new ReferrerDescriptor(TEST_REFERRER_STRING); + assertEquals("campsource", good.source); + assertEquals("campmed", good.medium); + assertEquals("term+here", good.term); + assertEquals("content", good.content); + assertEquals("name", good.campaign); + + // Uri.Builder is permissive. + ReferrerDescriptor bad = new ReferrerDescriptor(TEST_MALFORMED_REFERRER_STRING); + assertEquals("campsource", bad.source); + assertEquals("campmed", bad.medium); + assertFalse("term+here".equals(bad.term)); + assertNull(bad.content); + assertNull(bad.campaign); + + ReferrerDescriptor ugly = new ReferrerDescriptor(null); + assertNull(ugly.source); + assertNull(ugly.medium); + assertNull(ugly.term); + assertNull(ugly.content); + assertNull(ugly.campaign); + } +} diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index 2ab5b57efe7..a0214783eb3 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -2959,6 +2959,28 @@ "extended_statistics_ok": true, "description": "PLACES: Time to calculate the md5 hash for a backup" }, + "FENNEC_DISTRIBUTION_REFERRER_INVALID": { + "expires_in_version": "never", + "kind": "flag", + "description": "Whether the referrer intent specified an invalid distribution name", + "cpp_guard": "ANDROID" + }, + "FENNEC_DISTRIBUTION_CODE_CATEGORY": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 20, + "description": "First digit of HTTP result code, or error category, during distribution download", + "cpp_guard": "ANDROID" + }, + "FENNEC_DISTRIBUTION_DOWNLOAD_TIME_MS": { + "expires_in_version": "never", + "kind": "exponential", + "low": 100, + "high": "40000", + "n_buckets": 30, + "description": "Time taken to download a specified distribution file (msec)", + "cpp_guard": "ANDROID" + }, "FENNEC_FAVICONS_COUNT": { "expires_in_version": "never", "kind": "exponential", diff --git a/toolkit/devtools/discovery/discovery.js b/toolkit/devtools/discovery/discovery.js index 7e14e9f581c..fe54b81e1ed 100644 --- a/toolkit/devtools/discovery/discovery.js +++ b/toolkit/devtools/discovery/discovery.js @@ -39,10 +39,9 @@ const UDPSocket = CC("@mozilla.org/network/udp-socket;1", "nsIUDPSocket", "init"); -// TODO Bug 1027456: May need to reserve these with IANA const SCAN_PORT = 50624; const UPDATE_PORT = 50625; -const ADDRESS = "224.0.0.200"; +const ADDRESS = "224.0.0.115"; const REPLY_TIMEOUT = 5000; const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}); @@ -158,6 +157,8 @@ function Discovery() { this._onRemoteUpdate = this._onRemoteUpdate.bind(this); this._purgeMissingDevices = this._purgeMissingDevices.bind(this); + Services.obs.addObserver(this, "network-active-changed", false); + this._getSystemInfo(); } @@ -295,6 +296,35 @@ Discovery.prototype = { this._transports.update = null; }, + observe: function(subject, topic, data) { + if (topic !== "network-active-changed") { + return; + } + let activeNetwork = subject; + if (!activeNetwork) { + log("No active network"); + return; + } + activeNetwork = activeNetwork.QueryInterface(Ci.nsINetworkInterface); + log("Active network changed to: " + activeNetwork.type); + // UDP sockets go down when the device goes offline, so we'll restart them + // when the active network goes back to WiFi. + if (activeNetwork.type === Ci.nsINetworkInterface.NETWORK_TYPE_WIFI) { + this._restartListening(); + } + }, + + _restartListening: function() { + if (this._transports.scan) { + this._stopListeningForScan(); + this._startListeningForScan(); + } + if (this._transports.update) { + this._stopListeningForUpdate(); + this._startListeningForUpdate(); + } + }, + /** * When sending message, we can use either transport, so just pick the first * one currently alive. diff --git a/toolkit/devtools/server/actors/webaudio.js b/toolkit/devtools/server/actors/webaudio.js index 6472093f185..4cea0f9e925 100644 --- a/toolkit/devtools/server/actors/webaudio.js +++ b/toolkit/devtools/server/actors/webaudio.js @@ -363,7 +363,7 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({ let { caller, args, window, name } = functionCall.details; let source = caller; let dest = args[0]; - let isAudioParam = dest instanceof window.AudioParam; + let isAudioParam = dest ? getConstructorName(dest) === "AudioParam" : false; // audionode.connect(param) if (name === "connect" && isAudioParam) { @@ -433,8 +433,9 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({ }, "connect-param": { type: "connectParam", - source: Arg(0, "audionode"), - param: Arg(1, "string") + source: Option(0, "audionode"), + dest: Option(0, "audionode"), + param: Option(0, "string") }, "change-param": { type: "changeParam", @@ -461,12 +462,30 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({ // Ensure AudioNode is wrapped. node = new XPCNativeWrapper(node); + this._instrumentParams(node); + let actor = new AudioNodeActor(this.conn, node); this.manage(actor); this._nativeToActorID.set(node.id, actor.actorID); return actor; }, + /** + * Takes an XrayWrapper node, and attaches the node's `nativeID` + * to the AudioParams as `_parentID`, as well as the the type of param + * as a string on `_paramName`. + */ + _instrumentParams: function (node) { + let type = getConstructorName(node); + Object.keys(NODE_PROPERTIES[type]) + .filter(isAudioParam.bind(null, node)) + .forEach(paramName => { + let param = node[paramName]; + param._parentID = node.id; + param._paramName = paramName; + }); + }, + /** * Takes an AudioNode and returns the stored actor for it. * In some cases, we won't have an actor stored (for example, @@ -505,10 +524,15 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({ /** * Called when an audio node is connected to an audio param. - * Implement in bug 986705 */ - _onConnectParam: function (source, dest) { - // TODO bug 986705 + _onConnectParam: function (source, param) { + let sourceActor = this._getActorByNativeID(source.id); + let destActor = this._getActorByNativeID(param._parentID); + emit(this, "connect-param", { + source: sourceActor, + dest: destActor, + param: param._paramName + }); }, /** diff --git a/toolkit/identity/FirefoxAccounts.jsm b/toolkit/identity/FirefoxAccounts.jsm index 8ba3817f85c..e8374480caa 100644 --- a/toolkit/identity/FirefoxAccounts.jsm +++ b/toolkit/identity/FirefoxAccounts.jsm @@ -187,7 +187,7 @@ FxAccountsService.prototype = { error => { log.error("get assertion failed: " + JSON.stringify(error)); // Cancellation is passed through an error channel; here we reroute. - if (error.details && (error.details.error == "DIALOG_CLOSED_BY_USER")) { + if (error.error && (error.error.details == "DIALOG_CLOSED_BY_USER")) { return this.doCancel(aRPId); } this.doError(aRPId, error); diff --git a/toolkit/mozapps/installer/packager.mk b/toolkit/mozapps/installer/packager.mk index ba1cb54dbb5..8af082a63cd 100644 --- a/toolkit/mozapps/installer/packager.mk +++ b/toolkit/mozapps/installer/packager.mk @@ -612,7 +612,6 @@ NO_PKG_FILES += \ res/samples \ res/throbber \ shlibsign* \ - ssltunnel* \ certutil* \ pk12util* \ BadCertServer* \ @@ -629,6 +628,13 @@ NO_PKG_FILES += \ *.dSYM \ $(NULL) +# If a manifest has not been supplied, the following +# files should be excluded from the package too +ifndef MOZ_PKG_MANIFEST +NO_PKG_FILES += \ + ssltunnel* +endif + # browser/locales/Makefile uses this makefile for its variable defs, but # doesn't want the libs:: rule. ifndef PACKAGER_NO_LIBS