зеркало из https://github.com/mozilla/gecko-dev.git
merge m-c to elm
--HG-- rename : browser/base/content/test/Makefile.in => browser/base/content/test/general/Makefile.in
This commit is contained in:
Коммит
452dc0c60a
|
@ -1306,3 +1306,7 @@ pref("geo.wifi.uri", "https://www.googleapis.com/geolocation/v1/geolocate?key=%G
|
|||
// Necko IPC security checks only needed for app isolation for cookies/cache/etc:
|
||||
// currently irrelevant for desktop e10s
|
||||
pref("network.disable.ipc.security", true);
|
||||
|
||||
// The URL where remote content that composes the UI for Firefox Accounts should
|
||||
// be fetched.
|
||||
pref("firefox.accounts.remoteUrl", "http://accounts.dev.lcip.org/flow");
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#remote {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
function log(msg) {
|
||||
//dump("FXA: " + msg + "\n");
|
||||
};
|
||||
|
||||
let wrapper = {
|
||||
iframe: null,
|
||||
|
||||
init: function () {
|
||||
let iframe = document.getElementById("remote");
|
||||
this.iframe = iframe;
|
||||
iframe.addEventListener("load", this);
|
||||
iframe.src = this._getAccountsURI();
|
||||
},
|
||||
|
||||
handleEvent: function (evt) {
|
||||
switch (evt.type) {
|
||||
case "load":
|
||||
this.iframe.contentWindow.addEventListener("FirefoxAccountsCommand", this);
|
||||
this.iframe.removeEventListener("load", this);
|
||||
break;
|
||||
case "FirefoxAccountsCommand":
|
||||
this.handleRemoteCommand(evt);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onLogin: function (data) {
|
||||
log("Received: 'login'. Data:" + JSON.stringify(data));
|
||||
this.injectData("message", { status: "login" });
|
||||
},
|
||||
|
||||
onCreate: function (data) {
|
||||
log("Received: 'create'. Data:" + JSON.stringify(data));
|
||||
this.injectData("message", { status: "create" });
|
||||
},
|
||||
|
||||
onVerified: function (data) {
|
||||
log("Received: 'verified'. Data:" + JSON.stringify(data));
|
||||
this.injectData("message", { status: "verified" });
|
||||
},
|
||||
|
||||
_getAccountsURI: function () {
|
||||
return Services.urlFormatter.formatURLPref("firefox.accounts.remoteUrl");
|
||||
},
|
||||
|
||||
handleRemoteCommand: function (evt) {
|
||||
log('command: ' + evt.detail.command);
|
||||
let data = evt.detail.data;
|
||||
|
||||
switch (evt.detail.command) {
|
||||
case "create":
|
||||
this.onCreate(data);
|
||||
break;
|
||||
case "login":
|
||||
this.onLogin(data);
|
||||
break;
|
||||
case "verified":
|
||||
this.onVerified(data);
|
||||
break;
|
||||
default:
|
||||
log("Unexpected remote command received: " + evt.detail.command + ". Ignoring command.");
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
injectData: function (type, content) {
|
||||
let authUrl = this._getAccountsURI();
|
||||
|
||||
let data = {
|
||||
type: type,
|
||||
content: content
|
||||
};
|
||||
|
||||
this.iframe.contentWindow.postMessage(data, authUrl);
|
||||
},
|
||||
};
|
||||
|
||||
wrapper.init();
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- 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/. -->
|
||||
<!DOCTYPE html [
|
||||
<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
|
||||
%htmlDTD;
|
||||
<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
|
||||
%brandDTD;
|
||||
<!ENTITY % aboutAccountsDTD SYSTEM "chrome://browser/locale/aboutAccounts.dtd">
|
||||
%aboutAccountsDTD;
|
||||
]>
|
||||
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>&aboutAccounts.pageTitle;</title>
|
||||
<link rel="icon" type="image/png" id="favicon"
|
||||
href="chrome://branding/content/icon32.png"/>
|
||||
<link rel="stylesheet"
|
||||
href="chrome://browser/content/aboutaccounts/aboutaccounts.css"
|
||||
type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<iframe mozframetype="content" id="remote" />
|
||||
<script type="text/javascript;version=1.8"
|
||||
src="chrome://browser/content/aboutaccounts/aboutaccounts.js" />
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,88 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<script>
|
||||
|
||||
function init() {
|
||||
window.addEventListener("message", function process(e) {doTest(e)}, false);
|
||||
// unless we relinquish the eventloop,
|
||||
// tests will run before the chrome event handlers are ready
|
||||
setTimeout(doTest, 0);
|
||||
}
|
||||
|
||||
function checkStatusValue(payload, expectedValue) {
|
||||
return payload.status == expectedValue;
|
||||
}
|
||||
|
||||
var tests = [
|
||||
{
|
||||
info: "Check account creation",
|
||||
event: "create",
|
||||
payloadType: "message",
|
||||
validateResponse: function(payload) {
|
||||
return checkStatusValue(payload, "create");
|
||||
},
|
||||
},
|
||||
{
|
||||
info: "Check account verification",
|
||||
event: "verified",
|
||||
payloadType: "message",
|
||||
validateResponse: function(payload) {
|
||||
return checkStatusValue(payload, "verified");
|
||||
},
|
||||
},
|
||||
{
|
||||
info: "Check account log in",
|
||||
event: "login",
|
||||
payloadType: "message",
|
||||
validateResponse: function(payload) {
|
||||
return checkStatusValue(payload, "login");
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
var currentTest = -1;
|
||||
function doTest(evt) {
|
||||
if (evt) {
|
||||
if (currentTest < 0 || !evt.data.content)
|
||||
return; // not yet testing
|
||||
|
||||
var test = tests[currentTest];
|
||||
if (evt.data.type != test.payloadType)
|
||||
return; // skip unrequested events
|
||||
|
||||
var error = JSON.stringify(evt.data.content);
|
||||
var pass = false;
|
||||
try {
|
||||
pass = test.validateResponse(evt.data.content)
|
||||
} catch (e) {}
|
||||
reportResult(test.info, pass, error);
|
||||
}
|
||||
// start the next test if there are any left
|
||||
if (tests[++currentTest])
|
||||
sendToBrowser(tests[currentTest].event);
|
||||
else
|
||||
reportFinished();
|
||||
}
|
||||
|
||||
function reportResult(info, pass, error) {
|
||||
var data = {type: "testResult", info: info, pass: pass, error: error};
|
||||
window.parent.postMessage(data, "*");
|
||||
}
|
||||
|
||||
function reportFinished(cmd) {
|
||||
var data = {type: "testsComplete", count: tests.length};
|
||||
window.parent.postMessage(data, "*");
|
||||
}
|
||||
|
||||
function sendToBrowser(type) {
|
||||
var event = new CustomEvent("FirefoxAccountsCommand", {detail: {command: type}, bubbles: true});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body onload="init()">
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,89 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
|
||||
registerCleanupFunction(function() {
|
||||
// Ensure we don't pollute prefs for next tests.
|
||||
Services.prefs.clearUserPref("firefox.accounts.remoteUrl");
|
||||
});
|
||||
|
||||
let gTests = [
|
||||
|
||||
{
|
||||
desc: "Test the remote commands",
|
||||
setup: function ()
|
||||
{
|
||||
Services.prefs.setCharPref("firefox.accounts.remoteUrl",
|
||||
"https://example.com/browser/browser/base/content/test/accounts_testRemoteCommands.html");
|
||||
},
|
||||
run: function ()
|
||||
{
|
||||
let deferred = Promise.defer();
|
||||
|
||||
let results = 0;
|
||||
try {
|
||||
let win = gBrowser.contentWindow;
|
||||
win.addEventListener("message", function testLoad(e) {
|
||||
if (e.data.type == "testResult") {
|
||||
ok(e.data.pass, e.data.info);
|
||||
results++;
|
||||
}
|
||||
else if (e.data.type == "testsComplete") {
|
||||
is(results, e.data.count, "Checking number of results received matches the number of tests that should have run");
|
||||
win.removeEventListener("message", testLoad, false, true);
|
||||
deferred.resolve();
|
||||
}
|
||||
|
||||
}, false, true);
|
||||
|
||||
} catch(e) {
|
||||
ok(false, "Failed to get all commands");
|
||||
deferred.reject();
|
||||
}
|
||||
return deferred.promise;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
]; // gTests
|
||||
|
||||
function test()
|
||||
{
|
||||
waitForExplicitFinish();
|
||||
|
||||
Task.spawn(function () {
|
||||
for (let test of gTests) {
|
||||
info(test.desc);
|
||||
test.setup();
|
||||
|
||||
yield promiseNewTabLoadEvent("about:accounts");
|
||||
|
||||
yield test.run();
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
function promiseNewTabLoadEvent(aUrl, aEventType="load")
|
||||
{
|
||||
let deferred = Promise.defer();
|
||||
let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
|
||||
tab.linkedBrowser.addEventListener(aEventType, function load(event) {
|
||||
tab.linkedBrowser.removeEventListener(aEventType, load, true);
|
||||
let iframe = tab.linkedBrowser.contentDocument.getElementById("remote");
|
||||
iframe.addEventListener("load", function frameLoad(e) {
|
||||
iframe.removeEventListener("load", frameLoad, false);
|
||||
deferred.resolve();
|
||||
}, false);
|
||||
}, true);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
|
@ -57,6 +57,7 @@ endif
|
|||
|
||||
MOCHITEST_BROWSER_FILES = \
|
||||
head.js \
|
||||
accounts_testRemoteCommands.html \
|
||||
alltabslistener.html \
|
||||
app_bug575561.html \
|
||||
app_subframe_bug575561.html \
|
||||
|
@ -65,6 +66,7 @@ MOCHITEST_BROWSER_FILES = \
|
|||
blockPluginHard.xml \
|
||||
blockPluginVulnerableNoUpdate.xml \
|
||||
blockPluginVulnerableUpdatable.xml \
|
||||
browser_aboutAccounts.js \
|
||||
browser_aboutHealthReport.js \
|
||||
browser_aboutHome.js \
|
||||
browser_aboutSyncProgress.js \
|
||||
|
|
|
@ -52,6 +52,9 @@ browser.jar:
|
|||
content/browser/abouthealthreport/abouthealth.js (content/abouthealthreport/abouthealth.js)
|
||||
content/browser/abouthealthreport/abouthealth.css (content/abouthealthreport/abouthealth.css)
|
||||
#endif
|
||||
content/browser/aboutaccounts/aboutaccounts.xhtml (content/aboutaccounts/aboutaccounts.xhtml)
|
||||
content/browser/aboutaccounts/aboutaccounts.js (content/aboutaccounts/aboutaccounts.js)
|
||||
content/browser/aboutaccounts/aboutaccounts.css (content/aboutaccounts/aboutaccounts.css)
|
||||
content/browser/aboutRobots-icon.png (content/aboutRobots-icon.png)
|
||||
content/browser/aboutRobots-widget-left.png (content/aboutRobots-widget-left.png)
|
||||
content/browser/aboutSocialError.xhtml (content/aboutSocialError.xhtml)
|
||||
|
|
|
@ -90,6 +90,8 @@ static RedirEntry kRedirMap[] = {
|
|||
{ "healthreport", "chrome://browser/content/abouthealthreport/abouthealth.xhtml",
|
||||
nsIAboutModule::ALLOW_SCRIPT },
|
||||
#endif
|
||||
{ "accounts", "chrome://browser/content/aboutaccounts/aboutaccounts.xhtml",
|
||||
nsIAboutModule::ALLOW_SCRIPT },
|
||||
{ "app-manager", "chrome://browser/content/devtools/app-manager/index.xul",
|
||||
nsIAboutModule::ALLOW_SCRIPT },
|
||||
};
|
||||
|
|
|
@ -106,6 +106,7 @@ static const mozilla::Module::ContractIDEntry kBrowserContracts[] = {
|
|||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "permissions", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "preferences", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "downloads", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "accounts", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
#ifdef MOZ_SERVICES_HEALTHREPORT
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "healthreport", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
#endif
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<!-- 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/. -->
|
||||
|
||||
<!ENTITY aboutAccounts.pageTitle "&brandShortName; Accounts">
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
@AB_CD@.jar:
|
||||
% locale browser @AB_CD@ %locale/browser/
|
||||
locale/browser/aboutAccounts.dtd (%chrome/browser/aboutAccounts.dtd)
|
||||
locale/browser/aboutCertError.dtd (%chrome/browser/aboutCertError.dtd)
|
||||
locale/browser/aboutDialog.dtd (%chrome/browser/aboutDialog.dtd)
|
||||
locale/browser/aboutPrivateBrowsing.dtd (%chrome/browser/aboutPrivateBrowsing.dtd)
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
android:targetSdkVersion="16"/>
|
||||
|
||||
#include ../services/manifests/AnnouncementsAndroidManifest_permissions.xml.in
|
||||
#include ../services/manifests/FxAccountAndroidManifest_permissions.xml.in
|
||||
#include ../services/manifests/HealthReportAndroidManifest_permissions.xml.in
|
||||
#include ../services/manifests/SyncAndroidManifest_permissions.xml.in
|
||||
|
||||
|
@ -207,6 +208,7 @@
|
|||
</activity>
|
||||
|
||||
#include ../services/manifests/AnnouncementsAndroidManifest_activities.xml.in
|
||||
#include ../services/manifests/FxAccountAndroidManifest_services.xml.in
|
||||
#include ../services/manifests/SyncAndroidManifest_activities.xml.in
|
||||
#include ../services/manifests/HealthReportAndroidManifest_activities.xml.in
|
||||
|
||||
|
@ -237,6 +239,7 @@
|
|||
android:excludeFromRecents="true"/>
|
||||
|
||||
<provider android:name="org.mozilla.gecko.db.BrowserProvider"
|
||||
android:label="@string/sync_configure_engines_title_bookmarks"
|
||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.browser"
|
||||
android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER">
|
||||
|
||||
|
@ -252,16 +255,19 @@
|
|||
Process name is a mangled version to avoid a Talos bug. (Bug 750548.)
|
||||
-->
|
||||
<provider android:name="org.mozilla.gecko.db.PasswordsProvider"
|
||||
android:label="@string/sync_configure_engines_title_passwords"
|
||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.passwords"
|
||||
android:permission="@ANDROID_PACKAGE_NAME@.permissions.PASSWORD_PROVIDER"
|
||||
android:process="@MANGLED_ANDROID_PACKAGE_NAME@.PasswordsProvider"/>
|
||||
|
||||
<provider android:name="org.mozilla.gecko.db.FormHistoryProvider"
|
||||
android:label="@string/sync_configure_engines_title_history"
|
||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.formhistory"
|
||||
android:permission="@ANDROID_PACKAGE_NAME@.permissions.FORMHISTORY_PROVIDER"
|
||||
android:protectionLevel="signature"/>
|
||||
|
||||
<provider android:name="org.mozilla.gecko.db.TabsProvider"
|
||||
android:label="@string/sync_configure_engines_title_tabs"
|
||||
android:authorities="@ANDROID_PACKAGE_NAME@.db.tabs"
|
||||
android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
|
||||
|
||||
|
|
|
@ -415,6 +415,7 @@ GARBAGE_DIRS += classes db jars res sync services
|
|||
|
||||
MOZ_ANDROID_SHARED_ID = "$(ANDROID_PACKAGE_NAME).sharedID"
|
||||
MOZ_ANDROID_SHARED_ACCOUNT_TYPE = "$(ANDROID_PACKAGE_NAME)_sync"
|
||||
MOZ_ANDROID_SHARED_FXACCOUNT_TYPE = "$(ANDROID_PACKAGE_NAME)_account"
|
||||
|
||||
# Bug 567884 - Need a way to find appropriate icons during packaging
|
||||
ifeq ($(MOZ_APP_NAME),fennec)
|
||||
|
@ -427,15 +428,19 @@ ICON_PATH_XXHDPI = $(topsrcdir)/$(MOZ_BRANDING_DIRECTORY)/content/fennec_144x144
|
|||
ifeq (org.mozilla.firefox,$(ANDROID_PACKAGE_NAME))
|
||||
MOZ_ANDROID_SHARED_ID = "org.mozilla.firefox.sharedID"
|
||||
MOZ_ANDROID_SHARED_ACCOUNT_TYPE = "org.mozilla.firefox_sync"
|
||||
MOZ_ANDROID_SHARED_FXACCOUNT_TYPE = "org.mozilla.firefox_account"
|
||||
else ifeq (org.mozilla.firefox_beta,$(ANDROID_PACKAGE_NAME))
|
||||
MOZ_ANDROID_SHARED_ID = "org.mozilla.firefox.sharedID"
|
||||
MOZ_ANDROID_SHARED_ACCOUNT_TYPE = "org.mozilla.firefox_sync"
|
||||
MOZ_ANDROID_SHARED_FXACCOUNT_TYPE = "org.mozilla.firefox_account"
|
||||
else ifeq (org.mozilla.fennec_aurora,$(ANDROID_PACKAGE_NAME))
|
||||
MOZ_ANDROID_SHARED_ID = "org.mozilla.fennec.sharedID"
|
||||
MOZ_ANDROID_SHARED_ACCOUNT_TYPE = "org.mozilla.fennec_sync"
|
||||
MOZ_ANDROID_SHARED_FXACCOUNT_TYPE = "org.mozilla.fennec_account"
|
||||
else ifeq (org.mozilla.fennec,$(ANDROID_PACKAGE_NAME))
|
||||
MOZ_ANDROID_SHARED_ID = "org.mozilla.fennec.sharedID"
|
||||
MOZ_ANDROID_SHARED_ACCOUNT_TYPE = "org.mozilla.fennec_sync"
|
||||
MOZ_ANDROID_SHARED_FXACCOUNT_TYPE = "org.mozilla.fennec_account"
|
||||
endif
|
||||
|
||||
else
|
||||
|
@ -449,6 +454,9 @@ endif
|
|||
ifdef MOZ_ANDROID_SHARED_ACCOUNT_TYPE
|
||||
DEFINES += -DMOZ_ANDROID_SHARED_ACCOUNT_TYPE="$(MOZ_ANDROID_SHARED_ACCOUNT_TYPE)"
|
||||
endif
|
||||
ifdef MOZ_ANDROID_SHARED_FXACCOUNT_TYPE
|
||||
DEFINES += -DMOZ_ANDROID_SHARED_FXACCOUNT_TYPE="$(MOZ_ANDROID_SHARED_FXACCOUNT_TYPE)"
|
||||
endif
|
||||
|
||||
RES_LAYOUT = \
|
||||
$(SYNC_RES_LAYOUT) \
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
# These files are managed in the android-sync repo. Do not modify directly, or your changes will be lost.
|
||||
SYNC_PP_JAVA_FILES := \
|
||||
background/common/GlobalConstants.java \
|
||||
fxa/FxAccountConstants.java \
|
||||
sync/SyncConstants.java \
|
||||
background/announcements/AnnouncementsConstants.java \
|
||||
background/healthreport/HealthReportConstants.java \
|
||||
|
@ -58,6 +59,14 @@ SYNC_JAVA_FILES := \
|
|||
background/healthreport/upload/ObsoleteDocumentTracker.java \
|
||||
background/healthreport/upload/SubmissionClient.java \
|
||||
background/healthreport/upload/SubmissionPolicy.java \
|
||||
fxa/authenticator/FxAccountAuthenticator.java \
|
||||
fxa/authenticator/FxAccountAuthenticatorService.java \
|
||||
fxa/sync/FxAccountBookmarksSyncService.java \
|
||||
fxa/sync/FxAccountHistorySyncService.java \
|
||||
fxa/sync/FxAccountPasswordsSyncService.java \
|
||||
fxa/sync/FxAccountSyncAdapter.java \
|
||||
fxa/sync/FxAccountSyncService.java \
|
||||
fxa/sync/FxAccountTabsSyncService.java \
|
||||
sync/AlreadySyncingException.java \
|
||||
sync/CollectionKeys.java \
|
||||
sync/CommandProcessor.java \
|
||||
|
@ -346,9 +355,14 @@ SYNC_RES_XML := \
|
|||
$(NULL)
|
||||
|
||||
SYNC_PP_RES_XML := \
|
||||
res/xml/sync_syncadapter.xml \
|
||||
res/xml/sync_options.xml \
|
||||
res/xml/sync_syncadapter.xml \
|
||||
res/xml/sync_authenticator.xml \
|
||||
res/xml/fxaccount_tabs_syncadapter.xml \
|
||||
res/xml/fxaccount_passwords_syncadapter.xml \
|
||||
res/xml/fxaccount_history_syncadapter.xml \
|
||||
res/xml/fxaccount_bookmarks_syncadapter.xml \
|
||||
res/xml/fxaccount_authenticator.xml \
|
||||
$(NULL)
|
||||
|
||||
SYNC_THIRDPARTY_JAVA_FILES := \
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
#filter substitution
|
||||
/* 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.fxa;
|
||||
|
||||
public class FxAccountConstants {
|
||||
public static final String GLOBAL_LOG_TAG = "FxAccounts";
|
||||
public static final String ACCOUNT_TYPE = "@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@";
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
/* 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.fxa.authenticator;
|
||||
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.fxa.FxAccountConstants;
|
||||
|
||||
import android.accounts.AbstractAccountAuthenticator;
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountAuthenticatorResponse;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.NetworkErrorException;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
public class FxAccountAuthenticator extends AbstractAccountAuthenticator {
|
||||
public static final String LOG_TAG = FxAccountAuthenticator.class.getSimpleName();
|
||||
|
||||
protected final Context context;
|
||||
protected final AccountManager accountManager;
|
||||
|
||||
public FxAccountAuthenticator(Context context) {
|
||||
super(context);
|
||||
this.context = context;
|
||||
this.accountManager = AccountManager.get(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle addAccount(AccountAuthenticatorResponse response,
|
||||
String accountType, String authTokenType, String[] requiredFeatures,
|
||||
Bundle options)
|
||||
throws NetworkErrorException {
|
||||
Logger.debug(LOG_TAG, "addAccount");
|
||||
|
||||
final Bundle res = new Bundle();
|
||||
|
||||
if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
|
||||
res.putInt(AccountManager.KEY_ERROR_CODE, -1);
|
||||
res.putString(AccountManager.KEY_ERROR_MESSAGE, "Not adding unknown account type.");
|
||||
return res;
|
||||
}
|
||||
|
||||
final Account account = new Account("test@test.com", FxAccountConstants.ACCOUNT_TYPE);
|
||||
final String password = "password";
|
||||
final Bundle userData = Bundle.EMPTY;
|
||||
|
||||
if (!accountManager.addAccountExplicitly(account, password, userData)) {
|
||||
res.putInt(AccountManager.KEY_ERROR_CODE, -1);
|
||||
res.putString(AccountManager.KEY_ERROR_MESSAGE, "Failed to add account explicitly.");
|
||||
return res;
|
||||
}
|
||||
|
||||
Logger.info(LOG_TAG, "Added account named " + account.name + " of type " + account.type);
|
||||
|
||||
// Enable syncing by default.
|
||||
for (String authority : new String[] {
|
||||
AppConstants.ANDROID_PACKAGE_NAME + ".db.browser",
|
||||
AppConstants.ANDROID_PACKAGE_NAME + ".db.formhistory",
|
||||
AppConstants.ANDROID_PACKAGE_NAME + ".db.tabs",
|
||||
AppConstants.ANDROID_PACKAGE_NAME + ".db.passwords",
|
||||
}) {
|
||||
ContentResolver.setSyncAutomatically(account, authority, true);
|
||||
ContentResolver.setIsSyncable(account, authority, 1);
|
||||
}
|
||||
|
||||
res.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
|
||||
res.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options)
|
||||
throws NetworkErrorException {
|
||||
Logger.debug(LOG_TAG, "confirmCredentials");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
|
||||
Logger.debug(LOG_TAG, "editProperties");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle getAuthToken(final AccountAuthenticatorResponse response,
|
||||
final Account account, final String authTokenType, final Bundle options)
|
||||
throws NetworkErrorException {
|
||||
Logger.debug(LOG_TAG, "getAuthToken");
|
||||
|
||||
Logger.warn(LOG_TAG, "Returning null bundle for getAuthToken.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthTokenLabel(String authTokenType) {
|
||||
Logger.debug(LOG_TAG, "getAuthTokenLabel");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle hasFeatures(AccountAuthenticatorResponse response,
|
||||
Account account, String[] features) throws NetworkErrorException {
|
||||
Logger.debug(LOG_TAG, "hasFeatures");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle updateCredentials(AccountAuthenticatorResponse response,
|
||||
Account account, String authTokenType, Bundle options)
|
||||
throws NetworkErrorException {
|
||||
Logger.debug(LOG_TAG, "updateCredentials");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/* 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.fxa.authenticator;
|
||||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
|
||||
public class FxAccountAuthenticatorService extends Service {
|
||||
public static final String LOG_TAG = FxAccountAuthenticatorService.class.getSimpleName();
|
||||
|
||||
// Lazily initialized by <code>getAuthenticator</code>.
|
||||
protected FxAccountAuthenticator accountAuthenticator = null;
|
||||
|
||||
protected FxAccountAuthenticator getAuthenticator() {
|
||||
if (accountAuthenticator == null) {
|
||||
accountAuthenticator = new FxAccountAuthenticator(this);
|
||||
}
|
||||
|
||||
return accountAuthenticator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
Logger.debug(LOG_TAG, "onCreate");
|
||||
|
||||
accountAuthenticator = getAuthenticator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
Logger.debug(LOG_TAG, "onBind");
|
||||
|
||||
if (intent.getAction().equals(android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT)) {
|
||||
return getAuthenticator().getIBinder();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/* 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.fxa.sync;
|
||||
|
||||
public class FxAccountBookmarksSyncService extends FxAccountSyncService {
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/* 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.fxa.sync;
|
||||
|
||||
public class FxAccountHistorySyncService extends FxAccountSyncService {
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/* 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.fxa.sync;
|
||||
|
||||
public class FxAccountPasswordsSyncService extends FxAccountHistorySyncService {
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/* 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.fxa.sync;
|
||||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.content.AbstractThreadedSyncAdapter;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.Context;
|
||||
import android.content.SyncResult;
|
||||
import android.os.Bundle;
|
||||
|
||||
public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
private static final String LOG_TAG = FxAccountSyncAdapter.class.getSimpleName();
|
||||
|
||||
public FxAccountSyncAdapter(Context context, boolean autoInitialize) {
|
||||
super(context, autoInitialize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
|
||||
Logger.info(LOG_TAG, "Syncing FxAccount" +
|
||||
" account named " + account.name +
|
||||
" for authority " + authority +
|
||||
" with instance " + this + ".");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/* 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.fxa.sync;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
|
||||
public abstract class FxAccountSyncService extends Service {
|
||||
private static final Object syncAdapterLock = new Object();
|
||||
private static FxAccountSyncAdapter syncAdapter = null;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
synchronized (syncAdapterLock) {
|
||||
if (syncAdapter == null) {
|
||||
syncAdapter = new FxAccountSyncAdapter(getApplicationContext(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return syncAdapter.getSyncAdapterBinder();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/* 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.fxa.sync;
|
||||
|
||||
public class FxAccountTabsSyncService extends FxAccountSyncService {
|
||||
|
||||
}
|
|
@ -7,6 +7,8 @@
|
|||
<!ENTITY syncBrand.fullName.label "Firefox Sync">
|
||||
<!ENTITY syncBrand.shortName.label "Sync">
|
||||
|
||||
<!ENTITY fxaccountBrand.fullName.label "Firefox Account">
|
||||
|
||||
<!-- Main titles. -->
|
||||
<!ENTITY sync.app.name.label '&syncBrand.fullName.label;'>
|
||||
<!ENTITY sync.title.connect.label 'Connect to &syncBrand.shortName.label;'>
|
||||
|
@ -104,3 +106,6 @@
|
|||
<!ENTITY sync.text.redirect.to.set.up.sync.label 'Set up &syncBrand.fullName.label; on your device to send tabs to other devices.'>
|
||||
<!ENTITY sync.text.tab.sent.label 'Your tab was sent!'>
|
||||
<!ENTITY sync.text.tab.not.sent.label 'There was a problem sending your tab.'>
|
||||
|
||||
<!-- Firefox Account strings -->
|
||||
<!ENTITY fxaccount.label '&fxaccountBrand.fullName.label;'>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
#filter substitution
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- 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/. -->
|
||||
|
||||
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accountType="@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@"
|
||||
android:icon="@drawable/icon"
|
||||
android:smallIcon="@drawable/icon"
|
||||
android:label="@string/fxaccount_label"
|
||||
/>
|
|
@ -0,0 +1,13 @@
|
|||
#filter substitution
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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/. -->
|
||||
|
||||
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accountType="@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@"
|
||||
android:contentAuthority="@ANDROID_PACKAGE_NAME@.db.browser"
|
||||
android:isAlwaysSyncable="true"
|
||||
android:supportsUploading="true"
|
||||
android:userVisible="true"
|
||||
/>
|
|
@ -0,0 +1,13 @@
|
|||
#filter substitution
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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/. -->
|
||||
|
||||
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accountType="@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@"
|
||||
android:contentAuthority="@ANDROID_PACKAGE_NAME@.db.formhistory"
|
||||
android:isAlwaysSyncable="true"
|
||||
android:supportsUploading="true"
|
||||
android:userVisible="true"
|
||||
/>
|
|
@ -0,0 +1,13 @@
|
|||
#filter substitution
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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/. -->
|
||||
|
||||
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accountType="@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@"
|
||||
android:contentAuthority="@ANDROID_PACKAGE_NAME@.db.passwords"
|
||||
android:isAlwaysSyncable="true"
|
||||
android:supportsUploading="true"
|
||||
android:userVisible="true"
|
||||
/>
|
|
@ -0,0 +1,13 @@
|
|||
#filter substitution
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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/. -->
|
||||
|
||||
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accountType="@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@"
|
||||
android:contentAuthority="@ANDROID_PACKAGE_NAME@.db.tabs"
|
||||
android:isAlwaysSyncable="true"
|
||||
android:supportsUploading="true"
|
||||
android:userVisible="true"
|
||||
/>
|
|
@ -45,6 +45,14 @@ background/healthreport/upload/HealthReportUploadService.java
|
|||
background/healthreport/upload/ObsoleteDocumentTracker.java
|
||||
background/healthreport/upload/SubmissionClient.java
|
||||
background/healthreport/upload/SubmissionPolicy.java
|
||||
fxa/authenticator/FxAccountAuthenticator.java
|
||||
fxa/authenticator/FxAccountAuthenticatorService.java
|
||||
fxa/sync/FxAccountBookmarksSyncService.java
|
||||
fxa/sync/FxAccountHistorySyncService.java
|
||||
fxa/sync/FxAccountPasswordsSyncService.java
|
||||
fxa/sync/FxAccountSyncAdapter.java
|
||||
fxa/sync/FxAccountSyncService.java
|
||||
fxa/sync/FxAccountTabsSyncService.java
|
||||
sync/AlreadySyncingException.java
|
||||
sync/CollectionKeys.java
|
||||
sync/CommandProcessor.java
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
|
@ -0,0 +1,55 @@
|
|||
<service
|
||||
android:exported="true"
|
||||
android:name="org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticatorService" >
|
||||
<intent-filter >
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/fxaccount_authenticator" />
|
||||
</service>
|
||||
<service
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.gecko.fxa.sync.FxAccountBookmarksSyncService" >
|
||||
<intent-filter >
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/fxaccount_bookmarks_syncadapter" />
|
||||
</service>
|
||||
<service
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.gecko.fxa.sync.FxAccountHistorySyncService" >
|
||||
<intent-filter >
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/fxaccount_history_syncadapter" />
|
||||
</service>
|
||||
<service
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.gecko.fxa.sync.FxAccountPasswordsSyncService" >
|
||||
<intent-filter >
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/fxaccount_passwords_syncadapter" />
|
||||
</service>
|
||||
<service
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.gecko.fxa.sync.FxAccountTabsSyncService" >
|
||||
<intent-filter >
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/fxaccount_tabs_syncadapter" />
|
||||
</service>
|
|
@ -1,4 +1,5 @@
|
|||
background/common/GlobalConstants.java
|
||||
fxa/FxAccountConstants.java
|
||||
sync/SyncConstants.java
|
||||
background/announcements/AnnouncementsConstants.java
|
||||
background/healthreport/HealthReportConstants.java
|
||||
|
|
|
@ -97,3 +97,6 @@
|
|||
<string name="sync_text_redirect_to_set_up_sync">&sync.text.redirect.to.set.up.sync.label;</string>
|
||||
<string name="sync_text_tab_sent">&sync.text.tab.sent.label;</string>
|
||||
<string name="sync_text_tab_not_sent">&sync.text.tab.not.sent.label;</string>
|
||||
|
||||
<!-- Firefox Account strings -->
|
||||
<string name="fxaccount_label">&fxaccount.label;</string>
|
||||
|
|
|
@ -51,6 +51,17 @@ this.CryptoUtils = {
|
|||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Encode the message into UTF-8 and feed the resulting bytes into the
|
||||
* given hasher. Does not return a hash. This can be called multiple times
|
||||
* with a single hasher, but eventually you must extract the result
|
||||
* yourself.
|
||||
*/
|
||||
updateUTF8: function(message, hasher) {
|
||||
let bytes = this._utf8Converter.convertToByteArray(message, {});
|
||||
hasher.update(bytes, bytes.length);
|
||||
},
|
||||
|
||||
/**
|
||||
* UTF-8 encode a message and perform a SHA-1 over it.
|
||||
*
|
||||
|
@ -342,6 +353,159 @@ this.CryptoUtils = {
|
|||
|
||||
return header += ', ext="' + ext +'"';
|
||||
},
|
||||
|
||||
/**
|
||||
* Given an HTTP header value, strip out any attributes.
|
||||
*/
|
||||
|
||||
stripHeaderAttributes: function(value) {
|
||||
let value = value || "";
|
||||
let i = value.indexOf(";");
|
||||
return value.substring(0, (i >= 0) ? i : undefined).trim().toLowerCase();
|
||||
},
|
||||
|
||||
/**
|
||||
* Compute the HAWK client values (mostly the header) for an HTTP request.
|
||||
*
|
||||
* @param URI
|
||||
* (nsIURI) HTTP request URI.
|
||||
* @param method
|
||||
* (string) HTTP request method.
|
||||
* @param options
|
||||
* (object) extra parameters (all but "credentials" are optional):
|
||||
* credentials - (object, mandatory) HAWK credentials object.
|
||||
* All three keys are required:
|
||||
* id - (string) key identifier
|
||||
* key - (string) raw key bytes
|
||||
* algorithm - (string) which hash to use: "sha1" or "sha256"
|
||||
* ext - (string) application-specific data, included in MAC
|
||||
* localtimeOffsetMsec - (number) local clock offset (vs server)
|
||||
* payload - (string) payload to include in hash, containing the
|
||||
* HTTP request body. If not provided, the HAWK hash
|
||||
* will not cover the request body, and the server
|
||||
* should not check it either. This will be UTF-8
|
||||
* encoded into bytes before hashing. This function
|
||||
* cannot handle arbitrary binary data, sorry (the
|
||||
* UTF-8 encoding process will corrupt any codepoints
|
||||
* between U+0080 and U+00FF). Callers must be careful
|
||||
* to use an HTTP client function which encodes the
|
||||
* payload exactly the same way, otherwise the hash
|
||||
* will not match.
|
||||
* contentType - (string) payload Content-Type. This is included
|
||||
* (without any attributes like "charset=") in the
|
||||
* HAWK hash. It does *not* affect interpretation
|
||||
* of the "payload" property.
|
||||
* hash - (base64 string) pre-calculated payload hash. If
|
||||
* provided, "payload" is ignored.
|
||||
* ts - (number) pre-calculated timestamp, secs since epoch
|
||||
* now - (number) current time, ms-since-epoch, for tests
|
||||
* nonce - (string) pre-calculated nonce. Should only be defined
|
||||
* for testing as this function will generate a
|
||||
* cryptographically secure random one if not defined.
|
||||
* @returns
|
||||
* (object) Contains results of operation. The object has the
|
||||
* following keys:
|
||||
* field - (string) HAWK header, to use in Authorization: header
|
||||
* artifacts - (object) other generated values:
|
||||
* ts - (number) timestamp, in seconds since epoch
|
||||
* nonce - (string)
|
||||
* method - (string)
|
||||
* resource - (string) path plus querystring
|
||||
* host - (string)
|
||||
* port - (number)
|
||||
* hash - (string) payload hash (base64)
|
||||
* ext - (string) app-specific data
|
||||
* MAC - (string) request MAC (base64)
|
||||
*/
|
||||
computeHAWK: function(uri, method, options) {
|
||||
let credentials = options.credentials;
|
||||
let ts = options.ts || Math.floor(((options.now || Date.now()) +
|
||||
(options.localtimeOffsetMsec || 0))
|
||||
/ 1000);
|
||||
|
||||
let hash_algo, hmac_algo;
|
||||
if (credentials.algorithm == "sha1") {
|
||||
hash_algo = Ci.nsICryptoHash.SHA1;
|
||||
hmac_algo = Ci.nsICryptoHMAC.SHA1;
|
||||
} else if (credentials.algorithm == "sha256") {
|
||||
hash_algo = Ci.nsICryptoHash.SHA256;
|
||||
hmac_algo = Ci.nsICryptoHMAC.SHA256;
|
||||
} else {
|
||||
throw new Error("Unsupported algorithm: " + credentials.algorithm);
|
||||
}
|
||||
|
||||
let port;
|
||||
if (uri.port != -1) {
|
||||
port = uri.port;
|
||||
} else if (uri.scheme == "http") {
|
||||
port = 80;
|
||||
} else if (uri.scheme == "https") {
|
||||
port = 443;
|
||||
} else {
|
||||
throw new Error("Unsupported URI scheme: " + uri.scheme);
|
||||
}
|
||||
|
||||
let artifacts = {
|
||||
ts: ts,
|
||||
nonce: options.nonce || btoa(CryptoUtils.generateRandomBytes(8)),
|
||||
method: method.toUpperCase(),
|
||||
resource: uri.path, // This includes both path and search/queryarg.
|
||||
host: uri.asciiHost.toLowerCase(), // This includes punycoding.
|
||||
port: port.toString(10),
|
||||
hash: options.hash,
|
||||
ext: options.ext,
|
||||
};
|
||||
|
||||
let contentType = CryptoUtils.stripHeaderAttributes(options.contentType);
|
||||
|
||||
if (!artifacts.hash && options.hasOwnProperty("payload")) {
|
||||
let hasher = Cc["@mozilla.org/security/hash;1"]
|
||||
.createInstance(Ci.nsICryptoHash);
|
||||
hasher.init(hash_algo);
|
||||
CryptoUtils.updateUTF8("hawk.1.payload\n", hasher);
|
||||
CryptoUtils.updateUTF8(contentType+"\n", hasher);
|
||||
CryptoUtils.updateUTF8(options.payload, hasher);
|
||||
CryptoUtils.updateUTF8("\n", hasher);
|
||||
let hash = hasher.finish(false);
|
||||
// HAWK specifies this .hash to include trailing "==" padding.
|
||||
let hash_b64 = CommonUtils.encodeBase64URL(hash, true);
|
||||
artifacts.hash = hash_b64;
|
||||
}
|
||||
|
||||
let requestString = ("hawk.1.header" + "\n" +
|
||||
artifacts.ts.toString(10) + "\n" +
|
||||
artifacts.nonce + "\n" +
|
||||
artifacts.method + "\n" +
|
||||
artifacts.resource + "\n" +
|
||||
artifacts.host + "\n" +
|
||||
artifacts.port + "\n" +
|
||||
(artifacts.hash || "") + "\n");
|
||||
if (artifacts.ext) {
|
||||
requestString += artifacts.ext.replace("\\", "\\\\").replace("\n", "\\n");
|
||||
}
|
||||
requestString += "\n";
|
||||
|
||||
let hasher = CryptoUtils.makeHMACHasher(hmac_algo,
|
||||
CryptoUtils.makeHMACKey(credentials.key));
|
||||
artifacts.mac = btoa(CryptoUtils.digestBytes(requestString, hasher));
|
||||
// The output MAC uses "+" and "/", and padded== .
|
||||
|
||||
function escape(attribute) {
|
||||
// This is used for "x=y" attributes inside HTTP headers.
|
||||
return attribute.replace(/\\/g, "\\\\").replace(/\"/g, '\\"');
|
||||
}
|
||||
let header = ('Hawk id="' + credentials.id + '", ' +
|
||||
'ts="' + artifacts.ts + '", ' +
|
||||
'nonce="' + artifacts.nonce + '", ' +
|
||||
(artifacts.hash ? ('hash="' + artifacts.hash + '", ') : "") +
|
||||
(artifacts.ext ? ('ext="' + escape(artifacts.ext) + '", ') : "") +
|
||||
'mac="' + artifacts.mac + '"');
|
||||
return {
|
||||
artifacts: artifacts,
|
||||
field: header,
|
||||
};
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
XPCOMUtils.defineLazyGetter(CryptoUtils, "_utf8Converter", function() {
|
||||
|
|
|
@ -0,0 +1,281 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
|
||||
function run_test() {
|
||||
initTestLogging();
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_test(function test_hawk() {
|
||||
let compute = CryptoUtils.computeHAWK;
|
||||
|
||||
// vectors copied from the HAWK (node.js) tests
|
||||
let credentials_sha1 = {
|
||||
id: "123456",
|
||||
key: "2983d45yun89q",
|
||||
algorithm: "sha1",
|
||||
};
|
||||
|
||||
let method = "POST";
|
||||
let ts = 1353809207;
|
||||
let nonce = "Ygvqdz";
|
||||
let result;
|
||||
|
||||
let uri_http = CommonUtils.makeURI("http://example.net/somewhere/over/the/rainbow");
|
||||
let sha1_opts = { credentials: credentials_sha1,
|
||||
ext: "Bazinga!",
|
||||
ts: ts,
|
||||
nonce: nonce,
|
||||
payload: "something to write about",
|
||||
};
|
||||
result = compute(uri_http, method, sha1_opts);
|
||||
|
||||
// The HAWK spec uses non-urlsafe base64 (+/) for its output MAC string.
|
||||
do_check_eq(result.field,
|
||||
'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
|
||||
'hash="bsvY3IfUllw6V5rvk4tStEvpBhE=", ext="Bazinga!", ' +
|
||||
'mac="qbf1ZPG/r/e06F4ht+T77LXi5vw="'
|
||||
);
|
||||
do_check_eq(result.artifacts.ts, ts);
|
||||
do_check_eq(result.artifacts.nonce, nonce);
|
||||
do_check_eq(result.artifacts.method, method);
|
||||
do_check_eq(result.artifacts.resource, "/somewhere/over/the/rainbow");
|
||||
do_check_eq(result.artifacts.host, "example.net");
|
||||
do_check_eq(result.artifacts.port, 80);
|
||||
// artifacts.hash is the *payload* hash, not the overall request MAC.
|
||||
do_check_eq(result.artifacts.hash, "bsvY3IfUllw6V5rvk4tStEvpBhE=");
|
||||
do_check_eq(result.artifacts.ext, "Bazinga!");
|
||||
|
||||
let credentials_sha256 = {
|
||||
id: "123456",
|
||||
key: "2983d45yun89q",
|
||||
algorithm: "sha256",
|
||||
};
|
||||
|
||||
let uri_https = CommonUtils.makeURI("https://example.net/somewhere/over/the/rainbow");
|
||||
let sha256_opts = { credentials: credentials_sha256,
|
||||
ext: "Bazinga!",
|
||||
ts: ts,
|
||||
nonce: nonce,
|
||||
payload: "something to write about",
|
||||
contentType: "text/plain",
|
||||
};
|
||||
|
||||
result = compute(uri_https, method, sha256_opts);
|
||||
do_check_eq(result.field,
|
||||
'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
|
||||
'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' +
|
||||
'ext="Bazinga!", ' +
|
||||
'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="'
|
||||
);
|
||||
do_check_eq(result.artifacts.ts, ts);
|
||||
do_check_eq(result.artifacts.nonce, nonce);
|
||||
do_check_eq(result.artifacts.method, method);
|
||||
do_check_eq(result.artifacts.resource, "/somewhere/over/the/rainbow");
|
||||
do_check_eq(result.artifacts.host, "example.net");
|
||||
do_check_eq(result.artifacts.port, 443);
|
||||
do_check_eq(result.artifacts.hash, "2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=");
|
||||
do_check_eq(result.artifacts.ext, "Bazinga!");
|
||||
|
||||
let sha256_opts_noext = { credentials: credentials_sha256,
|
||||
ts: ts,
|
||||
nonce: nonce,
|
||||
payload: "something to write about",
|
||||
contentType: "text/plain",
|
||||
};
|
||||
result = compute(uri_https, method, sha256_opts_noext);
|
||||
do_check_eq(result.field,
|
||||
'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
|
||||
'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' +
|
||||
'mac="HTgtd0jPI6E4izx8e4OHdO36q00xFCU0FolNq3RiCYs="'
|
||||
);
|
||||
do_check_eq(result.artifacts.ts, ts);
|
||||
do_check_eq(result.artifacts.nonce, nonce);
|
||||
do_check_eq(result.artifacts.method, method);
|
||||
do_check_eq(result.artifacts.resource, "/somewhere/over/the/rainbow");
|
||||
do_check_eq(result.artifacts.host, "example.net");
|
||||
do_check_eq(result.artifacts.port, 443);
|
||||
do_check_eq(result.artifacts.hash, "2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=");
|
||||
|
||||
/* Leaving optional fields out should work, although of course then we can't
|
||||
* assert much about the resulting hashes. The resulting header should look
|
||||
* roughly like:
|
||||
* Hawk id="123456", ts="1378764955", nonce="QkynqsrS44M=", mac="/C5NsoAs2fVn+d/I5wMfwe2Gr1MZyAJ6pFyDHG4Gf9U="
|
||||
*/
|
||||
|
||||
result = compute(uri_https, method, { credentials: credentials_sha256 });
|
||||
let fields = result.field.split(" ");
|
||||
do_check_eq(fields[0], "Hawk");
|
||||
do_check_eq(fields[1], 'id="123456",'); // from creds.id
|
||||
do_check_true(fields[2].startsWith('ts="'));
|
||||
/* The HAWK spec calls for seconds-since-epoch, not ms-since-epoch.
|
||||
* Warning: this test will fail in the year 33658, and for time travellers
|
||||
* who journey earlier than 2001. Please plan accordingly. */
|
||||
do_check_true(result.artifacts.ts > 1000*1000*1000);
|
||||
do_check_true(result.artifacts.ts < 1000*1000*1000*1000);
|
||||
do_check_true(fields[3].startsWith('nonce="'));
|
||||
do_check_eq(fields[3].length, ('nonce="12345678901=",').length);
|
||||
do_check_eq(result.artifacts.nonce.length, ("12345678901=").length);
|
||||
|
||||
let result2 = compute(uri_https, method, { credentials: credentials_sha256 });
|
||||
do_check_neq(result.artifacts.nonce, result2.artifacts.nonce);
|
||||
|
||||
/* Using an upper-case URI hostname shouldn't affect the hash. */
|
||||
|
||||
let uri_https_upper = CommonUtils.makeURI("https://EXAMPLE.NET/somewhere/over/the/rainbow");
|
||||
result = compute(uri_https_upper, method, sha256_opts);
|
||||
do_check_eq(result.field,
|
||||
'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
|
||||
'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' +
|
||||
'ext="Bazinga!", ' +
|
||||
'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="'
|
||||
);
|
||||
|
||||
/* Using a lower-case method name shouldn't affect the hash. */
|
||||
result = compute(uri_https_upper, method.toLowerCase(), sha256_opts);
|
||||
do_check_eq(result.field,
|
||||
'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
|
||||
'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' +
|
||||
'ext="Bazinga!", ' +
|
||||
'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="'
|
||||
);
|
||||
|
||||
/* The localtimeOffsetMsec field should be honored. HAWK uses this to
|
||||
* compensate for clock skew between client and server: if the request is
|
||||
* rejected with a timestamp out-of-range error, the error includes the
|
||||
* server's time, and the client computes its clock offset and tries again.
|
||||
* Clients can remember this offset for a while.
|
||||
*/
|
||||
|
||||
result = compute(uri_https, method, { credentials: credentials_sha256,
|
||||
now: 1378848968650,
|
||||
});
|
||||
do_check_eq(result.artifacts.ts, 1378848968);
|
||||
|
||||
result = compute(uri_https, method, { credentials: credentials_sha256,
|
||||
now: 1378848968650,
|
||||
localtimeOffsetMsec: 1000*1000,
|
||||
});
|
||||
do_check_eq(result.artifacts.ts, 1378848968 + 1000);
|
||||
|
||||
/* Search/query-args in URIs should be included in the hash. */
|
||||
let makeURI = CommonUtils.makeURI;
|
||||
result = compute(makeURI("http://example.net/path"), method, sha256_opts);
|
||||
do_check_eq(result.artifacts.resource, "/path");
|
||||
do_check_eq(result.artifacts.mac, "WyKHJjWaeYt8aJD+H9UeCWc0Y9C+07ooTmrcrOW4MPI=");
|
||||
|
||||
result = compute(makeURI("http://example.net/path/"), method, sha256_opts);
|
||||
do_check_eq(result.artifacts.resource, "/path/");
|
||||
do_check_eq(result.artifacts.mac, "xAYp2MgZQFvTKJT9u8nsvMjshCRRkuaeYqQbYSFp9Qw=");
|
||||
|
||||
result = compute(makeURI("http://example.net/path?query=search"), method, sha256_opts);
|
||||
do_check_eq(result.artifacts.resource, "/path?query=search");
|
||||
do_check_eq(result.artifacts.mac, "C06a8pip2rA4QkBiosEmC32WcgFcW/R5SQC6kUWyqho=");
|
||||
|
||||
/* Test handling of the payload, which is supposed to be a bytestring
|
||||
(String with codepoints from U+0000 to U+00FF, pre-encoded). */
|
||||
|
||||
result = compute(makeURI("http://example.net/path"), method,
|
||||
{ credentials: credentials_sha256,
|
||||
ts: 1353809207,
|
||||
nonce: "Ygvqdz",
|
||||
});
|
||||
do_check_eq(result.artifacts.hash, undefined);
|
||||
do_check_eq(result.artifacts.mac, "S3f8E4hAURAqJxOlsYugkPZxLoRYrClgbSQ/3FmKMbY=");
|
||||
|
||||
result = compute(makeURI("http://example.net/path"), method,
|
||||
{ credentials: credentials_sha256,
|
||||
ts: 1353809207,
|
||||
nonce: "Ygvqdz",
|
||||
payload: "hello",
|
||||
});
|
||||
do_check_eq(result.artifacts.hash, "uZJnFj0XVBA6Rs1hEvdIDf8NraM0qRNXdFbR3NEQbVA=");
|
||||
do_check_eq(result.artifacts.mac, "pLsHHzngIn5CTJhWBtBr+BezUFvdd/IadpTp/FYVIRM=");
|
||||
|
||||
// update, utf-8 payload
|
||||
result = compute(makeURI("http://example.net/path"), method,
|
||||
{ credentials: credentials_sha256,
|
||||
ts: 1353809207,
|
||||
nonce: "Ygvqdz",
|
||||
payload: "andré@example.org", // non-ASCII
|
||||
});
|
||||
do_check_eq(result.artifacts.hash, "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k=");
|
||||
do_check_eq(result.artifacts.mac, "2B++3x5xfHEZbPZGDiK3IwfPZctkV4DUr2ORg1vIHvk=");
|
||||
|
||||
/* If "hash" is provided, "payload" is ignored. */
|
||||
result = compute(makeURI("http://example.net/path"), method,
|
||||
{ credentials: credentials_sha256,
|
||||
ts: 1353809207,
|
||||
nonce: "Ygvqdz",
|
||||
hash: "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k=",
|
||||
payload: "something else",
|
||||
});
|
||||
do_check_eq(result.artifacts.hash, "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k=");
|
||||
do_check_eq(result.artifacts.mac, "2B++3x5xfHEZbPZGDiK3IwfPZctkV4DUr2ORg1vIHvk=");
|
||||
|
||||
/* Test non-ascii hostname. HAWK (via the node.js "url" module) punycodes
|
||||
* "ëxample.net" into "xn--xample-ova.net" before hashing. I still think
|
||||
* punycode was a bad joke that got out of the lab and into a spec.
|
||||
*/
|
||||
|
||||
result = compute(makeURI("http://ëxample.net/path"), method,
|
||||
{ credentials: credentials_sha256,
|
||||
ts: 1353809207,
|
||||
nonce: "Ygvqdz",
|
||||
});
|
||||
do_check_eq(result.artifacts.mac, "pILiHl1q8bbNQIdaaLwAFyaFmDU70MGehFuCs3AA5M0=");
|
||||
do_check_eq(result.artifacts.host, "xn--xample-ova.net");
|
||||
|
||||
result = compute(makeURI("http://example.net/path"), method,
|
||||
{ credentials: credentials_sha256,
|
||||
ts: 1353809207,
|
||||
nonce: "Ygvqdz",
|
||||
ext: "backslash=\\ quote=\" EOF",
|
||||
});
|
||||
do_check_eq(result.artifacts.mac, "BEMW76lwaJlPX4E/dajF970T6+GzWvaeyLzUt8eOTOc=");
|
||||
do_check_eq(result.field, 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ext="backslash=\\\\ quote=\\\" EOF", mac="BEMW76lwaJlPX4E/dajF970T6+GzWvaeyLzUt8eOTOc="');
|
||||
|
||||
result = compute(makeURI("http://example.net:1234/path"), method,
|
||||
{ credentials: credentials_sha256,
|
||||
ts: 1353809207,
|
||||
nonce: "Ygvqdz",
|
||||
});
|
||||
do_check_eq(result.artifacts.mac, "6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE=");
|
||||
do_check_eq(result.field, 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", mac="6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="');
|
||||
|
||||
/* HAWK (the node.js library) uses a URL parser which stores the "port"
|
||||
* field as a string, but makeURI() gives us an integer. So we'll diverge
|
||||
* on ports with a leading zero. This test vector would fail on the node.js
|
||||
* library (HAWK-1.1.1), where they get a MAC of
|
||||
* "T+GcAsDO8GRHIvZLeepSvXLwDlFJugcZroAy9+uAtcw=". I think HAWK should be
|
||||
* updated to do what we do here, so port="01234" should get the same hash
|
||||
* as port="1234".
|
||||
*/
|
||||
result = compute(makeURI("http://example.net:01234/path"), method,
|
||||
{ credentials: credentials_sha256,
|
||||
ts: 1353809207,
|
||||
nonce: "Ygvqdz",
|
||||
});
|
||||
do_check_eq(result.artifacts.mac, "6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE=");
|
||||
do_check_eq(result.field, 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", mac="6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="');
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
|
||||
add_test(function test_strip_header_attributes() {
|
||||
let strip = CryptoUtils.stripHeaderAttributes;
|
||||
|
||||
do_check_eq(strip(undefined), "");
|
||||
do_check_eq(strip("text/plain"), "text/plain");
|
||||
do_check_eq(strip("TEXT/PLAIN"), "text/plain");
|
||||
do_check_eq(strip(" text/plain "), "text/plain");
|
||||
do_check_eq(strip("text/plain ; charset=utf-8 "), "text/plain");
|
||||
|
||||
run_next_test();
|
||||
});
|
|
@ -11,6 +11,7 @@ firefox-appdir = browser
|
|||
# Bug 676977: test hangs consistently on Android
|
||||
skip-if = os == "android"
|
||||
|
||||
[test_utils_hawk.js]
|
||||
[test_utils_hkdfExpand.js]
|
||||
[test_utils_httpmac.js]
|
||||
[test_utils_pbkdf2.js]
|
||||
|
|
Загрузка…
Ссылка в новой задаче