Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1578)

Unified Diff: sync/tools/testserver/chromiumsync.py

Issue 2130453004: [Sync] Move //sync to //components/sync. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Rebase. Created 4 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « sync/tools/testserver/DEPS ('k') | sync/tools/testserver/chromiumsync_test.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: sync/tools/testserver/chromiumsync.py
diff --git a/sync/tools/testserver/chromiumsync.py b/sync/tools/testserver/chromiumsync.py
deleted file mode 100644
index fbdc44b628eb6ee1d349dc9620589a7e08ecf435..0000000000000000000000000000000000000000
--- a/sync/tools/testserver/chromiumsync.py
+++ /dev/null
@@ -1,1641 +0,0 @@
-# Copyright 2013 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-"""An implementation of the server side of the Chromium sync protocol.
-
-The details of the protocol are described mostly by comments in the protocol
-buffer definition at chrome/browser/sync/protocol/sync.proto.
-"""
-
-import base64
-import cgi
-import copy
-import google.protobuf.text_format
-import hashlib
-import operator
-import pickle
-import random
-import string
-import sys
-import threading
-import time
-import urlparse
-import uuid
-
-import app_list_specifics_pb2
-import app_notification_specifics_pb2
-import app_setting_specifics_pb2
-import app_specifics_pb2
-import arc_package_specifics_pb2
-import article_specifics_pb2
-import autofill_specifics_pb2
-import bookmark_specifics_pb2
-import client_commands_pb2
-import dictionary_specifics_pb2
-import get_updates_caller_info_pb2
-import extension_setting_specifics_pb2
-import extension_specifics_pb2
-import favicon_image_specifics_pb2
-import favicon_tracking_specifics_pb2
-import history_delete_directive_specifics_pb2
-import managed_user_setting_specifics_pb2
-import managed_user_specifics_pb2
-import managed_user_shared_setting_specifics_pb2
-import managed_user_whitelist_specifics_pb2
-import nigori_specifics_pb2
-import password_specifics_pb2
-import preference_specifics_pb2
-import priority_preference_specifics_pb2
-import search_engine_specifics_pb2
-import session_specifics_pb2
-import sync_pb2
-import sync_enums_pb2
-import synced_notification_app_info_specifics_pb2
-import synced_notification_specifics_pb2
-import theme_specifics_pb2
-import typed_url_specifics_pb2
-import wifi_credential_specifics_pb2
-
-# An enumeration of the various kinds of data that can be synced.
-# Over the wire, this enumeration is not used: a sync object's type is
-# inferred by which EntitySpecifics field it has. But in the context
-# of a program, it is useful to have an enumeration.
-ALL_TYPES = (
- TOP_LEVEL, # The type of the 'Google Chrome' folder.
- APPS,
- APP_LIST,
- APP_NOTIFICATION,
- APP_SETTINGS,
- ARC_PACKAGE,
- ARTICLE,
- AUTOFILL,
- AUTOFILL_PROFILE,
- AUTOFILL_WALLET,
- AUTOFILL_WALLET_METADATA,
- BOOKMARK,
- DEVICE_INFO,
- DICTIONARY,
- EXPERIMENTS,
- EXTENSIONS,
- HISTORY_DELETE_DIRECTIVE,
- MANAGED_USER_SETTING,
- MANAGED_USER_SHARED_SETTING,
- MANAGED_USER_WHITELIST,
- MANAGED_USER,
- NIGORI,
- PASSWORD,
- PREFERENCE,
- PRIORITY_PREFERENCE,
- SEARCH_ENGINE,
- SESSION,
- SYNCED_NOTIFICATION,
- SYNCED_NOTIFICATION_APP_INFO,
- THEME,
- TYPED_URL,
- EXTENSION_SETTINGS,
- FAVICON_IMAGES,
- FAVICON_TRACKING,
- WIFI_CREDENTIAL) = range(35)
-
-# An enumeration on the frequency at which the server should send errors
-# to the client. This would be specified by the url that triggers the error.
-# Note: This enum should be kept in the same order as the enum in sync_test.h.
-SYNC_ERROR_FREQUENCY = (
- ERROR_FREQUENCY_NONE,
- ERROR_FREQUENCY_ALWAYS,
- ERROR_FREQUENCY_TWO_THIRDS) = range(3)
-
-# Well-known server tag of the top level 'Google Chrome' folder.
-TOP_LEVEL_FOLDER_TAG = 'google_chrome'
-
-# Given a sync type from ALL_TYPES, find the FieldDescriptor corresponding
-# to that datatype. Note that TOP_LEVEL has no such token.
-SYNC_TYPE_FIELDS = sync_pb2.EntitySpecifics.DESCRIPTOR.fields_by_name
-SYNC_TYPE_TO_DESCRIPTOR = {
- APP_LIST: SYNC_TYPE_FIELDS['app_list'],
- APP_NOTIFICATION: SYNC_TYPE_FIELDS['app_notification'],
- APP_SETTINGS: SYNC_TYPE_FIELDS['app_setting'],
- APPS: SYNC_TYPE_FIELDS['app'],
- ARC_PACKAGE: SYNC_TYPE_FIELDS['arc_package'],
- ARTICLE: SYNC_TYPE_FIELDS['article'],
- AUTOFILL: SYNC_TYPE_FIELDS['autofill'],
- AUTOFILL_PROFILE: SYNC_TYPE_FIELDS['autofill_profile'],
- AUTOFILL_WALLET: SYNC_TYPE_FIELDS['autofill_wallet'],
- AUTOFILL_WALLET_METADATA: SYNC_TYPE_FIELDS['wallet_metadata'],
- BOOKMARK: SYNC_TYPE_FIELDS['bookmark'],
- DEVICE_INFO: SYNC_TYPE_FIELDS['device_info'],
- DICTIONARY: SYNC_TYPE_FIELDS['dictionary'],
- EXPERIMENTS: SYNC_TYPE_FIELDS['experiments'],
- EXTENSION_SETTINGS: SYNC_TYPE_FIELDS['extension_setting'],
- EXTENSIONS: SYNC_TYPE_FIELDS['extension'],
- FAVICON_IMAGES: SYNC_TYPE_FIELDS['favicon_image'],
- FAVICON_TRACKING: SYNC_TYPE_FIELDS['favicon_tracking'],
- HISTORY_DELETE_DIRECTIVE: SYNC_TYPE_FIELDS['history_delete_directive'],
- MANAGED_USER_SHARED_SETTING:
- SYNC_TYPE_FIELDS['managed_user_shared_setting'],
- MANAGED_USER_SETTING: SYNC_TYPE_FIELDS['managed_user_setting'],
- MANAGED_USER_WHITELIST: SYNC_TYPE_FIELDS['managed_user_whitelist'],
- MANAGED_USER: SYNC_TYPE_FIELDS['managed_user'],
- NIGORI: SYNC_TYPE_FIELDS['nigori'],
- PASSWORD: SYNC_TYPE_FIELDS['password'],
- PREFERENCE: SYNC_TYPE_FIELDS['preference'],
- PRIORITY_PREFERENCE: SYNC_TYPE_FIELDS['priority_preference'],
- SEARCH_ENGINE: SYNC_TYPE_FIELDS['search_engine'],
- SESSION: SYNC_TYPE_FIELDS['session'],
- SYNCED_NOTIFICATION: SYNC_TYPE_FIELDS["synced_notification"],
- SYNCED_NOTIFICATION_APP_INFO:
- SYNC_TYPE_FIELDS["synced_notification_app_info"],
- THEME: SYNC_TYPE_FIELDS['theme'],
- TYPED_URL: SYNC_TYPE_FIELDS['typed_url'],
- WIFI_CREDENTIAL: SYNC_TYPE_FIELDS["wifi_credential"],
- }
-
-# The parent ID used to indicate a top-level node.
-ROOT_ID = '0'
-
-# Unix time epoch +1 day in struct_time format. The tuple corresponds to
-# UTC Thursday Jan 2 1970, 00:00:00, non-dst.
-# We have to add one day after start of epoch, since in timezones with positive
-# UTC offset time.mktime throws an OverflowError,
-# rather then returning negative number.
-FIRST_DAY_UNIX_TIME_EPOCH = (1970, 1, 2, 0, 0, 0, 4, 2, 0)
-ONE_DAY_SECONDS = 60 * 60 * 24
-
-# The number of characters in the server-generated encryption key.
-KEYSTORE_KEY_LENGTH = 16
-
-# The hashed client tags for some experiment nodes.
-KEYSTORE_ENCRYPTION_EXPERIMENT_TAG = "pis8ZRzh98/MKLtVEio2mr42LQA="
-PRE_COMMIT_GU_AVOIDANCE_EXPERIMENT_TAG = "Z1xgeh3QUBa50vdEPd8C/4c7jfE="
-
-class Error(Exception):
- """Error class for this module."""
-
-
-class ProtobufDataTypeFieldNotUnique(Error):
- """An entry should not have more than one data type present."""
-
-
-class DataTypeIdNotRecognized(Error):
- """The requested data type is not recognized."""
-
-
-class MigrationDoneError(Error):
- """A server-side migration occurred; clients must re-sync some datatypes.
-
- Attributes:
- datatypes: a list of the datatypes (python enum) needing migration.
- """
-
- def __init__(self, datatypes):
- self.datatypes = datatypes
-
-
-class StoreBirthdayError(Error):
- """The client sent a birthday that doesn't correspond to this server."""
-
-
-class TransientError(Error):
- """The client would be sent a transient error."""
-
-
-class SyncInducedError(Error):
- """The client would be sent an error."""
-
-
-class InducedErrorFrequencyNotDefined(Error):
- """The error frequency defined is not handled."""
-
-
-class ClientNotConnectedError(Error):
- """The client is not connected to the server."""
-
-
-def GetEntryType(entry):
- """Extract the sync type from a SyncEntry.
-
- Args:
- entry: A SyncEntity protobuf object whose type to determine.
- Returns:
- An enum value from ALL_TYPES if the entry's type can be determined, or None
- if the type cannot be determined.
- Raises:
- ProtobufDataTypeFieldNotUnique: More than one type was indicated by
- the entry.
- """
- if entry.server_defined_unique_tag == TOP_LEVEL_FOLDER_TAG:
- return TOP_LEVEL
- entry_types = GetEntryTypesFromSpecifics(entry.specifics)
- if not entry_types:
- return None
-
- # If there is more than one, either there's a bug, or else the caller
- # should use GetEntryTypes.
- if len(entry_types) > 1:
- raise ProtobufDataTypeFieldNotUnique
- return entry_types[0]
-
-
-def GetEntryTypesFromSpecifics(specifics):
- """Determine the sync types indicated by an EntitySpecifics's field(s).
-
- If the specifics have more than one recognized data type field (as commonly
- happens with the requested_types field of GetUpdatesMessage), all types
- will be returned. Callers must handle the possibility of the returned
- value having more than one item.
-
- Args:
- specifics: A EntitySpecifics protobuf message whose extensions to
- enumerate.
- Returns:
- A list of the sync types (values from ALL_TYPES) associated with each
- recognized extension of the specifics message.
- """
- return [data_type for data_type, field_descriptor
- in SYNC_TYPE_TO_DESCRIPTOR.iteritems()
- if specifics.HasField(field_descriptor.name)]
-
-
-def SyncTypeToProtocolDataTypeId(data_type):
- """Convert from a sync type (python enum) to the protocol's data type id."""
- return SYNC_TYPE_TO_DESCRIPTOR[data_type].number
-
-
-def ProtocolDataTypeIdToSyncType(protocol_data_type_id):
- """Convert from the protocol's data type id to a sync type (python enum)."""
- for data_type, field_descriptor in SYNC_TYPE_TO_DESCRIPTOR.iteritems():
- if field_descriptor.number == protocol_data_type_id:
- return data_type
- raise DataTypeIdNotRecognized
-
-
-def DataTypeStringToSyncTypeLoose(data_type_string):
- """Converts a human-readable string to a sync type (python enum).
-
- Capitalization and pluralization don't matter; this function is appropriate
- for values that might have been typed by a human being; e.g., command-line
- flags or query parameters.
- """
- if data_type_string.isdigit():
- return ProtocolDataTypeIdToSyncType(int(data_type_string))
- name = data_type_string.lower().rstrip('s')
- for data_type, field_descriptor in SYNC_TYPE_TO_DESCRIPTOR.iteritems():
- if field_descriptor.name.lower().rstrip('s') == name:
- return data_type
- raise DataTypeIdNotRecognized
-
-
-def MakeNewKeystoreKey():
- """Returns a new random keystore key."""
- return ''.join(random.choice(string.ascii_uppercase + string.digits)
- for x in xrange(KEYSTORE_KEY_LENGTH))
-
-
-def SyncTypeToString(data_type):
- """Formats a sync type enum (from ALL_TYPES) to a human-readable string."""
- return SYNC_TYPE_TO_DESCRIPTOR[data_type].name
-
-
-def CallerInfoToString(caller_info_source):
- """Formats a GetUpdatesSource enum value to a readable string."""
- return get_updates_caller_info_pb2.GetUpdatesCallerInfo \
- .DESCRIPTOR.enum_types_by_name['GetUpdatesSource'] \
- .values_by_number[caller_info_source].name
-
-
-def ShortDatatypeListSummary(data_types):
- """Formats compactly a list of sync types (python enums) for human eyes.
-
- This function is intended for use by logging. If the list of datatypes
- contains almost all of the values, the return value will be expressed
- in terms of the datatypes that aren't set.
- """
- included = set(data_types) - set([TOP_LEVEL])
- if not included:
- return 'nothing'
- excluded = set(ALL_TYPES) - included - set([TOP_LEVEL])
- if not excluded:
- return 'everything'
- simple_text = '+'.join(sorted([SyncTypeToString(x) for x in included]))
- all_but_text = 'all except %s' % (
- '+'.join(sorted([SyncTypeToString(x) for x in excluded])))
- if len(included) < len(excluded) or len(simple_text) <= len(all_but_text):
- return simple_text
- else:
- return all_but_text
-
-
-def GetDefaultEntitySpecifics(data_type):
- """Get an EntitySpecifics having a sync type's default field value."""
- specifics = sync_pb2.EntitySpecifics()
- if data_type in SYNC_TYPE_TO_DESCRIPTOR:
- descriptor = SYNC_TYPE_TO_DESCRIPTOR[data_type]
- getattr(specifics, descriptor.name).SetInParent()
- return specifics
-
-
-class PermanentItem(object):
- """A specification of one server-created permanent item.
-
- Attributes:
- tag: A known-to-the-client value that uniquely identifies a server-created
- permanent item.
- name: The human-readable display name for this item.
- parent_tag: The tag of the permanent item's parent. If ROOT_ID, indicates
- a top-level item. Otherwise, this must be the tag value of some other
- server-created permanent item.
- sync_type: A value from ALL_TYPES, giving the datatype of this permanent
- item. This controls which types of client GetUpdates requests will
- cause the permanent item to be created and returned.
- create_by_default: Whether the permanent item is created at startup or not.
- This value is set to True in the default case. Non-default permanent items
- are those that are created only when a client explicitly tells the server
- to do so.
- """
-
- def __init__(self, tag, name, parent_tag, sync_type, create_by_default=True):
- self.tag = tag
- self.name = name
- self.parent_tag = parent_tag
- self.sync_type = sync_type
- self.create_by_default = create_by_default
-
-
-class MigrationHistory(object):
- """A record of the migration events associated with an account.
-
- Each migration event invalidates one or more datatypes on all clients
- that had synced the datatype before the event. Such clients will continue
- to receive MigrationDone errors until they throw away their progress and
- re-sync that datatype from the beginning.
- """
- def __init__(self):
- self._migrations = {}
- for datatype in ALL_TYPES:
- self._migrations[datatype] = [1]
- self._next_migration_version = 2
-
- def GetLatestVersion(self, datatype):
- return self._migrations[datatype][-1]
-
- def CheckAllCurrent(self, versions_map):
- """Raises an error if any the provided versions are out of date.
-
- This function intentionally returns migrations in the order that they were
- triggered. Doing it this way allows the client to queue up two migrations
- in a row, so the second one is received while responding to the first.
-
- Arguments:
- version_map: a map whose keys are datatypes and whose values are versions.
-
- Raises:
- MigrationDoneError: if a mismatch is found.
- """
- problems = {}
- for datatype, client_migration in versions_map.iteritems():
- for server_migration in self._migrations[datatype]:
- if client_migration < server_migration:
- problems.setdefault(server_migration, []).append(datatype)
- if problems:
- raise MigrationDoneError(problems[min(problems.keys())])
-
- def Bump(self, datatypes):
- """Add a record of a migration, to cause errors on future requests."""
- for idx, datatype in enumerate(datatypes):
- self._migrations[datatype].append(self._next_migration_version)
- self._next_migration_version += 1
-
-
-class UpdateSieve(object):
- """A filter to remove items the client has already seen."""
- def __init__(self, request, migration_history=None):
- self._original_request = request
- self._state = {}
- self._migration_history = migration_history or MigrationHistory()
- self._migration_versions_to_check = {}
- if request.from_progress_marker:
- for marker in request.from_progress_marker:
- data_type = ProtocolDataTypeIdToSyncType(marker.data_type_id)
- if marker.HasField('timestamp_token_for_migration'):
- timestamp = marker.timestamp_token_for_migration
- if timestamp:
- self._migration_versions_to_check[data_type] = 1
- elif marker.token:
- (timestamp, version) = pickle.loads(marker.token)
- self._migration_versions_to_check[data_type] = version
- elif marker.HasField('token'):
- timestamp = 0
- else:
- raise ValueError('No timestamp information in progress marker.')
- data_type = ProtocolDataTypeIdToSyncType(marker.data_type_id)
- self._state[data_type] = timestamp
- elif request.HasField('from_timestamp'):
- for data_type in GetEntryTypesFromSpecifics(request.requested_types):
- self._state[data_type] = request.from_timestamp
- self._migration_versions_to_check[data_type] = 1
- if self._state:
- self._state[TOP_LEVEL] = min(self._state.itervalues())
-
- def SummarizeRequest(self):
- timestamps = {}
- for data_type, timestamp in self._state.iteritems():
- if data_type == TOP_LEVEL:
- continue
- timestamps.setdefault(timestamp, []).append(data_type)
- return ', '.join('<%s>@%d' % (ShortDatatypeListSummary(types), stamp)
- for stamp, types in sorted(timestamps.iteritems()))
-
- def CheckMigrationState(self):
- self._migration_history.CheckAllCurrent(self._migration_versions_to_check)
-
- def ClientWantsItem(self, item):
- """Return true if the client hasn't already seen an item."""
- return self._state.get(GetEntryType(item), sys.maxint) < item.version
-
- def HasAnyTimestamp(self):
- """Return true if at least one datatype was requested."""
- return bool(self._state)
-
- def GetMinTimestamp(self):
- """Return true the smallest timestamp requested across all datatypes."""
- return min(self._state.itervalues())
-
- def GetFirstTimeTypes(self):
- """Return a list of datatypes requesting updates from timestamp zero."""
- return [datatype for datatype, timestamp in self._state.iteritems()
- if timestamp == 0]
-
- def GetCreateMobileBookmarks(self):
- """Return true if the client has requested to create the 'Mobile Bookmarks'
- folder.
- """
- return (self._original_request.HasField('create_mobile_bookmarks_folder')
- and self._original_request.create_mobile_bookmarks_folder)
-
- def SaveProgress(self, new_timestamp, get_updates_response):
- """Write the new_timestamp or new_progress_marker fields to a response."""
- if self._original_request.from_progress_marker:
- for data_type, old_timestamp in self._state.iteritems():
- if data_type == TOP_LEVEL:
- continue
- new_marker = sync_pb2.DataTypeProgressMarker()
- new_marker.data_type_id = SyncTypeToProtocolDataTypeId(data_type)
- final_stamp = max(old_timestamp, new_timestamp)
- final_migration = self._migration_history.GetLatestVersion(data_type)
- new_marker.token = pickle.dumps((final_stamp, final_migration))
- get_updates_response.new_progress_marker.add().MergeFrom(new_marker)
- elif self._original_request.HasField('from_timestamp'):
- if self._original_request.from_timestamp < new_timestamp:
- get_updates_response.new_timestamp = new_timestamp
-
-
-class SyncDataModel(object):
- """Models the account state of one sync user."""
- _BATCH_SIZE = 100
-
- # Specify all the permanent items that a model might need.
- _PERMANENT_ITEM_SPECS = [
- PermanentItem('google_chrome_apps', name='Apps',
- parent_tag=ROOT_ID, sync_type=APPS),
- PermanentItem('google_chrome_app_list', name='App List',
- parent_tag=ROOT_ID, sync_type=APP_LIST),
- PermanentItem('google_chrome_app_notifications', name='App Notifications',
- parent_tag=ROOT_ID, sync_type=APP_NOTIFICATION),
- PermanentItem('google_chrome_app_settings',
- name='App Settings',
- parent_tag=ROOT_ID, sync_type=APP_SETTINGS),
- PermanentItem('google_chrome_arc_package', name='Arc Package',
- parent_tag=ROOT_ID, sync_type=ARC_PACKAGE),
- PermanentItem('google_chrome_bookmarks', name='Bookmarks',
- parent_tag=ROOT_ID, sync_type=BOOKMARK),
- PermanentItem('bookmark_bar', name='Bookmark Bar',
- parent_tag='google_chrome_bookmarks', sync_type=BOOKMARK),
- PermanentItem('other_bookmarks', name='Other Bookmarks',
- parent_tag='google_chrome_bookmarks', sync_type=BOOKMARK),
- PermanentItem('synced_bookmarks', name='Synced Bookmarks',
- parent_tag='google_chrome_bookmarks', sync_type=BOOKMARK,
- create_by_default=False),
- PermanentItem('google_chrome_autofill', name='Autofill',
- parent_tag=ROOT_ID, sync_type=AUTOFILL),
- PermanentItem('google_chrome_autofill_profiles', name='Autofill Profiles',
- parent_tag=ROOT_ID, sync_type=AUTOFILL_PROFILE),
- PermanentItem('google_chrome_autofill_wallet',
- name='Autofill Wallet Items', parent_tag=ROOT_ID,
- sync_type=AUTOFILL_WALLET),
- PermanentItem('google_chrome_autofill_wallet_metadata',
- name='Autofill Wallet Metadata', parent_tag=ROOT_ID,
- sync_type=AUTOFILL_WALLET_METADATA),
- PermanentItem('google_chrome_device_info', name='Device Info',
- parent_tag=ROOT_ID, sync_type=DEVICE_INFO),
- PermanentItem('google_chrome_experiments', name='Experiments',
- parent_tag=ROOT_ID, sync_type=EXPERIMENTS),
- PermanentItem('google_chrome_extension_settings',
- name='Extension Settings',
- parent_tag=ROOT_ID, sync_type=EXTENSION_SETTINGS),
- PermanentItem('google_chrome_extensions', name='Extensions',
- parent_tag=ROOT_ID, sync_type=EXTENSIONS),
- PermanentItem('google_chrome_history_delete_directives',
- name='History Delete Directives',
- parent_tag=ROOT_ID,
- sync_type=HISTORY_DELETE_DIRECTIVE),
- PermanentItem('google_chrome_favicon_images',
- name='Favicon Images',
- parent_tag=ROOT_ID,
- sync_type=FAVICON_IMAGES),
- PermanentItem('google_chrome_favicon_tracking',
- name='Favicon Tracking',
- parent_tag=ROOT_ID,
- sync_type=FAVICON_TRACKING),
- PermanentItem('google_chrome_managed_user_settings',
- name='Managed User Settings',
- parent_tag=ROOT_ID, sync_type=MANAGED_USER_SETTING),
- PermanentItem('google_chrome_managed_users',
- name='Managed Users',
- parent_tag=ROOT_ID, sync_type=MANAGED_USER),
- PermanentItem('google_chrome_managed_user_shared_settings',
- name='Managed User Shared Settings',
- parent_tag=ROOT_ID, sync_type=MANAGED_USER_SHARED_SETTING),
- PermanentItem('google_chrome_managed_user_whitelists',
- name='Managed User Whitelists', parent_tag=ROOT_ID,
- sync_type=MANAGED_USER_WHITELIST),
- PermanentItem('google_chrome_nigori', name='Nigori',
- parent_tag=ROOT_ID, sync_type=NIGORI),
- PermanentItem('google_chrome_passwords', name='Passwords',
- parent_tag=ROOT_ID, sync_type=PASSWORD),
- PermanentItem('google_chrome_preferences', name='Preferences',
- parent_tag=ROOT_ID, sync_type=PREFERENCE),
- PermanentItem('google_chrome_priority_preferences',
- name='Priority Preferences',
- parent_tag=ROOT_ID, sync_type=PRIORITY_PREFERENCE),
- PermanentItem('google_chrome_synced_notifications',
- name='Synced Notifications',
- parent_tag=ROOT_ID, sync_type=SYNCED_NOTIFICATION),
- PermanentItem('google_chrome_synced_notification_app_info',
- name='Synced Notification App Info',
- parent_tag=ROOT_ID, sync_type=SYNCED_NOTIFICATION_APP_INFO),
- PermanentItem('google_chrome_search_engines', name='Search Engines',
- parent_tag=ROOT_ID, sync_type=SEARCH_ENGINE),
- PermanentItem('google_chrome_sessions', name='Sessions',
- parent_tag=ROOT_ID, sync_type=SESSION),
- PermanentItem('google_chrome_themes', name='Themes',
- parent_tag=ROOT_ID, sync_type=THEME),
- PermanentItem('google_chrome_typed_urls', name='Typed URLs',
- parent_tag=ROOT_ID, sync_type=TYPED_URL),
- PermanentItem('google_chrome_wifi_credentials', name='WiFi Credentials',
- parent_tag=ROOT_ID, sync_type=WIFI_CREDENTIAL),
- PermanentItem('google_chrome_dictionary', name='Dictionary',
- parent_tag=ROOT_ID, sync_type=DICTIONARY),
- PermanentItem('google_chrome_articles', name='Articles',
- parent_tag=ROOT_ID, sync_type=ARTICLE),
- ]
-
- def __init__(self):
- # Monotonically increasing version number. The next object change will
- # take on this value + 1.
- self._version = 0
-
- # The definitive copy of this client's items: a map from ID string to a
- # SyncEntity protocol buffer.
- self._entries = {}
-
- self.ResetStoreBirthday()
- self.migration_history = MigrationHistory()
- self.induced_error = sync_pb2.ClientToServerResponse.Error()
- self.induced_error_frequency = 0
- self.sync_count_before_errors = 0
- self.acknowledge_managed_users = False
- self._keys = [MakeNewKeystoreKey()]
-
- def _SaveEntry(self, entry):
- """Insert or update an entry in the change log, and give it a new version.
-
- The ID fields of this entry are assumed to be valid server IDs. This
- entry will be updated with a new version number and sync_timestamp.
-
- Args:
- entry: The entry to be added or updated.
- """
- self._version += 1
- # Maintain a global (rather than per-item) sequence number and use it
- # both as the per-entry version as well as the update-progress timestamp.
- # This simulates the behavior of the original server implementation.
- entry.version = self._version
- entry.sync_timestamp = self._version
-
- # Preserve the originator info, which the client is not required to send
- # when updating.
- base_entry = self._entries.get(entry.id_string)
- if base_entry:
- entry.originator_cache_guid = base_entry.originator_cache_guid
- entry.originator_client_item_id = base_entry.originator_client_item_id
-
- self._entries[entry.id_string] = copy.deepcopy(entry)
-
- def _ServerTagToId(self, tag):
- """Determine the server ID from a server-unique tag.
-
- The resulting value is guaranteed not to collide with the other ID
- generation methods.
-
- Args:
- tag: The unique, known-to-the-client tag of a server-generated item.
- Returns:
- The string value of the computed server ID.
- """
- if not tag or tag == ROOT_ID:
- return tag
- spec = [x for x in self._PERMANENT_ITEM_SPECS if x.tag == tag][0]
- return self._MakeCurrentId(spec.sync_type, '<server tag>%s' % tag)
-
- def _TypeToTypeRootId(self, model_type):
- """Returns the server ID for the type root node of the given type."""
- tag = [x.tag for x in self._PERMANENT_ITEM_SPECS
- if x.sync_type == model_type][0]
- return self._ServerTagToId(tag)
-
- def _ClientTagToId(self, datatype, tag):
- """Determine the server ID from a client-unique tag.
-
- The resulting value is guaranteed not to collide with the other ID
- generation methods.
-
- Args:
- datatype: The sync type (python enum) of the identified object.
- tag: The unique, opaque-to-the-server tag of a client-tagged item.
- Returns:
- The string value of the computed server ID.
- """
- return self._MakeCurrentId(datatype, '<client tag>%s' % tag)
-
- def _ClientIdToId(self, datatype, client_guid, client_item_id):
- """Compute a unique server ID from a client-local ID tag.
-
- The resulting value is guaranteed not to collide with the other ID
- generation methods.
-
- Args:
- datatype: The sync type (python enum) of the identified object.
- client_guid: A globally unique ID that identifies the client which
- created this item.
- client_item_id: An ID that uniquely identifies this item on the client
- which created it.
- Returns:
- The string value of the computed server ID.
- """
- # Using the client ID info is not required here (we could instead generate
- # a random ID), but it's useful for debugging.
- return self._MakeCurrentId(datatype,
- '<server ID originally>%s/%s' % (client_guid, client_item_id))
-
- def _MakeCurrentId(self, datatype, inner_id):
- return '%d^%d^%s' % (datatype,
- self.migration_history.GetLatestVersion(datatype),
- inner_id)
-
- def _ExtractIdInfo(self, id_string):
- if not id_string or id_string == ROOT_ID:
- return None
- datatype_string, separator, remainder = id_string.partition('^')
- migration_version_string, separator, inner_id = remainder.partition('^')
- return (int(datatype_string), int(migration_version_string), inner_id)
-
- def _WritePosition(self, entry, parent_id):
- """Ensure the entry has an absolute, numeric position and parent_id.
-
- Historically, clients would specify positions using the predecessor-based
- references in the insert_after_item_id field; starting July 2011, this
- was changed and Chrome now sends up the absolute position. The server
- must store a position_in_parent value and must not maintain
- insert_after_item_id.
- Starting in Jan 2013, the client will also send up a unique_position field
- which should be saved and returned on subsequent GetUpdates.
-
- Args:
- entry: The entry for which to write a position. Its ID field are
- assumed to be server IDs. This entry will have its parent_id_string,
- position_in_parent and unique_position fields updated; its
- insert_after_item_id field will be cleared.
- parent_id: The ID of the entry intended as the new parent.
- """
-
- entry.parent_id_string = parent_id
- if not entry.HasField('position_in_parent'):
- entry.position_in_parent = 1337 # A debuggable, distinctive default.
- entry.ClearField('insert_after_item_id')
-
- def _ItemExists(self, id_string):
- """Determine whether an item exists in the changelog."""
- return id_string in self._entries
-
- def _CreatePermanentItem(self, spec):
- """Create one permanent item from its spec, if it doesn't exist.
-
- The resulting item is added to the changelog.
-
- Args:
- spec: A PermanentItem object holding the properties of the item to create.
- """
- id_string = self._ServerTagToId(spec.tag)
- if self._ItemExists(id_string):
- return
- print 'Creating permanent item: %s' % spec.name
- entry = sync_pb2.SyncEntity()
- entry.id_string = id_string
- entry.non_unique_name = spec.name
- entry.name = spec.name
- entry.server_defined_unique_tag = spec.tag
- entry.folder = True
- entry.deleted = False
- entry.specifics.CopyFrom(GetDefaultEntitySpecifics(spec.sync_type))
- self._WritePosition(entry, self._ServerTagToId(spec.parent_tag))
- self._SaveEntry(entry)
-
- def _CreateDefaultPermanentItems(self, requested_types):
- """Ensure creation of all default permanent items for a given set of types.
-
- Args:
- requested_types: A list of sync data types from ALL_TYPES.
- All default permanent items of only these types will be created.
- """
- for spec in self._PERMANENT_ITEM_SPECS:
- if spec.sync_type in requested_types and spec.create_by_default:
- self._CreatePermanentItem(spec)
-
- def ResetStoreBirthday(self):
- """Resets the store birthday to a random value."""
- # TODO(nick): uuid.uuid1() is better, but python 2.5 only.
- self.store_birthday = '%0.30f' % random.random()
-
- def StoreBirthday(self):
- """Gets the store birthday."""
- return self.store_birthday
-
- def GetChanges(self, sieve):
- """Get entries which have changed, oldest first.
-
- The returned entries are limited to being _BATCH_SIZE many. The entries
- are returned in strict version order.
-
- Args:
- sieve: An update sieve to use to filter out updates the client
- has already seen.
- Returns:
- A tuple of (version, entries, changes_remaining). Version is a new
- timestamp value, which should be used as the starting point for the
- next query. Entries is the batch of entries meeting the current
- timestamp query. Changes_remaining indicates the number of changes
- left on the server after this batch.
- """
- if not sieve.HasAnyTimestamp():
- return (0, [], 0)
- min_timestamp = sieve.GetMinTimestamp()
- first_time_types = sieve.GetFirstTimeTypes()
- self._CreateDefaultPermanentItems(first_time_types)
- # Mobile bookmark folder is not created by default, create it only when
- # client requested it.
- if (sieve.GetCreateMobileBookmarks() and
- first_time_types.count(BOOKMARK) > 0):
- self.TriggerCreateSyncedBookmarks()
-
- self.TriggerAcknowledgeManagedUsers()
-
- change_log = sorted(self._entries.values(),
- key=operator.attrgetter('version'))
- new_changes = [x for x in change_log if x.version > min_timestamp]
- # Pick batch_size new changes, and then filter them. This matches
- # the RPC behavior of the production sync server.
- batch = new_changes[:self._BATCH_SIZE]
- if not batch:
- # Client is up to date.
- return (min_timestamp, [], 0)
-
- # Restrict batch to requested types. Tombstones are untyped
- # and will always get included.
- filtered = [copy.deepcopy(item) for item in batch
- if item.deleted or sieve.ClientWantsItem(item)]
-
- # The new client timestamp is the timestamp of the last item in the
- # batch, even if that item was filtered out.
- return (batch[-1].version, filtered, len(new_changes) - len(batch))
-
- def GetKeystoreKeys(self):
- """Returns the encryption keys for this account."""
- print "Returning encryption keys: %s" % self._keys
- return self._keys
-
- def _CopyOverImmutableFields(self, entry):
- """Preserve immutable fields by copying pre-commit state.
-
- Args:
- entry: A sync entity from the client.
- """
- if entry.id_string in self._entries:
- if self._entries[entry.id_string].HasField(
- 'server_defined_unique_tag'):
- entry.server_defined_unique_tag = (
- self._entries[entry.id_string].server_defined_unique_tag)
-
- def _CheckVersionForCommit(self, entry):
- """Perform an optimistic concurrency check on the version number.
-
- Clients are only allowed to commit if they report having seen the most
- recent version of an object.
-
- Args:
- entry: A sync entity from the client. It is assumed that ID fields
- have been converted to server IDs.
- Returns:
- A boolean value indicating whether the client's version matches the
- newest server version for the given entry.
- """
- if entry.id_string in self._entries:
- # Allow edits/deletes if the version matches, and any undeletion.
- return (self._entries[entry.id_string].version == entry.version or
- self._entries[entry.id_string].deleted)
- else:
- # Allow unknown ID only if the client thinks it's new too.
- return entry.version == 0
-
- def _CheckParentIdForCommit(self, entry):
- """Check that the parent ID referenced in a SyncEntity actually exists.
-
- Args:
- entry: A sync entity from the client. It is assumed that ID fields
- have been converted to server IDs.
- Returns:
- A boolean value indicating whether the entity's parent ID is an object
- that actually exists (and is not deleted) in the current account state.
- """
- if entry.parent_id_string == ROOT_ID:
- # This is generally allowed.
- return True
- if (not entry.HasField('parent_id_string') and
- entry.HasField('client_defined_unique_tag')):
- return True # Unique client tag items do not need to specify a parent.
- if entry.parent_id_string not in self._entries:
- print 'Warning: Client sent unknown ID. Should never happen.'
- return False
- if entry.parent_id_string == entry.id_string:
- print 'Warning: Client sent circular reference. Should never happen.'
- return False
- if self._entries[entry.parent_id_string].deleted:
- # This can happen in a race condition between two clients.
- return False
- if not self._entries[entry.parent_id_string].folder:
- print 'Warning: Client sent non-folder parent. Should never happen.'
- return False
- return True
-
- def _RewriteIdsAsServerIds(self, entry, cache_guid, commit_session):
- """Convert ID fields in a client sync entry to server IDs.
-
- A commit batch sent by a client may contain new items for which the
- server has not generated IDs yet. And within a commit batch, later
- items are allowed to refer to earlier items. This method will
- generate server IDs for new items, as well as rewrite references
- to items whose server IDs were generated earlier in the batch.
-
- Args:
- entry: The client sync entry to modify.
- cache_guid: The globally unique ID of the client that sent this
- commit request.
- commit_session: A dictionary mapping the original IDs to the new server
- IDs, for any items committed earlier in the batch.
- """
- if entry.version == 0:
- data_type = GetEntryType(entry)
- if entry.HasField('client_defined_unique_tag'):
- # When present, this should determine the item's ID.
- new_id = self._ClientTagToId(data_type, entry.client_defined_unique_tag)
- else:
- new_id = self._ClientIdToId(data_type, cache_guid, entry.id_string)
- entry.originator_cache_guid = cache_guid
- entry.originator_client_item_id = entry.id_string
- commit_session[entry.id_string] = new_id # Remember the remapping.
- entry.id_string = new_id
- if entry.parent_id_string in commit_session:
- entry.parent_id_string = commit_session[entry.parent_id_string]
- if entry.insert_after_item_id in commit_session:
- entry.insert_after_item_id = commit_session[entry.insert_after_item_id]
-
- def ValidateCommitEntries(self, entries):
- """Raise an exception if a commit batch contains any global errors.
-
- Arguments:
- entries: an iterable containing commit-form SyncEntity protocol buffers.
-
- Raises:
- MigrationDoneError: if any of the entries reference a recently-migrated
- datatype.
- """
- server_ids_in_commit = set()
- local_ids_in_commit = set()
- for entry in entries:
- if entry.version:
- server_ids_in_commit.add(entry.id_string)
- else:
- local_ids_in_commit.add(entry.id_string)
- if entry.HasField('parent_id_string'):
- if entry.parent_id_string not in local_ids_in_commit:
- server_ids_in_commit.add(entry.parent_id_string)
-
- versions_present = {}
- for server_id in server_ids_in_commit:
- parsed = self._ExtractIdInfo(server_id)
- if parsed:
- datatype, version, _ = parsed
- versions_present.setdefault(datatype, []).append(version)
-
- self.migration_history.CheckAllCurrent(
- dict((k, min(v)) for k, v in versions_present.iteritems()))
-
- def CommitEntry(self, entry, cache_guid, commit_session):
- """Attempt to commit one entry to the user's account.
-
- Args:
- entry: A SyncEntity protobuf representing desired object changes.
- cache_guid: A string value uniquely identifying the client; this
- is used for ID generation and will determine the originator_cache_guid
- if the entry is new.
- commit_session: A dictionary mapping client IDs to server IDs for any
- objects committed earlier this session. If the entry gets a new ID
- during commit, the change will be recorded here.
- Returns:
- A SyncEntity reflecting the post-commit value of the entry, or None
- if the entry was not committed due to an error.
- """
- entry = copy.deepcopy(entry)
-
- # Generate server IDs for this entry, and write generated server IDs
- # from earlier entries into the message's fields, as appropriate. The
- # ID generation state is stored in 'commit_session'.
- self._RewriteIdsAsServerIds(entry, cache_guid, commit_session)
-
- # Sets the parent ID field for a client-tagged item. The client is allowed
- # to not specify parents for these types of items. The server can figure
- # out on its own what the parent ID for this entry should be.
- self._RewriteParentIdForUniqueClientEntry(entry)
-
- # Perform the optimistic concurrency check on the entry's version number.
- # Clients are not allowed to commit unless they indicate that they've seen
- # the most recent version of an object.
- if not self._CheckVersionForCommit(entry):
- return None
-
- # Check the validity of the parent ID; it must exist at this point.
- # TODO(nick): Implement cycle detection and resolution.
- if not self._CheckParentIdForCommit(entry):
- return None
-
- self._CopyOverImmutableFields(entry);
-
- # At this point, the commit is definitely going to happen.
-
- # Deletion works by storing a limited record for an entry, called a
- # tombstone. A sync server must track deleted IDs forever, since it does
- # not keep track of client knowledge (there's no deletion ACK event).
- if entry.deleted:
- def MakeTombstone(id_string, datatype):
- """Make a tombstone entry that will replace the entry being deleted.
-
- Args:
- id_string: Index of the SyncEntity to be deleted.
- Returns:
- A new SyncEntity reflecting the fact that the entry is deleted.
- """
- # Only the ID, version and deletion state are preserved on a tombstone.
- tombstone = sync_pb2.SyncEntity()
- tombstone.id_string = id_string
- tombstone.deleted = True
- tombstone.name = ''
- tombstone.specifics.CopyFrom(GetDefaultEntitySpecifics(datatype))
- return tombstone
-
- def IsChild(child_id):
- """Check if a SyncEntity is a child of entry, or any of its children.
-
- Args:
- child_id: Index of the SyncEntity that is a possible child of entry.
- Returns:
- True if it is a child; false otherwise.
- """
- if child_id not in self._entries:
- return False
- if self._entries[child_id].parent_id_string == entry.id_string:
- return True
- return IsChild(self._entries[child_id].parent_id_string)
-
- # Identify any children entry might have.
- child_ids = [child.id_string for child in self._entries.itervalues()
- if IsChild(child.id_string)]
-
- # Mark all children that were identified as deleted.
- for child_id in child_ids:
- datatype = GetEntryType(self._entries[child_id])
- self._SaveEntry(MakeTombstone(child_id, datatype))
-
- # Delete entry itself.
- datatype = GetEntryType(self._entries[entry.id_string])
- entry = MakeTombstone(entry.id_string, datatype)
- else:
- # Comments in sync.proto detail how the representation of positional
- # ordering works.
- #
- # We've almost fully deprecated the 'insert_after_item_id' field.
- # The 'position_in_parent' field is also deprecated, but as of Jan 2013
- # is still in common use. The 'unique_position' field is the latest
- # and greatest in positioning technology.
- #
- # This server supports 'position_in_parent' and 'unique_position'.
- self._WritePosition(entry, entry.parent_id_string)
-
- # Preserve the originator info, which the client is not required to send
- # when updating.
- base_entry = self._entries.get(entry.id_string)
- if base_entry and not entry.HasField('originator_cache_guid'):
- entry.originator_cache_guid = base_entry.originator_cache_guid
- entry.originator_client_item_id = base_entry.originator_client_item_id
-
- # Store the current time since the Unix epoch in milliseconds.
- entry.mtime = (int((time.mktime(time.gmtime()) -
- (time.mktime(FIRST_DAY_UNIX_TIME_EPOCH) - ONE_DAY_SECONDS))*1000))
-
- # Commit the change. This also updates the version number.
- self._SaveEntry(entry)
- return entry
-
- def _RewriteVersionInId(self, id_string):
- """Rewrites an ID so that its migration version becomes current."""
- parsed_id = self._ExtractIdInfo(id_string)
- if not parsed_id:
- return id_string
- datatype, old_migration_version, inner_id = parsed_id
- return self._MakeCurrentId(datatype, inner_id)
-
- def _RewriteParentIdForUniqueClientEntry(self, entry):
- """Sets the entry's parent ID field to the appropriate value.
-
- The client must always set enough of the specifics of the entries it sends
- up such that the server can identify its type. (See crbug.com/373859)
-
- The client is under no obligation to set the parent ID field. The server
- can always infer what the appropriate parent for this model type should be.
- Having the client not send the parent ID is a step towards the removal of
- type root nodes. (See crbug.com/373869)
-
- This server implements these features by "faking" the existing of a parent
- ID early on in the commit processing.
-
- This function has no effect on non-client-tagged items.
- """
- if not entry.HasField('client_defined_unique_tag'):
- return # Skip this processing for non-client-tagged types.
- data_type = GetEntryType(entry)
- entry.parent_id_string = self._TypeToTypeRootId(data_type)
-
- def TriggerMigration(self, datatypes):
- """Cause a migration to occur for a set of datatypes on this account.
-
- Clients will see the MIGRATION_DONE error for these datatypes until they
- resync them.
- """
- versions_to_remap = self.migration_history.Bump(datatypes)
- all_entries = self._entries.values()
- self._entries.clear()
- for entry in all_entries:
- new_id = self._RewriteVersionInId(entry.id_string)
- entry.id_string = new_id
- if entry.HasField('parent_id_string'):
- entry.parent_id_string = self._RewriteVersionInId(
- entry.parent_id_string)
- self._entries[entry.id_string] = entry
-
- def TriggerSyncTabFavicons(self):
- """Set the 'sync_tab_favicons' field to this account's nigori node.
-
- If the field is not currently set, will write a new nigori node entry
- with the field set. Else does nothing.
- """
-
- nigori_tag = "google_chrome_nigori"
- nigori_original = self._entries.get(self._ServerTagToId(nigori_tag))
- if (nigori_original.specifics.nigori.sync_tab_favicons):
- return
- nigori_new = copy.deepcopy(nigori_original)
- nigori_new.specifics.nigori.sync_tabs = True
- self._SaveEntry(nigori_new)
-
- def TriggerCreateSyncedBookmarks(self):
- """Create the Synced Bookmarks folder under the Bookmarks permanent item.
-
- Clients will then receive the Synced Bookmarks folder on future
- GetUpdates, and new bookmarks can be added within the Synced Bookmarks
- folder.
- """
-
- synced_bookmarks_spec, = [spec for spec in self._PERMANENT_ITEM_SPECS
- if spec.name == "Synced Bookmarks"]
- self._CreatePermanentItem(synced_bookmarks_spec)
-
- def TriggerEnableKeystoreEncryption(self):
- """Create the keystore_encryption experiment entity and enable it.
-
- A new entity within the EXPERIMENTS datatype is created with the unique
- client tag "keystore_encryption" if it doesn't already exist. The
- keystore_encryption message is then filled with |enabled| set to true.
- """
-
- experiment_id = self._ServerTagToId("google_chrome_experiments")
- keystore_encryption_id = self._ClientTagToId(
- EXPERIMENTS,
- KEYSTORE_ENCRYPTION_EXPERIMENT_TAG)
- keystore_entry = self._entries.get(keystore_encryption_id)
- if keystore_entry is None:
- keystore_entry = sync_pb2.SyncEntity()
- keystore_entry.id_string = keystore_encryption_id
- keystore_entry.name = "Keystore Encryption"
- keystore_entry.client_defined_unique_tag = (
- KEYSTORE_ENCRYPTION_EXPERIMENT_TAG)
- keystore_entry.folder = False
- keystore_entry.deleted = False
- keystore_entry.specifics.CopyFrom(GetDefaultEntitySpecifics(EXPERIMENTS))
- self._WritePosition(keystore_entry, experiment_id)
-
- keystore_entry.specifics.experiments.keystore_encryption.enabled = True
-
- self._SaveEntry(keystore_entry)
-
- def TriggerRotateKeystoreKeys(self):
- """Rotate the current set of keystore encryption keys.
-
- |self._keys| will have a new random encryption key appended to it. We touch
- the nigori node so that each client will receive the new encryption keys
- only once.
- """
-
- # Add a new encryption key.
- self._keys += [MakeNewKeystoreKey(), ]
-
- # Increment the nigori node's timestamp, so clients will get the new keys
- # on their next GetUpdates (any time the nigori node is sent back, we also
- # send back the keystore keys).
- nigori_tag = "google_chrome_nigori"
- self._SaveEntry(self._entries.get(self._ServerTagToId(nigori_tag)))
-
- def TriggerAcknowledgeManagedUsers(self):
- """Set the "acknowledged" flag for any managed user entities that don't have
- it set already.
- """
-
- if not self.acknowledge_managed_users:
- return
-
- managed_users = [copy.deepcopy(entry) for entry in self._entries.values()
- if entry.specifics.HasField('managed_user')
- and not entry.specifics.managed_user.acknowledged]
- for user in managed_users:
- user.specifics.managed_user.acknowledged = True
- self._SaveEntry(user)
-
- def TriggerEnablePreCommitGetUpdateAvoidance(self):
- """Sets the experiment to enable pre-commit GetUpdate avoidance."""
- experiment_id = self._ServerTagToId("google_chrome_experiments")
- pre_commit_gu_avoidance_id = self._ClientTagToId(
- EXPERIMENTS,
- PRE_COMMIT_GU_AVOIDANCE_EXPERIMENT_TAG)
- entry = self._entries.get(pre_commit_gu_avoidance_id)
- if entry is None:
- entry = sync_pb2.SyncEntity()
- entry.id_string = pre_commit_gu_avoidance_id
- entry.name = "Pre-commit GU avoidance"
- entry.client_defined_unique_tag = PRE_COMMIT_GU_AVOIDANCE_EXPERIMENT_TAG
- entry.folder = False
- entry.deleted = False
- entry.specifics.CopyFrom(GetDefaultEntitySpecifics(EXPERIMENTS))
- self._WritePosition(entry, experiment_id)
- entry.specifics.experiments.pre_commit_update_avoidance.enabled = True
- self._SaveEntry(entry)
-
- def SetInducedError(self, error, error_frequency,
- sync_count_before_errors):
- self.induced_error = error
- self.induced_error_frequency = error_frequency
- self.sync_count_before_errors = sync_count_before_errors
-
- def GetInducedError(self):
- return self.induced_error
-
- def _GetNextVersionNumber(self):
- """Set the version to one more than the greatest version number seen."""
- entries = sorted(self._entries.values(), key=operator.attrgetter('version'))
- if len(entries) < 1:
- raise ClientNotConnectedError
- return entries[-1].version + 1
-
-
-class TestServer(object):
- """An object to handle requests for one (and only one) Chrome Sync account.
-
- TestServer consumes the sync command messages that are the outermost
- layers of the protocol, performs the corresponding actions on its
- SyncDataModel, and constructs an appropriate response message.
- """
-
- def __init__(self):
- # The implementation supports exactly one account; its state is here.
- self.account = SyncDataModel()
- self.account_lock = threading.Lock()
- # Clients that have talked to us: a map from the full client ID
- # to its nickname.
- self.clients = {}
- self.client_name_generator = ('+' * times + chr(c)
- for times in xrange(0, sys.maxint) for c in xrange(ord('A'), ord('Z')))
- self.transient_error = False
- self.sync_count = 0
- # Gaia OAuth2 Token fields and their default values.
- self.response_code = 200
- self.request_token = 'rt1'
- self.access_token = 'at1'
- self.expires_in = 3600
- self.token_type = 'Bearer'
- # The ClientCommand to send back on each ServerToClientResponse. If set to
- # None, no ClientCommand should be sent.
- self._client_command = None
-
-
- def GetShortClientName(self, query):
- parsed = cgi.parse_qs(query[query.find('?')+1:])
- client_id = parsed.get('client_id')
- if not client_id:
- return '?'
- client_id = client_id[0]
- if client_id not in self.clients:
- self.clients[client_id] = self.client_name_generator.next()
- return self.clients[client_id]
-
- def CheckStoreBirthday(self, request):
- """Raises StoreBirthdayError if the request's birthday is a mismatch."""
- if not request.HasField('store_birthday'):
- return
- if self.account.StoreBirthday() != request.store_birthday:
- raise StoreBirthdayError
-
- def CheckTransientError(self):
- """Raises TransientError if transient_error variable is set."""
- if self.transient_error:
- raise TransientError
-
- def CheckSendError(self):
- """Raises SyncInducedError if needed."""
- if (self.account.induced_error.error_type !=
- sync_enums_pb2.SyncEnums.UNKNOWN):
- # Always means return the given error for all requests.
- if self.account.induced_error_frequency == ERROR_FREQUENCY_ALWAYS:
- raise SyncInducedError
- # This means the FIRST 2 requests of every 3 requests
- # return an error. Don't switch the order of failures. There are
- # test cases that rely on the first 2 being the failure rather than
- # the last 2.
- elif (self.account.induced_error_frequency ==
- ERROR_FREQUENCY_TWO_THIRDS):
- if (((self.sync_count -
- self.account.sync_count_before_errors) % 3) != 0):
- raise SyncInducedError
- else:
- raise InducedErrorFrequencyNotDefined
-
- def HandleMigrate(self, path):
- query = urlparse.urlparse(path)[4]
- code = 200
- self.account_lock.acquire()
- try:
- datatypes = [DataTypeStringToSyncTypeLoose(x)
- for x in urlparse.parse_qs(query).get('type',[])]
- if datatypes:
- self.account.TriggerMigration(datatypes)
- response = 'Migrated datatypes %s' % (
- ' and '.join(SyncTypeToString(x).upper() for x in datatypes))
- else:
- response = 'Please specify one or more <i>type=name</i> parameters'
- code = 400
- except DataTypeIdNotRecognized, error:
- response = 'Could not interpret datatype name'
- code = 400
- finally:
- self.account_lock.release()
- return (code, '<html><title>Migration: %d</title><H1>%d %s</H1></html>' %
- (code, code, response))
-
- def HandleSetInducedError(self, path):
- query = urlparse.urlparse(path)[4]
- self.account_lock.acquire()
- code = 200
- response = 'Success'
- error = sync_pb2.ClientToServerResponse.Error()
- try:
- error_type = urlparse.parse_qs(query)['error']
- action = urlparse.parse_qs(query)['action']
- error.error_type = int(error_type[0])
- error.action = int(action[0])
- try:
- error.url = (urlparse.parse_qs(query)['url'])[0]
- except KeyError:
- error.url = ''
- try:
- error.error_description =(
- (urlparse.parse_qs(query)['error_description'])[0])
- except KeyError:
- error.error_description = ''
- try:
- error_frequency = int((urlparse.parse_qs(query)['frequency'])[0])
- except KeyError:
- error_frequency = ERROR_FREQUENCY_ALWAYS
- self.account.SetInducedError(error, error_frequency, self.sync_count)
- response = ('Error = %d, action = %d, url = %s, description = %s' %
- (error.error_type, error.action,
- error.url,
- error.error_description))
- except error:
- response = 'Could not parse url'
- code = 400
- finally:
- self.account_lock.release()
- return (code, '<html><title>SetError: %d</title><H1>%d %s</H1></html>' %
- (code, code, response))
-
- def HandleCreateBirthdayError(self):
- self.account.ResetStoreBirthday()
- return (
- 200,
- '<html><title>Birthday error</title><H1>Birthday error</H1></html>')
-
- def HandleSetTransientError(self):
- self.transient_error = True
- return (
- 200,
- '<html><title>Transient error</title><H1>Transient error</H1></html>')
-
- def HandleSetSyncTabFavicons(self):
- """Set 'sync_tab_favicons' field of the nigori node for this account."""
- self.account.TriggerSyncTabFavicons()
- return (
- 200,
- '<html><title>Tab Favicons</title><H1>Tab Favicons</H1></html>')
-
- def HandleCreateSyncedBookmarks(self):
- """Create the Synced Bookmarks folder under Bookmarks."""
- self.account.TriggerCreateSyncedBookmarks()
- return (
- 200,
- '<html><title>Synced Bookmarks</title><H1>Synced Bookmarks</H1></html>')
-
- def HandleEnableKeystoreEncryption(self):
- """Enables the keystore encryption experiment."""
- self.account.TriggerEnableKeystoreEncryption()
- return (
- 200,
- '<html><title>Enable Keystore Encryption</title>'
- '<H1>Enable Keystore Encryption</H1></html>')
-
- def HandleRotateKeystoreKeys(self):
- """Rotate the keystore encryption keys."""
- self.account.TriggerRotateKeystoreKeys()
- return (
- 200,
- '<html><title>Rotate Keystore Keys</title>'
- '<H1>Rotate Keystore Keys</H1></html>')
-
- def HandleEnableManagedUserAcknowledgement(self):
- """Enable acknowledging newly created managed users."""
- self.account.acknowledge_managed_users = True
- return (
- 200,
- '<html><title>Enable Managed User Acknowledgement</title>'
- '<h1>Enable Managed User Acknowledgement</h1></html>')
-
- def HandleEnablePreCommitGetUpdateAvoidance(self):
- """Enables the pre-commit GU avoidance experiment."""
- self.account.TriggerEnablePreCommitGetUpdateAvoidance()
- return (
- 200,
- '<html><title>Enable pre-commit GU avoidance</title>'
- '<H1>Enable pre-commit GU avoidance</H1></html>')
-
- def HandleCommand(self, query, raw_request):
- """Decode and handle a sync command from a raw input of bytes.
-
- This is the main entry point for this class. It is safe to call this
- method from multiple threads.
-
- Args:
- raw_request: An iterable byte sequence to be interpreted as a sync
- protocol command.
- Returns:
- A tuple (response_code, raw_response); the first value is an HTTP
- result code, while the second value is a string of bytes which is the
- serialized reply to the command.
- """
- self.account_lock.acquire()
- self.sync_count += 1
- def print_context(direction):
- print '[Client %s %s %s.py]' % (self.GetShortClientName(query), direction,
- __name__),
-
- try:
- request = sync_pb2.ClientToServerMessage()
- request.MergeFromString(raw_request)
- contents = request.message_contents
-
- response = sync_pb2.ClientToServerResponse()
- response.error_code = sync_enums_pb2.SyncEnums.SUCCESS
-
- if self._client_command:
- response.client_command.CopyFrom(self._client_command)
-
- self.CheckStoreBirthday(request)
- response.store_birthday = self.account.store_birthday
- self.CheckTransientError()
- self.CheckSendError()
-
- print_context('->')
-
- if contents == sync_pb2.ClientToServerMessage.AUTHENTICATE:
- print 'Authenticate'
- # We accept any authentication token, and support only one account.
- # TODO(nick): Mock out the GAIA authentication as well; hook up here.
- response.authenticate.user.email = 'syncjuser@chromium'
- response.authenticate.user.display_name = 'Sync J User'
- elif contents == sync_pb2.ClientToServerMessage.COMMIT:
- print 'Commit %d item(s)' % len(request.commit.entries)
- self.HandleCommit(request.commit, response.commit)
- elif contents == sync_pb2.ClientToServerMessage.GET_UPDATES:
- print 'GetUpdates',
- self.HandleGetUpdates(request.get_updates, response.get_updates)
- print_context('<-')
- print '%d update(s)' % len(response.get_updates.entries)
- else:
- print 'Unrecognizable sync request!'
- return (400, None) # Bad request.
- return (200, response.SerializeToString())
- except MigrationDoneError, error:
- print_context('<-')
- print 'MIGRATION_DONE: <%s>' % (ShortDatatypeListSummary(error.datatypes))
- response = sync_pb2.ClientToServerResponse()
- response.store_birthday = self.account.store_birthday
- response.error_code = sync_enums_pb2.SyncEnums.MIGRATION_DONE
- response.migrated_data_type_id[:] = [
- SyncTypeToProtocolDataTypeId(x) for x in error.datatypes]
- return (200, response.SerializeToString())
- except StoreBirthdayError, error:
- print_context('<-')
- print 'NOT_MY_BIRTHDAY'
- response = sync_pb2.ClientToServerResponse()
- response.store_birthday = self.account.store_birthday
- response.error_code = sync_enums_pb2.SyncEnums.NOT_MY_BIRTHDAY
- return (200, response.SerializeToString())
- except TransientError, error:
- ### This is deprecated now. Would be removed once test cases are removed.
- print_context('<-')
- print 'TRANSIENT_ERROR'
- response.store_birthday = self.account.store_birthday
- response.error_code = sync_enums_pb2.SyncEnums.TRANSIENT_ERROR
- return (200, response.SerializeToString())
- except SyncInducedError, error:
- print_context('<-')
- print 'INDUCED_ERROR'
- response.store_birthday = self.account.store_birthday
- error = self.account.GetInducedError()
- response.error.error_type = error.error_type
- response.error.url = error.url
- response.error.error_description = error.error_description
- response.error.action = error.action
- return (200, response.SerializeToString())
- finally:
- self.account_lock.release()
-
- def HandleCommit(self, commit_message, commit_response):
- """Respond to a Commit request by updating the user's account state.
-
- Commit attempts stop after the first error, returning a CONFLICT result
- for any unattempted entries.
-
- Args:
- commit_message: A sync_pb.CommitMessage protobuf holding the content
- of the client's request.
- commit_response: A sync_pb.CommitResponse protobuf into which a reply
- to the client request will be written.
- """
- commit_response.SetInParent()
- batch_failure = False
- session = {} # Tracks ID renaming during the commit operation.
- guid = commit_message.cache_guid
-
- self.account.ValidateCommitEntries(commit_message.entries)
-
- for entry in commit_message.entries:
- server_entry = None
- if not batch_failure:
- # Try to commit the change to the account.
- server_entry = self.account.CommitEntry(entry, guid, session)
-
- # An entryresponse is returned in both success and failure cases.
- reply = commit_response.entryresponse.add()
- if not server_entry:
- reply.response_type = sync_pb2.CommitResponse.CONFLICT
- reply.error_message = 'Conflict.'
- batch_failure = True # One failure halts the batch.
- else:
- reply.response_type = sync_pb2.CommitResponse.SUCCESS
- # These are the properties that the server is allowed to override
- # during commit; the client wants to know their values at the end
- # of the operation.
- reply.id_string = server_entry.id_string
- if not server_entry.deleted:
- # Note: the production server doesn't actually send the
- # parent_id_string on commit responses, so we don't either.
- reply.position_in_parent = server_entry.position_in_parent
- reply.version = server_entry.version
- reply.name = server_entry.name
- reply.non_unique_name = server_entry.non_unique_name
- else:
- reply.version = entry.version + 1
-
- def HandleGetUpdates(self, update_request, update_response):
- """Respond to a GetUpdates request by querying the user's account.
-
- Args:
- update_request: A sync_pb.GetUpdatesMessage protobuf holding the content
- of the client's request.
- update_response: A sync_pb.GetUpdatesResponse protobuf into which a reply
- to the client request will be written.
- """
- update_response.SetInParent()
- update_sieve = UpdateSieve(update_request, self.account.migration_history)
-
- print CallerInfoToString(update_request.caller_info.source),
- print update_sieve.SummarizeRequest()
-
- update_sieve.CheckMigrationState()
-
- new_timestamp, entries, remaining = self.account.GetChanges(update_sieve)
-
- update_response.changes_remaining = remaining
- sending_nigori_node = False
- for entry in entries:
- if entry.name == 'Nigori':
- sending_nigori_node = True
- reply = update_response.entries.add()
- reply.CopyFrom(entry)
- update_sieve.SaveProgress(new_timestamp, update_response)
-
- if update_request.need_encryption_key or sending_nigori_node:
- update_response.encryption_keys.extend(self.account.GetKeystoreKeys())
-
- def HandleGetOauth2Token(self):
- return (int(self.response_code),
- '{\n'
- ' \"refresh_token\": \"' + self.request_token + '\",\n'
- ' \"access_token\": \"' + self.access_token + '\",\n'
- ' \"expires_in\": ' + str(self.expires_in) + ',\n'
- ' \"token_type\": \"' + self.token_type +'\"\n'
- '}')
-
- def HandleSetOauth2Token(self, response_code, request_token, access_token,
- expires_in, token_type):
- if response_code != 0:
- self.response_code = response_code
- if request_token != '':
- self.request_token = request_token
- if access_token != '':
- self.access_token = access_token
- if expires_in != 0:
- self.expires_in = expires_in
- if token_type != '':
- self.token_type = token_type
-
- return (200,
- '<html><title>Set OAuth2 Token</title>'
- '<H1>This server will now return the OAuth2 Token:</H1>'
- '<p>response_code: ' + str(self.response_code) + '</p>'
- '<p>request_token: ' + self.request_token + '</p>'
- '<p>access_token: ' + self.access_token + '</p>'
- '<p>expires_in: ' + str(self.expires_in) + '</p>'
- '<p>token_type: ' + self.token_type + '</p>'
- '</html>')
-
- def CustomizeClientCommand(self, sessions_commit_delay_seconds):
- """Customizes the value of the ClientCommand of ServerToClientResponse.
-
- Currently, this only allows for changing the sessions_commit_delay_seconds
- field.
-
- Args:
- sessions_commit_delay_seconds: The desired sync delay time for sessions.
- """
- if not self._client_command:
- self._client_command = client_commands_pb2.ClientCommand()
-
- self._client_command.sessions_commit_delay_seconds = \
- sessions_commit_delay_seconds
- return self._client_command
« no previous file with comments | « sync/tools/testserver/DEPS ('k') | sync/tools/testserver/chromiumsync_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698