| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/sync/engine/syncer_proto_util.h" |
| |
| #include <map> |
| #include <optional> |
| |
| #include "base/format_macros.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/notimplemented.h" |
| #include "base/notreached.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/time/time.h" |
| #include "components/sync/base/data_type.h" |
| #include "components/sync/base/time.h" |
| #include "components/sync/engine/cycle/sync_cycle_context.h" |
| #include "components/sync/engine/net/server_connection_manager.h" |
| #include "components/sync/engine/sync_protocol_error.h" |
| #include "components/sync/engine/syncer.h" |
| #include "components/sync/engine/traffic_logger.h" |
| #include "components/sync/protocol/data_type_progress_marker.pb.h" |
| #include "components/sync/protocol/sync_enums.pb.h" |
| #include "google_apis/google_api_keys.h" |
| #include "net/http/http_status_code.h" |
| |
| using std::string; |
| using std::stringstream; |
| using sync_pb::ClientToServerMessage; |
| using sync_pb::ClientToServerResponse; |
| |
| namespace syncer { |
| namespace { |
| |
| // Time to backoff syncing after receiving a throttled response. |
| constexpr base::TimeDelta kSyncDelayAfterThrottled = base::Hours(2); |
| |
| SyncerError ServerConnectionErrorAsSyncerError( |
| const HttpResponse::ServerConnectionCode server_status, |
| int net_error_code, |
| int http_status_code) { |
| switch (server_status) { |
| case HttpResponse::CONNECTION_UNAVAILABLE: |
| return SyncerError::NetworkError(net_error_code); |
| case HttpResponse::SYNC_SERVER_ERROR: |
| case HttpResponse::SYNC_AUTH_ERROR: |
| // This means the server returned an HTTP error. |
| return SyncerError::HttpError( |
| static_cast<net::HttpStatusCode>(http_status_code)); |
| case HttpResponse::SERVER_CONNECTION_OK: |
| case HttpResponse::NONE: |
| NOTREACHED(); |
| } |
| } |
| |
| SyncProtocolErrorType PBErrorTypeToSyncProtocolErrorType( |
| const sync_pb::SyncEnums::ErrorType& error_type) { |
| switch (error_type) { |
| case sync_pb::SyncEnums::SUCCESS: |
| return SYNC_SUCCESS; |
| case sync_pb::SyncEnums::NOT_MY_BIRTHDAY: |
| return NOT_MY_BIRTHDAY; |
| case sync_pb::SyncEnums::THROTTLED: |
| return THROTTLED; |
| case sync_pb::SyncEnums::TRANSIENT_ERROR: |
| return TRANSIENT_ERROR; |
| case sync_pb::SyncEnums::MIGRATION_DONE: |
| return MIGRATION_DONE; |
| case sync_pb::SyncEnums::DISABLED_BY_ADMIN: |
| return DISABLED_BY_ADMIN; |
| case sync_pb::SyncEnums::PARTIAL_FAILURE: |
| return PARTIAL_FAILURE; |
| case sync_pb::SyncEnums::CLIENT_DATA_OBSOLETE: |
| return CLIENT_DATA_OBSOLETE; |
| case sync_pb::SyncEnums::UNKNOWN: |
| return UNKNOWN_ERROR; |
| case sync_pb::SyncEnums::ENCRYPTION_OBSOLETE: |
| return ENCRYPTION_OBSOLETE; |
| } |
| |
| NOTREACHED(); |
| } |
| |
| ClientAction PBActionToClientAction(const sync_pb::SyncEnums::Action& action) { |
| switch (action) { |
| case sync_pb::SyncEnums::UPGRADE_CLIENT: |
| return UPGRADE_CLIENT; |
| case sync_pb::SyncEnums::UNKNOWN_ACTION: |
| return UNKNOWN_ACTION; |
| } |
| |
| NOTREACHED(); |
| } |
| |
| // Returns true iff `message` is an initial GetUpdates request. |
| bool IsVeryFirstGetUpdates(const ClientToServerMessage& message) { |
| if (!message.has_get_updates()) { |
| return false; |
| } |
| DCHECK_LT(0, message.get_updates().from_progress_marker_size()); |
| for (int i = 0; i < message.get_updates().from_progress_marker_size(); ++i) { |
| if (!message.get_updates().from_progress_marker(i).token().empty()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| // Returns true iff `message` should contain a store birthday. |
| bool IsBirthdayRequired(const ClientToServerMessage& message) { |
| if (message.has_clear_server_data()) { |
| return false; |
| } |
| if (message.has_commit()) { |
| return true; |
| } |
| if (message.has_get_updates()) { |
| return !IsVeryFirstGetUpdates(message); |
| } |
| NOTIMPLEMENTED(); |
| return true; |
| } |
| |
| SyncProtocolError ErrorCodeToSyncProtocolError( |
| const sync_pb::SyncEnums::ErrorType& error_type) { |
| SyncProtocolError error; |
| error.error_type = PBErrorTypeToSyncProtocolErrorType(error_type); |
| if (error_type == sync_pb::SyncEnums::NOT_MY_BIRTHDAY || |
| error_type == sync_pb::SyncEnums::ENCRYPTION_OBSOLETE) { |
| error.action = DISABLE_SYNC_ON_CLIENT; |
| } else if (error_type == sync_pb::SyncEnums::CLIENT_DATA_OBSOLETE) { |
| error.action = RESET_LOCAL_SYNC_DATA; |
| } else if (error_type == sync_pb::SyncEnums::DISABLED_BY_ADMIN) { |
| error.action = STOP_SYNC_FOR_DISABLED_ACCOUNT; |
| } // There is no other action we can compute for legacy server. |
| return error; |
| } |
| |
| // Verifies the store birthday, alerting/resetting as appropriate if there's a |
| // mismatch. Return false if the syncer should be stuck. |
| bool ProcessResponseBirthday(const ClientToServerResponse& response, |
| SyncCycleContext* context) { |
| const std::string& local_birthday = context->birthday(); |
| |
| if (local_birthday.empty()) { |
| if (!response.has_store_birthday()) { |
| DLOG(WARNING) << "Expected a birthday on first sync."; |
| return false; |
| } |
| |
| DVLOG(1) << "New store birthday: " << response.store_birthday(); |
| context->set_birthday(response.store_birthday()); |
| return true; |
| } |
| |
| // Error situation, but we're not stuck. |
| if (!response.has_store_birthday()) { |
| DLOG(WARNING) << "No birthday in server response?"; |
| return true; |
| } |
| |
| if (response.store_birthday() != local_birthday) { |
| DLOG(WARNING) << "Birthday changed, showing syncer stuck"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void SaveBagOfChipsFromResponse(const sync_pb::ClientToServerResponse& response, |
| SyncCycleContext* context) { |
| if (!response.has_new_bag_of_chips()) { |
| return; |
| } |
| std::string bag_of_chips; |
| if (response.new_bag_of_chips().SerializeToString(&bag_of_chips)) { |
| context->set_bag_of_chips(bag_of_chips); |
| } |
| } |
| |
| // Handle client commands returned by the server. |
| void ProcessClientCommand(const sync_pb::ClientCommand& command, |
| SyncCycle* cycle) { |
| CHECK(cycle); |
| |
| // Update our state for any other commands we've received. |
| if (command.has_max_commit_batch_size()) { |
| cycle->context()->set_max_commit_batch_size( |
| command.max_commit_batch_size()); |
| } |
| |
| if (command.has_set_sync_poll_interval()) { |
| base::TimeDelta interval = base::Seconds(command.set_sync_poll_interval()); |
| if (interval.is_zero()) { |
| DLOG(WARNING) << "Received zero poll interval from server. Ignoring."; |
| } else { |
| cycle->context()->set_poll_interval(interval); |
| cycle->delegate()->OnReceivedPollIntervalUpdate(interval); |
| } |
| } |
| |
| if (command.custom_nudge_delays_size() > 0) { |
| std::map<DataType, base::TimeDelta> delay_map; |
| for (int i = 0; i < command.custom_nudge_delays_size(); ++i) { |
| DataType type = GetDataTypeFromSpecificsFieldNumber( |
| command.custom_nudge_delays(i).datatype_id()); |
| if (type != UNSPECIFIED) { |
| delay_map[type] = |
| base::Milliseconds(command.custom_nudge_delays(i).delay_ms()); |
| } |
| } |
| cycle->delegate()->OnReceivedCustomNudgeDelays(delay_map); |
| } |
| |
| std::optional<int> max_tokens; |
| if (command.has_extension_types_max_tokens()) { |
| max_tokens = command.extension_types_max_tokens(); |
| } |
| std::optional<base::TimeDelta> refill_interval; |
| if (command.has_extension_types_refill_interval_seconds()) { |
| refill_interval = |
| base::Seconds(command.extension_types_refill_interval_seconds()); |
| } |
| std::optional<base::TimeDelta> depleted_quota_nudge_delay; |
| if (command.has_extension_types_depleted_quota_nudge_delay_seconds()) { |
| depleted_quota_nudge_delay = base::Seconds( |
| command.extension_types_depleted_quota_nudge_delay_seconds()); |
| } |
| if (max_tokens || refill_interval || depleted_quota_nudge_delay) { |
| cycle->delegate()->OnReceivedQuotaParamsForExtensionTypes( |
| max_tokens, refill_interval, depleted_quota_nudge_delay); |
| } |
| } |
| |
| } // namespace |
| |
| DataTypeSet GetTypesToMigrate(const ClientToServerResponse& response) { |
| return GetDataTypeSetFromSpecificsFieldNumberList( |
| response.migrated_data_type_id()); |
| } |
| |
| SyncProtocolError ConvertErrorPBToSyncProtocolError( |
| const sync_pb::ClientToServerResponse_Error& error) { |
| return {.error_type = PBErrorTypeToSyncProtocolErrorType(error.error_type()), |
| .error_description = error.error_description(), |
| .action = PBActionToClientAction(error.action()), |
| // THROTTLED and PARTIAL_FAILURE are currently the only error codes |
| // using `error_data_types`. In both cases, the types are throttled. |
| .error_data_types = error.error_data_type_ids_size() > 0 |
| ? GetDataTypeSetFromSpecificsFieldNumberList( |
| error.error_data_type_ids()) |
| : DataTypeSet()}; |
| } |
| |
| // static |
| SyncerError SyncerProtoUtil::HandleClientToServerMessageResponse( |
| const sync_pb::ClientToServerResponse& response, |
| SyncCycle* cycle, |
| DataTypeSet* partial_failure_data_types) { |
| LogClientToServerResponse(response); |
| |
| // Remember a bag of chips if it has been sent by the server. |
| SaveBagOfChipsFromResponse(response, cycle->context()); |
| |
| SyncProtocolError sync_protocol_error = |
| GetProtocolErrorFromResponse(response, cycle->context()); |
| |
| // Inform the delegate of the error we got. |
| cycle->delegate()->OnSyncProtocolError(sync_protocol_error); |
| |
| if (response.has_client_command()) { |
| ProcessClientCommand(response.client_command(), cycle); |
| } |
| |
| // Now do any special handling for the error type and decide on the return |
| // value. |
| // Partial failures (e.g. specific datatypes throttled or server returned |
| // PARTIAL_FAILURE) are reported as success. |
| bool should_report_success = false; |
| switch (sync_protocol_error.error_type) { |
| case UNKNOWN_ERROR: |
| LOG(WARNING) << "Sync protocol out-of-date. The server is using a more " |
| << "recent version."; |
| break; |
| case SYNC_SUCCESS: |
| should_report_success = true; |
| break; |
| case THROTTLED: |
| if (sync_protocol_error.error_data_types.empty()) { |
| DLOG(WARNING) << "Client fully throttled by syncer."; |
| cycle->delegate()->OnThrottled(GetThrottleDelay(response)); |
| } else { |
| // This is a special case, since server only throttle some of datatype, |
| // so can treat this case as partial failure. |
| DLOG(WARNING) << "Some types throttled by syncer."; |
| cycle->delegate()->OnTypesThrottled( |
| sync_protocol_error.error_data_types, GetThrottleDelay(response)); |
| if (partial_failure_data_types != nullptr) { |
| *partial_failure_data_types = sync_protocol_error.error_data_types; |
| } |
| should_report_success = true; |
| } |
| break; |
| case MIGRATION_DONE: |
| LOG_IF(ERROR, 0 >= response.migrated_data_type_id_size()) |
| << "MIGRATION_DONE but no types specified."; |
| cycle->delegate()->OnReceivedMigrationRequest( |
| GetTypesToMigrate(response)); |
| break; |
| case PARTIAL_FAILURE: |
| // This only happens when partial backoff during GetUpdates. |
| if (!sync_protocol_error.error_data_types.empty()) { |
| DLOG(WARNING) |
| << "Some types got partial failure by syncer during GetUpdates."; |
| cycle->delegate()->OnTypesBackedOff( |
| sync_protocol_error.error_data_types); |
| } |
| if (partial_failure_data_types != nullptr) { |
| *partial_failure_data_types = sync_protocol_error.error_data_types; |
| } |
| should_report_success = true; |
| break; |
| case TRANSIENT_ERROR: |
| case NOT_MY_BIRTHDAY: |
| case DISABLED_BY_ADMIN: |
| case CLIENT_DATA_OBSOLETE: |
| case ENCRYPTION_OBSOLETE: |
| break; |
| case CONFLICT: |
| case INVALID_MESSAGE: |
| // These error types should not be used at this stage. |
| NOTREACHED(); |
| } |
| |
| if (should_report_success) { |
| return SyncerError::Success(); |
| } |
| return SyncerError::ProtocolError(sync_protocol_error.error_type); |
| } |
| |
| // static |
| bool SyncerProtoUtil::IsSyncDisabledByAdmin( |
| const sync_pb::ClientToServerResponse& response) { |
| return (response.has_error_code() && |
| response.error_code() == sync_pb::SyncEnums::DISABLED_BY_ADMIN); |
| } |
| |
| // static |
| SyncProtocolError SyncerProtoUtil::GetProtocolErrorFromResponse( |
| const sync_pb::ClientToServerResponse& response, |
| SyncCycleContext* context) { |
| SyncProtocolError sync_protocol_error; |
| |
| // The DISABLED_BY_ADMIN error overrides other errors sent by the server. |
| if (IsSyncDisabledByAdmin(response)) { |
| sync_protocol_error.error_type = DISABLED_BY_ADMIN; |
| sync_protocol_error.action = STOP_SYNC_FOR_DISABLED_ACCOUNT; |
| } else if (response.has_error()) { |
| // If the server provides explicit error information, just honor it. |
| sync_protocol_error = ConvertErrorPBToSyncProtocolError(response.error()); |
| } else if (!ProcessResponseBirthday(response, context)) { |
| // If sync isn't disabled, first check for a birthday mismatch error. |
| if (response.error_code() == sync_pb::SyncEnums::CLIENT_DATA_OBSOLETE) { |
| // Server indicates that client needs to reset sync data. |
| sync_protocol_error.error_type = CLIENT_DATA_OBSOLETE; |
| sync_protocol_error.action = RESET_LOCAL_SYNC_DATA; |
| } else { |
| sync_protocol_error.error_type = NOT_MY_BIRTHDAY; |
| sync_protocol_error.action = DISABLE_SYNC_ON_CLIENT; |
| } |
| } else { |
| // Legacy server implementation. Compute the error based on `error_code`. |
| sync_protocol_error = ErrorCodeToSyncProtocolError(response.error_code()); |
| } |
| |
| // Trivially inferred actions. |
| if (sync_protocol_error.action == UNKNOWN_ACTION && |
| sync_protocol_error.error_type == ENCRYPTION_OBSOLETE) { |
| sync_protocol_error.action = DISABLE_SYNC_ON_CLIENT; |
| } |
| |
| return sync_protocol_error; |
| } |
| |
| // static |
| void SyncerProtoUtil::SetProtocolVersion(ClientToServerMessage* msg) { |
| const int current_version = |
| ClientToServerMessage::default_instance().protocol_version(); |
| msg->set_protocol_version(current_version); |
| } |
| |
| // static |
| bool SyncerProtoUtil::PostAndProcessHeaders(ServerConnectionManager* scm, |
| const ClientToServerMessage& msg, |
| ClientToServerResponse* response) { |
| DCHECK(msg.has_protocol_version()); |
| DCHECK_EQ(msg.protocol_version(), |
| ClientToServerMessage::default_instance().protocol_version()); |
| std::string buffer_in; |
| msg.SerializeToString(&buffer_in); |
| |
| UMA_HISTOGRAM_ENUMERATION("Sync.PostedClientToServerMessage", |
| msg.message_contents(), |
| ClientToServerMessage::Contents_MAX + 1); |
| |
| if (msg.has_get_updates()) { |
| UMA_HISTOGRAM_ENUMERATION("Sync.PostedGetUpdatesOrigin", |
| msg.get_updates().get_updates_origin(), |
| sync_pb::SyncEnums::GetUpdatesOrigin_ARRAYSIZE); |
| |
| for (const sync_pb::DataTypeProgressMarker& progress_marker : |
| msg.get_updates().from_progress_marker()) { |
| UMA_HISTOGRAM_ENUMERATION( |
| "Sync.PostedDataTypeGetUpdatesRequest", |
| DataTypeHistogramValue(GetDataTypeFromSpecificsFieldNumber( |
| progress_marker.data_type_id()))); |
| } |
| } |
| |
| const base::Time start_time = base::Time::Now(); |
| |
| // Fills in buffer_out. |
| std::string buffer_out; |
| HttpResponse http_response = |
| scm->PostBufferWithCachedAuth(buffer_in, &buffer_out); |
| if (http_response.server_status != HttpResponse::SERVER_CONNECTION_OK) { |
| LOG(WARNING) << "Error posting from syncer:" << http_response; |
| return false; |
| } |
| |
| if (!response->ParseFromString(buffer_out)) { |
| DLOG(WARNING) << "Error parsing response from sync server"; |
| return false; |
| } |
| |
| DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES( |
| "Sync.PostedClientToServerMessageLatency", |
| base::Time::Now() - start_time); |
| |
| // The error can be specified in 2 different fields, so consider both of them. |
| sync_pb::SyncEnums::ErrorType error_type = |
| response->has_error() ? response->error().error_type() |
| : response->error_code(); |
| if (error_type != sync_pb::SyncEnums::SUCCESS) { |
| base::UmaHistogramSparse("Sync.PostedClientToServerMessageError2", |
| error_type); |
| } |
| if (response->has_error() && |
| response->error().error_type() == sync_pb::SyncEnums::PARTIAL_FAILURE) { |
| DataTypeSet error_data_types = GetDataTypeSetFromSpecificsFieldNumberList( |
| response->error().error_data_type_ids()); |
| for (DataType data_type : error_data_types) { |
| base::UmaHistogramEnumeration( |
| "Sync.PostedClientToServerMessagePartialErrorDataType", |
| DataTypeHistogramValue(data_type)); |
| } |
| } |
| |
| return true; |
| } |
| |
| base::TimeDelta SyncerProtoUtil::GetThrottleDelay( |
| const ClientToServerResponse& response) { |
| base::TimeDelta throttle_delay = kSyncDelayAfterThrottled; |
| if (response.has_client_command()) { |
| const sync_pb::ClientCommand& command = response.client_command(); |
| if (command.has_throttle_delay_seconds()) { |
| throttle_delay = base::Seconds(command.throttle_delay_seconds()); |
| } |
| } |
| return throttle_delay; |
| } |
| |
| // static |
| void SyncerProtoUtil::AddRequiredFieldsToClientToServerMessage( |
| const SyncCycle* cycle, |
| sync_pb::ClientToServerMessage* msg) { |
| DCHECK(msg); |
| SetProtocolVersion(msg); |
| const std::string birthday = cycle->context()->birthday(); |
| if (!birthday.empty()) { |
| msg->set_store_birthday(birthday); |
| } |
| DCHECK(msg->has_store_birthday() || !IsBirthdayRequired(*msg)); |
| msg->mutable_bag_of_chips()->ParseFromString( |
| cycle->context()->bag_of_chips()); |
| msg->set_api_key(google_apis::GetAPIKey()); |
| msg->mutable_client_status()->CopyFrom(cycle->context()->client_status()); |
| } |
| |
| // static |
| SyncerError SyncerProtoUtil::PostClientToServerMessage( |
| const ClientToServerMessage& msg, |
| ClientToServerResponse* response, |
| SyncCycle* cycle, |
| DataTypeSet* partial_failure_data_types) { |
| DCHECK(response); |
| DCHECK(msg.has_protocol_version()); |
| DCHECK(msg.has_store_birthday() || !IsBirthdayRequired(msg)); |
| DCHECK(msg.has_bag_of_chips()); |
| DCHECK(msg.has_api_key()); |
| DCHECK(msg.has_client_status()); |
| |
| LogClientToServerMessage(msg); |
| if (!PostAndProcessHeaders(cycle->context()->connection_manager(), msg, |
| response)) { |
| // There was an error establishing communication with the server. |
| // We can not proceed beyond this point. |
| const HttpResponse::ServerConnectionCode server_status = |
| cycle->context()->connection_manager()->server_status(); |
| |
| DCHECK_NE(server_status, HttpResponse::NONE); |
| |
| if (server_status == HttpResponse::SERVER_CONNECTION_OK) { |
| // The server returned a response but there was a failure in processing |
| // it. |
| return SyncerError::ProtocolViolationError(); |
| } |
| |
| return ServerConnectionErrorAsSyncerError( |
| server_status, cycle->context()->connection_manager()->net_error_code(), |
| cycle->context()->connection_manager()->http_status_code()); |
| } |
| |
| return HandleClientToServerMessageResponse(*response, cycle, |
| partial_failure_data_types); |
| } |
| |
| // static |
| bool SyncerProtoUtil::ShouldMaintainPosition( |
| const sync_pb::SyncEntity& sync_entity) { |
| // Maintain positions for bookmarks that are not server-defined top-level |
| // folders. |
| return GetDataTypeFromSpecifics(sync_entity.specifics()) == BOOKMARKS && |
| !(sync_entity.folder() && |
| !sync_entity.server_defined_unique_tag().empty()); |
| } |
| |
| // static |
| bool SyncerProtoUtil::ShouldMaintainHierarchy( |
| const sync_pb::SyncEntity& sync_entity) { |
| // Maintain hierarchy for bookmarks or top-level items. |
| return GetDataTypeFromSpecifics(sync_entity.specifics()) == BOOKMARKS || |
| sync_entity.parent_id_string() == "0"; |
| } |
| |
| std::string SyncerProtoUtil::SyncEntityDebugString( |
| const sync_pb::SyncEntity& entry) { |
| const std::string& mtime_str = |
| GetTimeDebugString(ProtoTimeToTime(entry.mtime())); |
| const std::string& ctime_str = |
| GetTimeDebugString(ProtoTimeToTime(entry.ctime())); |
| return base::StringPrintf( |
| "id: %s, parent_id: %s, " |
| "version: %" PRId64 |
| "d, " |
| "mtime: %" PRId64 |
| "d (%s), " |
| "ctime: %" PRId64 |
| "d (%s), " |
| "name: %s, " |
| "d, " |
| "%s ", |
| entry.id_string().c_str(), entry.parent_id_string().c_str(), |
| entry.version(), entry.mtime(), mtime_str.c_str(), entry.ctime(), |
| ctime_str.c_str(), entry.name().c_str(), |
| entry.deleted() ? "deleted, " : ""); |
| } |
| |
| namespace { |
| std::string GetUpdatesResponseString( |
| const sync_pb::GetUpdatesResponse& response) { |
| std::string output; |
| output.append("GetUpdatesResponse:\n"); |
| for (int i = 0; i < response.entries_size(); i++) { |
| output.append(SyncerProtoUtil::SyncEntityDebugString(response.entries(i))); |
| output.append("\n"); |
| } |
| return output; |
| } |
| } // namespace |
| |
| std::string SyncerProtoUtil::ClientToServerResponseDebugString( |
| const ClientToServerResponse& response) { |
| // Add more handlers as needed. |
| std::string output; |
| if (response.has_get_updates()) { |
| output.append(GetUpdatesResponseString(response.get_updates())); |
| } |
| return output; |
| } |
| |
| } // namespace syncer |