Restructure, UniFFI, and document the fxa-client crate. (#3876)

This is a substantial refactor of the fxa-client crate, intended to bring it
up to speed with our latest best-practices around developing cross-platform
Rust components, in order to ease ongoing maintenance.

THe core change here is that I've deleted the hand-written Kotlin and Swift
bindings, replacing them with autogenerated bindings thanks to UniFFI.

There is still a little bit of hand-written Kotlin, since we have a layer that
automatically manages persistence via a callback. There is still a nontrivial
amount of hand-written Swift, since we have a higher-level state machine built
atop the lower-level fxa-client functionality (such state-machine also exists
for Kotlin, but lives in the android-components repo). If UniFFI works out then
we should look into moving more of that logic into shared Rust code over time.

To support the introduction of UniFFI, I have restructured to Rust crate so
that its public interface deliberately parallels the interface offered to
Kotlin and Swift, and have moved the implementation details into a submodule
named `internal`. It's my opinionated belief that structuring the crate this
way will help us focus on producing a nice consumer API, which is not always
the same thing as producing a nice Rust crate.

On top of that, I've also revamped the documentation in the crate, leaning
in to the use of `cargo doc` as the source of truth for both developer-
and consumer-facing documentation. Let's consider that an experiment and
see how we like it in practice.

Unfortunately, this is a big PR, but I don't think I could have made it
too much smaller. Hopefully it will be easier to review than its size
suggests since it's mostly additions and deletions rather than complex
changes.
This commit is contained in:
Ryan Kelly 2021-03-09 22:34:58 +11:00 коммит произвёл GitHub
Родитель ee6cb4d60d
Коммит d77600e32c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
84 изменённых файлов: 4267 добавлений и 4604 удалений

3
.gitignore поставляемый
Просмотреть файл

@ -22,6 +22,9 @@ xcuserdata
# Protobuf Swift
*.pb.swift
# UniFFI generated artifacts
components/**/ios/Generated
# Glean generated artifacts
megazords/ios/MozillaAppServices/Generated/Metrics.swift
megazords/ios/.venv

Просмотреть файл

@ -2,6 +2,15 @@
# Unreleased Changes
## FxA Client
### ⚠️ Breaking changes ⚠️
- The Kotlin and Swift bindings are now generated automatically using UniFFI.
As a result many small details of the API surface have changed, such as some
classes changing names to be consistent between Rust, Kotlin and Swift.
([#3876](https://github.com/mozilla/application-services/pull/3876))
[Full Changelog](https://github.com/mozilla/application-services/compare/v72.1.0...main)
- The top-level Rust workspace now builds with Rust 1.50

41
Cargo.lock сгенерированный
Просмотреть файл

@ -1118,8 +1118,6 @@ dependencies = [
"log 0.4.11",
"mockiato",
"mockito",
"prost",
"prost-derive",
"rand_rccrypto",
"rate-limiter",
"rc_crypto",
@ -1129,25 +1127,14 @@ dependencies = [
"sync-guid",
"sync15",
"thiserror",
"uniffi",
"uniffi_build",
"uniffi_macros",
"url",
"viaduct",
"viaduct-reqwest",
]
[[package]]
name = "fxaclient_ffi"
version = "0.1.0"
dependencies = [
"ffi-support 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"fxa-client",
"lazy_static",
"log 0.4.11",
"prost",
"serde_json",
"url",
"viaduct",
]
[[package]]
name = "generic-array"
version = "0.14.4"
@ -1219,6 +1206,12 @@ dependencies = [
"uuid",
]
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "h2"
version = "0.2.7"
@ -1640,7 +1633,7 @@ name = "megazord"
version = "0.1.0"
dependencies = [
"autofill",
"fxaclient_ffi",
"fxa-client",
"lazy_static",
"logins_ffi",
"nimbus-sdk",
@ -1655,7 +1648,7 @@ dependencies = [
name = "megazord_ios"
version = "0.1.0"
dependencies = [
"fxaclient_ffi",
"fxa-client",
"glean-ffi",
"logins_ffi",
"places-ffi",
@ -3640,6 +3633,18 @@ dependencies = [
"uniffi_bindgen",
]
[[package]]
name = "uniffi_macros"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10d472df0436970c100f0da4945c334e22ce78bd38ced931b3d6f1be435493e7"
dependencies = [
"glob",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "url"
version = "2.1.1"

Просмотреть файл

@ -3,7 +3,6 @@
members = [
"components/autofill",
"components/fxa-client",
"components/fxa-client/ffi",
"components/logins",
"components/logins/ffi",
"components/places",
@ -80,7 +79,6 @@ exclude = [
default-members = [
"components/autofill",
"components/fxa-client",
"components/fxa-client/ffi",
"components/logins",
"components/logins/ffi",
"components/places",

Просмотреть файл

@ -59,7 +59,8 @@ The following text applies to code linked from these dependencies:
[jexl-parser](https://github.com/mozilla/jexl-rs),
[uniffi](https://github.com/mozilla/uniffi-rs),
[uniffi_bindgen](https://github.com/mozilla/uniffi-rs),
[uniffi_build](https://github.com/mozilla/uniffi-rs)
[uniffi_build](https://github.com/mozilla/uniffi-rs),
[uniffi_macros](https://github.com/mozilla/uniffi-rs)
```
Mozilla Public License Version 2.0
@ -483,6 +484,7 @@ The following text applies to code linked from these dependencies:
[futures-task](https://github.com/rust-lang/futures-rs),
[futures-util](https://github.com/rust-lang/futures-rs),
[getrandom](https://github.com/rust-random/getrandom),
[glob](https://github.com/rust-lang/glob),
[hashbrown](https://github.com/rust-lang/hashbrown),
[hashlink](https://github.com/kyren/hashlink),
[heck](https://github.com/withoutboats/heck),

Просмотреть файл

@ -11,8 +11,6 @@ base64 = "0.12"
hex = "0.4"
lazy_static = "1.4"
log = "0.4"
prost = "0.6"
prost-derive = "0.6"
rate-limiter = { path = "../support/rate-limiter" }
rand_rccrypto = { path = "../support/rand_rccrypto" }
serde = { version = "1", features = ["rc"] }
@ -28,6 +26,11 @@ error-support = { path = "../support/error" }
thiserror = "1.0"
anyhow = "1.0"
sync-guid = { path = "../support/guid", features = ["random"] }
uniffi = "^0.7.2"
uniffi_macros = "^0.7.2"
[build-dependencies]
uniffi_build = { version = "^0.7.2", features=["builtin-bindgen"] }
[dev-dependencies]
viaduct-reqwest = { path = "../support/viaduct-reqwest" }

Просмотреть файл

@ -0,0 +1,117 @@
# Firefox Accounts Client
The fxa-client component lets applications integrate with the
[Firefox Accounts](https://mozilla.github.io/ecosystem-platform/docs/features/firefox-accounts/fxa-overview)
identity service.
* [Features](#features)
* [Using the Firefox Accounts client](#using-the-firefox-accounts-client)
* [Working on the Firefox Accounts client](#working-on-the-firefox-accounts-client)
## Features
The fxa-client component offers support for:
1. Letting users sign in to your app,
using either an email-and-password-based OAuth flow
or a QR-code-based pairing flow.
1. Accessing basic profile information about the signed-in user,
such as email address and display name.
1. Obtaining OAuth access tokens and client-side encryption keys,
in order to access account-enabled services such as Firefox Sync.
1. Listing and managing other applications connected to the
user's account.
1. Sending tabs to other applications that are capable
of receiving them.
1. Managing a device record for your signed-in application,
making it visible to other applications connected to the user's
account.
The component ***does not*** offer, and we have no concrete plans to offer:
* The ability to store data "in the cloud" associated with the user's account.
## Using the Firefox Accounts client
### Before using this component
This component does not currently integrate with the [Glean SDK](https://mozilla.github.io/glean/book/index.html)
and does not submit any telemetry, so you do not need to request a data-review before using this component.
### Prerequisites
To use this component, your application must be registered to [integrate with Firefox Accounts
as an OAuth client](https://mozilla.github.io/ecosystem-platform/docs/process/integration-with-fxa)
and have a unique OAuth `client_id`.
You should also be familiar with how to integrate application-services components
into an application on your target platform:
* **Android**: integrate via the
[service-firefox-accounts](https://github.com/mozilla-mobile/android-components/tree/master/components/service/firefox-accounts/README.md)
component from android-components, which provides higher-level conveniences for state management, persistence,
and integration with other Android components.
* **iOS**: start with the [guide to consuming rust components on
iOS](https://github.com/mozilla/application-services/blob/main/docs/howtos/consuming-rust-components-on-ios.md)
and take a look at the [higher-level Swift wrapper classes](./ios/FxAClient/).
* **Other Platforms**: we don't know yet; please reach out on slack to discuss!
### Core Concepts
You should understand the core concepts of OAuth and the Firefox Accounts system
before attempting to use this component. Please review the
[Firefox Accounts Documentation](https://mozilla.github.io/ecosystem-platform/docs/features/firefox-accounts/fxa-overview)
for more details.
In particular, you should understand [the different types of auth token](
https://github.com/mozilla/ecosystem-platform/pull/39)
in the FxA ecosystem and how each is used, as well as how [OAuth scopes](
https://github.com/mozilla/fxa/blob/main/packages/fxa-auth-server/docs/oauth/scopes.md)
work for accessing related services.
### Component API
For details on how to use this component, consult the [public interface definition](./src/fxa_client.udl) or view the generated Rust API documentation by running:
```
cargo doc --no-deps --open
```
## Working on the Firefox Accounts client
### Prerequisites
To effectively work on the FxA Client component, you will need to be familiar with:
* Our general [guidelines for contributors](../../docs/contributing.md).
* The [core concepts](#core-concepts) for users of the component, outlined above.
* The way we use [uniffi-rs](https://github.com/mozilla/uniffi-rs) to generate API wrappers for multiple languages, such as Kotlin and Swift.
### Overview
This component uses [uniffi-rs](https://github.com/mozilla/uniffi-rs) to create its
public API surface in Rust, and then generate bindings for Kotlin and Swift. The
code is organized as follows:
* The public API surface is implemented in [`./src/lib.rs`](./src/lib/rs), with matching
declarations in [`./src/fxa_client.udl`](./src/fxa_client.udl) to define how it gets
exposed to other languages.
* All the implementation details are written in Rust and can be found under
[`./src/internal/`](./src/internal).
* The [`./android/`](./android) directory contains android-specific build scripts that
generate Kotlin wrappers and publish them as an AAR. It also contains a small amount
of hand-written Kotlin code for things that are not yet supposed by UniFFI.
* The [`./ios/`](./ios) directory contains ios-specific build scripts that generate
Swift wrappers for consumption via an XCode build. It also contains some hand-written
Swift code to expose a higher-level convenience API.
### Detailed Docs
We use rustdoc to document both the public API of this component and its
various internal implementation details. View the docs by running:
```
cargo doc --no-deps --document-private-items --open
```
In particular, the `internal` sub-module contains most of the business
logic, and is delegated to from the public API.

Просмотреть файл

@ -1,7 +1,6 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.google.protobuf'
android {
ndkVersion rootProject.ext.build.ndkVersion
@ -29,12 +28,6 @@ android {
// we can actually find it during tests. (Unfortunately, each project
// has their own build dir)
test.resources.srcDirs += "${project(':full-megazord').buildDir}/rustJniLibs/desktop"
main {
proto {
srcDir '../src'
}
}
}
// This is required to support new AndroidX support libraries.
@ -67,20 +60,6 @@ configurations {
// Success!
jnaForTest
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.11.4'
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}
dependencies {
jnaForTest "net.java.dev.jna:jna:$jna_version@jar"
@ -88,7 +67,6 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.protobuf:protobuf-javalite:3.11.4'
api project(":full-megazord")
implementation project(":native-support")
@ -108,6 +86,20 @@ dependencies {
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
// We do the uniffi binding generation here:
android.libraryVariants.all { variant ->
def t = tasks.register("generate${variant.name.capitalize()}UniffiBindings", Exec) {
workingDir project.rootDir
commandLine 'cargo', 'uniffi-bindgen', 'generate', "${project.projectDir}/../src/fxa_client.udl", '--language', 'kotlin', '--out-dir', "${buildDir}/generated/source/uniffi/${variant.name}/java"
inputs.file '../src/fxa_client.udl'
outputs.dir "${buildDir}/generated/source/uniffi/${variant.name}/java/"
}
variant.javaCompileProvider.get().dependsOn(t)
def sourceSet = variant.sourceSets.find { it.name == variant.name }
sourceSet.java.srcDirs += new File(buildDir, "generated/source/uniffi/${variant.name}/java")
}
evaluationDependsOn(":full-megazord")
afterEvaluate {
// The `cargoBuild` task isn't available until after evaluation.

Просмотреть файл

@ -1,41 +0,0 @@
/* 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 mozilla.appservices.fxaclient
data class AccessTokenInfo(
val scope: String,
val token: String,
val key: ScopedKey?,
val expiresAt: Long
) {
companion object {
internal fun fromMessage(msg: MsgTypes.AccessTokenInfo): AccessTokenInfo {
return AccessTokenInfo(
scope = msg.scope,
token = msg.token,
key = if (msg.hasKey()) ScopedKey.fromMessage(msg.key) else null,
expiresAt = msg.expiresAt
)
}
}
}
data class ScopedKey(
val kty: String,
val scope: String,
val k: String,
val kid: String
) {
companion object {
internal fun fromMessage(msg: MsgTypes.ScopedKey): ScopedKey {
return ScopedKey(
kty = msg.kty,
scope = msg.scope,
k = msg.k,
kid = msg.kid
)
}
}
}

Просмотреть файл

@ -1,45 +0,0 @@
/* 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 mozilla.appservices.fxaclient
// https://proandroiddev.com/til-when-is-when-exhaustive-31d69f630a8b
val <T> T.exhaustive: T
get() = this
sealed class AccountEvent {
// A tab with all its history entries (back button).
class IncomingDeviceCommand(val command: mozilla.appservices.fxaclient.IncomingDeviceCommand) : AccountEvent()
class ProfileUpdated : AccountEvent()
class AccountAuthStateChanged : AccountEvent()
class AccountDestroyed : AccountEvent()
class DeviceConnected(val deviceName: String) : AccountEvent()
class DeviceDisconnected(val deviceId: String, val isLocalDevice: Boolean) : AccountEvent()
companion object {
private fun fromMessage(msg: MsgTypes.AccountEvent): AccountEvent {
return when (msg.type) {
MsgTypes.AccountEvent.AccountEventType.INCOMING_DEVICE_COMMAND -> IncomingDeviceCommand(
command = mozilla.appservices.fxaclient.IncomingDeviceCommand.fromMessage(msg.deviceCommand)
)
MsgTypes.AccountEvent.AccountEventType.PROFILE_UPDATED -> ProfileUpdated()
MsgTypes.AccountEvent.AccountEventType.ACCOUNT_AUTH_STATE_CHANGED -> AccountAuthStateChanged()
MsgTypes.AccountEvent.AccountEventType.ACCOUNT_DESTROYED -> AccountDestroyed()
MsgTypes.AccountEvent.AccountEventType.DEVICE_CONNECTED -> DeviceConnected(
deviceName = msg.deviceConnectedName
)
MsgTypes.AccountEvent.AccountEventType.DEVICE_DISCONNECTED -> DeviceDisconnected(
deviceId = msg.deviceDisconnectedData.deviceId,
isLocalDevice = msg.deviceDisconnectedData.isLocalDevice
)
null -> throw NullPointerException("AccountEvent type cannot be null.")
}.exhaustive
}
internal fun fromCollectionMessage(msg: MsgTypes.AccountEvents): Array<AccountEvent> {
return msg.eventsList.map {
fromMessage(it)
}.toTypedArray()
}
}
}

Просмотреть файл

@ -1,65 +0,0 @@
/* 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 mozilla.appservices.fxaclient
data class AuthorizationParams(
val clientId: String,
val scopes: Array<String>,
val state: String,
val accessType: String = "online",
val pkceParams: AuthorizationPKCEParams? = null,
val keysJwk: String? = null
) {
fun intoMessage(): MsgTypes.AuthorizationParams {
var b = MsgTypes.AuthorizationParams.newBuilder()
.setClientId(this.clientId)
.setScope(this.scopes.joinToString(" "))
.setState(this.state)
.setAccessType(this.accessType)
if (this.pkceParams != null)
b = b.setPkceParams(this.pkceParams.intoMessage())
if (this.keysJwk != null)
b = b.setKeysJwk(this.keysJwk)
return b.build()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AuthorizationParams
if (clientId != other.clientId) return false
if (!scopes.contentEquals(other.scopes)) return false
if (state != other.state) return false
if (accessType != other.accessType) return false
if (pkceParams != other.pkceParams) return false
if (keysJwk != other.keysJwk) return false
return true
}
override fun hashCode(): Int {
var result = clientId.hashCode()
result = 31 * result + scopes.contentHashCode()
result = 31 * result + state.hashCode()
result = 31 * result + accessType.hashCode()
result = 31 * result + (pkceParams?.hashCode() ?: 0)
result = 31 * result + (keysJwk?.hashCode() ?: 0)
return result
}
}
data class AuthorizationPKCEParams(
val codeChallenge: String,
val codeChallengeMethod: String = "S256"
) {
fun intoMessage(): MsgTypes.AuthorizationPKCEParams {
return MsgTypes.AuthorizationPKCEParams.newBuilder()
.setCodeChallenge(this.codeChallenge)
.setCodeChallengeMethod(this.codeChallengeMethod)
.build()
}
}

Просмотреть файл

@ -1,108 +0,0 @@
/* 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 mozilla.appservices.fxaclient
data class Device(
val id: String,
val displayName: String,
val deviceType: Type,
val pushSubscription: PushSubscription?,
val pushEndpointExpired: Boolean,
val isCurrentDevice: Boolean,
val lastAccessTime: Long?,
val capabilities: List<Capability>
) {
enum class Capability {
SEND_TAB;
companion object {
internal fun fromMessage(msg: MsgTypes.Device.Capability): Capability {
return when (msg) {
MsgTypes.Device.Capability.SEND_TAB -> SEND_TAB
}.exhaustive
}
}
}
enum class Type {
DESKTOP,
MOBILE,
TABLET,
VR,
TV,
UNKNOWN;
companion object {
internal fun fromMessage(msg: MsgTypes.Device.Type): Type {
return when (msg) {
MsgTypes.Device.Type.DESKTOP -> DESKTOP
MsgTypes.Device.Type.MOBILE -> MOBILE
MsgTypes.Device.Type.TABLET -> TABLET
MsgTypes.Device.Type.VR -> VR
MsgTypes.Device.Type.TV -> TV
else -> UNKNOWN
}
}
}
fun toNumber(): Int {
// the number resolves to values in fxa_msg_types.proto#L41
return when (this) {
DESKTOP -> MsgTypes.Device.Type.DESKTOP.number
MOBILE -> MsgTypes.Device.Type.MOBILE.number
TABLET -> MsgTypes.Device.Type.TABLET.number
VR -> MsgTypes.Device.Type.VR.number
TV -> MsgTypes.Device.Type.TV.number
else -> MsgTypes.Device.Type.UNKNOWN.number
}
}
}
data class PushSubscription(
val endpoint: String,
val publicKey: String,
val authKey: String
) {
companion object {
internal fun fromMessage(msg: MsgTypes.Device.PushSubscription): PushSubscription {
return PushSubscription(
endpoint = msg.endpoint,
publicKey = msg.publicKey,
authKey = msg.authKey
)
}
}
}
companion object {
internal fun fromMessage(msg: MsgTypes.Device): Device {
return Device(
id = msg.id,
displayName = msg.displayName,
deviceType = Type.fromMessage(msg.type),
pushSubscription = if (msg.hasPushSubscription()) {
PushSubscription.fromMessage(msg.pushSubscription)
} else null,
pushEndpointExpired = msg.pushEndpointExpired,
isCurrentDevice = msg.isCurrentDevice,
lastAccessTime = if (msg.hasLastAccessTime()) msg.lastAccessTime else null,
capabilities = msg.capabilitiesList.map { Capability.fromMessage(it) }
)
}
internal fun fromCollectionMessage(msg: MsgTypes.Devices): Array<Device> {
return msg.devicesList.map {
fromMessage(it)
}.toTypedArray()
}
}
}
fun Set<Device.Capability>.toCollectionMessage(): MsgTypes.Capabilities {
val builder = MsgTypes.Capabilities.newBuilder()
this.forEach {
when (it) {
Device.Capability.SEND_TAB -> builder.addCapability(MsgTypes.Device.Capability.SEND_TAB)
}.exhaustive
}
return builder.build()
}

Просмотреть файл

@ -1,12 +0,0 @@
/* 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 mozilla.appservices.fxaclient
open class FxaException(message: String) : Exception(message) {
class Unspecified(msg: String) : FxaException(msg)
class Unauthorized(msg: String) : FxaException(msg)
class Network(msg: String) : FxaException(msg)
class Panic(msg: String) : FxaException(msg)
}

Просмотреть файл

@ -1,37 +0,0 @@
/* 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 mozilla.appservices.fxaclient
data class TabHistoryEntry(
val title: String,
val url: String
)
sealed class IncomingDeviceCommand {
// A tab with all its history entries (back button).
class TabReceived(val from: Device?, val entries: Array<TabHistoryEntry>) : IncomingDeviceCommand()
companion object {
internal fun fromMessage(msg: MsgTypes.IncomingDeviceCommand): IncomingDeviceCommand {
return when (msg.type) {
MsgTypes.IncomingDeviceCommand.IncomingDeviceCommandType.TAB_RECEIVED -> {
val data = msg.tabReceivedData
TabReceived(
from = if (data.hasFrom()) Device.fromMessage(data.from) else null,
entries = data.entriesList.map {
TabHistoryEntry(title = it.title, url = it.url)
}.toTypedArray()
)
}
null -> throw NullPointerException("IncomingDeviceCommand type cannot be null.")
}.exhaustive
}
internal fun fromCollectionMessage(msg: MsgTypes.IncomingDeviceCommands): Array<IncomingDeviceCommand> {
return msg.commandsList.map {
fromMessage(it)
}.toTypedArray()
}
}
}

Просмотреть файл

@ -1,17 +0,0 @@
/* 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 mozilla.appservices.fxaclient
data class IntrospectInfo(
val active: Boolean
) {
companion object {
internal fun fromMessage(msg: MsgTypes.IntrospectInfo): IntrospectInfo {
return IntrospectInfo(
active = msg.active
)
}
}
}

Просмотреть файл

@ -1,54 +0,0 @@
/* 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 mozilla.appservices.fxaclient
data class MetricsParams(
val flowId: String? = null,
val flowBeginTime: Long? = null,
val deviceId: String? = null,
val utmSource: String? = null,
val utmContent: String? = null,
val utmMedium: String? = null,
val utmTerm: String? = null,
val utmCampaign: String? = null,
val entrypointExperiment: String? = null,
val entrypointVariation: String? = null
) {
fun intoMessage(): MsgTypes.MetricsParams {
var metricsParamsBuilder = MsgTypes.MetricsParams.newBuilder()
if (flowId != null) {
metricsParamsBuilder = metricsParamsBuilder.putParameters("flow_id", flowId)
}
if (flowBeginTime != null) {
metricsParamsBuilder = metricsParamsBuilder.putParameters("flow_begin_time", flowBeginTime.toString())
}
if (deviceId != null) {
metricsParamsBuilder = metricsParamsBuilder.putParameters("device_id", deviceId)
}
if (utmSource != null) {
metricsParamsBuilder = metricsParamsBuilder.putParameters("utm_source", utmSource)
}
if (utmContent != null) {
metricsParamsBuilder = metricsParamsBuilder.putParameters("utm_content", utmContent)
}
if (utmMedium != null) {
metricsParamsBuilder = metricsParamsBuilder.putParameters("utm_medium", utmMedium)
}
if (utmTerm != null) {
metricsParamsBuilder = metricsParamsBuilder.putParameters("utm_term", utmTerm)
}
if (utmCampaign != null) {
metricsParamsBuilder = metricsParamsBuilder.putParameters("utm_campaign", utmCampaign)
}
if (entrypointExperiment != null) {
metricsParamsBuilder = metricsParamsBuilder.putParameters("entrypoint_experiment", entrypointExperiment)
}
if (entrypointVariation != null) {
metricsParamsBuilder = metricsParamsBuilder.putParameters("entrypoint_variation", entrypointVariation)
}
return metricsParamsBuilder.build()
}
}

Просмотреть файл

@ -1,23 +0,0 @@
/* 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 mozilla.appservices.fxaclient
enum class MigrationState {
NONE,
COPY_SESSION_TOKEN,
REUSE_SESSION_TOKEN;
companion object {
@Suppress("TooGenericExceptionThrown")
internal fun fromNumber(v: Number): MigrationState {
return when (v) {
0 -> NONE
1 -> COPY_SESSION_TOKEN
2 -> REUSE_SESSION_TOKEN
else -> throw RuntimeException("[Bug] unknown MigrationState value $v")
}
}
}
}

Просмотреть файл

@ -5,37 +5,40 @@
package mozilla.appservices.fxaclient
import android.util.Log
import com.sun.jna.Native
import com.sun.jna.Pointer
import mozilla.appservices.fxaclient.rust.FxaHandle
import mozilla.appservices.fxaclient.rust.LibFxAFFI
import mozilla.appservices.fxaclient.rust.RustError
import mozilla.appservices.support.native.toNioDirectBuffer
import java.util.concurrent.atomic.AtomicLong
import org.json.JSONObject
/**
* FirefoxAccount represents the authentication state of a client.
* PersistedFirefoxAccount represents the authentication state of a client.
*
* This is a thin wrapper around the `FirefoxAccount` object exposed from Rust.
* Its main job is to transparently manage persistence of the account state by
* calling the provided callback at appropriate times.
*
* In future it would be nice to push this logic down into the Rust code,
* once UniFFI's support for callback interfaces is a little more battle-tested.
*
*/
class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : AutoCloseable {
private var handle: AtomicLong = AtomicLong(handle)
class PersistedFirefoxAccount(inner: FirefoxAccount, persistCallback: PersistCallback?) : AutoCloseable {
private var inner: FirefoxAccount = inner
private var persistCallback: PersistCallback? = persistCallback
/**
* Create a FirefoxAccount using the given config.
* Create a PersistedFirefoxAccount using the given config.
*
* This does not make network requests, and can be used on the main thread.
*
*/
constructor(config: Config, persistCallback: PersistCallback? = null) :
this(rustCall { e ->
LibFxAFFI.INSTANCE.fxa_new(
config.contentUrl,
config.clientId,
config.redirectUri,
config.tokenServerUrlOverride,
e
)
}, persistCallback) {
constructor(config: Config, persistCallback: PersistCallback? = null) : this(FirefoxAccount(
// This is kind of dumb - we take a Config object on the Kotlin side, destructure it into its fields
// to pass over the FFI, then the Rust side turns it back into its own variant of a Config object!
// That made sense when we had to write the FFI layer by hand, but we should see whether we can nicely
// expose the Rust Config interface to Kotlin and Swift and then just accept a Config here in the
// underlying `FirefoxAccount` constructor.
config.contentUrl,
config.clientId,
config.redirectUri,
config.tokenServerUrlOverride
), persistCallback) {
// Persist the newly created instance state.
this.tryPersistState()
}
@ -43,16 +46,14 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
companion object {
/**
* Restores the account's authentication state from a JSON string produced by
* [FirefoxAccount.toJSONString].
* [PersistedFirefoxAccount.toJSONString].
*
* This does not make network requests, and can be used on the main thread.
*
* @return [FirefoxAccount] representing the authentication state
* @return [PersistedFirefoxAccount] representing the authentication state
*/
fun fromJSONString(json: String, persistCallback: PersistCallback? = null): FirefoxAccount {
return FirefoxAccount(rustCall { e ->
LibFxAFFI.INSTANCE.fxa_from_json(json, e)
}, persistCallback)
fun fromJSONString(json: String, persistCallback: PersistCallback? = null): PersistedFirefoxAccount {
return PersistedFirefoxAccount(FirefoxAccount.fromJson(json), persistCallback)
}
}
@ -85,7 +86,7 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
val json: String
try {
json = this.toJSONString()
} catch (e: FxaException) {
} catch (e: FxaErrorException) {
Log.e("FirefoxAccount", "Error serializing the FirefoxAccount state.")
return
}
@ -106,21 +107,9 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
fun beginOAuthFlow(
scopes: Array<String>,
entrypoint: String,
metricsParams: MetricsParams = MetricsParams()
metricsParams: MetricsParams = MetricsParams(mapOf())
): String {
val scope = scopes.joinToString(" ")
val (nioBuf, len) = metricsParams.intoMessage().toNioDirectBuffer()
return rustCallWithLock { e ->
val ptr = Native.getDirectBufferPointer(nioBuf)
LibFxAFFI.INSTANCE.fxa_begin_oauth_flow(
this.handle.get(),
scope,
entrypoint,
ptr,
len,
e
)
}.getAndConsumeRustString()
return this.inner.beginOauthFlow(scopes.toList(), entrypoint, metricsParams)
}
/**
@ -138,22 +127,9 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
pairingUrl: String,
scopes: Array<String>,
entrypoint: String,
metricsParams: MetricsParams = MetricsParams()
metricsParams: MetricsParams = MetricsParams(mapOf())
): String {
val scope = scopes.joinToString(" ")
val (nioBuf, len) = metricsParams.intoMessage().toNioDirectBuffer()
return rustCallWithLock { e ->
val ptr = Native.getDirectBufferPointer(nioBuf)
LibFxAFFI.INSTANCE.fxa_begin_pairing_flow(
this.handle.get(),
pairingUrl,
scope,
entrypoint,
ptr,
len,
e
)
}.getAndConsumeRustString()
return this.inner.beginPairingFlow(pairingUrl, scopes.toList(), entrypoint, metricsParams)
}
/**
@ -165,9 +141,7 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* This performs network requests, and should not be used on the main thread.
*/
fun completeOAuthFlow(code: String, state: String) {
rustCallWithLock { e ->
LibFxAFFI.INSTANCE.fxa_complete_oauth_flow(this.handle.get(), code, state, e)
}
this.inner.completeOauthFlow(code, state)
this.tryPersistState()
}
@ -183,16 +157,10 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* The caller should then start the OAuth Flow again with the "profile" scope.
*/
fun getProfile(ignoreCache: Boolean): Profile {
val profileBuffer = rustCallWithLock { e ->
val ignoreCacheArg: Byte = if (ignoreCache) { 1 } else { 0 }
LibFxAFFI.INSTANCE.fxa_profile(this.handle.get(), ignoreCacheArg, e)
}
this.tryPersistState()
try {
val p = MsgTypes.Profile.parseFrom(profileBuffer.asCodedInputStream()!!)
return Profile.fromMessage(p)
return this.inner.getProfile(ignoreCache)
} finally {
LibFxAFFI.INSTANCE.fxa_bytebuffer_free(profileBuffer)
this.tryPersistState()
}
}
@ -216,9 +184,7 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* This performs network requests, and should not be used on the main thread.
*/
fun getTokenServerEndpointURL(): String {
return rustCallWithLock { e ->
LibFxAFFI.INSTANCE.fxa_get_token_server_endpoint_url(this.handle.get(), e)
}.getAndConsumeRustString()
return this.inner.getTokenServerEndpointUrl()
}
/**
@ -227,9 +193,7 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* This does not make network requests, and can be used on the main thread.
*/
fun getPairingAuthorityURL(): String {
return rustCallWithLock { e ->
LibFxAFFI.INSTANCE.fxa_get_pairing_authority_url(this.handle.get(), e)
}.getAndConsumeRustString()
return this.inner.getPairingAuthorityUrl()
}
/**
@ -238,9 +202,7 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* This does not make network requests, and can be used on the main thread.
*/
fun getConnectionSuccessURL(): String {
return rustCallWithLock { e ->
LibFxAFFI.INSTANCE.fxa_get_connection_success_url(this.handle.get(), e)
}.getAndConsumeRustString()
return this.inner.getConnectionSuccessUrl()
}
/**
@ -252,9 +214,7 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* The caller should then start the OAuth Flow again with the "profile" scope.
*/
fun getManageAccountURL(entrypoint: String): String {
return rustCallWithLock { e ->
LibFxAFFI.INSTANCE.fxa_get_manage_account_url(this.handle.get(), entrypoint, e)
}.getAndConsumeRustString()
return this.inner.getManageAccountUrl(entrypoint)
}
/**
@ -266,9 +226,7 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* The caller should then start the OAuth Flow again with the "profile" scope.
*/
fun getManageDevicesURL(entrypoint: String): String {
return rustCallWithLock { e ->
LibFxAFFI.INSTANCE.fxa_get_manage_devices_url(this.handle.get(), entrypoint, e)
}.getAndConsumeRustString()
return this.inner.getManageDevicesUrl(entrypoint)
}
/**
@ -285,29 +243,15 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* the desired scope.
*/
fun getAccessToken(scope: String, ttl: Long? = null): AccessTokenInfo {
val buffer = rustCallWithLock { e ->
// A zero ttl here means to use the server-controlled default.
LibFxAFFI.INSTANCE.fxa_get_access_token(this.handle.get(), scope, ttl ?: 0L, e)
}
this.tryPersistState()
try {
val msg = MsgTypes.AccessTokenInfo.parseFrom(buffer.asCodedInputStream()!!)
return AccessTokenInfo.fromMessage(msg)
return this.inner.getAccessToken(scope, ttl)
} finally {
LibFxAFFI.INSTANCE.fxa_bytebuffer_free(buffer)
this.tryPersistState()
}
}
fun checkAuthorizationStatus(): IntrospectInfo {
val buffer = rustCallWithLock { e ->
LibFxAFFI.INSTANCE.fxa_check_authorization_status(this.handle.get(), e)
}
try {
val msg = MsgTypes.IntrospectInfo.parseFrom(buffer.asCodedInputStream()!!)
return IntrospectInfo.fromMessage(msg)
} finally {
LibFxAFFI.INSTANCE.fxa_bytebuffer_free(buffer)
}
fun checkAuthorizationStatus(): AuthorizationInfo {
return this.inner.checkAuthorizationStatus()
}
/**
@ -316,9 +260,7 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* @throws FxaException Will send you an exception if there is no session token set
*/
fun getSessionToken(): String {
return rustCallWithLock { e ->
LibFxAFFI.INSTANCE.fxa_get_session_token(this.handle.get(), e)
}.getAndConsumeRustString()
return this.inner.getSessionToken()
}
/**
@ -327,9 +269,7 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* @throws FxaException Will send you an exception if there is no device id set
*/
fun getCurrentDeviceId(): String {
return rustCallWithLock { e ->
LibFxAFFI.INSTANCE.fxa_get_current_device_id(this.handle.get(), e)
}.getAndConsumeRustString()
return this.inner.getCurrentDeviceId()
}
/**
@ -339,18 +279,9 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* This performs network requests, and should not be used on the main thread.
*/
fun authorizeOAuthCode(
authParams: AuthorizationParams
authParams: AuthorizationParameters
): String {
val (nioBuf, len) = authParams.intoMessage().toNioDirectBuffer()
return rustCallWithLock { e ->
val ptr = Native.getDirectBufferPointer(nioBuf)
LibFxAFFI.INSTANCE.fxa_authorize_auth_code(
this.handle.get(),
ptr,
len,
e
)
}.getAndConsumeRustString()
return this.inner.authorizeCodeUsingSessionToken(authParams)
}
/**
@ -361,9 +292,7 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* again.
*/
fun clearAccessTokenCache() {
rustCallWithLock { e ->
LibFxAFFI.INSTANCE.fxa_clear_access_token_cache(this.handle.get(), e)
}
this.inner.clearAccessTokenCache()
}
/**
@ -378,17 +307,8 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
*/
fun migrateFromSessionToken(sessionToken: String, kSync: String, kXCS: String): JSONObject {
try {
val json = rustCallWithLock { e ->
LibFxAFFI.INSTANCE.fxa_migrate_from_session_token(
this.handle.get(),
sessionToken,
kSync,
kXCS,
0,
e
)
}.getAndConsumeRustString()
return JSONObject(json)
val res = this.inner.migrateFromSessionToken(sessionToken, kSync, kXCS, false)
return JSONObject(mapOf("total_duration" to res.totalDuration))
} finally {
// Even a failed migration might alter the persisted account state, if it's able to be retried.
// It's safe to call this unconditionally, as the underlying code will not leave partial states.
@ -402,12 +322,7 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* @return bool Returns a boolean if we are in a migration state
*/
fun isInMigrationState(): MigrationState {
// Bug: see https://youtrack.jetbrains.com/issue/KT-32104.
@Suppress("IMPLICIT_NOTHING_AS_TYPE_PARAMETER")
rustCall { e ->
val state = LibFxAFFI.INSTANCE.fxa_is_in_migration_state(this.handle.get(), e)
return MigrationState.fromNumber(state.toInt())
}
return this.inner.isInMigrationState()
}
/**
@ -422,10 +337,8 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
*/
fun copyFromSessionToken(sessionToken: String, kSync: String, kXCS: String): JSONObject {
try {
val json = rustCallWithLock { e ->
LibFxAFFI.INSTANCE.fxa_migrate_from_session_token(this.handle.get(), sessionToken, kSync, kXCS, 1, e)
}.getAndConsumeRustString()
return JSONObject(json)
val res = this.inner.migrateFromSessionToken(sessionToken, kSync, kXCS, true)
return JSONObject(mapOf("total_duration" to res.totalDuration))
} finally {
// Even a failed migration might alter the persisted account state, if it's able to be retried.
// It's safe to call this unconditionally, as the underlying code will not leave partial states.
@ -442,10 +355,8 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
*/
fun retryMigrateFromSessionToken(): JSONObject {
try {
val json = rustCallWithLock { e ->
LibFxAFFI.INSTANCE.fxa_retry_migrate_from_session_token(this.handle.get(), e)
}.getAndConsumeRustString()
return JSONObject(json)
val res = this.inner.retryMigrateFromSessionToken()
return JSONObject(mapOf("total_duration" to res.totalDuration))
} finally {
// A failure her might alter the persisted account state, if we discover a permanent migration failure.
// It's safe to call this unconditionally, as the underlying code will not leave partial states.
@ -463,9 +374,7 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* @return String containing the authentication details in JSON format
*/
fun toJSONString(): String {
return rustCallWithLock { e ->
LibFxAFFI.INSTANCE.fxa_to_json(this.handle.get(), e)
}.getAndConsumeRustString()
return this.inner.toJson()
}
/**
@ -479,8 +388,10 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* @param authKey Auth key used to encrypt push payloads
*/
fun setDevicePushSubscription(endpoint: String, publicKey: String, authKey: String) {
rustCall { e ->
LibFxAFFI.INSTANCE.fxa_set_push_subscription(this.handle.get(), endpoint, publicKey, authKey, e)
try {
return this.inner.setPushSubscription(DevicePushSubscription(endpoint, publicKey, authKey))
} finally {
this.tryPersistState()
}
}
@ -493,8 +404,10 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* @param displayName The current device display name
*/
fun setDeviceDisplayName(displayName: String) {
rustCall { e ->
LibFxAFFI.INSTANCE.fxa_set_device_name(this.handle.get(), displayName, e)
try {
return this.inner.setDeviceName(displayName)
} finally {
this.tryPersistState()
}
}
@ -504,16 +417,7 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* This performs network requests, and should not be used on the main thread.
*/
fun getDevices(ignoreCache: Boolean = false): Array<Device> {
val devicesBuffer = rustCall { e ->
val ignoreCacheArg: Byte = if (ignoreCache) { 1 } else { 0 }
LibFxAFFI.INSTANCE.fxa_get_devices(this.handle.get(), ignoreCacheArg, e)
}
try {
val devices = MsgTypes.Devices.parseFrom(devicesBuffer.asCodedInputStream()!!)
return Device.fromCollectionMessage(devices)
} finally {
LibFxAFFI.INSTANCE.fxa_bytebuffer_free(devicesBuffer)
}
return this.inner.getDevices(ignoreCache).toTypedArray()
}
/**
@ -523,9 +427,7 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* This performs network requests, and should not be used on the main thread.
*/
fun disconnect() {
rustCall { e ->
LibFxAFFI.INSTANCE.fxa_disconnect(this.handle.get(), e)
}
this.inner.disconnect()
this.tryPersistState()
}
@ -541,15 +443,10 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* @return A collection of [IncomingDeviceCommand] that should be handled by the caller.
*/
fun pollDeviceCommands(): Array<IncomingDeviceCommand> {
val eventsBuffer = rustCall { e ->
LibFxAFFI.INSTANCE.fxa_poll_device_commands(this.handle.get(), e)
}
this.tryPersistState()
try {
val commands = MsgTypes.IncomingDeviceCommands.parseFrom(eventsBuffer.asCodedInputStream()!!)
return IncomingDeviceCommand.fromCollectionMessage(commands)
return this.inner.pollDeviceCommands().toTypedArray()
} finally {
LibFxAFFI.INSTANCE.fxa_bytebuffer_free(eventsBuffer)
this.tryPersistState()
}
}
@ -562,15 +459,10 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* @return A collection of [AccountEvent] that should be handled by the caller.
*/
fun handlePushMessage(payload: String): Array<AccountEvent> {
val eventsBuffer = rustCall { e ->
LibFxAFFI.INSTANCE.fxa_handle_push_message(this.handle.get(), payload, e)
}
this.tryPersistState()
try {
val events = MsgTypes.AccountEvents.parseFrom(eventsBuffer.asCodedInputStream()!!)
return AccountEvent.fromCollectionMessage(events)
return this.inner.handlePushMessage(payload).toTypedArray()
} finally {
LibFxAFFI.INSTANCE.fxa_bytebuffer_free(eventsBuffer)
this.tryPersistState()
}
}
@ -581,12 +473,8 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
*
* This performs network requests, and should not be used on the main thread.
*/
fun initializeDevice(name: String, deviceType: Device.Type, supportedCapabilities: Set<Device.Capability>) {
val (nioBuf, len) = supportedCapabilities.toCollectionMessage().toNioDirectBuffer()
rustCall { e ->
val ptr = Native.getDirectBufferPointer(nioBuf)
LibFxAFFI.INSTANCE.fxa_initialize_device(this.handle.get(), name, deviceType.toNumber(), ptr, len, e)
}
fun initializeDevice(name: String, deviceType: DeviceType, supportedCapabilities: Set<DeviceCapability>) {
this.inner.initializeDevice(name, deviceType, supportedCapabilities.toList())
this.tryPersistState()
}
@ -600,12 +488,8 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
*
* This performs network requests, and should not be used on the main thread.
*/
fun ensureCapabilities(supportedCapabilities: Set<Device.Capability>) {
val (nioBuf, len) = supportedCapabilities.toCollectionMessage().toNioDirectBuffer()
rustCall { e ->
val ptr = Native.getDirectBufferPointer(nioBuf)
LibFxAFFI.INSTANCE.fxa_ensure_capabilities(this.handle.get(), ptr, len, e)
}
fun ensureCapabilities(supportedCapabilities: Set<DeviceCapability>) {
this.inner.ensureCapabilities(supportedCapabilities.toList())
this.tryPersistState()
}
@ -619,9 +503,7 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* @param url The url of the tab being sent
*/
fun sendSingleTab(targetDeviceId: String, title: String, url: String) {
rustCall { e ->
LibFxAFFI.INSTANCE.fxa_send_tab(this.handle.get(), targetDeviceId, title, url, e)
}
this.inner.sendSingleTab(targetDeviceId, title, url)
}
/**
@ -631,71 +513,11 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
* This does not make network requests, and can be used on the main thread.
*/
fun gatherTelemetry(): String {
return rustCallWithLock { e ->
LibFxAFFI.INSTANCE.fxa_gather_telemetry(this.handle.get(), e)
}.getAndConsumeRustString()
return this.inner.gatherTelemetry()
}
@Synchronized
override fun close() {
val handle = this.handle.getAndSet(0)
if (handle != 0L) {
rustCall { err ->
LibFxAFFI.INSTANCE.fxa_free(handle, err)
}
}
}
private inline fun <U> nullableRustCallWithLock(callback: (RustError.ByReference) -> U?): U? {
return synchronized(this) {
nullableRustCall { callback(it) }
}
}
private inline fun <U> rustCallWithLock(callback: (RustError.ByReference) -> U?): U {
return nullableRustCallWithLock(callback)!!
this.inner.destroy()
}
}
// In practice we usually need to be synchronized to call this safely, so it doesn't
// synchronize itself
private inline fun <U> nullableRustCall(callback: (RustError.ByReference) -> U?): U? {
val e = RustError.ByReference()
try {
val ret = callback(e)
if (e.isFailure()) {
throw e.intoException()
}
return ret
} finally {
// This only matters if `callback` throws (or does a non-local return, which
// we currently don't do)
e.ensureConsumed()
}
}
private inline fun <U> rustCall(callback: (RustError.ByReference) -> U?): U {
return nullableRustCall(callback)!!
}
/**
* Helper to read a null terminated String out of the Pointer and free it.
*
* Important: Do not use this pointer after this! For anything!
*/
internal fun Pointer.getAndConsumeRustString(): String {
try {
return this.getRustString()
} finally {
LibFxAFFI.INSTANCE.fxa_str_free(this)
}
}
/**
* Helper to read a null terminated string out of the pointer.
*
* Important: doesn't free the pointer, use [getAndConsumeRustString] for that!
*/
internal fun Pointer.getRustString(): String {
return this.getString(0, "utf8")
}

Просмотреть файл

@ -1,24 +0,0 @@
/* 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 mozilla.appservices.fxaclient
data class Profile(
val uid: String?,
val email: String?,
val avatar: String?,
val avatarDefault: Boolean,
val displayName: String?
) {
companion object {
internal fun fromMessage(msg: MsgTypes.Profile): Profile {
return Profile(uid = if (msg.hasUid()) msg.uid else null,
email = if (msg.hasEmail()) msg.email else null,
avatar = if (msg.hasAvatar()) msg.avatar else null,
avatarDefault = msg.avatarDefault,
displayName = if (msg.hasDisplayName()) msg.displayName else null
)
}
}
}

Просмотреть файл

@ -1,118 +0,0 @@
@file:Suppress("MaxLineLength")
/* 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 mozilla.appservices.fxaclient.rust
import com.sun.jna.Library
import com.sun.jna.Pointer
import mozilla.appservices.support.native.RustBuffer
import mozilla.appservices.support.native.loadIndirect
import org.mozilla.appservices.fxaclient.BuildConfig
@Suppress("FunctionNaming", "FunctionParameterNaming", "LongParameterList", "TooGenericExceptionThrown")
internal interface LibFxAFFI : Library {
companion object {
internal var INSTANCE: LibFxAFFI =
loadIndirect(componentName = "fxaclient", componentVersion = BuildConfig.LIBRARY_VERSION)
}
fun fxa_new(
contentUrl: String,
clientId: String,
redirectUri: String,
tokenServerUrlOverride: String?,
e: RustError.ByReference
): FxaHandle
fun fxa_from_json(json: String, e: RustError.ByReference): FxaHandle
fun fxa_to_json(fxa: Long, e: RustError.ByReference): Pointer?
fun fxa_begin_oauth_flow(
fxa: FxaHandle,
scopes: String,
entrypoint: String,
metrics_params: Pointer,
metrics_params_len: Int,
e: RustError.ByReference
): Pointer?
fun fxa_begin_pairing_flow(
fxa: FxaHandle,
pairingUrl: String,
scopes: String,
entrypoint: String,
metrics_params: Pointer,
metrics_params_len: Int,
e: RustError.ByReference
): Pointer?
fun fxa_profile(fxa: FxaHandle, ignoreCache: Byte, e: RustError.ByReference): RustBuffer.ByValue
fun fxa_get_token_server_endpoint_url(fxa: FxaHandle, e: RustError.ByReference): Pointer?
fun fxa_get_pairing_authority_url(fxa: FxaHandle, e: RustError.ByReference): Pointer?
fun fxa_get_connection_success_url(fxa: FxaHandle, e: RustError.ByReference): Pointer?
fun fxa_get_manage_account_url(fxa: FxaHandle, entrypoint: String, e: RustError.ByReference): Pointer?
fun fxa_get_manage_devices_url(fxa: FxaHandle, entrypoint: String, e: RustError.ByReference): Pointer?
fun fxa_complete_oauth_flow(fxa: FxaHandle, code: String, state: String, e: RustError.ByReference)
fun fxa_get_access_token(fxa: FxaHandle, scope: String, ttl: Long, e: RustError.ByReference): RustBuffer.ByValue
fun fxa_get_session_token(fxa: FxaHandle, e: RustError.ByReference): Pointer?
fun fxa_get_current_device_id(fxa: FxaHandle, e: RustError.ByReference): Pointer?
fun fxa_authorize_auth_code(fxa: FxaHandle, auth_params: Pointer, auth_params_len: Int, e: RustError.ByReference): Pointer?
fun fxa_check_authorization_status(fxa: FxaHandle, e: RustError.ByReference): RustBuffer.ByValue
fun fxa_clear_access_token_cache(fxa: FxaHandle, e: RustError.ByReference)
fun fxa_set_push_subscription(
fxa: FxaHandle,
endpoint: String,
publicKey: String,
authKey: String,
e: RustError.ByReference
)
fun fxa_set_device_name(fxa: FxaHandle, displayName: String, e: RustError.ByReference)
fun fxa_get_devices(fxa: FxaHandle, ignoreCache: Byte, e: RustError.ByReference): RustBuffer.ByValue
fun fxa_disconnect(fxa: FxaHandle, e: RustError.ByReference)
fun fxa_poll_device_commands(fxa: FxaHandle, e: RustError.ByReference): RustBuffer.ByValue
fun fxa_handle_push_message(fxa: FxaHandle, jsonPayload: String, e: RustError.ByReference): RustBuffer.ByValue
fun fxa_initialize_device(
fxa: FxaHandle,
name: String,
type: Int,
capabilities_data: Pointer,
capabilities_len: Int,
e: RustError.ByReference
)
fun fxa_ensure_capabilities(
fxa: FxaHandle,
capabilities_data: Pointer,
capabilities_len: Int,
e: RustError.ByReference
)
fun fxa_send_tab(fxa: FxaHandle, targetDeviceId: String, title: String, url: String, e: RustError.ByReference)
fun fxa_migrate_from_session_token(
fxa: FxaHandle,
sessionToken: String,
kSync: String,
kXCS: String,
copySessionToken: Byte,
e: RustError.ByReference
): Pointer?
fun fxa_is_in_migration_state(
fxa: FxaHandle,
e: RustError.ByReference
): Byte
fun fxa_retry_migrate_from_session_token(fxa: FxaHandle, e: RustError.ByReference): Pointer?
fun fxa_gather_telemetry(fxa: FxaHandle, e: RustError.ByReference): Pointer?
fun fxa_str_free(string: Pointer)
fun fxa_bytebuffer_free(buffer: RustBuffer.ByValue)
fun fxa_free(fxa: FxaHandle, err: RustError.ByReference)
}
internal typealias FxaHandle = Long

Просмотреть файл

@ -1,80 +0,0 @@
/* 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 mozilla.appservices.fxaclient.rust
import com.sun.jna.Pointer
import com.sun.jna.Structure
import mozilla.appservices.fxaclient.FxaException
import mozilla.appservices.fxaclient.getAndConsumeRustString
@Structure.FieldOrder("code", "message")
internal open class RustError : Structure() {
class ByReference : RustError(), Structure.ByReference
@JvmField var code: Int = 0
@JvmField var message: Pointer? = null
/**
* Does this represent success?
*/
fun isSuccess(): Boolean {
return code == 0
}
/**
* Does this represent failure?
*/
fun isFailure(): Boolean {
return code != 0
}
@Suppress("ReturnCount", "TooGenericExceptionThrown")
fun intoException(): FxaException {
if (!isFailure()) {
// It's probably a bad idea to throw here! We're probably leaking something if this is
// ever hit! (But we shouldn't ever hit it?)
throw RuntimeException("[Bug] intoException called on non-failure!")
}
val message = this.consumeErrorMessage()
when (code) {
3 -> return FxaException.Network(message)
2 -> return FxaException.Unauthorized(message)
-1 -> return FxaException.Panic(message)
// Note: `1` is used as a generic catch all, but we
// might as well handle the others the same way.
else -> return FxaException.Unspecified(message)
}
}
/**
* Get and consume the error message, or null if there is none.
*/
@Synchronized
fun consumeErrorMessage(): String {
val result = this.getMessage()
if (this.message != null) {
LibFxAFFI.INSTANCE.fxa_str_free(this.message!!)
this.message = null
}
if (result == null) {
throw NullPointerException("consumeErrorMessage called with null message!")
}
return result
}
@Synchronized
fun ensureConsumed() {
this.message?.getAndConsumeRustString()
this.message = null
}
/**
* Get the error message or null if there is none.
*/
fun getMessage(): String? {
return this.message?.getString(0, "utf8")
}
}

Просмотреть файл

@ -2,4 +2,6 @@
* 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/. */
pub mod send_tab;
fn main() {
uniffi_build::generate_scaffolding("./src/fxa_client.udl").unwrap();
}

Просмотреть файл

@ -1,22 +0,0 @@
[package]
name = "fxaclient_ffi"
edition = "2018"
version = "0.1.0"
authors = ["Edouard Oger <eoger@fastmail.com>"]
license = "MPL-2.0"
[lib]
name = "fxaclient_ffi"
crate-type = ["lib"]
[dependencies]
ffi-support = "0.4"
log = "0.4"
serde_json = "1"
lazy_static = "1.4"
url = "2.1"
prost = "0.6"
viaduct = { path = "../../viaduct" }
[dependencies.fxa-client]
path = "../"

Просмотреть файл

@ -1,624 +0,0 @@
/* 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/. */
#![allow(unknown_lints)]
#![warn(rust_2018_idioms)]
// Let's allow these in the FFI code, since it's usually just a coincidence if
// the closure is small.
#![allow(clippy::redundant_closure)]
use ffi_support::{
define_bytebuffer_destructor, define_handle_map_deleter, define_string_destructor, ByteBuffer,
ConcurrentHandleMap, ExternError, FfiStr,
};
use fxa_client::{
device::{Capability as DeviceCapability, CommandFetchReason, PushSubscription},
ffi::{from_protobuf_ptr, AuthorizationParameters, MetricsParams},
migrator::MigrationState,
msg_types, FirefoxAccount,
};
use std::os::raw::c_char;
use url::Url;
lazy_static::lazy_static! {
static ref ACCOUNTS: ConcurrentHandleMap<FirefoxAccount> = ConcurrentHandleMap::new();
}
/// Creates a [FirefoxAccount].
///
/// # Safety
///
/// A destructor [fxa_free] is provided for releasing the memory for this
/// pointer type.
#[no_mangle]
pub extern "C" fn fxa_new(
content_url: FfiStr<'_>,
client_id: FfiStr<'_>,
redirect_uri: FfiStr<'_>,
token_server_url_override: FfiStr<'_>,
err: &mut ExternError,
) -> u64 {
log::debug!("fxa_new");
ACCOUNTS.insert_with_output(err, || {
let content_url = content_url.as_str();
let client_id = client_id.as_str();
let redirect_uri = redirect_uri.as_str();
let token_server_url_override = token_server_url_override.as_opt_str();
FirefoxAccount::new(
content_url,
client_id,
redirect_uri,
token_server_url_override,
)
})
}
/// Restore a [FirefoxAccount] instance from an serialized state (created with [fxa_to_json]).
///
/// # Safety
///
/// A destructor [fxa_free] is provided for releasing the memory for this
/// pointer type.
#[no_mangle]
pub extern "C" fn fxa_from_json(json: FfiStr<'_>, err: &mut ExternError) -> u64 {
log::debug!("fxa_from_json");
ACCOUNTS.insert_with_result(err, || FirefoxAccount::from_json(json.as_str()))
}
/// Serializes the state of a [FirefoxAccount] instance. It can be restored later with [fxa_from_json].
///
/// It is the responsability of the caller to persist that serialized state regularly (after operations that mutate [FirefoxAccount])
/// in a **secure** location.
///
/// # Safety
///
/// A destructor [fxa_str_free] is provided for releasing the memory for this
/// pointer type.
#[no_mangle]
pub extern "C" fn fxa_to_json(handle: u64, error: &mut ExternError) -> *mut c_char {
log::debug!("fxa_to_json");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| fxa.to_json())
}
/// Fetches the profile associated with a Firefox Account.
///
/// The profile might get cached in-memory and the caller might get served a cached version.
/// To bypass this, the `ignore_cache` parameter can be set to `true`.
///
/// # Safety
///
/// A destructor [fxa_bytebuffer_free] is provided for releasing the memory for this
/// pointer type.
#[no_mangle]
pub extern "C" fn fxa_profile(
handle: u64,
ignore_cache: u8,
error: &mut ExternError,
) -> ByteBuffer {
log::debug!("fxa_profile");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| fxa.get_profile(ignore_cache != 0))
}
/// Get the pairing URL to navigate to on the Auth side (typically a computer).
///
/// # Safety
///
/// A destructor [fxa_str_free] is provided for releasing the memory for this
/// pointer type.
#[no_mangle]
pub extern "C" fn fxa_get_pairing_authority_url(
handle: u64,
error: &mut ExternError,
) -> *mut c_char {
log::debug!("fxa_get_pairing_authority_url");
ACCOUNTS.call_with_result(error, handle, |fxa| {
fxa.get_pairing_authority_url().map(Url::into_string)
})
}
/// Get the Sync token server endpoint URL.
///
/// # Safety
///
/// A destructor [fxa_str_free] is provided for releasing the memory for this
/// pointer type.
#[no_mangle]
pub extern "C" fn fxa_get_token_server_endpoint_url(
handle: u64,
error: &mut ExternError,
) -> *mut c_char {
log::debug!("fxa_get_token_server_endpoint_url");
ACCOUNTS.call_with_result(error, handle, |fxa| {
fxa.get_token_server_endpoint_url().map(Url::into_string)
})
}
/// Get the url to redirect after there has been a successful connection to FxA.
///
/// # Safety
///
/// A destructor [fxa_str_free] is provided for releasing the memory for this
/// pointer type.
#[no_mangle]
pub extern "C" fn fxa_get_connection_success_url(
handle: u64,
error: &mut ExternError,
) -> *mut c_char {
log::debug!("fxa_get_connection_success_url");
ACCOUNTS.call_with_result(error, handle, |fxa| {
fxa.get_connection_success_url().map(Url::into_string)
})
}
/// Get the url to open the user's account-management page.
///
/// # Safety
///
/// A destructor [fxa_str_free] is provided for releasing the memory for this
/// pointer type.
#[no_mangle]
pub extern "C" fn fxa_get_manage_account_url(
handle: u64,
entrypoint: FfiStr<'_>,
error: &mut ExternError,
) -> *mut c_char {
log::debug!("fxa_get_manage_account_url");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
fxa.get_manage_account_url(entrypoint.as_str())
.map(Url::into_string)
})
}
/// Get the url to open the user's devices-management page.
///
/// # Safety
///
/// A destructor [fxa_str_free] is provided for releasing the memory for this
/// pointer type.
#[no_mangle]
pub extern "C" fn fxa_get_manage_devices_url(
handle: u64,
entrypoint: FfiStr<'_>,
error: &mut ExternError,
) -> *mut c_char {
log::debug!("fxa_get_manage_devices_url");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
fxa.get_manage_devices_url(entrypoint.as_str())
.map(Url::into_string)
})
}
/// Request a OAuth token by starting a new pairing flow, by calling the content server pairing endpoint.
///
/// This function returns a URL string that the caller should open in a webview.
///
/// Pairing assumes you want keys by default, so you must provide a key-bearing scope.
///
/// # Safety
///
/// A destructor [fxa_str_free] is provided for releasing the memory for this
/// pointer type.
#[no_mangle]
pub unsafe extern "C" fn fxa_begin_pairing_flow(
handle: u64,
pairing_url: FfiStr<'_>,
scope: FfiStr<'_>,
entrypoint: FfiStr<'_>,
metrics_params: *const u8,
metrics_params_len: i32,
error: &mut ExternError,
) -> *mut c_char {
log::debug!("fxa_begin_pairing_flow");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
let pairing_url = pairing_url.as_str();
let scope = scope.as_str();
let scopes: Vec<&str> = scope.split(' ').collect();
let metrics_params = from_protobuf_ptr::<MetricsParams, msg_types::MetricsParams>(
metrics_params,
metrics_params_len,
)?;
fxa.begin_pairing_flow(
&pairing_url,
&scopes,
entrypoint.as_str(),
Some(metrics_params),
)
})
}
/// Request a OAuth token by starting a new OAuth flow.
///
/// This function returns a URL string that the caller should open in a webview.
///
/// Once the user has confirmed the authorization grant, they will get redirected to `redirect_url`:
/// the caller must intercept that redirection, extract the `code` and `state` query parameters and call
/// [fxa_complete_oauth_flow] to complete the flow.
///
/// # Safety
///
/// A destructor [fxa_str_free] is provided for releasing the memory for this
/// pointer type.
#[no_mangle]
pub unsafe extern "C" fn fxa_begin_oauth_flow(
handle: u64,
scope: FfiStr<'_>,
entrypoint: FfiStr<'_>,
metrics_params: *const u8,
metrics_params_len: i32,
error: &mut ExternError,
) -> *mut c_char {
log::debug!("fxa_begin_oauth_flow");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
let scope = scope.as_str();
let scopes: Vec<&str> = scope.split(' ').collect();
let metrics_params = from_protobuf_ptr::<MetricsParams, msg_types::MetricsParams>(
metrics_params,
metrics_params_len,
)?;
fxa.begin_oauth_flow(&scopes, entrypoint.as_str(), Some(metrics_params))
})
}
/// Finish an OAuth flow initiated by [fxa_begin_oauth_flow].
#[no_mangle]
pub extern "C" fn fxa_complete_oauth_flow(
handle: u64,
code: FfiStr<'_>,
state: FfiStr<'_>,
error: &mut ExternError,
) {
log::debug!("fxa_complete_oauth_flow");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
let code = code.as_str();
let state = state.as_str();
fxa.complete_oauth_flow(code, state)
});
}
/// Migrate from a logged-in sessionToken Firefox Account.
#[no_mangle]
pub extern "C" fn fxa_migrate_from_session_token(
handle: u64,
session_token: FfiStr<'_>,
k_sync: FfiStr<'_>,
k_xcs: FfiStr<'_>,
copy_session_token: u8,
error: &mut ExternError,
) -> *mut c_char {
log::debug!("fxa_migrate_from_session_token");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| -> fxa_client::Result<String> {
let session_token = session_token.as_str();
let k_sync = k_sync.as_str();
let k_xcs = k_xcs.as_str();
let migration_metrics =
fxa.migrate_from_session_token(session_token, k_sync, k_xcs, copy_session_token != 0)?;
let result = serde_json::to_string(&migration_metrics)?;
Ok(result)
})
}
/// Check if there is migration state.
#[no_mangle]
pub extern "C" fn fxa_is_in_migration_state(handle: u64, error: &mut ExternError) -> u8 {
log::debug!("fxa_is_in_migration_state");
ACCOUNTS.call_with_result(error, handle, |fxa| -> fxa_client::Result<MigrationState> {
Ok(fxa.is_in_migration_state())
})
}
/// Retry the migration attempt using the stored migration state.
#[no_mangle]
pub extern "C" fn fxa_retry_migrate_from_session_token(
handle: u64,
error: &mut ExternError,
) -> *mut c_char {
log::debug!("fxa_retry_migrate_from_session_token");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| -> fxa_client::Result<String> {
let migration_metrics = fxa.try_migration()?;
let result = serde_json::to_string(&migration_metrics)?;
Ok(result)
})
}
/// Try to get an access token.
///
/// If the system can't find a suitable token but has a `refresh token` or a `session_token`,
/// it will generate a new one on the go.
///
/// If not, the caller must start an OAuth flow with [fxa_begin_oauth_flow].
///
/// Arguments:
/// * scope: space-separated list of scopes that the token should have
/// * ttl: the time in seconds for which the token should be valid
/// (or zero to use the server-controlled default ttl)
///
/// # Safety
///
/// A destructor [fxa_bytebuffer_free] is provided for releasing the memory for this
/// pointer type.
#[no_mangle]
pub extern "C" fn fxa_get_access_token(
handle: u64,
scope: FfiStr<'_>,
ttl: u64,
error: &mut ExternError,
) -> ByteBuffer {
log::debug!("fxa_get_access_token");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
let scope = scope.as_str();
let time_left = if ttl > 0 { Some(ttl) } else { None };
fxa.get_access_token(scope, time_left)
})
}
/// Try to get a session token.
///
/// If the system can't find a suitable token it will return an error
///
/// # Safety
///
/// A destructor [fxa_bytebuffer_free] is provided for releasing the memory for this
/// pointer type.
#[no_mangle]
pub extern "C" fn fxa_get_session_token(handle: u64, error: &mut ExternError) -> *mut c_char {
log::debug!("fxa_get_session_token");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| fxa.get_session_token())
}
/// Retrieve the refresh token authorization status.
#[no_mangle]
pub extern "C" fn fxa_check_authorization_status(
handle: u64,
error: &mut ExternError,
) -> ByteBuffer {
log::debug!("fxa_check_authorization_status");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| fxa.check_authorization_status())
}
/// This method should be called when a request made with
/// an OAuth token failed with an authentication error.
/// It clears the internal cache of OAuth access tokens,
/// so the caller can try to call `fxa_get_access_token` or `fxa_profile`
/// again.
#[no_mangle]
pub extern "C" fn fxa_clear_access_token_cache(handle: u64, error: &mut ExternError) {
log::debug!("fxa_clear_access_token_cache");
ACCOUNTS.call_with_output_mut(error, handle, |fxa| fxa.clear_access_token_cache())
}
/// Try to get the current device id from state.
///
/// If the system can't find it then it will return an error
///
/// # Safety
///
/// A destructor [fxa_bytebuffer_free] is provided for releasing the memory for this
/// pointer type.
#[no_mangle]
pub extern "C" fn fxa_get_current_device_id(handle: u64, error: &mut ExternError) -> *mut c_char {
log::debug!("fxa_get_current_device_id");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| fxa.get_current_device_id())
}
/// Update the Push subscription information for the current device.
#[no_mangle]
pub extern "C" fn fxa_set_push_subscription(
handle: u64,
endpoint: FfiStr<'_>,
public_key: FfiStr<'_>,
auth_key: FfiStr<'_>,
error: &mut ExternError,
) {
log::debug!("fxa_set_push_subscription");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
let ps = PushSubscription {
endpoint: endpoint.into_string(),
public_key: public_key.into_string(),
auth_key: auth_key.into_string(),
};
// We don't really care about passing back the resulting Device record.
// We might in the future though.
fxa.set_push_subscription(&ps).map(|_| ())
})
}
/// Update the display name for the current device.
#[no_mangle]
pub extern "C" fn fxa_set_device_name(
handle: u64,
display_name: FfiStr<'_>,
error: &mut ExternError,
) {
log::debug!("fxa_set_device_name");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
// We don't really care about passing back the resulting Device record.
// We might in the future though.
fxa.set_device_name(display_name.as_str()).map(|_| ())
})
}
/// Fetch the devices (including the current one) in the current account.
///
/// Devices might get cached in-memory and the caller might get served a cached version.
/// To bypass this, the `ignore_cache` parameter can be set to `true`.
///
/// # Safety
///
/// A destructor [fxa_bytebuffer_free] is provided for releasing the memory for this
/// pointer type.
#[no_mangle]
pub extern "C" fn fxa_get_devices(
handle: u64,
ignore_cache: u8,
error: &mut ExternError,
) -> ByteBuffer {
log::debug!("fxa_get_devices");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
fxa.get_devices(ignore_cache != 0).map(|d| {
let devices = d.into_iter().map(|device| device.into()).collect();
fxa_client::msg_types::Devices { devices }
})
})
}
/// Try to get an OAuth code using a session token.
///
/// The system will use the stored `session_token` to provision a new code and return it.
///
/// # Safety
///
/// A destructor [fxa_bytebuffer_free] is provided for releasing the memory for this
/// pointer type.
#[no_mangle]
pub unsafe extern "C" fn fxa_authorize_auth_code(
handle: u64,
auth_params: *const u8,
auth_params_len: i32,
error: &mut ExternError,
) -> *mut c_char {
log::debug!("fxa_authorize_auth_code");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
let auth_params = from_protobuf_ptr::<
AuthorizationParameters,
msg_types::AuthorizationParams,
>(auth_params, auth_params_len)?;
fxa.authorize_code_using_session_token(auth_params)
})
}
/// Typically called during a password change flow.
/// Invalidate all tokens and get a new refresh token.
#[no_mangle]
pub extern "C" fn fxa_handle_session_token_change(
handle: u64,
new_session_token: FfiStr<'_>,
error: &mut ExternError,
) {
log::debug!("fxa_handle_session_token_change");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
let new_session_token = new_session_token.as_str();
fxa.handle_session_token_change(new_session_token)
})
}
/// Poll and parse available remote commands targeted to our own device.
///
/// # Safety
///
/// A destructor [fxa_bytebuffer_free] is provided for releasing the memory for this
/// pointer type.
#[no_mangle]
pub extern "C" fn fxa_poll_device_commands(handle: u64, error: &mut ExternError) -> ByteBuffer {
log::debug!("fxa_poll_device_commands");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
fxa.poll_device_commands(CommandFetchReason::Poll)
.map(|cmds| {
let commands = cmds.into_iter().map(|e| e.into()).collect();
fxa_client::msg_types::IncomingDeviceCommands { commands }
})
})
}
/// Disconnect from the account and optionaly destroy our device record.
#[no_mangle]
pub extern "C" fn fxa_disconnect(handle: u64, error: &mut ExternError) {
log::debug!("fxa_disconnect");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| -> fxa_client::Result<()> {
fxa.disconnect();
Ok(())
})
}
/// Handle a push payload coming from the Firefox Account servers.
///
/// # Safety
///
/// A destructor [fxa_bytebuffer_free] is provided for releasing the memory for this
/// pointer type.
#[no_mangle]
pub extern "C" fn fxa_handle_push_message(
handle: u64,
json_payload: FfiStr<'_>,
error: &mut ExternError,
) -> ByteBuffer {
log::debug!("fxa_handle_push_message");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
fxa.handle_push_message(json_payload.as_str()).map(|evs| {
let events = evs.into_iter().map(|e| e.into()).collect();
fxa_client::msg_types::AccountEvents { events }
})
})
}
/// Initalizes our own device, most of the time this will be called right after
/// logging-in for the first time.
///
/// # Safety
/// This function is unsafe because it will dereference `capabilities_data` and
/// read `capabilities_len` bytes from it.
#[no_mangle]
pub unsafe extern "C" fn fxa_initialize_device(
handle: u64,
name: FfiStr<'_>,
device_type: i32,
capabilities_data: *const u8,
capabilities_len: i32,
error: &mut ExternError,
) {
log::debug!("fxa_initialize_device");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
let capabilities =
DeviceCapability::from_protobuf_array_ptr(capabilities_data, capabilities_len)?;
// This should not fail as device_type i32 representation is derived from our .proto schema.
let device_type =
msg_types::device::Type::from_i32(device_type).expect("Unknown device type code");
fxa.initialize_device(name.as_str(), device_type.into(), &capabilities)
})
}
/// Ensure that the device capabilities are registered with the server.
///
/// # Safety
/// This function is unsafe because it will dereference `capabilities_data` and
/// read `capabilities_len` bytes from it.
#[no_mangle]
pub unsafe extern "C" fn fxa_ensure_capabilities(
handle: u64,
capabilities_data: *const u8,
capabilities_len: i32,
error: &mut ExternError,
) {
log::debug!("fxa_ensure_capabilities");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
let capabilities =
DeviceCapability::from_protobuf_array_ptr(capabilities_data, capabilities_len)?;
fxa.ensure_capabilities(&capabilities)
})
}
/// Send a tab to another device identified by its Device ID.
#[no_mangle]
pub extern "C" fn fxa_send_tab(
handle: u64,
target_device_id: FfiStr<'_>,
title: FfiStr<'_>,
url: FfiStr<'_>,
error: &mut ExternError,
) {
log::debug!("fxa_send_tab");
let target = target_device_id.as_str();
let title = title.as_str();
let url = url.as_str();
ACCOUNTS.call_with_result_mut(error, handle, |fxa| fxa.send_tab(target, title, url))
}
define_handle_map_deleter!(ACCOUNTS, fxa_free);
define_string_destructor!(fxa_str_free);
define_bytebuffer_destructor!(fxa_bytebuffer_free);
/// Gather telemetry collected by FxA.
#[no_mangle]
pub extern "C" fn fxa_gather_telemetry(handle: u64, error: &mut ExternError) -> *mut c_char {
log::debug!("fxa_gather_telemetry");
ACCOUNTS.call_with_result_mut(error, handle, |fxa| fxa.gather_telemetry())
}

Просмотреть файл

@ -1,74 +0,0 @@
/* 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/. */
import Foundation
public enum FirefoxAccountError: LocalizedError {
case unauthorized(message: String)
case network(message: String)
case unspecified(message: String)
case panic(message: String)
// Trying to finish an authentication that was never started with begin(...)Flow.
case noExistingAuthFlow
// Trying to finish a different authentication flow.
case wrongAuthFlow
/// Our implementation of the localizedError protocol -- (This shows up in Sentry)
public var errorDescription: String? {
switch self {
case let .unauthorized(message):
return "FirefoxAccountError.unauthorized: \(message)"
case let .network(message):
return "FirefoxAccountError.network: \(message)"
case let .unspecified(message):
return "FirefoxAccountError.unspecified: \(message)"
case let .panic(message):
return "FirefoxAccountError.panic: \(message)"
case .noExistingAuthFlow:
return "FirefoxAccountError.noExistingAuthFlow"
case .wrongAuthFlow:
return "FirefoxAccountError.wrongAuthFlow"
}
}
// The name is attempting to indicate that we free fxaError.message if it
// existed, and that it's a very bad idea to touch it after you call this
// function
static func fromConsuming(_ rustError: FxAError) -> FirefoxAccountError? {
let message = rustError.message
switch rustError.code {
case FxA_NoError:
return nil
case FxA_NetworkError:
return .network(message: String(freeingFxaString: message!))
case FxA_AuthenticationError:
return .unauthorized(message: String(freeingFxaString: message!))
case FxA_Other:
return .unspecified(message: String(freeingFxaString: message!))
case FxA_InternalPanic:
return .panic(message: String(freeingFxaString: message!))
default:
return .unspecified(message: String(freeingFxaString: message!))
}
}
@discardableResult
public static func unwrap<T>(_ callback: (UnsafeMutablePointer<FxAError>) throws -> T?) throws -> T {
guard let result = try tryUnwrap(callback) else {
throw ResultError.empty
}
return result
}
@discardableResult
public static func tryUnwrap<T>(_ callback: (UnsafeMutablePointer<FxAError>) throws -> T?) throws -> T? {
var err = FxAError(code: FxA_NoError, message: nil)
let returnedVal = try callback(&err)
if let fxaErr = FirefoxAccountError.fromConsuming(err) {
throw fxaErr
}
return returnedVal
}
}

Просмотреть файл

@ -1,11 +0,0 @@
/* 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/. */
import Foundation
extension Data {
init(rustBuffer: FxARustBuffer) {
self.init(bytes: rustBuffer.data!, count: Int(rustBuffer.len))
}
}

Просмотреть файл

@ -1,12 +0,0 @@
/* 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/. */
import Foundation
extension String {
init(freeingFxaString fxaString: UnsafeMutablePointer<CChar>) {
defer { fxa_str_free(fxaString) }
self.init(cString: fxaString)
}
}

Просмотреть файл

@ -1,129 +0,0 @@
/* 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/. */
import Foundation
/// This class provides low-level access to `RustFxAccount` through various asynchronous wrappers.
/// It should not be used anymore and is kept for backwards compatbility for the Lockwise iOS project.
@available(*, deprecated, message: "Use FxAccountManager instead")
open class FirefoxAccount: RustFxAccount {
/// Gets the logged-in user profile.
/// Throws `FirefoxAccountError.Unauthorized` if we couldn't find any suitable access token
/// to make that call. The caller should then start the OAuth Flow again with
/// the "profile" scope.
open func getProfile(completionHandler: @escaping (Profile?, Error?) -> Void) {
DispatchQueue.global().async {
do {
let profile = try super.getProfile(ignoreCache: false)
DispatchQueue.main.async { completionHandler(profile, nil) }
} catch {
DispatchQueue.main.async { completionHandler(nil, error) }
}
}
}
/// Request a OAuth token by starting a new OAuth flow.
///
/// This function returns a URL string that the caller should open in a webview.
///
/// Once the user has confirmed the authorization grant, they will get redirected to `redirect_url`:
/// the caller must intercept that redirection, extract the `code` and `state` query parameters and call
/// `completeOAuthFlow(...)` to complete the flow.
open func beginOAuthFlow(
scopes: [String],
entrypoint: String,
metricsParams: MetricsParams = MetricsParams.newEmpty(),
completionHandler: @escaping (URL?, Error?) -> Void
) {
DispatchQueue.global().async {
do {
let url = try super.beginOAuthFlow(
scopes: scopes,
entrypoint: entrypoint,
metricsParams: metricsParams
)
DispatchQueue.main.async { completionHandler(url, nil) }
} catch {
DispatchQueue.main.async { completionHandler(nil, error) }
}
}
}
/// Finish an OAuth flow initiated by `beginOAuthFlow(...)` and returns token/keys.
///
/// This resulting token might not have all the `scopes` the caller have requested (e.g. the user
/// might have denied some of them): it is the responsibility of the caller to accomodate that.
open func completeOAuthFlow(code: String, state: String, completionHandler: @escaping (Void, Error?) -> Void) {
DispatchQueue.global().async {
do {
try super.completeOAuthFlow(code: code, state: state)
DispatchQueue.main.async { completionHandler((), nil) }
} catch {
DispatchQueue.main.async { completionHandler((), error) }
}
}
}
/// Try to get an OAuth access token.
///
/// `ttl` corresponds to the time in seconds for which the token will be used.
/// Throws `FirefoxAccountError.Unauthorized` if we couldn't provide an access token
/// for this scope. The caller should then start the OAuth Flow again with
/// the desired scope.
open func getAccessToken(
scope: String,
ttl: UInt64? = nil,
completionHandler: @escaping (AccessTokenInfo?, Error?) -> Void
) {
DispatchQueue.global().async {
do {
let tokenInfo = try super.getAccessToken(scope: scope, ttl: ttl)
DispatchQueue.main.async { completionHandler(tokenInfo, nil) }
} catch {
DispatchQueue.main.async { completionHandler(nil, error) }
}
}
}
/// Check whether the refreshToken is active
open func checkAuthorizationStatus(completionHandler: @escaping (IntrospectInfo?, Error?) -> Void) {
DispatchQueue.global().async {
do {
let tokenInfo = try super.checkAuthorizationStatus()
DispatchQueue.main.async { completionHandler(tokenInfo, nil) }
} catch {
DispatchQueue.main.async { completionHandler(nil, error) }
}
}
}
/// This method should be called when a request made with
/// an OAuth token failed with an authentication error.
/// It clears the internal cache of OAuth access tokens,
/// so the caller can try to call `getAccessToken` or `getProfile`
/// again.
open func clearAccessTokenCache(completionHandler: @escaping (Void, Error?) -> Void) {
DispatchQueue.global().async {
do {
try super.clearAccessTokenCache()
DispatchQueue.main.async { completionHandler((), nil) }
} catch {
DispatchQueue.main.async { completionHandler((), error) }
}
}
}
/// Disconnect from the account and optionaly destroy our device record.
/// `beginOAuthFlow(...)` will need to be called to reconnect.
open func disconnect(completionHandler: @escaping (Void, Error?) -> Void) {
DispatchQueue.global().async {
do {
try super.disconnect()
DispatchQueue.main.async { completionHandler((), nil) }
} catch {
DispatchQueue.main.async { completionHandler((), error) }
}
}
}
}

Просмотреть файл

@ -1,226 +0,0 @@
/* 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/. */
import Foundation
/// This class inherits from `RustFxAccount` and adds:
/// - Automatic state persistence through `PersistCallback`.
/// - Auth error signaling through observer notifications.
class FxAccount: RustFxAccount {
private var persistCallback: PersistCallback?
/// Registers a persistance callback. The callback will get called every time
/// the `FxAccounts` state needs to be saved. The callback must
/// persist the passed string in a secure location (like the keychain).
public func registerPersistCallback(_ cb: PersistCallback) {
persistCallback = cb
}
/// Unregisters a persistance callback.
public func unregisterPersistCallback() {
persistCallback = nil
}
override func getProfile(ignoreCache: Bool) throws -> Profile {
defer { tryPersistState() }
return try notifyAuthErrors {
try super.getProfile(ignoreCache: ignoreCache)
}
}
override func beginOAuthFlow(
scopes: [String],
entrypoint: String,
metricsParams: MetricsParams = MetricsParams.newEmpty()
) throws -> URL {
return try notifyAuthErrors {
try super.beginOAuthFlow(
scopes: scopes,
entrypoint: entrypoint,
metricsParams: metricsParams
)
}
}
override func beginPairingFlow(
pairingUrl: String,
scopes: [String],
entrypoint: String,
metricsParams: MetricsParams = MetricsParams.newEmpty()
) throws -> URL {
return try notifyAuthErrors {
try super.beginPairingFlow(
pairingUrl: pairingUrl,
scopes: scopes,
entrypoint: entrypoint,
metricsParams: metricsParams
)
}
}
override func completeOAuthFlow(code: String, state: String) throws {
defer { tryPersistState() }
try notifyAuthErrors {
try super.completeOAuthFlow(code: code, state: state)
}
}
override func getAccessToken(scope: String, ttl: UInt64? = nil) throws -> AccessTokenInfo {
defer { tryPersistState() }
return try notifyAuthErrors {
try super.getAccessToken(scope: scope, ttl: ttl)
}
}
override func disconnect() throws {
defer { tryPersistState() }
try super.disconnect()
}
override func pollDeviceCommands() throws -> [IncomingDeviceCommand] {
defer { tryPersistState() }
return try notifyAuthErrors {
try super.pollDeviceCommands()
}
}
override func getDevices(ignoreCache: Bool = false) throws -> [Device] {
return try notifyAuthErrors {
try super.getDevices(ignoreCache: ignoreCache)
}
}
override func setDevicePushSubscription(endpoint: String, publicKey: String, authKey: String) throws {
try notifyAuthErrors {
try super.setDevicePushSubscription(endpoint: endpoint, publicKey: publicKey, authKey: authKey)
}
}
override func setDeviceDisplayName(_ name: String) throws {
try notifyAuthErrors {
try super.setDeviceDisplayName(name)
}
}
override func handlePushMessage(payload: String) throws -> [AccountEvent] {
defer { tryPersistState() }
return try notifyAuthErrors {
try super.handlePushMessage(payload: payload)
}
}
override func sendSingleTab(targetId: String, title: String, url: String) throws {
return try notifyAuthErrors {
try super.sendSingleTab(targetId: targetId, title: title, url: url)
}
}
override func initializeDevice(
name: String,
deviceType: DeviceType,
supportedCapabilities: [DeviceCapability]
) throws {
defer { tryPersistState() }
try notifyAuthErrors {
try super.initializeDevice(name: name, deviceType: deviceType, supportedCapabilities: supportedCapabilities)
}
}
override func ensureCapabilities(supportedCapabilities: [DeviceCapability]) throws {
defer { tryPersistState() }
try notifyAuthErrors {
try super.ensureCapabilities(supportedCapabilities: supportedCapabilities)
}
}
override func migrateFromSessionToken(sessionToken: String, kSync: String, kXCS: String) -> Bool {
defer { tryPersistState() }
do {
return try super.migrateFromSessionToken(sessionToken: sessionToken, kSync: kSync, kXCS: kXCS)
} catch {
FxALog.error("migrateFromSessionToken error: \(error).")
reportAccountMigrationError(error)
return false
}
}
override func retryMigrateFromSessionToken() -> Bool {
defer { tryPersistState() }
do {
return try super.retryMigrateFromSessionToken()
} catch {
FxALog.error("retryMigrateFromSessionToken error: \(error).")
reportAccountMigrationError(error)
return false
}
}
override func handleSessionTokenChange(sessionToken: String) throws {
defer { tryPersistState() }
try notifyAuthErrors {
try super.handleSessionTokenChange(sessionToken: sessionToken)
}
}
internal func reportAccountMigrationError(_ error: Error) {
// Not in migration state after throwing during migration = unrecoverable error.
if !isInMigrationState() {
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .accountMigrationFailed,
object: nil,
userInfo: ["error": error]
)
}
}
}
override func isInMigrationState() -> Bool {
do {
return try super.isInMigrationState()
} catch {
FxALog.error("isInMigrationState error: \(error).")
return false
}
}
override func clearAccessTokenCache() throws {
defer { tryPersistState() }
try super.clearAccessTokenCache()
}
private func tryPersistState() {
guard let cb = persistCallback else {
return
}
do {
let json = try toJSON()
cb.persist(json: json)
} catch {
// Ignore the error because the prior operation might have worked,
// but still log it.
FxALog.error("FxAccounts internal state serialization failed.")
}
}
internal func notifyAuthErrors<T>(_ cb: () throws -> T) rethrows -> T {
do {
return try cb()
} catch let error as FirefoxAccountError {
if case let .unauthorized(msg) = error {
FxALog.debug("Auth error caught: \(msg)")
notifyAuthError()
}
throw error
}
}
internal func notifyAuthError() {
NotificationCenter.default.post(name: .accountAuthException, object: nil)
}
}
public protocol PersistCallback {
func persist(json: String)
}

Просмотреть файл

@ -1,163 +0,0 @@
/* 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/. */
import Foundation
public struct Device {
public let id: String
public let displayName: String
public let deviceType: DeviceType
public let isCurrentDevice: Bool
public let lastAccessTime: UInt64?
public let capabilities: [DeviceCapability]
public let subscriptionExpired: Bool
public let subscription: DevicePushSubscription?
internal static func fromCollectionMsg(msg: MsgTypes_Devices) -> [Device] {
msg.devices.map { Device(msg: $0) }
}
internal init(msg: MsgTypes_Device) {
id = msg.id
displayName = msg.displayName
deviceType = DeviceType.fromMsg(msg: msg.type)
isCurrentDevice = msg.isCurrentDevice
lastAccessTime = msg.hasLastAccessTime ? msg.lastAccessTime : nil
capabilities = msg.capabilities.map { DeviceCapability.fromMsg(msg: $0) }
subscriptionExpired = msg.pushEndpointExpired
subscription = msg.hasPushSubscription ?
DevicePushSubscription(msg: msg.pushSubscription) :
nil
}
}
public enum DeviceType {
case desktop
case mobile
case tablet
case tv
case vr
case unknown
internal static func fromMsg(msg: MsgTypes_Device.TypeEnum) -> DeviceType {
switch msg {
case .desktop: return .desktop
case .mobile: return .mobile
case .tablet: return .tablet
case .tv: return .tv
case .vr: return .vr
case .unknown: return .unknown
}
}
internal func toMsg() -> MsgTypes_Device.TypeEnum {
switch self {
case .desktop: return .desktop
case .mobile: return .mobile
case .tablet: return .tablet
case .tv: return .tv
case .vr: return .vr
case .unknown: return .unknown
}
}
}
public enum DeviceCapability {
case sendTab
internal static func fromMsg(msg: MsgTypes_Device.Capability) -> DeviceCapability {
switch msg {
case .sendTab: return .sendTab
}
}
internal func toMsg() -> MsgTypes_Device.Capability {
switch self {
case .sendTab: return .sendTab
}
}
}
extension Array where Element == DeviceCapability {
func toCollectionMsg() -> MsgTypes_Capabilities {
MsgTypes_Capabilities.with {
$0.capability = self.map { $0.toMsg() }
}
}
}
public struct DevicePushSubscription {
public let endpoint: String
public let publicKey: String
public let authKey: String
public init(endpoint: String, publicKey: String, authKey: String) {
self.endpoint = endpoint
self.publicKey = publicKey
self.authKey = authKey
}
internal init(msg: MsgTypes_Device.PushSubscription) {
endpoint = msg.endpoint
publicKey = msg.publicKey
authKey = msg.authKey
}
}
public enum IncomingDeviceCommand {
case tabReceived(Device?, [TabData])
internal static func fromCollectionMsg(msg: MsgTypes_IncomingDeviceCommands) -> [IncomingDeviceCommand] {
msg.commands.map { IncomingDeviceCommand.fromMsg(msg: $0) }
}
internal static func fromMsg(msg: MsgTypes_IncomingDeviceCommand) -> IncomingDeviceCommand {
switch msg.type {
case .tabReceived: do {
let data = msg.tabReceivedData
let device = data.hasFrom ? Device(msg: data.from) : nil
let entries = data.entries.map { TabData(title: $0.title, url: $0.url) }
return .tabReceived(device, entries)
}
}
}
}
public enum AccountEvent {
case incomingDeviceCommand(IncomingDeviceCommand)
case deviceConnected(deviceName: String)
case deviceDisconnected(deviceId: String, isLocalDevice: Bool)
internal static func fromCollectionMsg(msg: MsgTypes_AccountEvents) -> [AccountEvent] {
msg.events.compactMap { AccountEvent.fromMsg(msg: $0) }
}
internal static func fromMsg(msg: MsgTypes_AccountEvent) -> AccountEvent? {
switch msg.type {
case .incomingDeviceCommand: do {
return .incomingDeviceCommand(IncomingDeviceCommand.fromMsg(msg: msg.deviceCommand))
}
case .deviceConnected: do {
return .deviceConnected(deviceName: msg.deviceConnectedName)
}
case .deviceDisconnected: do {
return .deviceDisconnected(
deviceId: msg.deviceDisconnectedData.deviceID,
isLocalDevice: msg.deviceDisconnectedData.isLocalDevice
)
}
// The following push messages are filtered upstream by the FxA server,
// because iOS requires all Push messages to show a UI notification to the user
// and in these cases it was deemed not useful.
case .profileUpdated: return nil
case .accountAuthStateChanged: return nil
case .accountDestroyed: return nil
}
}
}
public struct TabData {
public let title: String
public let url: String
}

Просмотреть файл

@ -15,9 +15,9 @@ public struct ConstellationState {
public class DeviceConstellation {
var constellationState: ConstellationState?
let account: FxAccount
let account: PersistedFirefoxAccount
init(account: FxAccount) {
init(account: PersistedFirefoxAccount) {
self.account = account
}
@ -36,7 +36,7 @@ public class DeviceConstellation {
do {
let devices = try self.account.getDevices(ignoreCache: true)
let localDevice = devices.first { $0.isCurrentDevice }
if localDevice?.subscriptionExpired ?? false {
if localDevice?.pushEndpointExpired ?? false {
FxALog.debug("Current device needs push endpoint registration.")
}
let remoteDevices = devices.filter { !$0.isCurrentDevice }
@ -64,7 +64,7 @@ public class DeviceConstellation {
public func setLocalDeviceName(name: String) {
DispatchQueue.global().async {
do {
try self.account.setDeviceDisplayName(name)
try self.account.setDeviceName(name)
// Update our list of devices in the background to reflect the change.
self.refreshState()
} catch {
@ -92,7 +92,7 @@ public class DeviceConstellation {
do {
switch e {
case let .sendTab(title, url): do {
try self.account.sendSingleTab(targetId: targetDeviceId, title: title, url: url)
try self.account.sendSingleTab(targetDeviceId: targetDeviceId, title: title, url: url)
}
}
} catch {
@ -105,11 +105,7 @@ public class DeviceConstellation {
public func setDevicePushSubscription(sub: DevicePushSubscription) {
DispatchQueue.global().async {
do {
try self.account.setDevicePushSubscription(
endpoint: sub.endpoint,
publicKey: sub.publicKey,
authKey: sub.authKey
)
try self.account.setDevicePushSubscription(sub: sub)
} catch {
FxALog.error("Failure setting push subscription: \(error).")
}

Просмотреть файл

@ -19,8 +19,8 @@ open class FxAccountManager {
var deviceConfig: DeviceConfig
let applicationScopes: [String]
var acct: FxAccount?
var account: FxAccount? {
var acct: PersistedFirefoxAccount?
var account: PersistedFirefoxAccount? {
get { return acct }
set {
acct = newValue
@ -95,7 +95,7 @@ open class FxAccountManager {
/// `finishAuthentication(...)` to complete the flow.
public func beginAuthentication(
entrypoint: String,
metricsParams: MetricsParams = MetricsParams.newEmpty(),
metrics: MetricsParams = MetricsParams(parameters: [:]),
completionHandler: @escaping (Result<URL, Error>) -> Void
) {
FxALog.info("beginAuthentication")
@ -104,7 +104,7 @@ open class FxAccountManager {
try account.beginOAuthFlow(
scopes: self.applicationScopes,
entrypoint: entrypoint,
metricsParams: metricsParams
metrics: metrics
)
}
DispatchQueue.main.async { completionHandler(result) }
@ -123,7 +123,7 @@ open class FxAccountManager {
public func beginPairingAuthentication(
pairingUrl: String,
entrypoint: String,
metricsParams: MetricsParams = MetricsParams.newEmpty(),
metrics: MetricsParams = MetricsParams(parameters: [:]),
completionHandler: @escaping (Result<URL, Error>) -> Void
) {
DispatchQueue.global().async {
@ -132,7 +132,7 @@ open class FxAccountManager {
pairingUrl: pairingUrl,
scopes: self.applicationScopes,
entrypoint: entrypoint,
metricsParams: metricsParams
metrics: metrics
)
}
DispatchQueue.main.async { completionHandler(result) }
@ -143,7 +143,7 @@ open class FxAccountManager {
/// and put it aside for later in `latestOAuthStateParam`.
/// Afterwards, in `finishAuthentication` we ensure that we are
/// finishing the correct (and same) authentication flow.
private func updatingLatestAuthState(_ beginFlowFn: (FxAccount) throws -> URL) -> Result<URL, Error> {
private func updatingLatestAuthState(_ beginFlowFn: (PersistedFirefoxAccount) throws -> URL) -> Result<URL, Error> {
do {
let url = try beginFlowFn(requireAccount())
let comps = URLComponents(url: url, resolvingAgainstBaseURL: true)
@ -194,9 +194,9 @@ open class FxAccountManager {
completionHandler: @escaping (Result<Void, Error>) -> Void
) {
if latestOAuthStateParam == nil {
DispatchQueue.main.async { completionHandler(.failure(FirefoxAccountError.noExistingAuthFlow)) }
DispatchQueue.main.async { completionHandler(.failure(FxaError.NoExistingAuthFlow(message: ""))) }
} else if authData.state != latestOAuthStateParam {
DispatchQueue.main.async { completionHandler(.failure(FirefoxAccountError.wrongAuthFlow)) }
DispatchQueue.main.async { completionHandler(.failure(FxaError.WrongAuthFlow(message: ""))) }
} else { /* state == latestAuthState */
processEvent(event: .authenticated(authData: authData)) {
DispatchQueue.main.async { completionHandler(.success(())) }
@ -378,12 +378,8 @@ open class FxAccountManager {
switch via {
case .logout: do {
// Clean up internal account state and destroy the current FxA device record.
do {
try requireAccount().disconnect()
FxALog.info("Disconnected FxA account")
} catch {
FxALog.error("Failed to fully disconnect the FxA account: \(error).")
}
requireAccount().disconnect()
FxALog.info("Disconnected FxA account")
profile = nil
constellation = nil
accountStorage.clear()
@ -407,7 +403,8 @@ open class FxAccountManager {
if acct.migrateFromSessionToken(
sessionToken: sessionToken,
kSync: kSync,
kXCS: kXCS
kXCS: kXCS,
copySessionToken: false
) {
return .authenticatedViaMigration
}
@ -597,7 +594,7 @@ open class FxAccountManager {
onError()
return nil
}
try account.clearAccessTokenCache()
account.clearAccessTokenCache()
// Make sure we're back on track by re-requesting the profile access token.
_ = try account.getAccessToken(scope: OAuthScope.profile)
return .recoveredFromAuthenticationProblem
@ -612,15 +609,15 @@ open class FxAccountManager {
return nil
}
internal func createAccount() -> FxAccount {
return try! FxAccount(config: config)
internal func createAccount() -> PersistedFirefoxAccount {
return PersistedFirefoxAccount(config: config)
}
internal func tryRestoreAccount() -> FxAccount? {
internal func tryRestoreAccount() -> PersistedFirefoxAccount? {
return accountStorage.read()
}
internal func makeDeviceConstellation(account: FxAccount) -> DeviceConstellation {
internal func makeDeviceConstellation(account: PersistedFirefoxAccount) -> DeviceConstellation {
return DeviceConstellation(account: account)
}
@ -657,7 +654,7 @@ open class FxAccountManager {
}
}
internal func requireAccount() -> FxAccount {
internal func requireAccount() -> PersistedFirefoxAccount {
if let acct = account {
return acct
}

Просмотреть файл

@ -1,72 +0,0 @@
/* 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/. */
import Foundation
public struct MetricsParams {
public let flowId: String?
public let flowBeginTime: UInt64?
public let deviceId: String?
public let utmSource: String?
public let utmContent: String?
public let utmMedium: String?
public let utmTerm: String?
public let utmCampaign: String?
public let entrypointExperiment: String?
public let entrypointVariation: String?
internal func toMsg() -> MsgTypes_MetricsParams {
var msg = MsgTypes_MetricsParams()
var params = [String: String]()
if flowId != nil {
params["flow_id"] = flowId
}
if flowBeginTime != nil {
params["flow_begin_time"] = String(describing: flowBeginTime)
}
if deviceId != nil {
params["device_id"] = deviceId
}
if utmSource != nil {
params["utm_source"] = utmSource
}
if utmContent != nil {
params["utm_content"] = utmContent
}
if utmMedium != nil {
params["utm_medium"] = utmMedium
}
if utmTerm != nil {
params["utm_term"] = flowId
}
if utmCampaign != nil {
params["utm_campaign"] = utmCampaign
}
if entrypointExperiment != nil {
params["entrypoint_experiment"] = entrypointExperiment
}
if entrypointVariation != nil {
params["entrypoint_variation"] = entrypointVariation
}
msg.parameters = params
return msg
}
public static func newEmpty() -> MetricsParams {
return MetricsParams(
flowId: nil,
flowBeginTime: nil,
deviceId: nil,
utmSource: nil,
utmContent: nil,
utmMedium: nil,
utmTerm: nil,
utmCampaign: nil,
entrypointExperiment: nil,
entrypointVariation: nil
)
}
}

Просмотреть файл

@ -4,21 +4,6 @@
import Foundation
enum MigrationState {
case none
case copySessionToken
case reuseSessionToken
internal static func fromNumber(_ number: UInt8) -> MigrationState {
switch number {
case 0: return .none
case 1: return .copySessionToken
case 2: return .reuseSessionToken
default: fatalError("Unreachable")
}
}
}
public enum MigrationResult {
// Sign-in failed due to an intermittent problem (such as a network failure). A retry attempt will
// be performed automatically during account manager initialization.

Просмотреть файл

@ -12,59 +12,3 @@ public enum OAuthScope {
// Necessary to obtain a sessionToken, which gives full access to the account.
public static let session: String = "https://identity.mozilla.com/tokens/session"
}
public struct ScopedKey {
public let kty: String
public let scope: String
public let k: String
public let kid: String
internal init(msg: MsgTypes_ScopedKey) {
kty = msg.kty
scope = msg.scope
k = msg.k
kid = msg.kid
}
}
public struct AccessTokenInfo {
public let scope: String
public let token: String
public let key: ScopedKey?
public let expiresAt: Date
internal init(msg: MsgTypes_AccessTokenInfo) {
scope = msg.scope
token = msg.token
key = msg.hasKey ? ScopedKey(msg: msg.key) : nil
expiresAt = Date(timeIntervalSince1970: Double(msg.expiresAt))
}
// For testing.
internal init(
scope: String,
token: String,
key: ScopedKey? = nil,
expiresAt: Date = Date()
) {
self.scope = scope
self.token = token
self.key = key
self.expiresAt = expiresAt
}
}
public struct IntrospectInfo {
public let active: Bool
internal init(msg: MsgTypes_IntrospectInfo) {
active = msg.active
}
// For testing.
internal init(
active: Bool
) {
self.active = active
}
}

Просмотреть файл

@ -1,32 +0,0 @@
/* 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/. */
import Foundation
public struct Avatar {
public let url: String
public let isDefault: Bool
}
public struct Profile {
public let uid: String
public let email: String
public let avatar: Avatar?
public let displayName: String?
internal init(msg: MsgTypes_Profile) {
uid = msg.uid
email = msg.email
avatar = msg.hasAvatar ? Avatar(url: msg.avatar, isDefault: msg.avatarDefault) : nil
displayName = msg.hasDisplayName ? msg.displayName : nil
}
// For testing.
internal init(uid: String, email: String, avatar: Avatar? = nil, displayName: String? = nil) {
self.uid = uid
self.email = email
self.avatar = avatar
self.displayName = displayName
}
}

Просмотреть файл

@ -14,7 +14,7 @@ class KeyChainAccountStorage {
keychainWrapper = KeychainWrapper.sharedAppContainerKeychain(keychainAccessGroup: keychainAccessGroup)
}
func read() -> FxAccount? {
func read() -> PersistedFirefoxAccount? {
// Firefox iOS v25.0 shipped with the default accessibility, which breaks Send Tab when the screen is locked.
// This method migrates the existing keychains to the correct accessibility.
keychainWrapper.ensureStringItemAccessibility(
@ -26,7 +26,7 @@ class KeyChainAccountStorage {
withAccessibility: KeyChainAccountStorage.accessibility
) {
do {
return try FxAccount(fromJsonState: json)
return try PersistedFirefoxAccount.fromJSON(data: json)
} catch {
FxALog.error("FxAccount internal state de-serialization failed: \(error).")
return nil

Просмотреть файл

@ -0,0 +1,331 @@
/* 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/. */
import Foundation
/// This class inherits from the Rust `FirefoxAccount` and adds:
/// - Automatic state persistence through `PersistCallback`.
/// - Auth error signaling through observer notifications.
/// - Some convenience higher-level datatypes, such as URLs rather than plain Strings.
///
/// Eventually we'd like to move all of this into the underlying Rust code, once UniFFI
/// grows support for these extra features:
/// - Callback interfaces in Swift: https://github.com/mozilla/uniffi-rs/issues/353
/// - Higher-level data types: https://github.com/mozilla/uniffi-rs/issues/348
///
/// It's not yet clear how we might integrate with observer notifications in
/// a cross-platform way, though.
///
class PersistedFirefoxAccount {
private var persistCallback: PersistCallback?
private var inner: FirefoxAccount
init(inner: FirefoxAccount) {
self.inner = inner
}
public convenience init(config: FxAConfig) {
self.init(inner: FirefoxAccount(contentUrl: config.contentUrl,
clientId: config.clientId,
redirectUri: config.redirectUri,
tokenServerUrlOverride: config.tokenServerUrlOverride))
}
/// Registers a persistance callback. The callback will get called every time
/// the `FxAccounts` state needs to be saved. The callback must
/// persist the passed string in a secure location (like the keychain).
public func registerPersistCallback(_ cb: PersistCallback) {
persistCallback = cb
}
/// Unregisters a persistance callback.
public func unregisterPersistCallback() {
persistCallback = nil
}
public static func fromJSON(data: String) throws -> PersistedFirefoxAccount {
return PersistedFirefoxAccount(inner: try FirefoxAccount.fromJson(data: data))
}
public func toJSON() throws -> String {
try inner.toJson()
}
public func beginOAuthFlow(
scopes: [String],
entrypoint: String,
metrics: MetricsParams = MetricsParams(parameters: [:])
) throws -> URL {
return try notifyAuthErrors {
URL(string: try self.inner.beginOauthFlow(
scopes: scopes,
entrypoint: entrypoint,
metrics: metrics
))!
}
}
public func getPairingAuthorityURL() throws -> URL {
return URL(string: try inner.getPairingAuthorityUrl())!
}
public func beginPairingFlow(
pairingUrl: String,
scopes: [String],
entrypoint: String,
metrics: MetricsParams = MetricsParams(parameters: [:])
) throws -> URL {
return try notifyAuthErrors {
URL(string: try self.inner.beginPairingFlow(pairingUrl: pairingUrl,
scopes: scopes,
entrypoint: entrypoint,
metrics: metrics))!
}
}
public func completeOAuthFlow(code: String, state: String) throws {
defer { tryPersistState() }
try notifyAuthErrors {
try self.inner.completeOauthFlow(code: code, state: state)
}
}
public func checkAuthorizationStatus() throws -> AuthorizationInfo {
defer { tryPersistState() }
return try notifyAuthErrors {
try self.inner.checkAuthorizationStatus()
}
}
public func disconnect() {
defer { tryPersistState() }
inner.disconnect()
}
public func getProfile(ignoreCache: Bool) throws -> Profile {
defer { tryPersistState() }
return try notifyAuthErrors {
try self.inner.getProfile(ignoreCache: ignoreCache)
}
}
public func initializeDevice(
name: String,
deviceType: DeviceType,
supportedCapabilities: [DeviceCapability]
) throws {
defer { tryPersistState() }
try notifyAuthErrors {
try self.inner.initializeDevice(name: name,
deviceType: deviceType,
supportedCapabilities: supportedCapabilities)
}
}
public func getCurrentDeviceId() throws -> String {
return try notifyAuthErrors {
try self.inner.getCurrentDeviceId()
}
}
public func getDevices(ignoreCache: Bool = false) throws -> [Device] {
return try notifyAuthErrors {
try self.inner.getDevices(ignoreCache: ignoreCache)
}
}
public func getAttachedClients() throws -> [AttachedClient] {
return try notifyAuthErrors {
try self.inner.getAttachedClients()
}
}
public func setDeviceName(_ name: String) throws {
defer { tryPersistState() }
try notifyAuthErrors {
try self.inner.setDeviceName(displayName: name)
}
}
public func clearDeviceName() throws {
defer { tryPersistState() }
try notifyAuthErrors {
try self.inner.clearDeviceName()
}
}
public func ensureCapabilities(supportedCapabilities: [DeviceCapability]) throws {
defer { tryPersistState() }
try notifyAuthErrors {
try self.inner.ensureCapabilities(supportedCapabilities: supportedCapabilities)
}
}
public func setDevicePushSubscription(sub: DevicePushSubscription) throws {
try notifyAuthErrors {
try self.inner.setPushSubscription(subscription: sub)
}
}
public func handlePushMessage(payload: String) throws -> [AccountEvent] {
defer { tryPersistState() }
return try notifyAuthErrors {
try self.inner.handlePushMessage(payload: payload)
}
}
public func pollDeviceCommands() throws -> [IncomingDeviceCommand] {
defer { tryPersistState() }
return try notifyAuthErrors {
try self.inner.pollDeviceCommands()
}
}
public func sendSingleTab(targetDeviceId: String, title: String, url: String) throws {
return try notifyAuthErrors {
try self.inner.sendSingleTab(targetDeviceId: targetDeviceId, title: title, url: url)
}
}
public func getTokenServerEndpointURL() throws -> URL {
return URL(string: try inner.getTokenServerEndpointUrl())!
}
public func getConnectionSuccessURL() throws -> URL {
return URL(string: try inner.getConnectionSuccessUrl())!
}
public func getManageAccountURL(entrypoint: String) throws -> URL {
return URL(string: try inner.getManageAccountUrl(entrypoint: entrypoint))!
}
public func getManageDevicesURL(entrypoint: String) throws -> URL {
return URL(string: try inner.getManageDevicesUrl(entrypoint: entrypoint))!
}
public func getAccessToken(scope: String, ttl: UInt64? = nil) throws -> AccessTokenInfo {
defer { tryPersistState() }
return try notifyAuthErrors {
try self.inner.getAccessToken(scope: scope, ttl: ttl == nil ? nil : Int64(clamping: ttl!))
}
}
public func getSessionToken() throws -> String {
defer { tryPersistState() }
return try notifyAuthErrors {
try self.inner.getSessionToken()
}
}
public func handleSessionTokenChange(sessionToken: String) throws {
defer { tryPersistState() }
return try notifyAuthErrors {
try self.inner.handleSessionTokenChange(sessionToken: sessionToken)
}
}
public func authorizeCodeUsingSessionToken(params: AuthorizationParameters) throws -> String {
defer { tryPersistState() }
return try notifyAuthErrors {
try self.inner.authorizeCodeUsingSessionToken(params: params)
}
}
public func clearAccessTokenCache() {
defer { tryPersistState() }
inner.clearAccessTokenCache()
}
public func gatherTelemetry() throws -> String {
return try notifyAuthErrors {
try self.inner.gatherTelemetry()
}
}
// TODO: not sure why we switched to returning a bool for the Swift wrapper here,
// we should review and see if we can make it consistent with Rust and Kotlin.
public func migrateFromSessionToken(
sessionToken: String,
kSync: String,
kXCS: String,
copySessionToken: Bool
) -> Bool {
defer { tryPersistState() }
do {
_ = try inner.migrateFromSessionToken(sessionToken: sessionToken,
kSync: kSync,
kXcs: kXCS,
copySessionToken: copySessionToken)
return true
} catch {
FxALog.error("migrateFromSessionToken error: \(error).")
reportAccountMigrationError(error)
return false
}
}
public func retryMigrateFromSessionToken() -> Bool {
defer { tryPersistState() }
do {
_ = try inner.retryMigrateFromSessionToken()
return true
} catch {
FxALog.error("retryMigrateFromSessionToken error: \(error).")
reportAccountMigrationError(error)
return false
}
}
internal func reportAccountMigrationError(_ error: Error) {
// Not in migration state after throwing during migration == unrecoverable error.
if isInMigrationState() {
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .accountMigrationFailed,
object: nil,
userInfo: ["error": error]
)
}
}
}
public func isInMigrationState() -> Bool {
return inner.isInMigrationState() != .none
}
private func tryPersistState() {
guard let cb = persistCallback else {
return
}
do {
let json = try toJSON()
cb.persist(json: json)
} catch {
// Ignore the error because the prior operation might have worked,
// but still log it.
FxALog.error("FxAccounts internal state serialization failed.")
}
}
internal func notifyAuthErrors<T>(_ cb: () throws -> T) rethrows -> T {
do {
return try cb()
} catch let error as FxaError {
if case let .Authentication(msg) = error {
FxALog.debug("Auth error caught: \(msg)")
notifyAuthError()
}
throw error
}
}
internal func notifyAuthError() {
NotificationCenter.default.post(name: .accountAuthException, object: nil)
}
}
public protocol PersistCallback {
func persist(json: String)
}

Просмотреть файл

@ -1,170 +0,0 @@
/* 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/. */
#pragma once
#include <Foundation/NSObjCRuntime.h>
#include <stdint.h>
/*
* This file contains headers for all of the structs and functions that map
* directly to the functions defined in fxa-client/src/ffi.rs,
* fxa-client/ffi/src/lib.rs, and components/support/ffi/src/error.rs.
*
* The C in this file is specifically formatted to be used with Objective C and
* Swift and contains macros and flags that will not be recognised by other C
* based languages.
*/
/*
Error codes reported by the fxa-client library, from fxa-client/src/ffi.rs
*/
typedef enum FxAErrorCode {
FxA_InternalPanic = -1,
FxA_NoError = 0,
FxA_Other = 1,
FxA_AuthenticationError = 2,
FxA_NetworkError = 3,
} FxAErrorCode;
/*
A mapping of the ExternError repr(C) Rust struct, from
components/support/ffi/src/error.rs.
*/
typedef struct FxAError {
FxAErrorCode code;
char *_Nullable message;
} FxAError;
/*
A mapping of the ByteBuffer repr(C) Rust struct, from
components/support/ffi/src/lib.rs.
*/
typedef struct FxARustBuffer {
int64_t len;
uint8_t *_Nullable data;
} FxARustBuffer;
typedef uint64_t FirefoxAccountHandle;
char *_Nullable fxa_begin_oauth_flow(FirefoxAccountHandle handle,
const char *_Nonnull scopes,
const char *_Nonnull entrypoint,
uint8_t const *_Nonnull metrics_params,
int32_t metrics_params_len,
FxAError *_Nonnull out);
char *_Nullable fxa_begin_pairing_flow(FirefoxAccountHandle handle,
const char *_Nonnull pairing_url,
const char *_Nonnull scopes,
const char *_Nonnull entrypoint,
uint8_t const *_Nonnull metrics_params,
int32_t metrics_params_len,
FxAError *_Nonnull out);
void fxa_complete_oauth_flow(FirefoxAccountHandle handle,
const char *_Nonnull code,
const char *_Nonnull state,
FxAError *_Nonnull out);
FxARustBuffer fxa_get_access_token(FirefoxAccountHandle handle,
const char *_Nonnull scope, uint64_t ttl,
FxAError *_Nonnull out);
char *_Nullable fxa_get_session_token(FirefoxAccountHandle handle,
FxAError *_Nonnull out);
void fxa_clear_access_token_cache(FirefoxAccountHandle handle,
FxAError *_Nonnull out);
void fxa_handle_session_token_change(FirefoxAccountHandle handle,
const char *_Nonnull new_session_token,
FxAError *_Nonnull out);
void fxa_disconnect(FirefoxAccountHandle handle, FxAError *_Nonnull out);
FxARustBuffer fxa_check_authorization_status(FirefoxAccountHandle handle,
FxAError *_Nonnull out);
FirefoxAccountHandle fxa_from_json(const char *_Nonnull json,
FxAError *_Nonnull out);
char *_Nullable fxa_to_json(FirefoxAccountHandle handle,
FxAError *_Nonnull out);
FirefoxAccountHandle fxa_new(const char *_Nonnull content_base,
const char *_Nonnull client_id,
const char *_Nonnull redirect_uri,
const char *_Nullable token_server_url_override,
FxAError *_Nonnull out);
FxARustBuffer fxa_profile(FirefoxAccountHandle handle, uint8_t ignore_cache,
FxAError *_Nonnull out);
FxARustBuffer fxa_get_devices(FirefoxAccountHandle handle, uint8_t ignore_cache,
FxAError *_Nonnull out);
FxARustBuffer fxa_poll_device_commands(FirefoxAccountHandle handle,
FxAError *_Nonnull out);
FxARustBuffer fxa_handle_push_message(FirefoxAccountHandle handle,
const char *_Nonnull payload,
FxAError *_Nonnull out);
void fxa_send_tab(FirefoxAccountHandle handle, const char *_Nonnull targetId,
const char *_Nonnull title, const char *_Nonnull url,
FxAError *_Nonnull out);
void fxa_set_device_name(FirefoxAccountHandle handle,
const char *_Nonnull displayName,
FxAError *_Nonnull out);
void fxa_set_push_subscription(FirefoxAccountHandle handle,
const char *_Nonnull endpoint,
const char *_Nonnull publicKey,
const char *_Nonnull authKey,
FxAError *_Nonnull out);
void fxa_initialize_device(FirefoxAccountHandle handle,
const char *_Nonnull name, int32_t device_type,
uint8_t const *_Nonnull capabilities_ptr,
int32_t capabilities_len, FxAError *_Nonnull out);
void fxa_ensure_capabilities(FirefoxAccountHandle handle,
uint8_t const *_Nonnull capabilities_ptr,
int32_t capabilities_len, FxAError *_Nonnull out);
char *_Nullable fxa_migrate_from_session_token(
FirefoxAccountHandle handle, const char *_Nonnull sessionToken,
const char *_Nonnull kSync, const char *_Nonnull kXCS,
uint8_t copySessionToken, FxAError *_Nonnull out);
char *_Nullable fxa_retry_migrate_from_session_token(
FirefoxAccountHandle handle, FxAError *_Nonnull out);
uint8_t fxa_is_in_migration_state(FirefoxAccountHandle handle,
FxAError *_Nonnull out);
char *_Nullable fxa_get_token_server_endpoint_url(FirefoxAccountHandle handle,
FxAError *_Nonnull out);
char *_Nullable fxa_get_pairing_authority_url(FirefoxAccountHandle handle,
FxAError *_Nonnull out);
char *_Nullable fxa_get_connection_success_url(FirefoxAccountHandle handle,
FxAError *_Nonnull out);
char *_Nullable fxa_get_manage_account_url(FirefoxAccountHandle handle,
const char *_Nonnull entrypoint,
FxAError *_Nonnull out);
char *_Nullable fxa_get_manage_devices_url(FirefoxAccountHandle handle,
const char *_Nonnull entrypoint,
FxAError *_Nonnull out);
char *_Nullable fxa_gather_telemetry(FirefoxAccountHandle handle,
FxAError *_Nonnull out);
void fxa_str_free(char *_Nullable ptr);
void fxa_free(FirefoxAccountHandle h, FxAError *_Nonnull out);
void fxa_bytebuffer_free(FxARustBuffer buffer);

Просмотреть файл

@ -1,371 +0,0 @@
/* 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/. */
import Foundation
import SwiftProtobuf
/// This class wraps Rust calls safely and performs necessary type conversions.
open class RustFxAccount {
private let raw: UInt64
internal init(raw: UInt64) {
self.raw = raw
}
/// Create a `RustFxAccount` from scratch. This is suitable for callers using the
/// OAuth Flow.
public required convenience init(config: FxAConfig) throws {
let pointer = try rustCall { err in
fxa_new(config.contentUrl, config.clientId, config.redirectUri, config.tokenServerUrlOverride, err)
}
self.init(raw: pointer)
}
/// Restore a previous instance of `RustFxAccount` from a serialized state (obtained with `toJSON(...)`).
public required convenience init(fromJsonState state: String) throws {
let pointer = try rustCall { err in fxa_from_json(state, err) }
self.init(raw: pointer)
}
deinit {
if self.raw != 0 {
try! rustCall { err in
// Is `try!` the right thing to do? We should only hit an error here
// for panics and handle misuse, both inidicate bugs in our code
// (the first in the rust code, the 2nd in this swift wrapper).
fxa_free(self.raw, err)
}
}
}
/// Serializes the state of a `RustFxAccount` instance. It can be restored
/// later with `fromJSON(...)`. It is the responsability of the caller to
/// persist that serialized state regularly (after operations that mutate
/// `RustFxAccount`) in a **secure** location.
open func toJSON() throws -> String {
let ptr = try rustCall { err in fxa_to_json(self.raw, err) }
return String(freeingFxaString: ptr)
}
/// Gets the logged-in user profile.
/// Throws `FirefoxAccountError.Unauthorized` if we couldn't find any suitable access token
/// to make that call. The caller should then start the OAuth Flow again with
/// the "profile" scope.
open func getProfile(ignoreCache: Bool) throws -> Profile {
let ignoreCacheArg = UInt8(ignoreCache ? 1 : 0)
let ptr = try rustCall { err in
fxa_profile(self.raw, ignoreCacheArg, err)
}
defer { fxa_bytebuffer_free(ptr) }
let msg = try! MsgTypes_Profile(serializedData: Data(rustBuffer: ptr))
return Profile(msg: msg)
}
open func getTokenServerEndpointURL() throws -> URL {
let ptr = try rustCall { err in
fxa_get_token_server_endpoint_url(self.raw, err)
}
return URL(string: String(freeingFxaString: ptr))!
}
open func getPairingAuthorityURL() throws -> URL {
let ptr = try rustCall { err in
fxa_get_pairing_authority_url(self.raw, err)
}
return URL(string: String(freeingFxaString: ptr))!
}
open func getConnectionSuccessURL() throws -> URL {
let ptr = try rustCall { err in
fxa_get_connection_success_url(self.raw, err)
}
return URL(string: String(freeingFxaString: ptr))!
}
open func getManageAccountURL(entrypoint: String) throws -> URL {
let ptr = try rustCall { err in
fxa_get_manage_account_url(self.raw, entrypoint, err)
}
return URL(string: String(freeingFxaString: ptr))!
}
open func getManageDevicesURL(entrypoint: String) throws -> URL {
let ptr = try rustCall { err in
fxa_get_manage_devices_url(self.raw, entrypoint, err)
}
return URL(string: String(freeingFxaString: ptr))!
}
/// Request a OAuth token by starting a new OAuth flow.
///
/// This function returns a URL string that the caller should open in a webview.
///
/// Once the user has confirmed the authorization grant, they will get redirected to `redirect_url`:
/// the caller must intercept that redirection, extract the `code` and `state` query parameters and call
/// `completeOAuthFlow(...)` to complete the flow.
open func beginOAuthFlow(
scopes: [String],
entrypoint: String,
metricsParams: MetricsParams
) throws -> URL {
let scope = scopes.joined(separator: " ")
let (metricsParamsData, metricsParamsLen) = msgToBuffer(msg: metricsParams.toMsg())
let ptr = try metricsParamsData.withUnsafeBytes { bytes in
try rustCall { err in
fxa_begin_oauth_flow(
self.raw,
scope,
entrypoint,
bytes.bindMemory(to: UInt8.self).baseAddress!,
metricsParamsLen,
err
)
}
}
return URL(string: String(freeingFxaString: ptr))!
}
open func beginPairingFlow(
pairingUrl: String,
scopes: [String],
entrypoint: String,
metricsParams: MetricsParams
) throws -> URL {
let scope = scopes.joined(separator: " ")
let (metricsParamsData, metricsParamsLen) = msgToBuffer(msg: metricsParams.toMsg())
let ptr = try metricsParamsData.withUnsafeBytes { bytes in
try rustCall { err in
fxa_begin_pairing_flow(
self.raw,
pairingUrl,
scope,
entrypoint,
bytes.bindMemory(to: UInt8.self).baseAddress!,
metricsParamsLen,
err
)
}
}
return URL(string: String(freeingFxaString: ptr))!
}
/// Finish an OAuth flow initiated by `beginOAuthFlow(...)` and returns token/keys.
///
/// This resulting token might not have all the `scopes` the caller have requested (e.g. the user
/// might have denied some of them): it is the responsibility of the caller to accomodate that.
open func completeOAuthFlow(code: String, state: String) throws {
try rustCall { err in
fxa_complete_oauth_flow(self.raw, code, state, err)
}
}
/// Try to get an OAuth access token.
///
/// `ttl` corresponds to the time in seconds for which the token will be used.
/// Throws `FirefoxAccountError.Unauthorized` if we couldn't provide an access token
/// for this scope. The caller should then start the OAuth Flow again with
/// the desired scope.
open func getAccessToken(scope: String, ttl: UInt64? = nil) throws -> AccessTokenInfo {
let ptr = try rustCall { err in
// A zero ttl here means to use the server-controlled default.
fxa_get_access_token(self.raw, scope, ttl ?? .zero, err)
}
defer { fxa_bytebuffer_free(ptr) }
let msg = try! MsgTypes_AccessTokenInfo(serializedData: Data(rustBuffer: ptr))
return AccessTokenInfo(msg: msg)
}
/// Get the session token. If non-present an error will be thrown.
open func getSessionToken() throws -> String {
let ptr = try rustCall { err in
fxa_get_session_token(self.raw, err)
}
return String(freeingFxaString: ptr)
}
/// Check whether the refreshToken is active
open func checkAuthorizationStatus() throws -> IntrospectInfo {
let ptr = try rustCall { err in
fxa_check_authorization_status(self.raw, err)
}
defer { fxa_bytebuffer_free(ptr) }
let msg = try! MsgTypes_IntrospectInfo(serializedData: Data(rustBuffer: ptr))
return IntrospectInfo(msg: msg)
}
/// This method should be called when a request made with
/// an OAuth token failed with an authentication error.
/// It clears the internal cache of OAuth access tokens,
/// so the caller can try to call `getAccessToken` or `getProfile`
/// again.
open func clearAccessTokenCache() throws {
try rustCall { err in
fxa_clear_access_token_cache(self.raw, err)
}
}
/// Disconnect from the account and optionaly destroy our device record.
/// `beginOAuthFlow(...)` will need to be called to reconnect.
open func disconnect() throws {
try rustCall { err in
fxa_disconnect(self.raw, err)
}
}
open func getDevices(ignoreCache: Bool = false) throws -> [Device] {
let ignoreCacheArg = UInt8(ignoreCache ? 1 : 0)
let ptr = try rustCall { err in
fxa_get_devices(self.raw, ignoreCacheArg, err)
}
defer { fxa_bytebuffer_free(ptr) }
let msg = try! MsgTypes_Devices(serializedData: Data(rustBuffer: ptr))
return Device.fromCollectionMsg(msg: msg)
}
open func setDeviceDisplayName(_ name: String) throws {
try rustCall { err in
fxa_set_device_name(self.raw, name, err)
}
}
open func pollDeviceCommands() throws -> [IncomingDeviceCommand] {
let ptr = try rustCall { err in
fxa_poll_device_commands(self.raw, err)
}
defer { fxa_bytebuffer_free(ptr) }
let msg = try! MsgTypes_IncomingDeviceCommands(serializedData: Data(rustBuffer: ptr))
return IncomingDeviceCommand.fromCollectionMsg(msg: msg)
}
open func handlePushMessage(payload: String) throws -> [AccountEvent] {
let ptr = try rustCall { err in
fxa_handle_push_message(self.raw, payload, err)
}
defer { fxa_bytebuffer_free(ptr) }
let msg = try! MsgTypes_AccountEvents(serializedData: Data(rustBuffer: ptr))
return AccountEvent.fromCollectionMsg(msg: msg)
}
open func sendSingleTab(targetId: String, title: String, url: String) throws {
try rustCall { err in
fxa_send_tab(self.raw, targetId, title, url, err)
}
}
open func setDevicePushSubscription(endpoint: String, publicKey: String, authKey: String) throws {
try rustCall { err in
fxa_set_push_subscription(self.raw, endpoint, publicKey, authKey, err)
}
}
open func initializeDevice(
name: String,
deviceType: DeviceType,
supportedCapabilities: [DeviceCapability]
) throws {
let (data, size) = msgToBuffer(msg: supportedCapabilities.toCollectionMsg())
try data.withUnsafeBytes { bytes in
try rustCall { err in
fxa_initialize_device(
self.raw,
name,
Int32(deviceType.toMsg().rawValue),
bytes.bindMemory(to: UInt8.self).baseAddress!,
size,
err
)
}
}
}
open func ensureCapabilities(supportedCapabilities: [DeviceCapability]) throws {
let (data, size) = msgToBuffer(msg: supportedCapabilities.toCollectionMsg())
try data.withUnsafeBytes { bytes in
try rustCall { err in
fxa_ensure_capabilities(
self.raw,
bytes.bindMemory(to: UInt8.self).baseAddress!,
size,
err
)
}
}
}
open func migrateFromSessionToken(sessionToken: String, kSync: String, kXCS: String) throws -> Bool {
let json = try nullableRustCall { err in
fxa_migrate_from_session_token(self.raw, sessionToken, kSync, kXCS, 0 /* reuse session token */, err)
}
defer {
if let json = json {
fxa_str_free(json)
}
}
// We don't parse the JSON coz nobody uses it...
return json != nil
}
open func retryMigrateFromSessionToken() throws -> Bool {
let json = try nullableRustCall { err in
fxa_retry_migrate_from_session_token(self.raw, err)
}
defer {
if let json = json {
fxa_str_free(json)
}
}
return json != nil
}
open func isInMigrationState() throws -> Bool {
let number = try rustCall { err in
fxa_is_in_migration_state(self.raw, err)
}
let state = MigrationState.fromNumber(number)
// We never initiate a "copy-session-token" migration,
// so we can just return a boolean.
return state == .reuseSessionToken
}
open func handleSessionTokenChange(sessionToken: String) throws {
try rustCall { err in
fxa_handle_session_token_change(self.raw, sessionToken, err)
}
}
open func gatherTelemetry() throws -> String? {
let maybeEvents = try nullableRustCall { err in fxa_gather_telemetry(self.raw, err) }
guard let events = maybeEvents else {
return nil
}
return String(freeingFxaString: events)
}
private func msgToBuffer(msg: SwiftProtobuf.Message) -> (Data, Int32) {
let data = try! msg.serializedData()
let size = Int32(data.count)
return (data, size)
}
}
// This queue serves as a semaphore to the rust layer.
private let fxaRustQueue = DispatchQueue(label: "com.mozilla.fxa-rust")
internal func rustCall<T>(_ cb: (UnsafeMutablePointer<FxAError>) throws -> T?) throws -> T {
return try FirefoxAccountError.unwrap { err in
try fxaRustQueue.sync {
try cb(err)
}
}
}
internal func nullableRustCall<T>(_ cb: (UnsafeMutablePointer<FxAError>) throws -> T?) throws -> T? {
return try FirefoxAccountError.tryUnwrap { err in
try fxaRustQueue.sync {
try cb(err)
}
}
}

Просмотреть файл

@ -1,345 +0,0 @@
/* 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/. */
//! This module implement the traits and some types that make the FFI code easier to manage.
//!
//! Note that the FxA FFI is older than the other FFIs in application-services, and has (direct,
//! low-level) bindings that live in the mozilla-mobile/android-components repo. As a result, it's a
//! bit harder to change (anything breaking the ABI requires careful synchronization of updates
//! across two repos), and doesn't follow all the same conventions that are followed by the other
//! FFIs.
//!
//! None of this is that bad in practice, but much of it is not ideal.
pub use crate::oauth::{AuthorizationPKCEParams, AuthorizationParameters, MetricsParams};
use crate::{
commands,
device::{Capability as DeviceCapability, Device, PushSubscription, Type as DeviceType},
msg_types, send_tab, AccessTokenInfo, AccountEvent, Error, ErrorKind, IncomingDeviceCommand,
IntrospectInfo, Profile, Result, ScopedKey,
};
use ffi_support::{
implement_into_ffi_by_delegation, implement_into_ffi_by_protobuf, ErrorCode, ExternError,
};
pub mod error_codes {
// Note: -1 and 0 (panic and success) codes are reserved by the ffi-support library
/// Catch-all error code used for anything that's not a panic or covered by AUTHENTICATION.
pub const OTHER: i32 = 1;
/// Used by `ErrorKind::NoCachedTokens`, `ErrorKind::NoScopedKey`
/// and `ErrorKind::RemoteError`'s where `code == 401`.
pub const AUTHENTICATION: i32 = 2;
/// Code for network errors.
pub const NETWORK: i32 = 3;
}
/// # Safety
/// data is a raw pointer to the protobuf data
/// get_buffer will return an error if the length is invalid,
/// or if the pointer is a null pointer
pub unsafe fn from_protobuf_ptr<T, F: prost::Message + Default + Into<T>>(
data: *const u8,
len: i32,
) -> Result<T> {
let buffer = get_buffer(data, len)?;
let item: Result<F, _> = prost::Message::decode(buffer);
item.map(|inner| inner.into()).map_err(|e| e.into())
}
fn get_code(err: &Error) -> ErrorCode {
match err.kind() {
ErrorKind::RemoteError { code: 401, .. }
| ErrorKind::NoRefreshToken
| ErrorKind::NoScopedKey(_)
| ErrorKind::NoCachedToken(_) => {
log::warn!("Authentication error: {:?}", err);
ErrorCode::new(error_codes::AUTHENTICATION)
}
ErrorKind::RequestError(_) => {
log::warn!("Network error: {:?}", err);
ErrorCode::new(error_codes::NETWORK)
}
_ => {
log::warn!("Unexpected error: {:?}", err);
ErrorCode::new(error_codes::OTHER)
}
}
}
impl From<Error> for ExternError {
fn from(err: Error) -> ExternError {
ExternError::new_error(get_code(&err), err.to_string())
}
}
impl From<AccessTokenInfo> for msg_types::AccessTokenInfo {
fn from(a: AccessTokenInfo) -> Self {
msg_types::AccessTokenInfo {
scope: a.scope,
token: a.token,
key: a.key.map(Into::into),
expires_at: a.expires_at,
}
}
}
impl From<IntrospectInfo> for msg_types::IntrospectInfo {
fn from(a: IntrospectInfo) -> Self {
msg_types::IntrospectInfo { active: a.active }
}
}
impl From<ScopedKey> for msg_types::ScopedKey {
fn from(sk: ScopedKey) -> Self {
msg_types::ScopedKey {
kty: sk.kty,
scope: sk.scope,
k: sk.k,
kid: sk.kid,
}
}
}
impl From<Profile> for msg_types::Profile {
fn from(p: Profile) -> Self {
Self {
avatar: Some(p.avatar),
avatar_default: Some(p.avatar_default),
display_name: p.display_name,
email: Some(p.email),
uid: Some(p.uid),
}
}
}
fn command_to_capability(command: &str) -> Option<msg_types::device::Capability> {
match command {
commands::send_tab::COMMAND_NAME => Some(msg_types::device::Capability::SendTab),
_ => None,
}
}
impl From<Device> for msg_types::Device {
fn from(d: Device) -> Self {
let capabilities = d
.available_commands
.keys()
.filter_map(|c| command_to_capability(c).map(|cc| cc as i32))
.collect();
Self {
id: d.common.id,
display_name: d.common.display_name,
r#type: Into::<msg_types::device::Type>::into(d.common.device_type) as i32,
push_subscription: d.common.push_subscription.map(Into::into),
push_endpoint_expired: d.common.push_endpoint_expired,
is_current_device: d.is_current_device,
last_access_time: d.last_access_time,
capabilities,
}
}
}
impl From<DeviceType> for msg_types::device::Type {
fn from(t: DeviceType) -> Self {
match t {
DeviceType::Desktop => msg_types::device::Type::Desktop,
DeviceType::Mobile => msg_types::device::Type::Mobile,
DeviceType::Tablet => msg_types::device::Type::Tablet,
DeviceType::VR => msg_types::device::Type::Vr,
DeviceType::TV => msg_types::device::Type::Tv,
DeviceType::Unknown => msg_types::device::Type::Unknown,
}
}
}
impl From<msg_types::device::Type> for DeviceType {
fn from(t: msg_types::device::Type) -> Self {
match t {
msg_types::device::Type::Desktop => DeviceType::Desktop,
msg_types::device::Type::Mobile => DeviceType::Mobile,
msg_types::device::Type::Tablet => DeviceType::Tablet,
msg_types::device::Type::Vr => DeviceType::VR,
msg_types::device::Type::Tv => DeviceType::TV,
msg_types::device::Type::Unknown => DeviceType::Unknown,
}
}
}
impl From<PushSubscription> for msg_types::device::PushSubscription {
fn from(p: PushSubscription) -> Self {
Self {
endpoint: p.endpoint,
public_key: p.public_key,
auth_key: p.auth_key,
}
}
}
impl From<AccountEvent> for msg_types::AccountEvent {
fn from(e: AccountEvent) -> Self {
match e {
AccountEvent::IncomingDeviceCommand(command) => Self {
r#type: msg_types::account_event::AccountEventType::IncomingDeviceCommand as i32,
data: Some(msg_types::account_event::Data::DeviceCommand(
(*command).into(),
)),
},
AccountEvent::ProfileUpdated => Self {
r#type: msg_types::account_event::AccountEventType::ProfileUpdated as i32,
data: None,
},
AccountEvent::AccountAuthStateChanged => Self {
r#type: msg_types::account_event::AccountEventType::AccountAuthStateChanged as i32,
data: None,
},
AccountEvent::AccountDestroyed => Self {
r#type: msg_types::account_event::AccountEventType::AccountDestroyed as i32,
data: None,
},
AccountEvent::DeviceConnected { device_name } => Self {
r#type: msg_types::account_event::AccountEventType::DeviceConnected as i32,
data: Some(msg_types::account_event::Data::DeviceConnectedName(
device_name,
)),
},
AccountEvent::DeviceDisconnected {
device_id,
is_local_device,
} => Self {
r#type: msg_types::account_event::AccountEventType::DeviceDisconnected as i32,
data: Some(msg_types::account_event::Data::DeviceDisconnectedData(
msg_types::account_event::DeviceDisconnectedData {
device_id,
is_local_device,
},
)),
},
}
}
}
impl From<IncomingDeviceCommand> for msg_types::IncomingDeviceCommand {
fn from(data: IncomingDeviceCommand) -> Self {
match data {
IncomingDeviceCommand::TabReceived { sender, payload } => Self {
r#type: msg_types::incoming_device_command::IncomingDeviceCommandType::TabReceived
as i32,
data: Some(msg_types::incoming_device_command::Data::TabReceivedData(
msg_types::incoming_device_command::SendTabData {
from: sender.map(Into::into),
entries: payload.entries.into_iter().map(Into::into).collect(),
},
)),
},
}
}
}
impl From<send_tab::TabHistoryEntry>
for msg_types::incoming_device_command::send_tab_data::TabHistoryEntry
{
fn from(data: send_tab::TabHistoryEntry) -> Self {
Self {
title: data.title,
url: data.url,
}
}
}
impl From<msg_types::device::Capability> for DeviceCapability {
fn from(cap: msg_types::device::Capability) -> Self {
match cap {
msg_types::device::Capability::SendTab => DeviceCapability::SendTab,
}
}
}
impl DeviceCapability {
/// # Safety
/// Deref pointer thus unsafe
pub unsafe fn from_protobuf_array_ptr(ptr: *const u8, len: i32) -> Result<Vec<Self>> {
let buffer = get_buffer(ptr, len)?;
let capabilities: Result<msg_types::Capabilities, _> = prost::Message::decode(buffer);
Ok(capabilities
.map(|cc| cc.to_capabilities_vec())
.unwrap_or_else(|_| vec![]))
}
}
impl msg_types::Capabilities {
pub fn to_capabilities_vec(&self) -> Vec<DeviceCapability> {
self.capability
.iter()
.map(|c| msg_types::device::Capability::from_i32(*c).unwrap().into())
.collect()
}
}
unsafe fn get_buffer<'a>(data: *const u8, len: i32) -> Result<&'a [u8]> {
match len {
len if len < 0 => Err(ErrorKind::InvalidBufferLength(len).into()),
0 => Ok(&[]),
_ => {
if data.is_null() {
return Err(ErrorKind::NullPointer.into());
}
Ok(std::slice::from_raw_parts(data, len as usize))
}
}
}
impl From<msg_types::AuthorizationParams> for AuthorizationParameters {
fn from(proto_params: msg_types::AuthorizationParams) -> Self {
Self {
client_id: proto_params.client_id,
scope: proto_params
.scope
.split_whitespace()
.map(|s| s.to_string())
.collect(),
state: proto_params.state,
access_type: proto_params.access_type,
pkce_params: proto_params
.pkce_params
.map(|pkce_params| pkce_params.into()),
keys_jwk: proto_params.keys_jwk,
}
}
}
impl From<msg_types::MetricsParams> for MetricsParams {
fn from(proto_metrics_params: msg_types::MetricsParams) -> Self {
Self {
parameters: proto_metrics_params.parameters,
}
}
}
impl From<msg_types::AuthorizationPkceParams> for AuthorizationPKCEParams {
fn from(proto_key_params: msg_types::AuthorizationPkceParams) -> Self {
Self {
code_challenge: proto_key_params.code_challenge,
code_challenge_method: proto_key_params.code_challenge_method,
}
}
}
implement_into_ffi_by_protobuf!(msg_types::Profile);
implement_into_ffi_by_delegation!(Profile, msg_types::Profile);
implement_into_ffi_by_protobuf!(msg_types::AccessTokenInfo);
implement_into_ffi_by_delegation!(AccessTokenInfo, msg_types::AccessTokenInfo);
implement_into_ffi_by_protobuf!(msg_types::IntrospectInfo);
implement_into_ffi_by_delegation!(IntrospectInfo, msg_types::IntrospectInfo);
implement_into_ffi_by_protobuf!(msg_types::Device);
implement_into_ffi_by_delegation!(Device, msg_types::Device);
implement_into_ffi_by_protobuf!(msg_types::Devices);
implement_into_ffi_by_delegation!(AccountEvent, msg_types::AccountEvent);
implement_into_ffi_by_protobuf!(msg_types::AccountEvent);
implement_into_ffi_by_protobuf!(msg_types::AccountEvents);
implement_into_ffi_by_delegation!(IncomingDeviceCommand, msg_types::IncomingDeviceCommand);
implement_into_ffi_by_protobuf!(msg_types::IncomingDeviceCommand);
implement_into_ffi_by_protobuf!(msg_types::IncomingDeviceCommands);

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1,142 +0,0 @@
syntax = "proto2";
// Note: this file name must be unique due to how the iOS megazord works :(
package mozilla.appservices.fxaclient.protobuf;
option java_package = "mozilla.appservices.fxaclient";
option java_outer_classname = "MsgTypes";
option swift_prefix = "MsgTypes_";
option optimize_for = LITE_RUNTIME;
message Profile {
optional string uid = 1;
optional string email = 2;
optional string avatar = 3;
optional bool avatar_default = 4;
optional string display_name = 5;
}
message AccessTokenInfo {
required string scope = 1;
required string token = 2;
optional ScopedKey key = 3;
required uint64 expires_at = 4;
}
message IntrospectInfo {
required bool active = 1;
}
message ScopedKey {
required string kty = 1;
required string scope = 2;
required string k = 3;
required string kid = 4;
}
message Device {
message PushSubscription {
required string endpoint = 1;
required string public_key = 2;
required string auth_key = 3;
}
enum Capability {
SEND_TAB = 1;
}
enum Type {
DESKTOP = 1;
MOBILE = 2;
TABLET = 3;
VR = 4;
TV = 5;
UNKNOWN = 6;
}
required string id = 1;
required string display_name = 2;
required Type type = 3;
optional PushSubscription push_subscription = 4;
required bool push_endpoint_expired = 5;
required bool is_current_device = 6;
optional uint64 last_access_time = 7;
repeated Capability capabilities = 8;
}
message Devices {
repeated Device devices = 1;
}
message Capabilities {
repeated Device.Capability capability = 1;
}
message IncomingDeviceCommand {
enum IncomingDeviceCommandType {
TAB_RECEIVED = 1; // `data` set to `tab_received_data`.
}
required IncomingDeviceCommandType type = 1;
message SendTabData {
message TabHistoryEntry {
required string title = 1;
required string url = 2;
}
optional Device from = 1;
repeated TabHistoryEntry entries = 2;
}
oneof data {
SendTabData tab_received_data = 2;
};
}
message IncomingDeviceCommands {
repeated IncomingDeviceCommand commands = 1;
}
// This is basically an enum with associated values,
// but it's a bit harder to model in proto2.
message AccountEvent {
enum AccountEventType {
INCOMING_DEVICE_COMMAND = 1; // `data` set to `device_command`.
PROFILE_UPDATED = 2;
DEVICE_CONNECTED = 3; // `data` set to `device_connected_name`.
ACCOUNT_AUTH_STATE_CHANGED = 4;
DEVICE_DISCONNECTED = 5; // `data` set to `device_disconnected_data`.
ACCOUNT_DESTROYED = 6;
}
required AccountEventType type = 1;
message DeviceDisconnectedData {
required string device_id = 1;
required bool is_local_device = 2;
}
oneof data {
IncomingDeviceCommand device_command = 2;
string device_connected_name = 3;
DeviceDisconnectedData device_disconnected_data = 4;
}
}
message AccountEvents {
repeated AccountEvent events = 1;
}
message AuthorizationPKCEParams {
required string code_challenge = 1;
required string code_challenge_method = 2;
}
message AuthorizationParams {
required string client_id = 1;
required string scope = 2;
required string state = 3;
required string access_type = 4;
optional AuthorizationPKCEParams pkce_params = 5;
optional string keys_jwk = 6;
}
message MetricsParams {
map<string, string> parameters = 1;
}

Просмотреть файл

@ -2,8 +2,8 @@
* 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/. */
pub use crate::oauth::{AuthorizationPKCEParams, AuthorizationParameters};
use crate::{error::*, http_client, scoped_keys::ScopedKey, util::Xorable, Config};
pub use super::oauth::AuthorizationParameters;
use super::{error::*, http_client, scoped_keys::ScopedKey, util::Xorable, Config};
pub use http_client::{
derive_auth_key_from_session_token, send_authorization_request, send_verification,
AuthorizationRequestParameters,

Просмотреть файл

@ -0,0 +1,34 @@
/* 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 std::convert::TryFrom;
pub mod send_tab;
pub use send_tab::SendTabPayload;
use super::device::Device;
use super::error::*;
// Currently public for use by example crates, but should be made private eventually.
#[derive(Clone, Debug)]
pub enum IncomingDeviceCommand {
TabReceived {
sender: Option<Device>,
payload: SendTabPayload,
},
}
impl TryFrom<IncomingDeviceCommand> for crate::IncomingDeviceCommand {
type Error = Error;
fn try_from(cmd: IncomingDeviceCommand) -> Result<Self> {
Ok(match cmd {
IncomingDeviceCommand::TabReceived { sender, payload } => {
crate::IncomingDeviceCommand::TabReceived {
sender: sender.map(crate::Device::try_from).transpose()?,
payload: payload.into(),
}
}
})
}
}

Просмотреть файл

@ -13,12 +13,14 @@
/// uses the obtained public key to encrypt the `SendTabPayload` it created that
/// contains the tab to send and finally forms the `EncryptedSendTabPayload` that is
/// then sent to the target device.
use crate::{device::Device, error::*, scoped_keys::ScopedKey, scopes, telemetry};
use serde_derive::*;
use rc_crypto::ece::{self, Aes128GcmEceWebPush, EcKeyComponents, WebPushParams};
use rc_crypto::ece_crypto::{RcCryptoLocalKeyPair, RcCryptoRemotePublicKey};
use serde_derive::*;
use sync15::{EncryptedPayload, KeyBundle};
use super::super::{device::Device, error::*, scoped_keys::ScopedKey, scopes, telemetry};
pub const COMMAND_NAME: &str = "https://identity.mozilla.com/cmd/open-uri";
#[derive(Debug, Serialize, Deserialize)]
@ -37,7 +39,7 @@ impl EncryptedSendTabPayload {
}
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SendTabPayload {
pub entries: Vec<TabHistoryEntry>,
#[serde(rename = "flowID", default)]
@ -46,6 +48,16 @@ pub struct SendTabPayload {
pub stream_id: String,
}
impl From<SendTabPayload> for crate::SendTabPayload {
fn from(payload: SendTabPayload) -> Self {
crate::SendTabPayload {
entries: payload.entries.into_iter().map(From::from).collect(),
flow_id: payload.flow_id,
stream_id: payload.stream_id,
}
}
}
impl SendTabPayload {
pub fn single_tab(title: &str, url: &str) -> (Self, telemetry::SentCommand) {
let sent_telemetry: telemetry::SentCommand = Default::default();
@ -78,12 +90,21 @@ impl SendTabPayload {
}
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TabHistoryEntry {
pub title: String,
pub url: String,
}
impl From<TabHistoryEntry> for crate::TabHistoryEntry {
fn from(e: TabHistoryEntry) -> Self {
crate::TabHistoryEntry {
title: e.title,
url: e.url,
}
}
}
#[derive(Serialize, Deserialize, Clone)]
pub(crate) enum VersionnedPrivateSendTabKeys {
V1(PrivateSendTabKeysV1),

Просмотреть файл

@ -2,7 +2,7 @@
* 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 crate::{error::*, http_client};
use super::{error::*, http_client};
use serde_derive::{Deserialize, Serialize};
use std::{cell::RefCell, sync::Arc};
use url::Url;

Просмотреть файл

@ -2,19 +2,22 @@
* 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/. */
pub use crate::http_client::{
use std::{
collections::{HashMap, HashSet},
convert::TryFrom,
};
use serde_derive::*;
pub use super::http_client::{
DeviceLocation as Location, DeviceType as Type, GetDeviceResponse as Device, PushSubscription,
};
use crate::{
commands,
use super::{
commands::{self, IncomingDeviceCommand},
error::*,
http_client::{
DeviceUpdateRequest, DeviceUpdateRequestBuilder, PendingCommand, UpdateDeviceResponse,
},
telemetry, util, CachedResponse, FirefoxAccount, IncomingDeviceCommand,
http_client::{DeviceUpdateRequest, DeviceUpdateRequestBuilder, PendingCommand},
telemetry, util, CachedResponse, FirefoxAccount,
};
use serde_derive::*;
use std::collections::{HashMap, HashSet};
// An devices response is considered fresh for `DEVICES_FRESHNESS_THRESHOLD` ms.
const DEVICES_FRESHNESS_THRESHOLD: u64 = 60_000; // 1 minute
@ -42,7 +45,9 @@ impl FirefoxAccount {
}
let refresh_token = self.get_refresh_token()?;
let response = self.client.devices(&self.state.config, &refresh_token)?;
let response = self
.client
.get_devices(&self.state.config, &refresh_token)?;
self.devices_cache = Some(CachedResponse {
response: response.clone(),
@ -98,15 +103,13 @@ impl FirefoxAccount {
device_type: Type,
capabilities: &[Capability],
) -> Result<()> {
let commands = self.register_capabilities(capabilities)?;
let commands = self.register_capabilities(&capabilities)?;
let update = DeviceUpdateRequestBuilder::new()
.display_name(name)
.device_type(&device_type)
.available_commands(&commands)
.build();
let resp = self.update_device(update)?;
self.state.current_device_id = Option::from(resp.id);
Ok(())
self.update_device(update)
}
/// Register a set of device capabilities against the current device.
@ -130,13 +133,11 @@ impl FirefoxAccount {
{
return Ok(());
}
let commands = self.register_capabilities(capabilities)?;
let commands = self.register_capabilities(&capabilities)?;
let update = DeviceUpdateRequestBuilder::new()
.available_commands(&commands)
.build();
let resp = self.update_device(update)?;
self.state.current_device_id = Option::from(resp.id);
Ok(())
self.update_device(update)
}
/// Re-register the device capabilities, this should only be used internally.
@ -215,7 +216,7 @@ impl FirefoxAccount {
let refresh_token = self.get_refresh_token()?;
let pending_commands =
self.client
.pending_commands(&self.state.config, refresh_token, index, limit)?;
.get_pending_commands(&self.state.config, refresh_token, index, limit)?;
if pending_commands.messages.is_empty() {
return Ok(Vec::new());
}
@ -269,22 +270,19 @@ impl FirefoxAccount {
}
}
pub fn set_device_name(&mut self, name: &str) -> Result<UpdateDeviceResponse> {
pub fn set_device_name(&mut self, name: &str) -> Result<()> {
let update = DeviceUpdateRequestBuilder::new().display_name(name).build();
self.update_device(update)
}
pub fn clear_device_name(&mut self) -> Result<UpdateDeviceResponse> {
pub fn clear_device_name(&mut self) -> Result<()> {
let update = DeviceUpdateRequestBuilder::new()
.clear_display_name()
.build();
self.update_device(update)
}
pub fn set_push_subscription(
&mut self,
push_subscription: &PushSubscription,
) -> Result<UpdateDeviceResponse> {
pub fn set_push_subscription(&mut self, push_subscription: PushSubscription) -> Result<()> {
let update = DeviceUpdateRequestBuilder::new()
.push_subscription(&push_subscription)
.build();
@ -295,11 +293,7 @@ impl FirefoxAccount {
// for the device because the server does not have a `PATCH commands`
// endpoint yet.
#[allow(dead_code)]
pub(crate) fn register_command(
&mut self,
command: &str,
value: &str,
) -> Result<UpdateDeviceResponse> {
pub(crate) fn register_command(&mut self, command: &str, value: &str) -> Result<()> {
self.state.device_capabilities.clear();
let mut commands = HashMap::new();
commands.insert(command.to_owned(), value.to_owned());
@ -312,7 +306,7 @@ impl FirefoxAccount {
// TODO: this currently deletes every command registered for the device
// because the server does not have a `PATCH commands` endpoint yet.
#[allow(dead_code)]
pub(crate) fn unregister_command(&mut self, _: &str) -> Result<UpdateDeviceResponse> {
pub(crate) fn unregister_command(&mut self, _: &str) -> Result<()> {
self.state.device_capabilities.clear();
let commands = HashMap::new();
let update = DeviceUpdateRequestBuilder::new()
@ -322,7 +316,7 @@ impl FirefoxAccount {
}
#[allow(dead_code)]
pub(crate) fn clear_commands(&mut self) -> Result<UpdateDeviceResponse> {
pub(crate) fn clear_commands(&mut self) -> Result<()> {
self.state.device_capabilities.clear();
let update = DeviceUpdateRequestBuilder::new()
.clear_available_commands()
@ -336,7 +330,7 @@ impl FirefoxAccount {
device_type: &Type,
push_subscription: &Option<PushSubscription>,
commands: &HashMap<String, String>,
) -> Result<UpdateDeviceResponse> {
) -> Result<()> {
self.state.device_capabilities.clear();
let mut builder = DeviceUpdateRequestBuilder::new()
.display_name(display_name)
@ -348,13 +342,16 @@ impl FirefoxAccount {
self.update_device(builder.build())
}
fn update_device(&mut self, update: DeviceUpdateRequest<'_>) -> Result<UpdateDeviceResponse> {
fn update_device(&mut self, update: DeviceUpdateRequest<'_>) -> Result<()> {
let refresh_token = self.get_refresh_token()?;
let res = self
.client
.update_device(&self.state.config, refresh_token, update);
.update_device_record(&self.state.config, refresh_token, update);
match res {
Ok(resp) => Ok(resp),
Ok(resp) => {
self.state.current_device_id = Option::from(resp.id);
Ok(())
}
Err(err) => {
// We failed to write an update to the server.
// Clear local state so that we'll be sure to retry later.
@ -378,13 +375,80 @@ pub enum Capability {
SendTab,
}
impl From<crate::DeviceCapability> for Capability {
fn from(cap: crate::DeviceCapability) -> Self {
match cap {
crate::DeviceCapability::SendTab => Capability::SendTab,
}
}
}
impl From<Capability> for crate::DeviceCapability {
fn from(cap: Capability) -> Self {
match cap {
Capability::SendTab => crate::DeviceCapability::SendTab,
}
}
}
impl TryFrom<Device> for crate::Device {
type Error = Error;
fn try_from(d: Device) -> Result<Self> {
let capabilities: Vec<_> = d
.available_commands
.keys()
.filter_map(|k| match k.as_str() {
commands::send_tab::COMMAND_NAME => Some(Capability::SendTab),
_ => None,
})
.map(Into::into)
.collect();
Ok(crate::Device {
id: d.common.id,
display_name: d.common.display_name,
device_type: d.common.device_type.into(),
capabilities,
push_subscription: d.common.push_subscription.map(Into::into),
push_endpoint_expired: d.common.push_endpoint_expired,
is_current_device: d.is_current_device,
last_access_time: d.last_access_time.map(TryFrom::try_from).transpose()?,
})
}
}
impl From<Type> for crate::DeviceType {
fn from(type_: Type) -> Self {
match type_ {
Type::Desktop => crate::DeviceType::Desktop,
Type::Mobile => crate::DeviceType::Mobile,
Type::Tablet => crate::DeviceType::Tablet,
Type::VR => crate::DeviceType::VR,
Type::TV => crate::DeviceType::TV,
Type::Unknown => crate::DeviceType::Unknown,
}
}
}
impl From<crate::DeviceType> for Type {
fn from(type_: crate::DeviceType) -> Self {
match type_ {
crate::DeviceType::Desktop => Type::Desktop,
crate::DeviceType::Mobile => Type::Mobile,
crate::DeviceType::Tablet => Type::Tablet,
crate::DeviceType::VR => Type::VR,
crate::DeviceType::TV => Type::TV,
crate::DeviceType::Unknown => Type::Unknown,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http_client::*;
use crate::oauth::RefreshToken;
use crate::scoped_keys::ScopedKey;
use crate::Config;
use crate::internal::http_client::*;
use crate::internal::oauth::RefreshToken;
use crate::internal::scoped_keys::ScopedKey;
use crate::internal::Config;
use std::collections::HashSet;
use std::sync::Arc;
@ -413,7 +477,7 @@ mod tests {
// Do an initial call to ensure_capabilities().
let mut client = FxAClientMock::new();
client
.expect_update_device(
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
@ -450,7 +514,7 @@ mod tests {
// Do an initial call to ensure_capabilities().
let mut client = FxAClientMock::new();
client
.expect_update_device(
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
@ -471,7 +535,7 @@ mod tests {
// Do another call with reduced capabilities.
let mut client = FxAClientMock::new();
client
.expect_update_device(
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
@ -493,7 +557,7 @@ mod tests {
let mut restored = FirefoxAccount::from_json(&saved).unwrap();
let mut client = FxAClientMock::new();
client
.expect_update_device(
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
@ -520,7 +584,7 @@ mod tests {
// Do an initial call to ensure_capabilities().
let mut client = FxAClientMock::new();
client
.expect_update_device(
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
@ -541,7 +605,7 @@ mod tests {
// Do another call with reduced capabilities.
let mut client = FxAClientMock::new();
client
.expect_update_device(
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
@ -563,7 +627,7 @@ mod tests {
let mut restored = FirefoxAccount::from_json(&saved).unwrap();
let mut client = FxAClientMock::new();
client
.expect_update_device(
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
@ -588,7 +652,7 @@ mod tests {
// Do an initial call to ensure_capabilities().
let mut client = FxAClientMock::new();
client
.expect_update_device(
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
@ -618,7 +682,7 @@ mod tests {
}
.into()));
client
.expect_devices(mockiato::Argument::any, mockiato::Argument::any)
.expect_get_devices(mockiato::Argument::any, mockiato::Argument::any)
.returns_once(Err(ErrorKind::RemoteError {
code: 500,
errno: 999,
@ -658,7 +722,7 @@ mod tests {
// It should re-register, as server-side state may have changed.
let mut client = FxAClientMock::new();
client
.expect_update_device(
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("newRefreshTok"),
mockiato::Argument::any,
@ -682,7 +746,7 @@ mod tests {
// Do an initial call to ensure_capabilities(), that fails.
let mut client = FxAClientMock::new();
client
.expect_update_device(
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
@ -702,7 +766,7 @@ mod tests {
// Do another call, which should re-attempt the update.
let mut client = FxAClientMock::new();
client
.expect_update_device(
.expect_update_device_record(
mockiato::Argument::any,
|arg| arg.partial_eq("refreshtok"),
mockiato::Argument::any,
@ -725,7 +789,7 @@ mod tests {
let mut fxa = setup();
let mut client = FxAClientMock::new();
client
.expect_devices(mockiato::Argument::any, mockiato::Argument::any)
.expect_get_devices(mockiato::Argument::any, mockiato::Argument::any)
.times(1)
.returns_once(Ok(vec![Device {
common: DeviceResponseCommon {
@ -776,7 +840,7 @@ mod tests {
let mut fxa = setup();
let mut client = FxAClientMock::new();
client
.expect_devices(mockiato::Argument::any, mockiato::Argument::any)
.expect_get_devices(mockiato::Argument::any, mockiato::Argument::any)
.times(1)
.returns_once(Err(ErrorKind::RemoteError {
code: 500,

Просмотреть файл

@ -121,8 +121,8 @@ pub enum ErrorKind {
#[error("HAWK error: {0}")]
HawkError(#[from] hawk::Error),
#[error("Protobuf decode error: {0}")]
ProtobufDecodeError(#[from] prost::DecodeError),
#[error("Integer conversion error: {0}")]
IntegerConversionError(#[from] std::num::TryFromIntError),
}
error_support::define_error! {
@ -138,12 +138,37 @@ error_support::define_error! {
(UnexpectedStatus, viaduct::UnexpectedStatus),
(MalformedUrl, url::ParseError),
(SyncError, sync15::Error),
(ProtobufDecodeError, prost::DecodeError),
}
}
error_support::define_error_conversions! {
ErrorKind {
(HawkError, hawk::Error),
(IntegerConversionError, std::num::TryFromIntError),
}
}
// The public FFI puts the errors into three buckets, this helps us
// convert between them. Maybe in future we can use uniffi to expose
// more error info to the caller?
impl From<super::Error> for crate::FxaError {
fn from(err: super::Error) -> crate::FxaError {
match err.kind() {
super::ErrorKind::RemoteError { code: 401, .. }
| super::ErrorKind::NoRefreshToken
| super::ErrorKind::NoScopedKey(_)
| super::ErrorKind::NoCachedToken(_) => {
log::warn!("Authentication error: {:?}", err);
crate::FxaError::Authentication
}
super::ErrorKind::RequestError(_) => {
log::warn!("Network error: {:?}", err);
crate::FxaError::Network
}
_ => {
log::warn!("Unexpected error: {:?}", err);
crate::FxaError::Other
}
}
}
}

Просмотреть файл

@ -2,7 +2,13 @@
* 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 crate::{config::Config, error::*};
//! Low-level API for talking to the FxA server.
//!
//! This module is responsible for talking to the FxA server over HTTP,
//! serializing request bodies and deserializing response payloads into
//! live objects that can be inspected by other parts of the code.
use super::{config::Config, error::*};
use rc_crypto::{
digest,
hawk::{Credentials, Key, PayloadHasher, RequestBuilder, SHA256},
@ -22,58 +28,79 @@ const HAWK_HKDF_SALT: [u8; 32] = [0b0; 32];
const HAWK_KEY_LENGTH: usize = 32;
const RETRY_AFTER_DEFAULT_SECONDS: u64 = 10;
/// Trait defining the low-level API for talking to the FxA server.
///
/// These are all the methods and datatypes used for interacting with
/// the FxA server. It's defined as a trait mostly to make it mockable
/// for testing purposes.
///
/// The default live implementation of this trait can be found in
/// the [`Client`] struct, and you should consult that struct for
/// documentation on specific methods.
///
/// A brief note on names: you'll see that the method names on this trait
/// try to follow a pattern of `<verb>_<noun>[_<additional info>]()`.
/// Please try to keep to this pattern if you add methods to this trait.
///
/// Consistent names are helpful at the best of times, but they're
/// particularly important here because so many of the nouns in OAuth
/// contain embedded verbs! For example: at a glance, does a method
/// named `refresh_token` refresh something called a "token", or does
/// it return something called a "refresh token"? Using unambiguous
/// verbs to start each method helps avoid confusion here.
///
#[cfg_attr(test, mockiato::mockable)]
pub(crate) trait FxAClient {
fn refresh_token_with_code(
fn create_refresh_token_using_authorization_code(
&self,
config: &Config,
code: &str,
code_verifier: &str,
) -> Result<OAuthTokenResponse>;
fn refresh_token_with_session_token(
fn create_refresh_token_using_session_token(
&self,
config: &Config,
session_token: &str,
scopes: &[&str],
) -> Result<OAuthTokenResponse>;
fn oauth_introspect_refresh_token(
fn check_refresh_token_status(
&self,
config: &Config,
refresh_token: &str,
) -> Result<IntrospectResponse>;
fn access_token_with_refresh_token(
fn create_access_token_using_refresh_token(
&self,
config: &Config,
refresh_token: &str,
ttl: Option<u64>,
scopes: &[&str],
) -> Result<OAuthTokenResponse>;
fn access_token_with_session_token(
fn create_access_token_using_session_token(
&self,
config: &Config,
session_token: &str,
scopes: &[&str],
) -> Result<OAuthTokenResponse>;
fn authorization_code_using_session_token(
fn create_authorization_code_using_session_token(
&self,
config: &Config,
session_token: &str,
auth_params: AuthorizationRequestParameters,
) -> Result<OAuthAuthResponse>;
fn duplicate_session(
fn duplicate_session_token(
&self,
config: &Config,
session_token: &str,
) -> Result<DuplicateTokenResponse>;
fn destroy_access_token(&self, config: &Config, token: &str) -> Result<()>;
fn destroy_refresh_token(&self, config: &Config, token: &str) -> Result<()>;
fn profile(
fn get_profile(
&self,
config: &Config,
profile_access_token: &str,
etag: Option<String>,
) -> Result<Option<ResponseAndETag<ProfileResponse>>>;
fn pending_commands(
fn get_pending_commands(
&self,
config: &Config,
refresh_token: &str,
@ -88,28 +115,28 @@ pub(crate) trait FxAClient {
target: &str,
payload: &serde_json::Value,
) -> Result<()>;
fn devices(&self, config: &Config, refresh_token: &str) -> Result<Vec<GetDeviceResponse>>;
fn update_device(
fn update_device_record(
&self,
config: &Config,
refresh_token: &str,
update: DeviceUpdateRequest<'_>,
) -> Result<UpdateDeviceResponse>;
fn destroy_device(&self, config: &Config, refresh_token: &str, id: &str) -> Result<()>;
fn attached_clients(
fn destroy_device_record(&self, config: &Config, refresh_token: &str, id: &str) -> Result<()>;
fn get_devices(&self, config: &Config, refresh_token: &str) -> Result<Vec<GetDeviceResponse>>;
fn get_attached_clients(
&self,
config: &Config,
session_token: &str,
) -> Result<Vec<GetAttachedClientResponse>>;
fn scoped_key_data(
fn get_scoped_key_data(
&self,
config: &Config,
session_token: &str,
client_id: &str,
scope: &str,
) -> Result<HashMap<String, ScopedKeyDataResponse>>;
fn fxa_client_configuration(&self, config: &Config) -> Result<ClientConfigurationResponse>;
fn openid_configuration(&self, config: &Config) -> Result<OpenIdConfigurationResponse>;
fn get_fxa_client_configuration(&self, config: &Config) -> Result<ClientConfigurationResponse>;
fn get_openid_configuration(&self, config: &Config) -> Result<OpenIdConfigurationResponse>;
}
enum HttpClientState {
@ -124,17 +151,17 @@ pub struct Client {
state: Mutex<HashMap<String, HttpClientState>>,
}
impl FxAClient for Client {
fn fxa_client_configuration(&self, config: &Config) -> Result<ClientConfigurationResponse> {
fn get_fxa_client_configuration(&self, config: &Config) -> Result<ClientConfigurationResponse> {
// Why go through two-levels of indirection? It looks kinda dumb.
// Well, `config:Config` also needs to fetch the config, but does not have access
// to an instance of `http_client`, so it calls the helper function directly.
fxa_client_configuration(config.client_config_url()?)
}
fn openid_configuration(&self, config: &Config) -> Result<OpenIdConfigurationResponse> {
fn get_openid_configuration(&self, config: &Config) -> Result<OpenIdConfigurationResponse> {
openid_configuration(config.openid_config_url()?)
}
fn profile(
fn get_profile(
&self,
config: &Config,
access_token: &str,
@ -160,8 +187,7 @@ impl FxAClient for Client {
}))
}
// For the one-off generation of a `refresh_token` and associated meta from transient credentials.
fn refresh_token_with_code(
fn create_refresh_token_using_authorization_code(
&self,
config: &Config,
code: &str,
@ -176,7 +202,7 @@ impl FxAClient for Client {
self.make_oauth_token_request(config, serde_json::to_value(req_body).unwrap())
}
fn refresh_token_with_session_token(
fn create_refresh_token_using_session_token(
&self,
config: &Config,
session_token: &str,
@ -198,7 +224,7 @@ impl FxAClient for Client {
// For the regular generation of an `access_token` from long-lived credentials.
fn access_token_with_refresh_token(
fn create_access_token_using_refresh_token(
&self,
config: &Config,
refresh_token: &str,
@ -214,7 +240,7 @@ impl FxAClient for Client {
self.make_oauth_token_request(config, serde_json::to_value(req).unwrap())
}
fn access_token_with_session_token(
fn create_access_token_using_session_token(
&self,
config: &Config,
session_token: &str,
@ -233,7 +259,7 @@ impl FxAClient for Client {
self.make_request(request)?.json().map_err(Into::into)
}
fn authorization_code_using_session_token(
fn create_authorization_code_using_session_token(
&self,
config: &Config,
session_token: &str,
@ -249,7 +275,7 @@ impl FxAClient for Client {
Ok(self.make_request(request)?.json()?)
}
fn oauth_introspect_refresh_token(
fn check_refresh_token_status(
&self,
config: &Config,
refresh_token: &str,
@ -262,7 +288,7 @@ impl FxAClient for Client {
Ok(self.make_request(Request::post(url).json(&body))?.json()?)
}
fn duplicate_session(
fn duplicate_session_token(
&self,
config: &Config,
session_token: &str,
@ -293,7 +319,7 @@ impl FxAClient for Client {
self.destroy_token_helper(config, &body)
}
fn pending_commands(
fn get_pending_commands(
&self,
config: &Config,
refresh_token: &str,
@ -332,14 +358,14 @@ impl FxAClient for Client {
Ok(())
}
fn devices(&self, config: &Config, refresh_token: &str) -> Result<Vec<GetDeviceResponse>> {
fn get_devices(&self, config: &Config, refresh_token: &str) -> Result<Vec<GetDeviceResponse>> {
let url = config.auth_url_path("v1/account/devices")?;
let request =
Request::get(url).header(header_names::AUTHORIZATION, bearer_token(refresh_token))?;
Ok(self.make_request(request)?.json()?)
}
fn update_device(
fn update_device_record(
&self,
config: &Config,
refresh_token: &str,
@ -353,7 +379,7 @@ impl FxAClient for Client {
Ok(self.make_request(request)?.json()?)
}
fn destroy_device(&self, config: &Config, refresh_token: &str, id: &str) -> Result<()> {
fn destroy_device_record(&self, config: &Config, refresh_token: &str, id: &str) -> Result<()> {
let body = json!({
"id": id,
});
@ -367,7 +393,7 @@ impl FxAClient for Client {
Ok(())
}
fn attached_clients(
fn get_attached_clients(
&self,
config: &Config,
session_token: &str,
@ -378,7 +404,7 @@ impl FxAClient for Client {
Ok(self.make_request(request)?.json()?)
}
fn scoped_key_data(
fn get_scoped_key_data(
&self,
config: &Config,
session_token: &str,
@ -528,8 +554,8 @@ pub struct AuthorizationRequestParameters {
pub keys_jwe: Option<String>,
}
// Keeping those functions out of the FxAClient trate becouse functions in the
// FxAClient trate with a `test only` feature upsets the mockiato proc macro
// Keeping those functions out of the FxAClient trait becouse functions in the
// FxAClient trait with a `test only` feature upsets the mockiato proc macro
// And it's okay since they are only used in tests. (if they were not test only
// Mockiato would not complain)
#[cfg(feature = "integration_test")]
@ -708,6 +734,26 @@ pub struct PushSubscription {
pub auth_key: String,
}
impl From<crate::DevicePushSubscription> for PushSubscription {
fn from(sub: crate::DevicePushSubscription) -> Self {
PushSubscription {
endpoint: sub.endpoint,
public_key: sub.public_key,
auth_key: sub.auth_key,
}
}
}
impl From<PushSubscription> for crate::DevicePushSubscription {
fn from(sub: PushSubscription) -> Self {
crate::DevicePushSubscription {
endpoint: sub.endpoint,
public_key: sub.public_key,
auth_key: sub.auth_key,
}
}
}
/// We use the double Option pattern in this struct.
/// The outer option represents the existence of the field
/// and the inner option its value or null.
@ -791,7 +837,6 @@ impl<'a> DeviceUpdateRequestBuilder<'a> {
self
}
#[allow(dead_code)]
pub fn device_type(mut self, device_type: &'a DeviceType) -> Self {
self.device_type = Some(Some(device_type));
self
@ -928,6 +973,18 @@ pub struct ProfileResponse {
pub avatar_default: bool,
}
impl From<ProfileResponse> for crate::Profile {
fn from(p: ProfileResponse) -> Self {
crate::Profile {
uid: p.uid,
email: p.email,
display_name: p.display_name,
avatar: p.avatar,
is_default_avatar: p.avatar_default,
}
}
}
#[derive(Deserialize)]
pub struct ScopedKeyDataResponse {
pub identifier: String,

Просмотреть файл

@ -2,41 +2,13 @@
* 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 crate::{error::*, scoped_keys::ScopedKey, scopes, FirefoxAccount};
use ffi_support::IntoFfi;
pub use crate::{FxAMigrationResult, MigrationState};
use super::{error::*, scoped_keys::ScopedKey, scopes, FirefoxAccount};
use serde_derive::*;
use std::convert::TryFrom;
use std::time::Instant;
// Values to pass back to calling code over the FFI.
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)]
pub struct FxAMigrationResult {
pub total_duration: u128,
}
pub enum MigrationState {
// No in-flight migration.
None,
// An in-flight migration that will copy the sessionToken.
CopySessionToken,
// An in-flight migration that will re-use the sessionToken.
ReuseSessionToken,
}
unsafe impl IntoFfi for MigrationState {
type Value = u8;
fn ffi_default() -> u8 {
0
}
fn into_ffi_value(self) -> u8 {
match self {
MigrationState::None => 0,
MigrationState::CopySessionToken => 1,
MigrationState::ReuseSessionToken => 2,
}
}
}
// Migration-related data that we may need to serialize in the persisted account state.
#[derive(Clone, Serialize, Deserialize)]
@ -129,7 +101,8 @@ impl FirefoxAccount {
self.state.in_flight_migration = None;
let metrics = FxAMigrationResult {
total_duration: import_start.elapsed().as_millis(),
// The foreign-language bindings are limited to an i64.
total_duration: i64::try_from(import_start.elapsed().as_secs()).unwrap_or(i64::MAX),
};
Ok(metrics)
@ -150,7 +123,7 @@ impl FirefoxAccount {
let migration_session_token = if migration_data.copy_session_token {
let duplicate_session = self
.client
.duplicate_session(&self.state.config, &migration_data.session_token)?;
.duplicate_session_token(&self.state.config, &migration_data.session_token)?;
duplicate_session.session_token
} else {
@ -164,7 +137,7 @@ impl FirefoxAccount {
let k_sync = base64::encode_config(&k_sync, base64::URL_SAFE_NO_PAD);
let k_xcs = hex::decode(&migration_data.k_xcs)?;
let k_xcs = base64::encode_config(&k_xcs, base64::URL_SAFE_NO_PAD);
let scoped_key_data = self.client.scoped_key_data(
let scoped_key_data = self.client.get_scoped_key_data(
&self.state.config,
&migration_session_token,
&self.state.config.client_id,
@ -182,7 +155,7 @@ impl FirefoxAccount {
};
// Trade our session token for a refresh token.
let oauth_response = self.client.refresh_token_with_session_token(
let oauth_response = self.client.create_refresh_token_using_session_token(
&self.state.config,
&migration_session_token,
&[scopes::PROFILE, scopes::OLD_SYNC],
@ -203,7 +176,7 @@ impl FirefoxAccount {
#[cfg(test)]
mod tests {
use super::*;
use crate::{http_client::*, Config};
use crate::internal::{http_client::*, Config};
use std::collections::HashMap;
use std::sync::Arc;
@ -223,7 +196,9 @@ mod tests {
// Initial attempt fails with a server-side failure, which we can retry.
let mut client = FxAClientMock::new();
client
.expect_duplicate_session(mockiato::Argument::any, |arg| arg.partial_eq("session"))
.expect_duplicate_session_token(mockiato::Argument::any, |arg| {
arg.partial_eq("session")
})
.returns_once(Err(ErrorKind::RemoteError {
code: 500,
errno: 999,
@ -250,7 +225,9 @@ mod tests {
// It makes a lot of network requests, so we have a lot to mock!
let mut client = FxAClientMock::new();
client
.expect_duplicate_session(mockiato::Argument::any, |arg| arg.partial_eq("session"))
.expect_duplicate_session_token(mockiato::Argument::any, |arg| {
arg.partial_eq("session")
})
.returns_once(Ok(DuplicateTokenResponse {
uid: "userid".to_string(),
session_token: "dup_session".to_string(),
@ -267,7 +244,7 @@ mod tests {
},
);
client
.expect_scoped_key_data(
.expect_get_scoped_key_data(
mockiato::Argument::any,
|arg| arg.partial_eq("dup_session"),
|arg| arg.partial_eq("12345678"),
@ -275,7 +252,7 @@ mod tests {
)
.returns_once(Ok(key_data));
client
.expect_refresh_token_with_session_token(
.expect_create_refresh_token_using_session_token(
mockiato::Argument::any,
|arg| arg.partial_eq("dup_session"),
|arg| arg.unordered_vec_eq([scopes::PROFILE, scopes::OLD_SYNC].to_vec()),
@ -305,7 +282,9 @@ mod tests {
let mut client = FxAClientMock::new();
client
.expect_duplicate_session(mockiato::Argument::any, |arg| arg.partial_eq("session"))
.expect_duplicate_session_token(mockiato::Argument::any, |arg| {
arg.partial_eq("session")
})
.returns_once(Err(ErrorKind::RemoteError {
code: 400,
errno: 102,
@ -345,7 +324,7 @@ mod tests {
let mut client = FxAClientMock::new();
client
.expect_scoped_key_data(
.expect_get_scoped_key_data(
mockiato::Argument::any,
|arg| arg.partial_eq("session"),
|arg| arg.partial_eq("12345678"),
@ -377,7 +356,7 @@ mod tests {
// to duplicate the sessionToken rather than reusing it).
let mut client = FxAClientMock::new();
client
.expect_scoped_key_data(
.expect_get_scoped_key_data(
mockiato::Argument::any,
|arg| arg.partial_eq("session"),
|arg| arg.partial_eq("12345678"),

Просмотреть файл

@ -0,0 +1,559 @@
/* 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/. */
//! # Internal implementation details for the fxa_client crate.
//!
// Currently public for use by example crates, but should be made private eventually.
pub use self::{commands::IncomingDeviceCommand, config::Config};
use self::{
error::*,
oauth::{AuthCircuitBreaker, OAuthFlow, OAUTH_WEBCHANNEL_REDIRECT},
state_persistence::State,
telemetry::FxaTelemetry,
};
use serde_derive::*;
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
sync::Arc,
};
use url::Url;
#[cfg(feature = "integration_test")]
pub mod auth;
mod commands;
pub mod config;
pub mod device;
pub mod error;
mod http_client;
mod migrator;
mod oauth;
mod profile;
mod push;
mod scoped_keys;
mod scopes;
mod send_tab;
mod state_persistence;
mod telemetry;
mod util;
type FxAClient = dyn http_client::FxAClient + Sync + Send;
// FIXME: https://github.com/myelin-ai/mockiato/issues/106.
#[cfg(test)]
unsafe impl<'a> Send for http_client::FxAClientMock<'a> {}
#[cfg(test)]
unsafe impl<'a> Sync for http_client::FxAClientMock<'a> {}
// It this struct is modified, please check if the
// `FirefoxAccount.start_over` function also needs
// to be modified.
pub struct FirefoxAccount {
client: Arc<FxAClient>,
state: State,
flow_store: HashMap<String, OAuthFlow>,
attached_clients_cache: Option<CachedResponse<Vec<http_client::GetAttachedClientResponse>>>,
devices_cache: Option<CachedResponse<Vec<http_client::GetDeviceResponse>>>,
auth_circuit_breaker: AuthCircuitBreaker,
// 'telemetry' is only currently used by `&mut self` functions, but that's
// not something we want to insist on going forward, so RefCell<> it.
telemetry: RefCell<FxaTelemetry>,
}
impl FirefoxAccount {
fn from_state(state: State) -> Self {
Self {
client: Arc::new(http_client::Client::new()),
state,
flow_store: HashMap::new(),
attached_clients_cache: None,
devices_cache: None,
auth_circuit_breaker: Default::default(),
telemetry: RefCell::new(FxaTelemetry::new()),
}
}
/// Create a new `FirefoxAccount` instance using a `Config`.
///
/// **💾 This method alters the persisted account state.**
pub fn with_config(config: Config) -> Self {
Self::from_state(State {
config,
refresh_token: None,
scoped_keys: HashMap::new(),
last_handled_command: None,
commands_data: HashMap::new(),
device_capabilities: HashSet::new(),
session_token: None,
current_device_id: None,
last_seen_profile: None,
access_token_cache: HashMap::new(),
in_flight_migration: None,
})
}
/// Create a new `FirefoxAccount` instance.
///
/// * `content_url` - The Firefox Account content server URL.
/// * `client_id` - The OAuth `client_id`.
/// * `redirect_uri` - The OAuth `redirect_uri`.
/// * `token_server_url_override` - Override the Token Server URL provided
/// by the FxA's autoconfig endpoint.
///
/// **💾 This method alters the persisted account state.**
pub fn new(
content_url: &str,
client_id: &str,
redirect_uri: &str,
token_server_url_override: Option<&str>,
) -> Self {
let mut config = Config::new(content_url, client_id, redirect_uri);
if let Some(token_server_url_override) = token_server_url_override {
config.override_token_server_url(token_server_url_override.as_ref());
}
Self::with_config(config)
}
#[cfg(test)]
pub(crate) fn set_client(&mut self, client: Arc<FxAClient>) {
self.client = client;
}
/// Restore a `FirefoxAccount` instance from a serialized state
/// created using `to_json`.
pub fn from_json(data: &str) -> Result<Self> {
let state = state_persistence::state_from_json(data)?;
Ok(Self::from_state(state))
}
/// Serialize a `FirefoxAccount` instance internal state
/// to be restored later using `from_json`.
pub fn to_json(&self) -> Result<String> {
state_persistence::state_to_json(&self.state)
}
/// Clear the attached clients and devices cache
pub fn clear_devices_and_attached_clients_cache(&mut self) {
self.attached_clients_cache = None;
self.devices_cache = None;
}
/// Clear the whole persisted/cached state of the account, but keep just
/// enough information to eventually reconnect to the same user account later.
pub fn start_over(&mut self) {
self.state = self.state.start_over();
self.flow_store.clear();
self.clear_devices_and_attached_clients_cache();
self.telemetry.replace(FxaTelemetry::new());
}
/// Get the Sync Token Server endpoint URL.
pub fn get_token_server_endpoint_url(&self) -> Result<String> {
Ok(self.state.config.token_server_endpoint_url()?.into_string())
}
/// Get the pairing URL to navigate to on the Auth side (typically
/// a computer).
pub fn get_pairing_authority_url(&self) -> Result<String> {
// Special case for the production server, we use the shorter firefox.com/pair URL.
if self.state.config.content_url()? == Url::parse(config::CONTENT_URL_RELEASE)? {
return Ok("https://firefox.com/pair".to_owned());
}
// Similarly special case for the China server.
if self.state.config.content_url()? == Url::parse(config::CONTENT_URL_CHINA)? {
return Ok("https://firefox.com.cn/pair".to_owned());
}
Ok(self.state.config.pair_url()?.into_string())
}
/// Get the "connection succeeded" page URL.
/// It is typically used to redirect the user after
/// having intercepted the OAuth login-flow state/code
/// redirection.
pub fn get_connection_success_url(&self) -> Result<String> {
let mut url = self.state.config.connect_another_device_url()?;
url.query_pairs_mut()
.append_pair("showSuccessMessage", "true");
Ok(url.into_string())
}
/// Get the "manage account" page URL.
/// It is typically used in the application's account status UI,
/// to link the user out to a webpage where they can manage
/// all the details of their account.
///
/// * `entrypoint` - Application-provided string identifying the UI touchpoint
/// through which the page was accessed, for metrics purposes.
pub fn get_manage_account_url(&mut self, entrypoint: &str) -> Result<String> {
let mut url = self.state.config.settings_url()?;
url.query_pairs_mut().append_pair("entrypoint", entrypoint);
if self.state.config.redirect_uri == OAUTH_WEBCHANNEL_REDIRECT {
url.query_pairs_mut()
.append_pair("context", "oauth_webchannel_v1");
}
self.add_account_identifiers_to_url(url)
}
/// Get the "manage devices" page URL.
/// It is typically used in the application's account status UI,
/// to link the user out to a webpage where they can manage
/// the devices connected to their account.
///
/// * `entrypoint` - Application-provided string identifying the UI touchpoint
/// through which the page was accessed, for metrics purposes.
pub fn get_manage_devices_url(&mut self, entrypoint: &str) -> Result<String> {
let mut url = self.state.config.settings_clients_url()?;
url.query_pairs_mut().append_pair("entrypoint", entrypoint);
self.add_account_identifiers_to_url(url)
}
fn add_account_identifiers_to_url(&mut self, mut url: Url) -> Result<String> {
let profile = self.get_profile(false)?;
url.query_pairs_mut()
.append_pair("uid", &profile.uid)
.append_pair("email", &profile.email);
Ok(url.into_string())
}
fn get_refresh_token(&self) -> Result<&str> {
match self.state.refresh_token {
Some(ref token_info) => Ok(&token_info.token),
None => Err(ErrorKind::NoRefreshToken.into()),
}
}
/// Disconnect from the account and optionally destroy our device record. This will
/// leave the account object in a state where it can eventually reconnect to the same user.
/// This is a "best effort" infallible method: e.g. if the network is unreachable,
/// the device could still be in the FxA devices manager.
///
/// **💾 This method alters the persisted account state.**
pub fn disconnect(&mut self) {
let current_device_result;
{
current_device_result = self.get_current_device();
}
if let Some(ref refresh_token) = self.state.refresh_token {
// Delete the current device (which deletes the refresh token), or
// the refresh token directly if we don't have a device.
let destroy_result = match current_device_result {
// If we get an error trying to fetch our device record we'll at least
// still try to delete the refresh token itself.
Ok(Some(device)) => self.client.destroy_device_record(
&self.state.config,
&refresh_token.token,
&device.id,
),
_ => self
.client
.destroy_refresh_token(&self.state.config, &refresh_token.token),
};
if let Err(e) = destroy_result {
log::warn!("Error while destroying the device: {}", e);
}
}
self.start_over();
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub(crate) struct CachedResponse<T> {
response: T,
cached_at: u64,
etag: String,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::internal::device::*;
use crate::internal::http_client::FxAClientMock;
use crate::internal::oauth::*;
#[test]
fn test_fxa_is_send() {
fn is_send<T: Send>() {}
is_send::<FirefoxAccount>();
}
#[test]
fn test_serialize_deserialize() {
let config = Config::stable_dev("12345678", "https://foo.bar");
let fxa1 = FirefoxAccount::with_config(config);
let fxa1_json = fxa1.to_json().unwrap();
drop(fxa1);
let fxa2 = FirefoxAccount::from_json(&fxa1_json).unwrap();
let fxa2_json = fxa2.to_json().unwrap();
assert_eq!(fxa1_json, fxa2_json);
}
#[test]
fn test_get_connection_success_url() {
let config = Config::new("https://stable.dev.lcip.org", "12345678", "https://foo.bar");
let fxa = FirefoxAccount::with_config(config);
let url = fxa.get_connection_success_url().unwrap();
assert_eq!(
url,
"https://stable.dev.lcip.org/connect_another_device?showSuccessMessage=true"
.to_string()
);
}
#[test]
fn test_get_manage_account_url() {
let config = Config::new("https://stable.dev.lcip.org", "12345678", "https://foo.bar");
let mut fxa = FirefoxAccount::with_config(config);
// No current user -> Error.
match fxa.get_manage_account_url("test").unwrap_err().kind() {
ErrorKind::NoCachedToken(_) => {}
_ => panic!("error not NoCachedToken"),
};
// With current user -> expected Url.
fxa.add_cached_profile("123", "test@example.com");
let url = fxa.get_manage_account_url("test").unwrap();
assert_eq!(
url,
"https://stable.dev.lcip.org/settings?entrypoint=test&uid=123&email=test%40example.com"
.to_string()
);
}
#[test]
fn test_get_manage_account_url_with_webchannel_redirect() {
let config = Config::new(
"https://stable.dev.lcip.org",
"12345678",
OAUTH_WEBCHANNEL_REDIRECT,
);
let mut fxa = FirefoxAccount::with_config(config);
fxa.add_cached_profile("123", "test@example.com");
let url = fxa.get_manage_account_url("test").unwrap();
assert_eq!(
url,
"https://stable.dev.lcip.org/settings?entrypoint=test&context=oauth_webchannel_v1&uid=123&email=test%40example.com"
.to_string()
);
}
#[test]
fn test_get_manage_devices_url() {
let config = Config::new("https://stable.dev.lcip.org", "12345678", "https://foo.bar");
let mut fxa = FirefoxAccount::with_config(config);
// No current user -> Error.
match fxa.get_manage_devices_url("test").unwrap_err().kind() {
ErrorKind::NoCachedToken(_) => {}
_ => panic!("error not NoCachedToken"),
};
// With current user -> expected Url.
fxa.add_cached_profile("123", "test@example.com");
let url = fxa.get_manage_devices_url("test").unwrap();
assert_eq!(
url,
"https://stable.dev.lcip.org/settings/clients?entrypoint=test&uid=123&email=test%40example.com"
.to_string()
);
}
#[test]
fn test_disconnect_no_refresh_token() {
let config = Config::new("https://stable.dev.lcip.org", "12345678", "https://foo.bar");
let mut fxa = FirefoxAccount::with_config(config);
fxa.add_cached_token(
"profile",
AccessTokenInfo {
scope: "profile".to_string(),
token: "profiletok".to_string(),
key: None,
expires_at: u64::max_value(),
},
);
let client = FxAClientMock::new();
fxa.set_client(Arc::new(client));
assert!(!fxa.state.access_token_cache.is_empty());
fxa.disconnect();
assert!(fxa.state.access_token_cache.is_empty());
}
#[test]
fn test_disconnect_device() {
let config = Config::stable_dev("12345678", "https://foo.bar");
let mut fxa = FirefoxAccount::with_config(config);
fxa.state.refresh_token = Some(RefreshToken {
token: "refreshtok".to_string(),
scopes: HashSet::default(),
});
let mut client = FxAClientMock::new();
client
.expect_get_devices(mockiato::Argument::any, |token| {
token.partial_eq("refreshtok")
})
.times(1)
.returns_once(Ok(vec![
Device {
common: http_client::DeviceResponseCommon {
id: "1234a".to_owned(),
display_name: "My Device".to_owned(),
device_type: http_client::DeviceType::Mobile,
push_subscription: None,
available_commands: HashMap::default(),
push_endpoint_expired: false,
},
is_current_device: true,
location: http_client::DeviceLocation {
city: None,
country: None,
state: None,
state_code: None,
},
last_access_time: None,
},
Device {
common: http_client::DeviceResponseCommon {
id: "a4321".to_owned(),
display_name: "My Other Device".to_owned(),
device_type: http_client::DeviceType::Desktop,
push_subscription: None,
available_commands: HashMap::default(),
push_endpoint_expired: false,
},
is_current_device: false,
location: http_client::DeviceLocation {
city: None,
country: None,
state: None,
state_code: None,
},
last_access_time: None,
},
]));
client
.expect_destroy_device_record(
mockiato::Argument::any,
|token| token.partial_eq("refreshtok"),
|device_id| device_id.partial_eq("1234a"),
)
.times(1)
.returns_once(Ok(()));
fxa.set_client(Arc::new(client));
assert!(fxa.state.refresh_token.is_some());
fxa.disconnect();
assert!(fxa.state.refresh_token.is_none());
}
#[test]
fn test_disconnect_no_device() {
let config = Config::stable_dev("12345678", "https://foo.bar");
let mut fxa = FirefoxAccount::with_config(config);
fxa.state.refresh_token = Some(RefreshToken {
token: "refreshtok".to_string(),
scopes: HashSet::default(),
});
let mut client = FxAClientMock::new();
client
.expect_get_devices(mockiato::Argument::any, |token| {
token.partial_eq("refreshtok")
})
.times(1)
.returns_once(Ok(vec![Device {
common: http_client::DeviceResponseCommon {
id: "a4321".to_owned(),
display_name: "My Other Device".to_owned(),
device_type: http_client::DeviceType::Desktop,
push_subscription: None,
available_commands: HashMap::default(),
push_endpoint_expired: false,
},
is_current_device: false,
location: http_client::DeviceLocation {
city: None,
country: None,
state: None,
state_code: None,
},
last_access_time: None,
}]));
client
.expect_destroy_refresh_token(mockiato::Argument::any, |token| {
token.partial_eq("refreshtok")
})
.times(1)
.returns_once(Ok(()));
fxa.set_client(Arc::new(client));
assert!(fxa.state.refresh_token.is_some());
fxa.disconnect();
assert!(fxa.state.refresh_token.is_none());
}
#[test]
fn test_disconnect_network_errors() {
let config = Config::stable_dev("12345678", "https://foo.bar");
let mut fxa = FirefoxAccount::with_config(config);
fxa.state.refresh_token = Some(RefreshToken {
token: "refreshtok".to_string(),
scopes: HashSet::default(),
});
let mut client = FxAClientMock::new();
client
.expect_get_devices(mockiato::Argument::any, |token| {
token.partial_eq("refreshtok")
})
.times(1)
.returns_once(Ok(vec![]));
client
.expect_destroy_refresh_token(mockiato::Argument::any, |token| {
token.partial_eq("refreshtok")
})
.times(1)
.returns_once(Err(ErrorKind::RemoteError {
code: 500,
errno: 101,
error: "Did not work!".to_owned(),
message: "Did not work!".to_owned(),
info: "Did not work!".to_owned(),
}
.into()));
fxa.set_client(Arc::new(client));
assert!(fxa.state.refresh_token.is_some());
fxa.disconnect();
assert!(fxa.state.refresh_token.is_none());
}
#[test]
fn test_get_pairing_authority_url() {
let config = Config::new("https://foo.bar", "12345678", "https://foo.bar");
let fxa = FirefoxAccount::with_config(config);
assert_eq!(
fxa.get_pairing_authority_url().unwrap().as_str(),
"https://foo.bar/pair"
);
let config = Config::release("12345678", "https://foo.bar");
let fxa = FirefoxAccount::with_config(config);
assert_eq!(
fxa.get_pairing_authority_url().unwrap().as_str(),
"https://firefox.com/pair"
);
let config = Config::china("12345678", "https://foo.bar");
let fxa = FirefoxAccount::with_config(config);
assert_eq!(
fxa.get_pairing_authority_url().unwrap().as_str(),
"https://firefox.com.cn/pair"
)
}
}

Просмотреть файл

@ -3,18 +3,20 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
pub mod attached_clients;
use crate::{
use super::{
error::*,
http_client::{AuthorizationRequestParameters, OAuthTokenResponse},
http_client::{
AuthorizationRequestParameters, IntrospectResponse as IntrospectInfo, OAuthTokenResponse,
},
scoped_keys::{ScopedKey, ScopedKeysFlow},
util, FirefoxAccount,
};
pub use crate::{AuthorizationParameters, MetricsParams};
use jwcrypto::{EncryptionAlgorithm, EncryptionParameters};
use rate_limiter::RateLimiter;
use rc_crypto::digest;
use serde_derive::*;
use std::convert::TryFrom;
use std::convert::{TryFrom, TryInto};
use std::{
collections::{HashMap, HashSet},
iter::FromIterator,
@ -50,7 +52,7 @@ impl FirefoxAccount {
let resp = match self.state.refresh_token {
Some(ref refresh_token) => {
if refresh_token.scopes.contains(scope) {
self.client.access_token_with_refresh_token(
self.client.create_access_token_using_refresh_token(
&self.state.config,
&refresh_token.token,
ttl,
@ -61,7 +63,7 @@ impl FirefoxAccount {
}
}
None => match self.state.session_token {
Some(ref session_token) => self.client.access_token_with_session_token(
Some(ref session_token) => self.client.create_access_token_using_session_token(
&self.state.config,
&session_token,
&[scope],
@ -99,7 +101,7 @@ impl FirefoxAccount {
Some(ref refresh_token) => {
self.auth_circuit_breaker.check()?;
self.client
.oauth_introspect_refresh_token(&self.state.config, &refresh_token.token)?
.check_refresh_token_status(&self.state.config, &refresh_token.token)?
}
None => return Err(ErrorKind::NoRefreshToken.into()),
};
@ -199,7 +201,7 @@ impl FirefoxAccount {
// Validate request to ensure that the client is actually allowed to request
// the scopes they requested
let allowed_scopes = self.client.scoped_key_data(
let allowed_scopes = self.client.get_scoped_key_data(
&self.state.config,
&session_token,
&auth_params.client_id,
@ -250,17 +252,12 @@ impl FirefoxAccount {
scope: auth_params.scope.join(" "),
state: auth_params.state,
access_type: auth_params.access_type,
code_challenge: auth_params
.pkce_params
.as_ref()
.map(|param| param.code_challenge.clone()),
code_challenge_method: auth_params
.pkce_params
.map(|param| param.code_challenge_method),
code_challenge: auth_params.code_challenge,
code_challenge_method: auth_params.code_challenge_method,
keys_jwe,
};
let resp = self.client.authorization_code_using_session_token(
let resp = self.client.create_authorization_code_using_session_token(
&self.state.config,
&session_token,
auth_request_params,
@ -317,9 +314,9 @@ impl FirefoxAccount {
Some(oauth_flow) => oauth_flow,
None => return Err(ErrorKind::UnknownOAuthState.into()),
};
let resp = self.client.refresh_token_with_code(
let resp = self.client.create_refresh_token_using_authorization_code(
&self.state.config,
&code,
code,
&oauth_flow.code_verifier,
)?;
self.handle_oauth_response(resp, oauth_flow.scoped_keys_flow)
@ -420,7 +417,7 @@ impl FirefoxAccount {
.as_ref()
.ok_or(ErrorKind::NoRefreshToken)?;
let scopes: Vec<&str> = old_refresh_token.scopes.iter().map(AsRef::as_ref).collect();
let resp = self.client.refresh_token_with_session_token(
let resp = self.client.create_refresh_token_using_session_token(
&self.state.config,
&session_token,
&scopes,
@ -450,7 +447,7 @@ impl FirefoxAccount {
#[cfg(feature = "integration_test")]
pub fn new_logged_in(
config: crate::Config,
config: super::Config,
session_token: &str,
scoped_keys: HashMap<String, ScopedKey>,
) -> Self {
@ -491,22 +488,6 @@ impl AuthCircuitBreaker {
}
}
#[derive(Clone)]
pub struct AuthorizationPKCEParams {
pub code_challenge: String,
pub code_challenge_method: String,
}
#[derive(Clone)]
pub struct AuthorizationParameters {
pub client_id: String,
pub scope: Vec<String>,
pub state: String,
pub access_type: String,
pub pkce_params: Option<AuthorizationPKCEParams>,
pub keys_jwk: Option<String>,
}
impl TryFrom<Url> for AuthorizationParameters {
type Error = Error;
@ -530,27 +511,18 @@ impl TryFrom<Url> for AuthorizationParameters {
.ok_or(ErrorKind::MissingUrlParameter("access_type"))?;
let code_challenge = query_map.get("code_challenge").cloned();
let code_challenge_method = query_map.get("code_challenge_method").cloned();
let pkce_params = match (code_challenge, code_challenge_method) {
(Some(code_challenge), Some(code_challenge_method)) => Some(AuthorizationPKCEParams {
code_challenge,
code_challenge_method,
}),
_ => None,
};
let keys_jwk = query_map.get("keys_jwk").cloned();
Ok(Self {
client_id,
scope: scope.split_whitespace().map(|s| s.to_string()).collect(),
state,
access_type,
pkce_params,
code_challenge,
code_challenge_method,
keys_jwk,
})
}
}
pub struct MetricsParams {
pub parameters: std::collections::HashMap<String, String>,
}
impl MetricsParams {
fn append_params_to_url(&self, url: &mut Url) {
@ -590,6 +562,18 @@ pub struct AccessTokenInfo {
pub expires_at: u64, // seconds since epoch
}
impl TryFrom<AccessTokenInfo> for crate::AccessTokenInfo {
type Error = Error;
fn try_from(info: AccessTokenInfo) -> Result<Self> {
Ok(crate::AccessTokenInfo {
scope: info.scope,
token: info.token,
key: info.key.map(ScopedKey::into),
expires_at: info.expires_at.try_into()?,
})
}
}
impl std::fmt::Debug for AccessTokenInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AccessTokenInfo")
@ -600,15 +584,16 @@ impl std::fmt::Debug for AccessTokenInfo {
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct IntrospectInfo {
pub active: bool,
impl From<IntrospectInfo> for crate::AuthorizationInfo {
fn from(r: IntrospectInfo) -> Self {
crate::AuthorizationInfo { active: r.active }
}
}
#[cfg(test)]
mod tests {
use super::super::{http_client::*, Config};
use super::*;
use crate::{http_client::*, Config};
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
@ -895,7 +880,7 @@ mod tests {
let mut client = FxAClientMock::new();
client
.expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
.expect_check_refresh_token_status(mockiato::Argument::any, |token| {
token.partial_eq("refresh_token")
})
.times(1)
@ -921,31 +906,31 @@ mod tests {
// This copy-pasta (equivalent to `.returns(..).times(5)`) is there
// because `Error` is not cloneable :/
client
.expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
.expect_check_refresh_token_status(mockiato::Argument::any, |token| {
token.partial_eq("refresh_token")
})
.returns_once(Ok(IntrospectResponse { active: true }));
client
.expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
.expect_check_refresh_token_status(mockiato::Argument::any, |token| {
token.partial_eq("refresh_token")
})
.returns_once(Ok(IntrospectResponse { active: true }));
client
.expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
.expect_check_refresh_token_status(mockiato::Argument::any, |token| {
token.partial_eq("refresh_token")
})
.returns_once(Ok(IntrospectResponse { active: true }));
client
.expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
.expect_check_refresh_token_status(mockiato::Argument::any, |token| {
token.partial_eq("refresh_token")
})
.returns_once(Ok(IntrospectResponse { active: true }));
client
.expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
.expect_check_refresh_token_status(mockiato::Argument::any, |token| {
token.partial_eq("refresh_token")
})
.returns_once(Ok(IntrospectResponse { active: true }));
client.expect_oauth_introspect_refresh_token_calls_in_order();
client.expect_check_refresh_token_status_calls_in_order();
fxa.set_client(Arc::new(client));
for _ in 0..5 {
@ -957,7 +942,7 @@ mod tests {
}
}
use crate::scopes;
use crate::internal::scopes;
#[test]
fn test_auth_code_pair_valid_not_allowed_scope() {
@ -972,7 +957,7 @@ mod tests {
.chain(not_allowed_scope.chars())
.collect::<String>();
client
.expect_scoped_key_data(
.expect_get_scoped_key_data(
mockiato::Argument::any,
|arg| arg.partial_eq("session"),
|arg| arg.partial_eq("12345678"),
@ -992,7 +977,8 @@ mod tests {
scope: vec![scopes::OLD_SYNC.to_string(), not_allowed_scope.to_string()],
state: "somestate".to_string(),
access_type: "offline".to_string(),
pkce_params: None,
code_challenge: None,
code_challenge_method: None,
keys_jwk: None,
};
let res = fxa.authorize_code_using_session_token(auth_params);
@ -1035,7 +1021,7 @@ mod tests {
},
);
client
.expect_scoped_key_data(
.expect_get_scoped_key_data(
mockiato::Argument::any,
|arg| arg.partial_eq("session"),
|arg| arg.partial_eq("12345678"),
@ -1049,7 +1035,8 @@ mod tests {
scope: vec![scopes::OLD_SYNC.to_string(), invalid_scope.to_string()],
state: "somestate".to_string(),
access_type: "offline".to_string(),
pkce_params: None,
code_challenge: None,
code_challenge_method: None,
keys_jwk: None,
};
let res = fxa.authorize_code_using_session_token(auth_params);
@ -1079,7 +1066,7 @@ mod tests {
},
);
client
.expect_scoped_key_data(
.expect_get_scoped_key_data(
mockiato::Argument::any,
|arg| arg.partial_eq("session"),
|arg| arg.partial_eq("12345678"),
@ -1092,7 +1079,8 @@ mod tests {
scope: vec![scopes::OLD_SYNC.to_string()],
state: "somestate".to_string(),
access_type: "offline".to_string(),
pkce_params: None,
code_challenge: None,
code_challenge_method: None,
keys_jwk: Some("IAmAVerySecretKeysJWkInBase64".to_string()),
};
let res = fxa.authorize_code_using_session_token(auth_params);

Просмотреть файл

@ -2,8 +2,10 @@
* 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/. */
pub use crate::http_client::GetAttachedClientResponse as AttachedClient;
use crate::{error::*, util, CachedResponse, FirefoxAccount};
use std::convert::{TryFrom, TryInto};
pub use super::super::http_client::GetAttachedClientResponse as AttachedClient;
use super::super::{error::*, util, CachedResponse, FirefoxAccount};
// An attached clients response is considered fresh for `ATTACHED_CLIENTS_FRESHNESS_THRESHOLD` ms.
const ATTACHED_CLIENTS_FRESHNESS_THRESHOLD: u64 = 60_000; // 1 minute
@ -19,7 +21,7 @@ impl FirefoxAccount {
let session_token = self.get_session_token()?;
let response = self
.client
.attached_clients(&self.state.config, &session_token)?;
.get_attached_clients(&self.state.config, &session_token)?;
self.attached_clients_cache = Some(CachedResponse {
response: response.clone(),
@ -31,10 +33,26 @@ impl FirefoxAccount {
}
}
impl TryFrom<AttachedClient> for crate::AttachedClient {
type Error = Error;
fn try_from(c: AttachedClient) -> Result<Self> {
Ok(crate::AttachedClient {
client_id: c.client_id,
device_id: c.device_id,
device_type: c.device_type.map(Into::into),
is_current_session: c.is_current_session,
name: c.name,
created_time: c.created_time.map(TryInto::try_into).transpose()?,
last_access_time: c.last_access_time.map(TryInto::try_into).transpose()?,
scope: c.scope,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
use crate::internal::{
config::Config,
http_client::{DeviceType, FxAClientMock},
};
@ -48,7 +66,7 @@ mod tests {
let mut client = FxAClientMock::new();
client
.expect_attached_clients(mockiato::Argument::any, |arg| arg.partial_eq("session"))
.expect_get_attached_clients(mockiato::Argument::any, |arg| arg.partial_eq("session"))
.times(1)
.returns_once(Ok(vec![AttachedClient {
client_id: Some("12345678".into()),
@ -92,7 +110,7 @@ mod tests {
let mut client = FxAClientMock::new();
client
.expect_attached_clients(mockiato::Argument::any, |arg| arg.partial_eq("session"))
.expect_get_attached_clients(mockiato::Argument::any, |arg| arg.partial_eq("session"))
.times(1)
.returns_once(Err(ErrorKind::RemoteError {
code: 500,

Просмотреть файл

@ -2,8 +2,8 @@
* 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/. */
pub use crate::http_client::ProfileResponse as Profile;
use crate::{error::*, scopes, util, CachedResponse, FirefoxAccount};
pub use super::http_client::ProfileResponse as Profile;
use super::{error::*, scopes, util, CachedResponse, FirefoxAccount};
// A cached profile response is considered fresh for `PROFILE_FRESHNESS_THRESHOLD` ms.
const PROFILE_FRESHNESS_THRESHOLD: u64 = 120_000; // 2 minutes
@ -47,7 +47,7 @@ impl FirefoxAccount {
let profile_access_token = self.get_access_token(scopes::PROFILE, None)?.token;
match self
.client
.profile(&self.state.config, &profile_access_token, etag)?
.get_profile(&self.state.config, &profile_access_token, etag)?
{
Some(response_and_etag) => {
if let Some(etag) = response_and_etag.etag {
@ -83,7 +83,7 @@ impl FirefoxAccount {
#[cfg(test)]
mod tests {
use super::*;
use crate::{
use crate::internal::{
http_client::*,
oauth::{AccessTokenInfo, RefreshToken},
Config,
@ -123,7 +123,7 @@ mod tests {
let mut client = FxAClientMock::new();
client
.expect_profile(
.expect_get_profile(
mockiato::Argument::any,
|token| token.partial_eq("profiletok"),
mockiato::Argument::any,
@ -169,7 +169,7 @@ mod tests {
let mut client = FxAClientMock::new();
// First call to profile() we fail with 401.
client
.expect_profile(
.expect_get_profile(
mockiato::Argument::any,
|token| token.partial_eq("bad_access_token"),
mockiato::Argument::any,
@ -184,7 +184,7 @@ mod tests {
}.into()));
// Then we'll try to get a new access token.
client
.expect_access_token_with_refresh_token(
.expect_create_access_token_using_refresh_token(
mockiato::Argument::any,
|token| token.partial_eq("refreshtok"),
mockiato::Argument::any,
@ -201,7 +201,7 @@ mod tests {
}));
// Then hooray it works!
client
.expect_profile(
.expect_get_profile(
mockiato::Argument::any,
|token| token.partial_eq("good_profile_token"),
mockiato::Argument::any,

Просмотреть файл

@ -2,8 +2,11 @@
* 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 crate::device::CommandFetchReason;
use crate::{error::*, AccountEvent, FirefoxAccount};
use std::convert::TryInto;
use super::device::CommandFetchReason;
use super::{error::*, FirefoxAccount};
use crate::AccountEvent;
use serde_derive::Deserialize;
impl FirefoxAccount {
@ -33,15 +36,19 @@ impl FirefoxAccount {
match payload {
PushPayload::CommandReceived(CommandReceivedPushPayload { index, .. }) => {
if cfg!(target_os = "ios") {
self.ios_fetch_device_command(index)
.map(|cmd| vec![AccountEvent::IncomingDeviceCommand(Box::new(cmd))])
let cmd = self.ios_fetch_device_command(index)?;
Ok(vec![AccountEvent::CommandReceived {
command: cmd.try_into()?,
}])
} else {
self.poll_device_commands(CommandFetchReason::Push(index))
.map(|cmds| {
cmds.into_iter()
.map(|cmd| AccountEvent::IncomingDeviceCommand(Box::new(cmd)))
.collect()
let cmds = self.poll_device_commands(CommandFetchReason::Push(index))?;
cmds.into_iter()
.map(|command| {
Ok(AccountEvent::CommandReceived {
command: command.try_into()?,
})
})
.collect()
}
}
PushPayload::ProfileUpdated => {
@ -146,9 +153,11 @@ pub struct AccountDestroyedPushPayload {
#[cfg(test)]
mod tests {
use super::*;
use crate::http_client::FxAClientMock;
use crate::http_client::IntrospectResponse;
use crate::CachedResponse;
use crate::internal::http_client::FxAClientMock;
use crate::internal::http_client::IntrospectResponse;
use crate::internal::oauth::RefreshToken;
use crate::internal::CachedResponse;
use crate::internal::Config;
use std::sync::Arc;
#[test]
@ -159,8 +168,10 @@ mod tests {
#[test]
fn test_push_profile_updated() {
let mut fxa =
FirefoxAccount::with_config(crate::Config::stable_dev("12345678", "https://foo.bar"));
let mut fxa = FirefoxAccount::with_config(crate::internal::Config::stable_dev(
"12345678",
"https://foo.bar",
));
fxa.add_cached_profile("123", "test@example.com");
let json = "{\"version\":1,\"command\":\"fxaccounts:profile_updated\"}";
let events = fxa.handle_push_message(json).unwrap();
@ -175,9 +186,9 @@ mod tests {
#[test]
fn test_push_device_disconnected_local() {
let mut fxa =
FirefoxAccount::with_config(crate::Config::stable_dev("12345678", "https://foo.bar"));
FirefoxAccount::with_config(Config::stable_dev("12345678", "https://foo.bar"));
let refresh_token_scopes = std::collections::HashSet::new();
fxa.state.refresh_token = Some(crate::oauth::RefreshToken {
fxa.state.refresh_token = Some(crate::internal::oauth::RefreshToken {
token: "refresh_token".to_owned(),
scopes: refresh_token_scopes,
});
@ -201,17 +212,17 @@ mod tests {
#[test]
fn test_push_password_reset() {
let mut fxa =
FirefoxAccount::with_config(crate::Config::stable_dev("12345678", "https://foo.bar"));
FirefoxAccount::with_config(Config::stable_dev("12345678", "https://foo.bar"));
let mut client = FxAClientMock::new();
client
.expect_oauth_introspect_refresh_token(mockiato::Argument::any, |token| {
.expect_check_refresh_token_status(mockiato::Argument::any, |token| {
token.partial_eq("refresh_token")
})
.times(1)
.returns_once(Ok(IntrospectResponse { active: true }));
fxa.set_client(Arc::new(client));
let refresh_token_scopes = std::collections::HashSet::new();
fxa.state.refresh_token = Some(crate::oauth::RefreshToken {
fxa.state.refresh_token = Some(RefreshToken {
token: "refresh_token".to_owned(),
scopes: refresh_token_scopes,
});
@ -229,8 +240,10 @@ mod tests {
#[test]
fn test_push_device_disconnected_remote() {
let mut fxa =
FirefoxAccount::with_config(crate::Config::stable_dev("12345678", "https://foo.bar"));
let mut fxa = FirefoxAccount::with_config(crate::internal::Config::stable_dev(
"12345678",
"https://foo.bar",
));
let json = "{\"version\":1,\"command\":\"fxaccounts:device_disconnected\",\"data\":{\"id\":\"remote_id\"}}";
let events = fxa.handle_push_message(json).unwrap();
assert_eq!(events.len(), 1);
@ -249,7 +262,7 @@ mod tests {
#[test]
fn test_handle_push_message_ignores_unknown_command() {
let mut fxa =
FirefoxAccount::with_config(crate::Config::stable_dev("12345678", "https://foo.bar"));
FirefoxAccount::with_config(Config::stable_dev("12345678", "https://foo.bar"));
let json = "{\"version\":1,\"command\":\"huh\"}";
let events = fxa.handle_push_message(json).unwrap();
assert!(events.is_empty());
@ -258,7 +271,7 @@ mod tests {
#[test]
fn test_handle_push_message_ignores_unknown_command_with_data() {
let mut fxa =
FirefoxAccount::with_config(crate::Config::stable_dev("12345678", "https://foo.bar"));
FirefoxAccount::with_config(Config::stable_dev("12345678", "https://foo.bar"));
let json = "{\"version\":1,\"command\":\"huh\",\"data\":{\"value\":42}}";
let events = fxa.handle_push_message(json).unwrap();
assert!(events.is_empty());
@ -267,7 +280,7 @@ mod tests {
#[test]
fn test_handle_push_message_errors_on_garbage_data() {
let mut fxa =
FirefoxAccount::with_config(crate::Config::stable_dev("12345678", "https://foo.bar"));
FirefoxAccount::with_config(Config::stable_dev("12345678", "https://foo.bar"));
let json = "{\"wtf\":\"bbq\"}";
fxa.handle_push_message(json).unwrap_err();
}

Просмотреть файл

@ -2,10 +2,11 @@
* 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 crate::{error::*, FirefoxAccount};
use jwcrypto::{self, DecryptionParameters, Jwk};
use rc_crypto::{agreement, agreement::EphemeralKeyPair};
use serde_derive::{Deserialize, Serialize};
use super::{error::*, FirefoxAccount};
pub use crate::ScopedKey;
impl FirefoxAccount {
pub(crate) fn get_scoped_key(&self, scope: &str) -> Result<&ScopedKey> {
@ -16,15 +17,6 @@ impl FirefoxAccount {
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct ScopedKey {
pub kty: String,
pub scope: String,
/// URL Safe Base 64 encoded key.
pub k: String,
pub kid: String,
}
impl ScopedKey {
pub fn key_bytes(&self) -> Result<Vec<u8>> {
Ok(base64::decode_config(&self.k, base64::URL_SAFE_NO_PAD)?)

Просмотреть файл

@ -2,14 +2,17 @@
* 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/. */
pub use crate::commands::send_tab::{SendTabPayload, TabHistoryEntry};
use crate::{
commands::send_tab::{
self, EncryptedSendTabPayload, PrivateSendTabKeys, PublicSendTabKeys, SendTabKeysPayload,
use super::{
commands::{
send_tab::{
self, EncryptedSendTabPayload, PrivateSendTabKeys, PublicSendTabKeys,
SendTabKeysPayload, SendTabPayload,
},
IncomingDeviceCommand,
},
error::*,
http_client::GetDeviceResponse,
scopes, telemetry, FirefoxAccount, IncomingDeviceCommand,
scopes, telemetry, FirefoxAccount,
};
impl FirefoxAccount {
@ -42,8 +45,13 @@ impl FirefoxAccount {
/// telemetry for these cases.
/// This probably requires a new "Tab" struct with the title and url.
/// android-components has SendToAllUseCase(), so this isn't just theoretical.
/// See https://github.com/mozilla/application-services/issues/3402
pub fn send_tab(&mut self, target_device_id: &str, title: &str, url: &str) -> Result<()> {
/// See <https://github.com/mozilla/application-services/issues/3402>
pub fn send_single_tab(
&mut self,
target_device_id: &str,
title: &str,
url: &str,
) -> Result<()> {
let devices = self.get_devices(false)?;
let target = devices
.iter()

Просмотреть файл

@ -2,6 +2,8 @@
* 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/. */
//! Serialization of `FirefoxAccount` state to/from a JSON string.
//!
//! This module implements the ability to serialize a `FirefoxAccount` struct to and from
//! a JSON string. The idea is that calling code will use this to persist the account state
//! to storage.
@ -28,7 +30,7 @@
use serde_derive::*;
use std::collections::{HashMap, HashSet};
use crate::{
use super::{
config::Config,
device::Capability as DeviceCapability,
migrator::MigrationData,
@ -38,15 +40,19 @@ use crate::{
CachedResponse, Result,
};
// These are public API for working with the persisted state.
// These are the public API for working with the persisted state.
pub(crate) type State = StateV2;
/// Parse a `State` from a JSON string, performing migrations if necessary.
///
pub(crate) fn state_from_json(data: &str) -> Result<State> {
let stored_state: PersistedState = serde_json::from_str(data)?;
upgrade_state(stored_state)
}
/// Serialize a `State` to a JSON string.
///
pub(crate) fn state_to_json(state: &State) -> Result<String> {
let state = PersistedState::V2(state.clone());
serde_json::to_string(&state).map_err(Into::into)
@ -59,9 +65,9 @@ fn upgrade_state(in_state: PersistedState) -> Result<State> {
}
}
// `PersistedState` is a tagged container for one of the state versions.
// Serde picks the right `StructVX` to deserialized based on the schema_version tag.
/// `PersistedState` is a tagged container for one of the state versions.
/// Serde picks the right `StructVX` to deserialized based on the schema_version tag.
///
#[derive(Serialize, Deserialize)]
#[serde(tag = "schema_version")]
#[allow(clippy::large_enum_variant)]
@ -71,18 +77,18 @@ enum PersistedState {
V2(StateV2),
}
// `StateV2` is the current state schema. It and its fields all need to be public
// so that they can be used directly elsewhere in the crate.
//
// If you want to modify what gets stored in the state, consider the following:
//
// * Is the change backwards-compatible with previously-serialized data?
// If so then you'll need to tell serde how to fill in a suitable default.
// If not then you'll need to make a new `StateV3` and implement an explicit migration.
//
// * Does the new field need to be modified when the user disconnects from the account?
// If so then you'll need to update `StateV2.start_over` function.
/// `StateV2` is the current state schema. It and its fields all need to be public
/// so that they can be used directly elsewhere in the crate.
///
/// If you want to modify what gets stored in the state, consider the following:
///
/// * Is the change backwards-compatible with previously-serialized data?
/// If so then you'll need to tell serde how to fill in a suitable default.
/// If not then you'll need to make a new `StateV3` and implement an explicit migration.
///
/// * Does the new field need to be modified when the user disconnects from the account?
/// If so then you'll need to update `StateV2.start_over` function.
///
#[derive(Clone, Serialize, Deserialize)]
pub(crate) struct StateV2 {
pub(crate) config: Config,
@ -105,8 +111,12 @@ pub(crate) struct StateV2 {
}
impl StateV2 {
/// Clear the whole persisted state of the account, but keep just enough
/// information to eventually reconnect to the same user account later.
/// Clear (almost all of) the persisted state of the account.
///
/// This method keep just enough information to be able to eventually reconnect
/// to the same user account later. To completely forget the previously-signed-in
/// user, simply discard the persisted data.
///
pub(crate) fn start_over(&self) -> StateV2 {
StateV2 {
config: self.config.clone(),
@ -125,10 +135,11 @@ impl StateV2 {
}
}
// Migration from `StateV1`. There was a lot of changing of structs and renaming of fields,
// but the key change is that we went from supporting multiple active refresh_tokens to
// only supporting a single one.
/// Migration from `StateV1` to `StateV2`.
/// There was a lot of changing of structs and renaming of fields,
/// but the key change is that we went from supporting multiple active
/// refresh_tokens to only supporting a single one.
///
impl From<StateV1> for Result<StateV2> {
fn from(state: StateV1) -> Self {
let mut all_refresh_tokens: Vec<V1AuthInfo> = vec![];
@ -190,15 +201,15 @@ impl From<StateV1> for Result<StateV2> {
}
}
// `StateV1` was a previous state schema.
//
// The below is sufficient to read existing state data serialized in this form, but should not
// be used to create new data using that schema, so it is deliberately private and deliberately
// does not derive(Serialize).
//
// If you find yourself modifying this code, you're almost certainly creating a potential data-migration
// problem and should reconsider.
/// `StateV1` was a previous state schema.
///
/// The below is sufficient to read existing state data serialized in this form, but should not
/// be used to create new data using that schema, so it is deliberately private and deliberately
/// does not derive(Serialize).
///
/// If you find yourself modifying this code, you're almost certainly creating a potential data-migration
/// problem and should reconsider.
///
#[derive(Deserialize)]
struct StateV1 {
client_id: String,

Просмотреть файл

@ -2,7 +2,7 @@
* 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 crate::{error::*, FirefoxAccount};
use super::{error::*, FirefoxAccount};
use serde_derive::*;
use sync_guid::Guid;

Просмотреть файл

@ -2,7 +2,7 @@
* 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 crate::error::*;
use super::error::*;
use rc_crypto::rand;
use std::time::{SystemTime, UNIX_EPOCH};

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1,208 +0,0 @@
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Profile {
#[prost(string, optional, tag="1")]
pub uid: ::std::option::Option<std::string::String>,
#[prost(string, optional, tag="2")]
pub email: ::std::option::Option<std::string::String>,
#[prost(string, optional, tag="3")]
pub avatar: ::std::option::Option<std::string::String>,
#[prost(bool, optional, tag="4")]
pub avatar_default: ::std::option::Option<bool>,
#[prost(string, optional, tag="5")]
pub display_name: ::std::option::Option<std::string::String>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct AccessTokenInfo {
#[prost(string, required, tag="1")]
pub scope: std::string::String,
#[prost(string, required, tag="2")]
pub token: std::string::String,
#[prost(message, optional, tag="3")]
pub key: ::std::option::Option<ScopedKey>,
#[prost(uint64, required, tag="4")]
pub expires_at: u64,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct IntrospectInfo {
#[prost(bool, required, tag="1")]
pub active: bool,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ScopedKey {
#[prost(string, required, tag="1")]
pub kty: std::string::String,
#[prost(string, required, tag="2")]
pub scope: std::string::String,
#[prost(string, required, tag="3")]
pub k: std::string::String,
#[prost(string, required, tag="4")]
pub kid: std::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Device {
#[prost(string, required, tag="1")]
pub id: std::string::String,
#[prost(string, required, tag="2")]
pub display_name: std::string::String,
#[prost(enumeration="device::Type", required, tag="3")]
pub r#type: i32,
#[prost(message, optional, tag="4")]
pub push_subscription: ::std::option::Option<device::PushSubscription>,
#[prost(bool, required, tag="5")]
pub push_endpoint_expired: bool,
#[prost(bool, required, tag="6")]
pub is_current_device: bool,
#[prost(uint64, optional, tag="7")]
pub last_access_time: ::std::option::Option<u64>,
#[prost(enumeration="device::Capability", repeated, packed="false", tag="8")]
pub capabilities: ::std::vec::Vec<i32>,
}
pub mod device {
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PushSubscription {
#[prost(string, required, tag="1")]
pub endpoint: std::string::String,
#[prost(string, required, tag="2")]
pub public_key: std::string::String,
#[prost(string, required, tag="3")]
pub auth_key: std::string::String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum Capability {
SendTab = 1,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum Type {
Desktop = 1,
Mobile = 2,
Tablet = 3,
Vr = 4,
Tv = 5,
Unknown = 6,
}
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Devices {
#[prost(message, repeated, tag="1")]
pub devices: ::std::vec::Vec<Device>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Capabilities {
#[prost(enumeration="device::Capability", repeated, packed="false", tag="1")]
pub capability: ::std::vec::Vec<i32>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct IncomingDeviceCommand {
#[prost(enumeration="incoming_device_command::IncomingDeviceCommandType", required, tag="1")]
pub r#type: i32,
#[prost(oneof="incoming_device_command::Data", tags="2")]
pub data: ::std::option::Option<incoming_device_command::Data>,
}
pub mod incoming_device_command {
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SendTabData {
#[prost(message, optional, tag="1")]
pub from: ::std::option::Option<super::Device>,
#[prost(message, repeated, tag="2")]
pub entries: ::std::vec::Vec<send_tab_data::TabHistoryEntry>,
}
pub mod send_tab_data {
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TabHistoryEntry {
#[prost(string, required, tag="1")]
pub title: std::string::String,
#[prost(string, required, tag="2")]
pub url: std::string::String,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum IncomingDeviceCommandType {
/// `data` set to `tab_received_data`.
TabReceived = 1,
}
#[derive(Clone, PartialEq, ::prost::Oneof)]
pub enum Data {
#[prost(message, tag="2")]
TabReceivedData(SendTabData),
}
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct IncomingDeviceCommands {
#[prost(message, repeated, tag="1")]
pub commands: ::std::vec::Vec<IncomingDeviceCommand>,
}
/// This is basically an enum with associated values,
/// but it's a bit harder to model in proto2.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct AccountEvent {
#[prost(enumeration="account_event::AccountEventType", required, tag="1")]
pub r#type: i32,
#[prost(oneof="account_event::Data", tags="2, 3, 4")]
pub data: ::std::option::Option<account_event::Data>,
}
pub mod account_event {
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct DeviceDisconnectedData {
#[prost(string, required, tag="1")]
pub device_id: std::string::String,
#[prost(bool, required, tag="2")]
pub is_local_device: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum AccountEventType {
/// `data` set to `device_command`.
IncomingDeviceCommand = 1,
ProfileUpdated = 2,
/// `data` set to `device_connected_name`.
DeviceConnected = 3,
AccountAuthStateChanged = 4,
/// `data` set to `device_disconnected_data`.
DeviceDisconnected = 5,
AccountDestroyed = 6,
}
#[derive(Clone, PartialEq, ::prost::Oneof)]
pub enum Data {
#[prost(message, tag="2")]
DeviceCommand(super::IncomingDeviceCommand),
#[prost(string, tag="3")]
DeviceConnectedName(std::string::String),
#[prost(message, tag="4")]
DeviceDisconnectedData(DeviceDisconnectedData),
}
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct AccountEvents {
#[prost(message, repeated, tag="1")]
pub events: ::std::vec::Vec<AccountEvent>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct AuthorizationPkceParams {
#[prost(string, required, tag="1")]
pub code_challenge: std::string::String,
#[prost(string, required, tag="2")]
pub code_challenge_method: std::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct AuthorizationParams {
#[prost(string, required, tag="1")]
pub client_id: std::string::String,
#[prost(string, required, tag="2")]
pub scope: std::string::String,
#[prost(string, required, tag="3")]
pub state: std::string::String,
#[prost(string, required, tag="4")]
pub access_type: std::string::String,
#[prost(message, optional, tag="5")]
pub pkce_params: ::std::option::Option<AuthorizationPkceParams>,
#[prost(string, optional, tag="6")]
pub keys_jwk: ::std::option::Option<std::string::String>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct MetricsParams {
#[prost(map="string, string", tag="1")]
pub parameters: ::std::collections::HashMap<std::string::String, std::string::String>,
}

Просмотреть файл

@ -0,0 +1,3 @@
[bindings.kotlin]
package_name = "mozilla.appservices.fxaclient"
cdylib_name = "megazord"

Просмотреть файл

@ -2,19 +2,24 @@
* 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/. */
// Utilities for command-line utilities which want to use fxa credentials.
use crate::prompt::prompt_string;
use fxa_client::{error, AccessTokenInfo, Config, FirefoxAccount};
use std::collections::HashMap;
/// Utilities for command-line utilities which want to use fxa credentials.
use std::{
collections::HashMap,
convert::TryInto,
fs,
io::{Read, Write},
};
use sync15::{KeyBundle, Sync15StorageClientInit};
use url::Url;
use anyhow::Result;
use url::Url;
// This crate awkardly uses some internal implementation details of the fxa-client crate,
// because we haven't worked on exposing those test-only features via UniFFI.
use fxa_client::internal::{config::Config, error, FirefoxAccount};
use fxa_client::AccessTokenInfo;
use sync15::{KeyBundle, Sync15StorageClientInit};
use crate::prompt::prompt_string;
// Defaults - not clear they are the best option, but they are a currently
// working option.
@ -60,7 +65,11 @@ fn create_fxa_creds(path: &str, cfg: Config) -> Result<FirefoxAccount> {
acct.complete_oauth_flow(&query_params["code"], &query_params["state"])?;
// Device registration.
acct.initialize_device("CLI Device", fxa_client::device::Type::Desktop, &[])?;
acct.initialize_device(
"CLI Device",
fxa_client::internal::device::Type::Desktop,
&[],
)?;
let mut file = fs::File::create(path)?;
write!(file, "{}", acct.to_json()?)?;
file.flush()?;
@ -82,7 +91,7 @@ fn get_account_and_token(
let mut acct = load_or_create_fxa_creds(cred_file, config.clone())?;
// `scope` could be a param, but I can't see it changing.
match acct.get_access_token(SYNC_SCOPE, None) {
Ok(t) => Ok((acct, t)),
Ok(t) => Ok((acct, t.try_into()?)),
Err(e) => {
match e.kind() {
// We can retry an auth error.
@ -90,7 +99,7 @@ fn get_account_and_token(
println!("Saw an auth error using stored credentials - recreating them...");
acct = create_fxa_creds(cred_file, config)?;
let token = acct.get_access_token(SYNC_SCOPE, None)?;
Ok((acct, token))
Ok((acct, token.try_into()?))
}
_ => Err(e.into()),
}

Просмотреть файл

@ -4,7 +4,7 @@
use cli_support::prompt::prompt_string;
use dialoguer::Select;
use fxa_client::{device, Config, FirefoxAccount, IncomingDeviceCommand};
use fxa_client::internal::{device, Config, FirefoxAccount, IncomingDeviceCommand};
use std::{
collections::HashMap,
fs,
@ -147,7 +147,7 @@ fn main() -> Result<()> {
let url: String = prompt_string("URL").unwrap();
acct.lock()
.unwrap()
.send_tab(&target.id, &title, &url)
.send_single_tab(&target.id, &title, &url)
.unwrap();
println!("Tab sent!");
}

Просмотреть файл

@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use cli_support::prompt::prompt_string;
use fxa_client::{Config, FirefoxAccount};
use fxa_client::internal::{Config, FirefoxAccount};
use std::{thread, time};
static CLIENT_ID: &str = "3c49430b43dfba77";

Просмотреть файл

@ -2,7 +2,7 @@
* 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 cli_support::prompt::prompt_string;
use fxa_client::{Config, FirefoxAccount};
use fxa_client::internal::{Config, FirefoxAccount};
use std::collections::HashMap;
use url::Url;

Просмотреть файл

@ -9,7 +9,7 @@ license = "MPL-2.0"
crate-type = ["cdylib"]
[dependencies]
fxaclient_ffi = { path = "../../components/fxa-client/ffi" }
fxa-client = { path = "../../components/fxa-client" }
logins_ffi = { path = "../../components/logins/ffi" }
places-ffi = { path = "../../components/places/ffi" }
push-ffi = { path = "../../components/push/ffi" }

Просмотреть файл

@ -37,7 +37,8 @@ The following text applies to code linked from these dependencies:
[jexl-parser](https://github.com/mozilla/jexl-rs),
[uniffi](https://github.com/mozilla/uniffi-rs),
[uniffi_bindgen](https://github.com/mozilla/uniffi-rs),
[uniffi_build](https://github.com/mozilla/uniffi-rs)
[uniffi_build](https://github.com/mozilla/uniffi-rs),
[uniffi_macros](https://github.com/mozilla/uniffi-rs)
```
Mozilla Public License Version 2.0
@ -439,6 +440,7 @@ The following text applies to code linked from these dependencies:
[fallible-streaming-iterator](https://github.com/sfackler/fallible-streaming-iterator),
[ffi-support](https://github.com/mozilla/application-services),
[getrandom](https://github.com/rust-random/getrandom),
[glob](https://github.com/rust-lang/glob),
[hashbrown](https://github.com/rust-lang/hashbrown),
[hashlink](https://github.com/kyren/hashlink),
[heck](https://github.com/withoutboats/heck),

Просмотреть файл

@ -40,6 +40,10 @@ the details of which are reproduced below.
<name>Mozilla Public License 2.0: uniffi_build</name>
<url>https://github.com/mozilla/uniffi-rs/blob/main/LICENSE</url>
</license>
<license>
<name>Mozilla Public License 2.0: uniffi_macros</name>
<url>https://github.com/mozilla/uniffi-rs/blob/main/LICENSE</url>
</license>
<license>
<name>Apache License 2.0: ahash</name>
<url>https://github.com/tkaitchuck/ahash/blob/master/LICENSE-APACHE</url>
@ -124,6 +128,10 @@ the details of which are reproduced below.
<name>Apache License 2.0: getrandom</name>
<url>https://github.com/rust-random/getrandom/blob/master/LICENSE-APACHE</url>
</license>
<license>
<name>Apache License 2.0: glob</name>
<url>https://github.com/rust-lang/glob/blob/master/LICENSE-APACHE</url>
</license>
<license>
<name>Apache License 2.0: hashbrown</name>
<url>https://github.com/rust-lang/hashbrown/blob/master/LICENSE-APACHE</url>

Просмотреть файл

@ -9,7 +9,7 @@ use std::ffi::CString;
use std::os::raw::c_char;
pub use autofill;
pub use fxaclient_ffi;
pub use fxa_client;
pub use logins_ffi;
pub use nimbus;
pub use places_ffi;

Просмотреть файл

@ -11,7 +11,9 @@ the details of which are reproduced below.
* [MIT License: atty](#mit-license-atty)
* [MIT License: bincode](#mit-license-bincode)
* [MIT License: bytes](#mit-license-bytes)
* [MIT License: cargo_metadata](#mit-license-cargo_metadata)
* [MIT License: caseless](#mit-license-caseless)
* [MIT License: clap](#mit-license-clap)
* [MIT License: dashmap](#mit-license-dashmap)
* [MIT License: h2](#mit-license-h2)
* [MIT License: http-body](#mit-license-http-body)
@ -21,13 +23,16 @@ the details of which are reproduced below.
* [MIT License: mime_guess](#mit-license-mime_guess)
* [MIT License: miniz_oxide](#mit-license-miniz_oxide)
* [MIT License: mio](#mit-license-mio)
* [MIT License: nom](#mit-license-nom)
* [MIT License: ordered-float](#mit-license-ordered-float)
* [MIT License: oslog](#mit-license-oslog)
* [MIT License: slab](#mit-license-slab)
* [MIT License: textwrap](#mit-license-textwrap)
* [MIT License: tokio, tokio-tls, tokio-util, tracing, tracing-core, tracing-futures](#mit-license-tokio-tokio-tls-tokio-util-tracing-tracing-core-tracing-futures)
* [MIT License: tower-service](#mit-license-tower-service)
* [MIT License: try-lock](#mit-license-try-lock)
* [MIT License: want](#mit-license-want)
* [MIT License: weedle](#mit-license-weedle)
* [CC0-1.0 License: base16](#cc0-10-license-base16)
* [ISC License: ring](#isc-license-ring)
* [BSD-2-Clause License: arrayref](#bsd-2-clause-license-arrayref)
@ -40,7 +45,11 @@ The following text applies to code linked from these dependencies:
[NSPR](https://hg.mozilla.org/projects/nspr),
[NSS](https://hg.mozilla.org/projects/nss),
[ece](https://github.com/mozilla/rust-ece),
[hawk](https://github.com/taskcluster/rust-hawk)
[hawk](https://github.com/taskcluster/rust-hawk),
[uniffi](https://github.com/mozilla/uniffi-rs),
[uniffi_bindgen](https://github.com/mozilla/uniffi-rs),
[uniffi_build](https://github.com/mozilla/uniffi-rs),
[uniffi_macros](https://github.com/mozilla/uniffi-rs)
```
Mozilla Public License Version 2.0
@ -425,6 +434,10 @@ The following text applies to code linked from these dependencies:
[adler](https://github.com/jonas-schievink/adler.git),
[ahash](https://github.com/tkaitchuck/ahash),
[anyhow](https://github.com/dtolnay/anyhow),
[askama](https://github.com/djc/askama),
[askama_derive](https://github.com/djc/askama),
[askama_escape](https://github.com/djc/askama),
[askama_shared](https://github.com/djc/askama),
[autocfg](https://github.com/cuviper/autocfg),
[base64](https://github.com/marshallpierce/rust-base64),
[bitflags](https://github.com/bitflags/bitflags),
@ -453,8 +466,10 @@ The following text applies to code linked from these dependencies:
[futures-task](https://github.com/rust-lang/futures-rs),
[futures-util](https://github.com/rust-lang/futures-rs),
[getrandom](https://github.com/rust-random/getrandom),
[glob](https://github.com/rust-lang/glob),
[hashbrown](https://github.com/rust-lang/hashbrown),
[hashlink](https://github.com/kyren/hashlink),
[heck](https://github.com/withoutboats/heck),
[hex](https://github.com/KokaKiwi/rust-hex),
[http](https://github.com/hyperium/http),
[httparse](https://github.com/seanmonstar/httparse),
@ -506,6 +521,8 @@ The following text applies to code linked from these dependencies:
[ryu](https://github.com/dtolnay/ryu),
[security-framework-sys](https://github.com/kornelski/rust-security-framework),
[security-framework](https://github.com/kornelski/rust-security-framework),
[semver-parser](https://github.com/steveklabnik/semver-parser),
[semver](https://github.com/steveklabnik/semver),
[serde](https://github.com/serde-rs/serde),
[serde_derive](https://github.com/serde-rs/serde),
[serde_json](https://github.com/serde-rs/json),
@ -513,6 +530,7 @@ The following text applies to code linked from these dependencies:
[smallbitvec](https://github.com/servo/smallbitvec),
[smallvec](https://github.com/servo/rust-smallvec),
[socket2](https://github.com/alexcrichton/socket2-rs),
[static_assertions](https://github.com/nvzqz/static-assertions-rs),
[swift-protobuf](https://github.com/apple/swift-protobuf),
[syn](https://github.com/dtolnay/syn),
[tempfile](https://github.com/Stebalien/tempfile),
@ -521,9 +539,12 @@ The following text applies to code linked from these dependencies:
[thread_local](https://github.com/Amanieu/thread_local-rs),
[time](https://github.com/time-rs/time),
[tinyvec](https://github.com/Lokathor/tinyvec),
[toml](https://github.com/alexcrichton/toml-rs),
[unicase](https://github.com/seanmonstar/unicase),
[unicode-bidi](https://github.com/servo/unicode-bidi),
[unicode-normalization](https://github.com/unicode-rs/unicode-normalization),
[unicode-segmentation](https://github.com/unicode-rs/unicode-segmentation),
[unicode-width](https://github.com/unicode-rs/unicode-width),
[unicode-xid](https://github.com/unicode-rs/unicode-xid),
[url](https://github.com/servo/rust-url),
[uuid](https://github.com/uuid-rs/uuid),
@ -889,6 +910,38 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
```
-------------
## MIT License: cargo_metadata
The following text applies to code linked from these dependencies:
[cargo_metadata](https://github.com/oli-obk/cargo_metadata)
```
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
```
-------------
## MIT License: caseless
@ -920,6 +973,36 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
-------------
## MIT License: clap
The following text applies to code linked from these dependencies:
[clap](https://github.com/clap-rs/clap)
```
The MIT License (MIT)
Copyright (c) 2015-2016 Kevin B. Knapp
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
-------------
## MIT License: dashmap
@ -1199,6 +1282,35 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
```
-------------
## MIT License: nom
The following text applies to code linked from these dependencies:
[nom](https://github.com/Geal/nom)
```
Copyright (c) 2014-2019 Geoffroy Couprie
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
-------------
## MIT License: ordered-float
@ -1297,6 +1409,36 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
```
-------------
## MIT License: textwrap
The following text applies to code linked from these dependencies:
[textwrap](https://github.com/mgeisler/textwrap)
```
MIT License
Copyright (c) 2016 Martin Geisler
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
-------------
## MIT License: tokio, tokio-tls, tokio-util, tracing, tracing-core, tracing-futures
@ -1429,6 +1571,29 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
```
-------------
## MIT License: weedle
The following text applies to code linked from these dependencies:
[weedle](https://github.com/rustwasm/weedle)
```
Copyright 2018-Present Sharad Chand
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
```
-------------
## CC0-1.0 License: base16

Просмотреть файл

@ -7,7 +7,7 @@
FOUNDATION_EXPORT double MegazordClientVersionNumber;
FOUNDATION_EXPORT const unsigned char MegazordClientVersionString[];
#import "RustFxAFFI.h"
#import "uniffi_fxa_client-Bridging-Header.h"
#import "RustPasswordAPI.h"
#import "RustLogFFI.h"
#import "RustPlacesAPI.h"

Просмотреть файл

@ -9,7 +9,8 @@
/* Begin PBXBuildFile section */
1379D0692404BE1300AABD16 /* logins_msg_types.proto in Sources */ = {isa = PBXBuildFile; fileRef = 1379D0682404BE1300AABD16 /* logins_msg_types.proto */; };
137C5893240257340016932F /* Data+LoginsRustBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 137C5892240257340016932F /* Data+LoginsRustBuffer.swift */; };
78B6532724B39B2400ED1555 /* FxAccountMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B6532624B39B2400ED1555 /* FxAccountMetrics.swift */; };
99FAA19B25E61D5D001E2231 /* uniffi_fxa_client-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 99FAA19A25E61D5D001E2231 /* uniffi_fxa_client-Bridging-Header.h */; settings = {ATTRIBUTES = (Public, ); }; };
99FAA19F25E65CA5001E2231 /* FxAccountOAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99FAA19E25E65CA5001E2231 /* FxAccountOAuth.swift */; };
BF1A879025064A4C00FED88E /* Dispatchers.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1A878C25064A4C00FED88E /* Dispatchers.swift */; };
BF1A879125064A4C00FED88E /* Glean.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1A878D25064A4C00FED88E /* Glean.swift */; };
BF1A879225064A4C00FED88E /* GleanFfi.h in Headers */ = {isa = PBXBuildFile; fileRef = BF1A878E25064A4C00FED88E /* GleanFfi.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -53,9 +54,6 @@
C852EED6220A29FE00A6E79A /* LoginsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C852EECD220A29FE00A6E79A /* LoginsStorage.swift */; };
C852EED7220A29FE00A6E79A /* LoginStoreError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C852EECF220A29FE00A6E79A /* LoginStoreError.swift */; };
C852EED8220A29FE00A6E79A /* LockError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C852EED0220A29FE00A6E79A /* LockError.swift */; };
C852EEE7220A2A2B00A6E79A /* FirefoxAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = C852EEDD220A2A2B00A6E79A /* FirefoxAccount.swift */; };
C852EEE8220A2A2B00A6E79A /* String+Free_FxAClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = C852EEDF220A2A2B00A6E79A /* String+Free_FxAClient.swift */; };
C852EEEB220A2A2B00A6E79A /* FxAError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C852EEE3220A2A2B00A6E79A /* FxAError.swift */; };
C852EEEF220A2E9400A6E79A /* libmegazord_ios.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C852EEEE220A2E9400A6E79A /* libmegazord_ios.a */; };
CD02CF3922568BCC00124DA2 /* SyncUnlockInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD02CF3822568BCC00124DA2 /* SyncUnlockInfo.swift */; };
CD440A422238A08E003F004B /* SwiftProtobuf.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB7DE84C2214D30B00E7CF17 /* SwiftProtobuf.framework */; };
@ -67,15 +65,14 @@
CD85A45822361E890099BFA9 /* Data+PlacesRustBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD85A44E22361E880099BFA9 /* Data+PlacesRustBuffer.swift */; };
CD85A45922361E890099BFA9 /* Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD85A44F22361E880099BFA9 /* Bookmark.swift */; };
CD85A45A22361E890099BFA9 /* PlacesError.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD85A45122361E880099BFA9 /* PlacesError.swift */; };
CDC0089D2236CAB900893800 /* fxa_msg_types.proto in Sources */ = {isa = PBXBuildFile; fileRef = CDC0089C2236CAB900893800 /* fxa_msg_types.proto */; };
CDC0089F2236CAD100893800 /* places_msg_types.proto in Sources */ = {isa = PBXBuildFile; fileRef = CDC0089E2236CAD100893800 /* places_msg_types.proto */; };
CDC21B14221DCE3700AA71E5 /* RustLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC21B12221DCE3700AA71E5 /* RustLog.swift */; };
CDC21B15221DCE3700AA71E5 /* RustLogFFI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC21B13221DCE3700AA71E5 /* RustLogFFI.h */; settings = {ATTRIBUTES = (Public, ); }; };
CE0A9AB424E4A2CC00914A16 /* fxa_client.udl in Sources */ = {isa = PBXBuildFile; fileRef = CE0A9AB224E4A25C00914A16 /* fxa_client.udl */; };
CE13F19B23F330DF005187A7 /* FxAccountMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE13F19A23F330DF005187A7 /* FxAccountMigration.swift */; };
CE13F19D23F448D0005187A7 /* FxAccountState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE13F19C23F448D0005187A7 /* FxAccountState.swift */; };
CE1445AC23D6058B00B1E808 /* FxAccountConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1445AB23D6058B00B1E808 /* FxAccountConfig.swift */; };
CE1445AF23D6315200B1E808 /* FxAccountLogging.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1445AE23D6315200B1E808 /* FxAccountLogging.swift */; };
CE1ADA9722249FDA00E89714 /* Data+RustBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1ADA9622249FDA00E89714 /* Data+RustBuffer.swift */; };
CE1B09A3231863D7006226E1 /* KeychainWrapper+.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1B09A2231863D7006226E1 /* KeychainWrapper+.swift */; };
CE1B09A5231865BC006226E1 /* FxAccountStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1B09A4231865BC006226E1 /* FxAccountStorage.swift */; };
CE2D04C5231822AC00AF5722 /* FxAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D04C4231822AC00AF5722 /* FxAccountManager.swift */; };
@ -86,15 +83,10 @@
CE96459C23D7A7D500B662F8 /* SwiftKeychainWrapper.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE96459823D7A77500B662F8 /* SwiftKeychainWrapper.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CE9FFA13242D4E7B0011029E /* Viaduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9FFA12242D4E7B0011029E /* Viaduct.swift */; };
CEB1A06823D8A42D005BD4DD /* FxAccountMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB1A06723D8A42D005BD4DD /* FxAccountMocks.swift */; };
CED443CB23CCF385007E5FA6 /* FxAccountDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED443CA23CCF385007E5FA6 /* FxAccountDevice.swift */; };
CED443CD23CD13E5007E5FA6 /* FxAccountDeviceConstellation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED443CC23CD13E5007E5FA6 /* FxAccountDeviceConstellation.swift */; };
CEDDBC8C23DB438F00CFF5AA /* RustFxAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDDBC8B23DB438F00CFF5AA /* RustFxAccount.swift */; };
CEDDBC8E23DB4CD600CFF5AA /* FxAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDDBC8D23DB4CD600CFF5AA /* FxAccount.swift */; };
CEDDBC9023DB4F7400CFF5AA /* FxAccountOAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDDBC8F23DB4F7400CFF5AA /* FxAccountOAuth.swift */; };
CEDDBC9223DB4F9A00CFF5AA /* FxAccountProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDDBC9123DB4F9A00CFF5AA /* FxAccountProfile.swift */; };
CEDDBC8E23DB4CD600CFF5AA /* PersistedFirefoxAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDDBC8D23DB4CD600CFF5AA /* PersistedFirefoxAccount.swift */; };
CEFB1EB022EF708B0001E20F /* ResultError.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFB1EAF22EF708B0001E20F /* ResultError.swift */; };
D05434A1225680D900FDE4EF /* MozillaAppServices.h in Headers */ = {isa = PBXBuildFile; fileRef = C852EEF2220A3C6800A6E79A /* MozillaAppServices.h */; settings = {ATTRIBUTES = (Public, ); }; };
D05434A2225680F400FDE4EF /* RustFxAFFI.h in Headers */ = {isa = PBXBuildFile; fileRef = C852EEE0220A2A2B00A6E79A /* RustFxAFFI.h */; settings = {ATTRIBUTES = (Public, ); }; };
D05434A32256810200FDE4EF /* RustPasswordAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = C852EEC8220A29FE00A6E79A /* RustPasswordAPI.h */; settings = {ATTRIBUTES = (Public, ); }; };
EB7DE84D2214D30B00E7CF17 /* SwiftProtobuf.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB7DE84C2214D30B00E7CF17 /* SwiftProtobuf.framework */; };
EB879D7F221234EB00753DC9 /* MozillaAppServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D202020914D0D00F1C8FA /* MozillaAppServices.framework */; };
@ -102,6 +94,20 @@
/* End PBXBuildFile section */
/* Begin PBXBuildRule section */
CE0617CE24E4978C0068E903 /* PBXBuildRule */ = {
isa = PBXBuildRule;
compilerSpec = com.apple.compilers.proxy.script;
filePatterns = "*.udl";
fileType = pattern.proxy;
inputFiles = (
);
isEditable = 1;
outputFiles = (
"$(INPUT_FILE_DIR)/../ios/Generated/$(INPUT_FILE_BASE).swift",
"$(INPUT_FILE_DIR)/../ios/Generated/uniffi_$(INPUT_FILE_BASE)-Bridging-Header.h",
);
script = "$HOME/.cargo/bin/cargo uniffi-bindgen generate $INPUT_FILE_PATH --language swift --out-dir $INPUT_FILE_DIR/../ios/Generated\n";
};
EB7DE84A2214D28C00E7CF17 /* PBXBuildRule */ = {
isa = PBXBuildRule;
compilerSpec = com.apple.compilers.proxy.script;
@ -144,7 +150,9 @@
/* Begin PBXFileReference section */
1379D0682404BE1300AABD16 /* logins_msg_types.proto */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.protobuf; name = logins_msg_types.proto; path = ../../src/logins_msg_types.proto; sourceTree = "<group>"; };
137C5892240257340016932F /* Data+LoginsRustBuffer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Data+LoginsRustBuffer.swift"; path = "Extensions/Data+LoginsRustBuffer.swift"; sourceTree = "<group>"; };
78B6532624B39B2400ED1555 /* FxAccountMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountMetrics.swift; sourceTree = "<group>"; };
99FAA19125E60D57001E2231 /* fxa_client.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = fxa_client.swift; path = ../../Generated/fxa_client.swift; sourceTree = "<group>"; };
99FAA19A25E61D5D001E2231 /* uniffi_fxa_client-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "uniffi_fxa_client-Bridging-Header.h"; path = "../../Generated/uniffi_fxa_client-Bridging-Header.h"; sourceTree = "<group>"; };
99FAA19E25E65CA5001E2231 /* FxAccountOAuth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FxAccountOAuth.swift; sourceTree = "<group>"; };
BF1A878C25064A4C00FED88E /* Dispatchers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dispatchers.swift; sourceTree = "<group>"; };
BF1A878D25064A4C00FED88E /* Glean.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Glean.swift; sourceTree = "<group>"; };
BF1A878E25064A4C00FED88E /* GleanFfi.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GleanFfi.h; sourceTree = "<group>"; };
@ -190,10 +198,6 @@
C852EECD220A29FE00A6E79A /* LoginsStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginsStorage.swift; sourceTree = "<group>"; };
C852EECF220A29FE00A6E79A /* LoginStoreError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginStoreError.swift; sourceTree = "<group>"; };
C852EED0220A29FE00A6E79A /* LockError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockError.swift; sourceTree = "<group>"; };
C852EEDD220A2A2B00A6E79A /* FirefoxAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirefoxAccount.swift; sourceTree = "<group>"; };
C852EEDF220A2A2B00A6E79A /* String+Free_FxAClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Free_FxAClient.swift"; sourceTree = "<group>"; };
C852EEE0220A2A2B00A6E79A /* RustFxAFFI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RustFxAFFI.h; sourceTree = "<group>"; };
C852EEE3220A2A2B00A6E79A /* FxAError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FxAError.swift; sourceTree = "<group>"; };
C852EEEE220A2E9400A6E79A /* libmegazord_ios.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libmegazord_ios.a; path = ../../target/universal/release/libmegazord_ios.a; sourceTree = "<group>"; };
C852EEF2220A3C6800A6E79A /* MozillaAppServices.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MozillaAppServices.h; sourceTree = "<group>"; };
CD02CF3822568BCC00124DA2 /* SyncUnlockInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncUnlockInfo.swift; sourceTree = "<group>"; };
@ -204,15 +208,14 @@
CD85A44E22361E880099BFA9 /* Data+PlacesRustBuffer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+PlacesRustBuffer.swift"; sourceTree = "<group>"; };
CD85A44F22361E880099BFA9 /* Bookmark.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bookmark.swift; sourceTree = "<group>"; };
CD85A45122361E880099BFA9 /* PlacesError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlacesError.swift; sourceTree = "<group>"; };
CDC0089C2236CAB900893800 /* fxa_msg_types.proto */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.protobuf; name = fxa_msg_types.proto; path = ../../src/fxa_msg_types.proto; sourceTree = "<group>"; };
CDC0089E2236CAD100893800 /* places_msg_types.proto */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.protobuf; name = places_msg_types.proto; path = ../../src/places_msg_types.proto; sourceTree = "<group>"; };
CDC21B12221DCE3700AA71E5 /* RustLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RustLog.swift; sourceTree = "<group>"; };
CDC21B13221DCE3700AA71E5 /* RustLogFFI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RustLogFFI.h; sourceTree = "<group>"; };
CE0A9AB224E4A25C00914A16 /* fxa_client.udl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = fxa_client.udl; path = ../../src/fxa_client.udl; sourceTree = "<group>"; };
CE13F19A23F330DF005187A7 /* FxAccountMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountMigration.swift; sourceTree = "<group>"; };
CE13F19C23F448D0005187A7 /* FxAccountState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountState.swift; sourceTree = "<group>"; };
CE1445AB23D6058B00B1E808 /* FxAccountConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountConfig.swift; sourceTree = "<group>"; };
CE1445AE23D6315200B1E808 /* FxAccountLogging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountLogging.swift; sourceTree = "<group>"; };
CE1ADA9622249FDA00E89714 /* Data+RustBuffer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+RustBuffer.swift"; sourceTree = "<group>"; };
CE1B09A2231863D7006226E1 /* KeychainWrapper+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeychainWrapper+.swift"; sourceTree = "<group>"; };
CE1B09A4231865BC006226E1 /* FxAccountStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountStorage.swift; sourceTree = "<group>"; };
CE2D04C4231822AC00AF5722 /* FxAccountManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FxAccountManager.swift; sourceTree = "<group>"; };
@ -223,12 +226,8 @@
CE9D202020914D0D00F1C8FA /* MozillaAppServices.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MozillaAppServices.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CE9FFA12242D4E7B0011029E /* Viaduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Viaduct.swift; sourceTree = "<group>"; };
CEB1A06723D8A42D005BD4DD /* FxAccountMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountMocks.swift; sourceTree = "<group>"; };
CED443CA23CCF385007E5FA6 /* FxAccountDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountDevice.swift; sourceTree = "<group>"; };
CED443CC23CD13E5007E5FA6 /* FxAccountDeviceConstellation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountDeviceConstellation.swift; sourceTree = "<group>"; };
CEDDBC8B23DB438F00CFF5AA /* RustFxAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RustFxAccount.swift; sourceTree = "<group>"; };
CEDDBC8D23DB4CD600CFF5AA /* FxAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccount.swift; sourceTree = "<group>"; };
CEDDBC8F23DB4F7400CFF5AA /* FxAccountOAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountOAuth.swift; sourceTree = "<group>"; };
CEDDBC9123DB4F9A00CFF5AA /* FxAccountProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FxAccountProfile.swift; sourceTree = "<group>"; };
CEDDBC8D23DB4CD600CFF5AA /* PersistedFirefoxAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistedFirefoxAccount.swift; sourceTree = "<group>"; };
CEFB1EAF22EF708B0001E20F /* ResultError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultError.swift; sourceTree = "<group>"; };
EB7DE84C2214D30B00E7CF17 /* SwiftProtobuf.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftProtobuf.framework; path = ../../Carthage/Build/iOS/SwiftProtobuf.framework; sourceTree = "<group>"; };
EB879D7A221234EB00753DC9 /* MozillaAppServicesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MozillaAppServicesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@ -263,6 +262,15 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
99FAA19025E60D2C001E2231 /* Generated */ = {
isa = PBXGroup;
children = (
99FAA19A25E61D5D001E2231 /* uniffi_fxa_client-Bridging-Header.h */,
99FAA19125E60D57001E2231 /* fxa_client.swift */,
);
path = Generated;
sourceTree = "<group>";
};
BF1A877F2506490700FED88E /* Glean */ = {
isa = PBXGroup;
children = (
@ -419,46 +427,22 @@
C852EEDA220A2A2B00A6E79A /* FxAClient */ = {
isa = PBXGroup;
children = (
CDC0089C2236CAB900893800 /* fxa_msg_types.proto */,
C852EEDD220A2A2B00A6E79A /* FirefoxAccount.swift */,
CEDDBC8B23DB438F00CFF5AA /* RustFxAccount.swift */,
CEDDBC8D23DB4CD600CFF5AA /* FxAccount.swift */,
78B6532624B39B2400ED1555 /* FxAccountMetrics.swift */,
99FAA19025E60D2C001E2231 /* Generated */,
CE0A9AB224E4A25C00914A16 /* fxa_client.udl */,
CEDDBC8D23DB4CD600CFF5AA /* PersistedFirefoxAccount.swift */,
99FAA19E25E65CA5001E2231 /* FxAccountOAuth.swift */,
CE1445AB23D6058B00B1E808 /* FxAccountConfig.swift */,
CED443CA23CCF385007E5FA6 /* FxAccountDevice.swift */,
CED443CC23CD13E5007E5FA6 /* FxAccountDeviceConstellation.swift */,
CE1445AE23D6315200B1E808 /* FxAccountLogging.swift */,
CE2D04C4231822AC00AF5722 /* FxAccountManager.swift */,
CE13F19A23F330DF005187A7 /* FxAccountMigration.swift */,
CEDDBC8F23DB4F7400CFF5AA /* FxAccountOAuth.swift */,
CEDDBC9123DB4F9A00CFF5AA /* FxAccountProfile.swift */,
CE13F19C23F448D0005187A7 /* FxAccountState.swift */,
CE1B09A4231865BC006226E1 /* FxAccountStorage.swift */,
C852EEDE220A2A2B00A6E79A /* Extensions */,
C852EEE0220A2A2B00A6E79A /* RustFxAFFI.h */,
C852EEE2220A2A2B00A6E79A /* Errors */,
);
name = FxAClient;
path = "../../components/fxa-client/ios/FxAClient";
sourceTree = "<group>";
};
C852EEDE220A2A2B00A6E79A /* Extensions */ = {
isa = PBXGroup;
children = (
CE1ADA9622249FDA00E89714 /* Data+RustBuffer.swift */,
C852EEDF220A2A2B00A6E79A /* String+Free_FxAClient.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
C852EEE2220A2A2B00A6E79A /* Errors */ = {
isa = PBXGroup;
children = (
C852EEE3220A2A2B00A6E79A /* FxAError.swift */,
);
path = Errors;
sourceTree = "<group>";
};
CD85A44822361E880099BFA9 /* Places */ = {
isa = PBXGroup;
children = (
@ -585,11 +569,11 @@
buildActionMask = 2147483647;
files = (
D05434A1225680D900FDE4EF /* MozillaAppServices.h in Headers */,
D05434A2225680F400FDE4EF /* RustFxAFFI.h in Headers */,
D05434A32256810200FDE4EF /* RustPasswordAPI.h in Headers */,
CDC21B15221DCE3700AA71E5 /* RustLogFFI.h in Headers */,
CE58B2F8242D54340089F091 /* RustViaductFFI.h in Headers */,
CD85A45522361E890099BFA9 /* RustPlacesAPI.h in Headers */,
99FAA19B25E61D5D001E2231 /* uniffi_fxa_client-Bridging-Header.h in Headers */,
BF1A879225064A4C00FED88E /* GleanFfi.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -609,6 +593,7 @@
CE9D201E20914D0D00F1C8FA /* Resources */,
);
buildRules = (
CE0617CE24E4978C0068E903 /* PBXBuildRule */,
EB7DE84A2214D28C00E7CF17 /* PBXBuildRule */,
);
dependencies = (
@ -644,7 +629,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1010;
LastUpgradeCheck = 1010;
LastUpgradeCheck = 1240;
ORGANIZATIONNAME = Mozilla;
TargetAttributes = {
CE9D201F20914D0D00F1C8FA = {
@ -743,11 +728,9 @@
1379D0692404BE1300AABD16 /* logins_msg_types.proto in Sources */,
BF1A87B525064A8100FED88E /* CounterMetric.swift in Sources */,
CE13F19B23F330DF005187A7 /* FxAccountMigration.swift in Sources */,
CEDDBC9023DB4F7400CFF5AA /* FxAccountOAuth.swift in Sources */,
CE0A9AB424E4A2CC00914A16 /* fxa_client.udl in Sources */,
137C5893240257340016932F /* Data+LoginsRustBuffer.swift in Sources */,
BF1A879525064A6600FED88E /* Configuration.swift in Sources */,
CE1ADA9722249FDA00E89714 /* Data+RustBuffer.swift in Sources */,
C852EEE8220A2A2B00A6E79A /* String+Free_FxAClient.swift in Sources */,
BF1A87D725064B4C00FED88E /* Metrics.swift in Sources */,
CDC0089F2236CAD100893800 /* places_msg_types.proto in Sources */,
BF1A87B025064A8100FED88E /* LabeledMetric.swift in Sources */,
@ -757,27 +740,22 @@
BF1A87BB25064A8100FED88E /* TimespanMetric.swift in Sources */,
BF1A879025064A4C00FED88E /* Dispatchers.swift in Sources */,
BF1A87C425064AA500FED88E /* GleanLifecycleObserver.swift in Sources */,
CEDDBC8E23DB4CD600CFF5AA /* FxAccount.swift in Sources */,
CEDDBC8C23DB438F00CFF5AA /* RustFxAccount.swift in Sources */,
CEDDBC8E23DB4CD600CFF5AA /* PersistedFirefoxAccount.swift in Sources */,
CED443CD23CD13E5007E5FA6 /* FxAccountDeviceConstellation.swift in Sources */,
BF1A87B825064A8100FED88E /* JweMetric.swift in Sources */,
BF1A87B225064A8100FED88E /* DatetimeMetric.swift in Sources */,
BF1A87AA25064A8100FED88E /* RecordedExperimentData.swift in Sources */,
BF1A879125064A4C00FED88E /* Glean.swift in Sources */,
CEDDBC9223DB4F9A00CFF5AA /* FxAccountProfile.swift in Sources */,
BF1A87C925064AB300FED88E /* ErrorType.swift in Sources */,
C852EEEB220A2A2B00A6E79A /* FxAError.swift in Sources */,
BF1A87BA25064A8100FED88E /* StringMetric.swift in Sources */,
99FAA19F25E65CA5001E2231 /* FxAccountOAuth.swift in Sources */,
C852EED7220A29FE00A6E79A /* LoginStoreError.swift in Sources */,
BF1A87D025064AC000FED88E /* Sysctl.swift in Sources */,
CD02CF3922568BCC00124DA2 /* SyncUnlockInfo.swift in Sources */,
CDC0089D2236CAB900893800 /* fxa_msg_types.proto in Sources */,
CD85A45A22361E890099BFA9 /* PlacesError.swift in Sources */,
BF1A87AB25064A8100FED88E /* DistributionData.swift in Sources */,
CE1445AC23D6058B00B1E808 /* FxAccountConfig.swift in Sources */,
78B6532724B39B2400ED1555 /* FxAccountMetrics.swift in Sources */,
CD85A45422361E890099BFA9 /* Places.swift in Sources */,
CED443CB23CCF385007E5FA6 /* FxAccountDevice.swift in Sources */,
C852EED8220A29FE00A6E79A /* LockError.swift in Sources */,
BF1A87AC25064A8100FED88E /* MemoryUnit.swift in Sources */,
BF1A87BE25064A9400FED88E /* HttpPingUploader.swift in Sources */,
@ -790,7 +768,6 @@
CD85A45722361E890099BFA9 /* String+Free_Places.swift in Sources */,
C852EED5220A29FE00A6E79A /* LoginRecord.swift in Sources */,
CE9FFA13242D4E7B0011029E /* Viaduct.swift in Sources */,
C852EEE7220A2A2B00A6E79A /* FirefoxAccount.swift in Sources */,
CD85A45822361E890099BFA9 /* Data+PlacesRustBuffer.swift in Sources */,
BF1A87B925064A8100FED88E /* TimingDistributionMetric.swift in Sources */,
CDC21B14221DCE3700AA71E5 /* RustLog.swift in Sources */,
@ -874,6 +851,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = mozilla.org.MozillaAppServicesTests;
PRODUCT_NAME = "$(TARGET_NAME)";
VALIDATE_WORKSPACE = NO;
};
name = Debug;
};
@ -888,6 +866,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = mozilla.org.MozillaAppServicesTests;
PRODUCT_NAME = "$(TARGET_NAME)";
VALIDATE_WORKSPACE = NO;
};
name = Release;
};

Просмотреть файл

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1010"
LastUpgradeVersion = "1240"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@ -27,6 +27,15 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CE9D201F20914D0D00F1C8FA"
BuildableName = "MozillaAppServices.framework"
BlueprintName = "MozillaAppServices"
ReferencedContainer = "container:MozillaAppServices.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
@ -39,17 +48,6 @@
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "CE9D201F20914D0D00F1C8FA"
BuildableName = "MozillaAppServices.framework"
BlueprintName = "MozillaAppServices"
ReferencedContainer = "container:MozillaAppServices.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@ -70,8 +68,6 @@
ReferencedContainer = "container:MozillaAppServices.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

Просмотреть файл

@ -173,7 +173,7 @@ class FxAccountManagerTests: XCTestCase {
func testAccountRestorationEnsureCapabilitiesNonAuthError() {
class MockAccount: MockFxAccount {
override func ensureCapabilities(supportedCapabilities _: [DeviceCapability]) throws {
throw FirefoxAccountError.network(message: "The WiFi cable is detached.")
throw FxaError.Network(message: "The WiFi cable is detached.")
}
}
let mgr = mockFxAManager()
@ -203,12 +203,12 @@ class FxAccountManagerTests: XCTestCase {
class MockAccount: MockFxAccount {
override func ensureCapabilities(supportedCapabilities _: [DeviceCapability]) throws {
notifyAuthError()
throw FirefoxAccountError.unauthorized(message: "Your token is expired yo.")
throw FxaError.Authentication(message: "Your token is expired yo.")
}
override func checkAuthorizationStatus() throws -> IntrospectInfo {
override func checkAuthorizationStatus() throws -> AuthorizationInfo {
_ = try super.checkAuthorizationStatus()
return IntrospectInfo(active: false)
return AuthorizationInfo(active: false)
}
}
let mgr = mockFxAManager()
@ -301,7 +301,7 @@ class FxAccountManagerTests: XCTestCase {
profileCallCount += 1
if profileCallCount == 1 {
notifyAuthError()
throw FirefoxAccountError.unauthorized(message: "Uh oh.")
throw FxaError.Authentication(message: "Uh oh.")
} else {
return profile
}

Просмотреть файл

@ -8,7 +8,7 @@ import Foundation
// Arrays are not thread-safe in Swift.
let queue = DispatchQueue(label: "InvocationsArrayQueue")
class MockFxAccount: FxAccount {
class MockFxAccount: PersistedFirefoxAccount {
var invocations: [MethodInvocation] = []
enum MethodInvocation {
case checkAuthorizationStatus
@ -22,14 +22,14 @@ class MockFxAccount: FxAccount {
}
init() {
super.init(raw: 0)
super.init(inner: FirefoxAccount(contentUrl: "", clientId: "", redirectUri: "", tokenServerUrlOverride: nil))
}
required convenience init(fromJsonState _: String) throws {
fatalError("init(fromJsonState:) has not been implemented")
}
required convenience init(config _: FxAConfig) throws {
required convenience init(config _: FxAConfig) {
fatalError("init(config:) has not been implemented")
}
@ -54,46 +54,46 @@ class MockFxAccount: FxAccount {
queue.sync { invocations.append(.ensureCapabilities) }
}
override func checkAuthorizationStatus() throws -> IntrospectInfo {
override func checkAuthorizationStatus() throws -> AuthorizationInfo {
queue.sync { invocations.append(.checkAuthorizationStatus) }
return IntrospectInfo(active: true)
return AuthorizationInfo(active: true)
}
override func clearAccessTokenCache() throws {
override func clearAccessTokenCache() {
queue.sync { invocations.append(.clearAccessTokenCache) }
}
override func getAccessToken(scope _: String, ttl _: UInt64? = nil) throws -> AccessTokenInfo {
queue.sync { invocations.append(.getAccessToken) }
return AccessTokenInfo(scope: "profile", token: "toktok")
return AccessTokenInfo(scope: "profile", token: "toktok", key: nil, expiresAt: Int64.max)
}
override func getProfile(ignoreCache _: Bool) throws -> Profile {
queue.sync { invocations.append(.getProfile) }
return Profile(uid: "uid", email: "foo@bar.bobo")
return Profile(uid: "uid", email: "foo@bar.bobo", displayName: "Bobo the Foo", avatar: "https://example.com/avatar.png", isDefaultAvatar: false)
}
override func beginOAuthFlow(
scopes _: [String],
entrypoint _: String,
metricsParams _: MetricsParams = MetricsParams.newEmpty()
metrics _: MetricsParams = MetricsParams(parameters: [:])
) throws -> URL {
return URL(string: "https://foo.bar/oauth?state=bobo")!
}
}
class MockFxAccountManager: FxAccountManager {
var storedAccount: FxAccount?
var storedAccount: PersistedFirefoxAccount?
override func createAccount() -> FxAccount {
override func createAccount() -> PersistedFirefoxAccount {
return MockFxAccount()
}
override func makeDeviceConstellation(account _: FxAccount) -> DeviceConstellation {
override func makeDeviceConstellation(account _: PersistedFirefoxAccount) -> DeviceConstellation {
return MockDeviceConstellation(account: account)
}
override func tryRestoreAccount() -> FxAccount? {
override func tryRestoreAccount() -> PersistedFirefoxAccount? {
return storedAccount
}
}
@ -106,7 +106,7 @@ class MockDeviceConstellation: DeviceConstellation {
case refreshState
}
override init(account: FxAccount?) {
override init(account: PersistedFirefoxAccount?) {
super.init(account: account ?? MockFxAccount())
}

Просмотреть файл

@ -9,7 +9,7 @@ license = "MPL-2.0"
crate-type = ["staticlib"]
[dependencies]
fxaclient_ffi = { path = "../../../components/fxa-client/ffi" }
fxa-client = { path = "../../../components/fxa-client" }
logins_ffi = { path = "../../../components/logins/ffi" }
places-ffi = { path = "../../../components/places/ffi" }
rc_log_ffi = { path = "../../../components/rc_log" }

Просмотреть файл

@ -5,7 +5,7 @@
#![allow(unknown_lints)]
#![warn(rust_2018_idioms)]
pub use fxaclient_ffi;
pub use fxa_client;
pub use glean_ffi;
pub use logins_ffi;
pub use places_ffi;

Просмотреть файл

@ -3,7 +3,7 @@ http://creativecommons.org/publicdomain/zero/1.0/ */
use crate::Opts;
use anyhow::Result;
use fxa_client::{self, auth, Config as FxaConfig, FirefoxAccount};
use fxa_client::internal::{auth, config::Config as FxaConfig, FirefoxAccount};
use logins::PasswordStore;
use serde_json::json;
use std::collections::HashMap;
@ -220,7 +220,7 @@ impl Drop for TestAccount {
}
pub struct TestClient {
pub fxa: fxa_client::FirefoxAccount,
pub fxa: fxa_client::internal::FirefoxAccount,
pub test_acct: Arc<TestAccount>,
// XXX do this more generically...
pub logins_store: PasswordStore,
@ -257,7 +257,11 @@ impl TestClient {
fxa.complete_oauth_flow(&code, &state)?;
log::info!("OAuth flow finished");
fxa.initialize_device("Testing Device", fxa_client::device::Type::Desktop, &[])?;
fxa.initialize_device(
"Testing Device",
fxa_client::internal::device::Type::Desktop,
&[],
)?;
Ok(Self {
fxa,

Просмотреть файл

@ -599,6 +599,16 @@ PACKAGE_METADATA_FIXUPS = {
"fixup": "https://raw.githubusercontent.com/mozilla/uniffi-rs/main/LICENSE",
}
},
"uniffi_macros": {
"license_url": {
"check": None,
"fixup": "https://github.com/mozilla/uniffi-rs/blob/main/LICENSE",
},
"license_file": {
"check": None,
"fixup": "https://raw.githubusercontent.com/mozilla/uniffi-rs/main/LICENSE",
}
},
"uniffi": {
"license_url": {
"check": None,

Просмотреть файл

@ -2,9 +2,6 @@
# `dir` refers to the directory where the protobuf file can be found, relative to this file location.
# (Optional) `out_dir` refers to where the compiled rust file should be saved. If not present `dir` is used.
["fxa_msg_types.proto"]
dir = "../components/fxa-client/src/"
["logins_msg_types.proto"]
dir = "../components/logins/src/"