/// <summary> /// Gets the serialization format of the payload in the incoming request based on the content-type header. /// According to the HTTP header RFC http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html, the content type /// has the following format: /// /// "Content-Type" ":" media-type /// media-type = type "/" subtype *( ";" parameter ) /// /// We ignore the media-type parameter. /// </summary> internal SyncSerializationFormat GetRequestContentSerializationFormat() { if (String.IsNullOrEmpty(_operationContext.IncomingRequest.ContentType)) { throw SyncServiceException.CreateBadRequestError(Strings.NoContentTypeInHeader); } // Get type /subtype without the media type parameter string[] contentTypeParts = _operationContext.IncomingRequest.ContentType.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); if (0 == contentTypeParts.Length) { throw SyncServiceException.CreateBadRequestError(Strings.UnsupportedContentType); } if (contentTypeParts[0].Equals(CONTENT_TYPE_APPLICATION_ATOM, StringComparison.OrdinalIgnoreCase)) { return(SyncSerializationFormat.ODataAtom); } if (contentTypeParts[0].Equals(CONTENT_TYPE_APPLICATION_JSON, StringComparison.OrdinalIgnoreCase)) { return(SyncSerializationFormat.ODataJson); } throw SyncServiceException.CreateBadRequestError(Strings.UnsupportedContentType); }
/// <summary> /// Gets the current request type. /// </summary> /// <returns>Request command type</returns> private RequestCommand GetRequestCommandType() { // Check if this is a request for $syncscopes if (_serviceHost.RelativeUriSegments.Length == 1 && 0 == String.Compare(_serviceHost.RelativeUriSegments[0], SyncServiceConstants.SYNC_SCOPES_URL_SEGMENT, StringComparison.InvariantCultureIgnoreCase)) { // Only GET is allowed for $syncscopes, throw an error if the request http method is not GET. if (0 != String.Compare(_serviceHost.RequestHttpMethod, SyncServiceConstants.HTTP_VERB_GET, StringComparison.InvariantCultureIgnoreCase)) { throw SyncServiceException.CreateMethodNotAllowed(Strings.InvalidHttpMethodForSyncScopesRequest, SyncServiceConstants.HTTP_VERB_GET); } return(RequestCommand.SyncScopes); } // The item at index 1 of the RelativeUriSegments array contains the operation name. if (_serviceHost.RelativeUriSegments.Length < 2 || String.IsNullOrEmpty(_serviceHost.RelativeUriSegments[1].Trim())) { throw SyncServiceException.CreateBadRequestError(Strings.MissingOperationNameinRequest); } RequestCommand requestCommand; string operation = _serviceHost.RelativeUriSegments[1]; switch (operation.ToLowerInvariant()) { case SyncServiceConstants.SYNC_OPERATION_UPLOAD_CHANGES: // Only POST is allowed for upload changes, throw an error if the request http method is not POST. if (0 != String.Compare(_serviceHost.RequestHttpMethod, SyncServiceConstants.HTTP_VERB_POST, StringComparison.InvariantCultureIgnoreCase)) { throw SyncServiceException.CreateMethodNotAllowed(Strings.InvalidHttpMethodForUploadChangesRequest, SyncServiceConstants.HTTP_VERB_GET); } requestCommand = RequestCommand.UploadChanges; break; case SyncServiceConstants.SYNC_OPERATION_DOWNLOAD_CHANGES: requestCommand = RequestCommand.DownloadChanges; break; case SyncServiceConstants.SYNC_OPERATION_SCOPE_METADATA: // Only GET is allowed for $metadata, throw an error if the request http method is not GET. if (0 != String.Compare(_serviceHost.RequestHttpMethod, SyncServiceConstants.HTTP_VERB_GET, StringComparison.InvariantCultureIgnoreCase)) { throw SyncServiceException.CreateMethodNotAllowed(Strings.InvalidHttpMethodForScopeMetadataRequest, SyncServiceConstants.HTTP_VERB_GET); } requestCommand = RequestCommand.ScopeMetadata; break; default: throw SyncServiceException.CreateBadRequestError(Strings.InvalidOperationName); } return(requestCommand); }
/// <summary> /// Gets the scope name from the query string. /// </summary> /// <returns>scope name</returns> private string GetScopeName() { // The item at index 0 of the RelativeUriSegments array contains the scope name. if (_serviceHost.RelativeUriSegments.Length < 1 || String.IsNullOrEmpty(_serviceHost.RelativeUriSegments[0].Trim())) { throw SyncServiceException.CreateBadRequestError(Strings.MissingScopeNameInRequest); } // Scope names are compared in a case insensitive manner and used as keys in lower case. return(_serviceHost.RelativeUriSegments[0].ToLowerInvariant()); }
internal static string GetContentType(SyncSerializationFormat format) { switch (format) { case SyncSerializationFormat.ODataAtom: return("application/atom+xml"); case SyncSerializationFormat.ODataJson: return("application/json"); default: throw SyncServiceException.CreateBadRequestError("Unsupported serialization format"); } }
/// <summary> /// Verify query parameters for '$' etc. /// </summary> internal void VerifyQueryParameters() { NameValueCollection values = _operationContext.IncomingRequest.UriTemplateMatch.QueryParameters; var queryDictionary = new Dictionary <string, string>(); foreach (string key in values.Keys) { if (!queryDictionary.ContainsKey(key)) { queryDictionary.Add(key, values[key]); } else { throw SyncServiceException.CreateBadRequestError(Strings.DuplicateParametersInRequestUri); } } }
/// <summary> /// Get a dictionary that contains command parameters. /// </summary> /// <param name="queryStringCollection">Querystring represented as a NameValueCollection</param> /// <returns>Dictionary of command parameter types and values.</returns> private Dictionary <CommandParamType, object> GetCommandParameters(NameValueCollection queryStringCollection) { var commandParams = new Dictionary <CommandParamType, object>(); Dictionary <string, string> filterParams = GetFilterParamsFromIncomingRequest(queryStringCollection); commandParams.Add(CommandParamType.FilterParameters, filterParams); string scopeName = GetScopeName(); // Check the scope requested against the list of enabled scopes. if (!_configuration.ScopeNames.Contains(scopeName.ToLowerInvariant())) { throw SyncServiceException.CreateBadRequestError(Strings.SyncScopeNotSupported); } commandParams.Add(CommandParamType.ScopeName, scopeName); return(commandParams); }
internal Request(RequestCommand requestCommand, HttpContextServiceHost serviceHost, Dictionary <CommandParamType, object> commandParams, byte[] blob, List <IOfflineEntity> entities, SyncSerializationFormat responseSerializationFormat) { IdToTempIdMapping = new Dictionary <string, string>(); RequestCommand = requestCommand; ServiceHost = serviceHost; CommandParams = commandParams; SyncBlob = blob; ResponseSerializationFormat = responseSerializationFormat; if (null != entities && requestCommand != RequestCommand.UploadChanges) { throw SyncServiceException.CreateBadRequestError(Strings.EntitiesOnlyAllowedForUploadChangesRequest); } EntityList = entities; }
/// <summary> /// Validate the HTTP Verb for GET/POST and check if the URL matches the allowed format. /// </summary> internal void ValidateRequestHttpVerbAndSegments() { if (_operationContext == null) { throw SyncServiceException.CreateBadRequestError(Strings.NullWebOperationContext); } // Only allow GET and POST verbs if (0 != String.Compare(RequestHttpMethod, "GET", StringComparison.InvariantCultureIgnoreCase) && 0 != String.Compare(RequestHttpMethod, "POST", StringComparison.InvariantCultureIgnoreCase)) { SyncTracer.Warning("Request HTTP method is not GET or POST. HTTP Method: {0}", RequestHttpMethod ?? String.Empty); throw SyncServiceException.CreateMethodNotAllowed(Strings.UnsupportedHttpMethod, "GET, POST"); } // Check if we have less than 2 relative segments. The maximum segments we can have is 2 // which is for /scope/operation. if (RelativeUriSegments.Length > 2) { throw SyncServiceException.CreateBadRequestError(Strings.InvalidUrlFormat); } }
/// <summary> /// Get the value for a query string item. /// </summary> /// <param name="item">Item to search for in the incoming request uri.</param> /// <returns>Value of the item.</returns> internal string GetQueryStringItem(string item) { NameValueCollection values = _operationContext.IncomingRequest.UriTemplateMatch.QueryParameters; // Check if we have the item by using a direct function (good performance) // Note: the underlying data structure in a NameValueCollection is a hashtable. string[] strArray = values.GetValues(item); // If no match found, then loop through all keys and do a case insensitive search. if ((strArray == null) || (strArray.Length == 0)) { string str = null; foreach (string str2 in values.Keys) { if ((str2 != null) && StringComparer.OrdinalIgnoreCase.Equals(str2.Trim(), item)) { if (str != null) { // Bad request throw SyncServiceException.CreateBadRequestError(Strings.DuplicateParametersInRequestUri); } str = str2; strArray = values.GetValues(str2); } } if ((strArray == null) || (strArray.Length == 0)) { return(null); } } if (strArray.Length != 1) { // syntax error, cannot have multiple querystring parameters with the same name. throw SyncServiceException.CreateBadRequestError(Strings.DuplicateParametersInRequestUri); } return(strArray[0]); }
/// <summary> /// Parse the Id string and populate key fields of the object. This is called when the client sends tombstones /// and the Key fields are not present in the input payload. /// The approach used is to parse each key field individually and set it to a property with the same name. /// For example: For http://host/service.svc/Tag(ID=1), we parse out ID=1 and then populate the ID property of the targetObject with the /// value 1. /// </summary> /// <param name="entity">Entity for which we need to set the key fields.</param> /// <param name="serviceBaseUri"> /// Base Uri of the service. The ServiceMetadata.Id property has the Uri which we want to strip off before attempting to /// parse the keys and values. /// </param> internal static void ParseIdStringAndPopulateKeyFields(IOfflineEntity entity, Uri serviceBaseUri) { Debug.Assert(null != entity); Debug.Assert(!String.IsNullOrEmpty(entity.ServiceMetadata.Id)); string idString = entity.ServiceMetadata.Id; // Remove the ServiceUri and the entity type name from the EntityId // Note: Case sensitive comparisons are made since the client isn't supposed to change the Id for an // entity. string serviceUriWithTableName = serviceBaseUri + "/" + entity.GetType().Name; // If the Id does not have the correct format of serviceUri/TableName, then we should not continue further. if (!idString.StartsWith(serviceUriWithTableName, false, CultureInfo.InvariantCulture)) { throw SyncServiceException.CreateBadRequestError(String.Format(Strings.EntityIdFormatIsIncorrect, idString)); } // Remove the host and the table name from the Id. // Example: http://host/service.svc/table(id=123) will become (id=123) idString = idString.Remove(0, serviceUriWithTableName.Length); // Remove leading '/' if any. After this the id string is of the format (ID=1) or (ID=guid'<guidValue>') // If there are multiple Id values then, they are comma separated.));)) if (idString.StartsWith("/")) { idString = idString.Substring(1); } // Make sure the ( and ) parenthesis exist. if (String.IsNullOrEmpty(idString) || idString[0] != '(' || idString[idString.Length - 1] != ')') { throw SyncServiceException.CreateBadRequestError(String.Format(Strings.EntityIdFormatIsIncorrect, entity.ServiceMetadata.Id)); } // Remove the ( and ) characters. idString = idString.Substring(1, idString.Length - 2); // Get the key properties for the entity. var keyFieldPropertyInfoList = ReflectionUtility.GetPrimaryKeysPropertyInfoMapping(entity.GetType()); // Split the string and get individual keyvalue pair strings. They key and value are still a single string separated by '='. // for types such as Guid, the value will be prefixed with 'guid'. string[] primaryKeyValuePair = idString.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); // Throw if there is a mismatch between the key count of the entity and that passed in the URI. if (primaryKeyValuePair.Length != keyFieldPropertyInfoList.Length) { throw SyncServiceException.CreateBadRequestError(String.Format(Strings.BadRequestKeyCountMismatch, entity.ServiceMetadata.Id, entity.GetType())); } // At this point, we have key value pairs of the form "ID=1". foreach (var keyValuePair in primaryKeyValuePair) { // example: ID=1 string[] keyValue = keyValuePair.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries); // Every key should have only 2 components. Debug.Assert(2 == keyValue.Length); // Get the property from the key list. string key = keyValue[0].Trim(); var propertyInfo = keyFieldPropertyInfoList.Where(p => p.Name == key).FirstOrDefault(); if (null == propertyInfo) { throw SyncServiceException.CreateBadRequestError( String.Format(Strings.BadRequestKeyNotFoundInResource, key, entity.ServiceMetadata.Id, entity.GetType())); } // Get typed value of the value. object targetValue; // Parse the value based on the target type. if (!ODataIdParser.TryKeyStringToPrimitive(keyValue[1], propertyInfo.PropertyType, out targetValue)) { throw SyncServiceException.CreateBadRequestError( String.Format(Strings.UnableToParseKeyValueForProperty, keyValuePair, entity.ServiceMetadata.Id)); } // Set the property value. propertyInfo.SetValue(entity, targetValue, null); } }
/// <summary> /// Read and parse the incoming request stream for a POST request. /// </summary> private void ReadIncomingRequestStreamForPost() { if (null == _serviceHost.RequestStream || !_serviceHost.RequestStream.CanRead) { SyncTracer.Info("Request stream for HTTP POST is empty, null or cannot be read."); return; } try { var reader = WebUtil.GetSyncReader(_serviceHost.GetRequestContentSerializationFormat(), _serviceHost.RequestStream, _configuration.TypeToTableGlobalNameMapping.Keys.ToArray()); reader.Start(); while (reader.Next()) { switch (reader.ItemType) { case ReaderItemType.Entry: IOfflineEntity entity = reader.GetItem(); if (entity.ServiceMetadata.IsTombstone) { if (String.IsNullOrEmpty(entity.ServiceMetadata.Id)) { throw SyncServiceException.CreateBadRequestError(Strings.TombstoneEntityHasNoId); } WebUtil.ParseIdStringAndPopulateKeyFields(entity, _serviceHost.ServiceBaseUri); } _entityList.Add(entity); bool hasTempId = false; if (reader.HasTempId()) { // Save the entity id to tempId mapping for use later when writing response. _idToTempIdMapping.Add(WebUtil.GenerateOfflineEntityId(entity), reader.GetTempId()); hasTempId = true; } // Make sure, we have atleast one of Id or TempId if (String.IsNullOrEmpty(entity.ServiceMetadata.Id) && !hasTempId) { throw SyncServiceException.CreateBadRequestError(Strings.BothIdAndTempIdAreMissing); } break; case ReaderItemType.SyncBlob: _syncBlob = reader.GetServerBlob(); break; } } } catch (XmlException exception) { SyncTracer.Warning("XmlException: {0}", WebUtil.GetExceptionMessage(exception)); throw SyncServiceException.CreateBadRequestError(Strings.BadRequestPayload); } }