private static ErrorHandler CreateHandler(IDataService service, AstoriaRequestMessage requestMessage, Exception exception, Version defaultResponseVersion) { Debug.Assert(service != null, "service != null"); Debug.Assert(service.Configuration != null, "service.Configuration != null"); string acceptableContentTypes = null; string requestAcceptCharsetHeader = null; Version responseVersion = defaultResponseVersion; if (requestMessage != null) { acceptableContentTypes = requestMessage.GetAcceptableContentTypes(); requestAcceptCharsetHeader = requestMessage.GetRequestAcceptCharsetHeader(); try { Version maxProtocolVersion = service.Configuration.DataServiceBehavior.MaxProtocolVersion.ToVersion(); requestMessage.InitializeRequestVersionHeaders(maxProtocolVersion); responseVersion = VersionUtil.GetResponseVersionForError(requestMessage.GetAcceptableContentTypes(), requestMessage.RequestMaxVersion, maxProtocolVersion); } catch (Exception e) { if (!CommonUtil.IsCatchableExceptionType(e)) { throw; } // Ignore exceptions as we should use the default response version. } } return(new ErrorHandler(exception, service.Configuration.UseVerboseErrors, responseVersion, acceptableContentTypes, requestAcceptCharsetHeader)); }
/// <summary>Handles an exception when processing a batch request.</summary> /// <param name='service'>Data service doing the processing.</param> /// <param name='exception'>Exception thrown.</param> /// <param name='batchWriter'>Output writer for the batch.</param> /// <param name="responseStream">Underlying response stream.</param> internal static void HandleBatchInStreamError(IDataService service, Exception exception, ODataBatchWriter batchWriter, Stream responseStream) { Debug.Assert(service != null, "service != null"); Debug.Assert(exception != null, "exception != null"); Debug.Assert(responseStream != null, "responseStream != null"); Debug.Assert(service.Configuration != null, "service.Configuration != null - it should have been initialized by now"); Debug.Assert(CommonUtil.IsCatchableExceptionType(exception), "CommonUtil.IsCatchableExceptionType(exception) - "); AstoriaRequestMessage requestMessage = service.OperationContext == null ? null : service.OperationContext.RequestMessage; ErrorHandler handler = CreateHandler(service, requestMessage, exception, VersionUtil.DataServiceDefaultResponseVersion); service.InternalHandleException(handler.exceptionArgs); // Make sure to flush the batch writer before we write anything to the underlying stream batchWriter.Flush(); // Note the OData protocol spec did not defined the behavior when an exception is encountered outside of a batch operation. // The batch writer in ODataLib doesn't allow WriteError in this case. // Unfortunately the shipped behavior on the server is we serialize out an error payload in XML format. We need to keep the // existing behavior. The batch client doesn't know how to deserialize an error payload outside of a batch operation however. using (XmlWriter xmlWriter = XmlUtil.CreateXmlWriterAndWriteProcessingInstruction(responseStream, handler.encoding)) { ODataError error = handler.exceptionArgs.CreateODataError(); ErrorUtils.WriteXmlError(xmlWriter, error, handler.exceptionArgs.UseVerboseErrors, MaxInnerErrorDepth); } }
/// <summary> /// Gets the $callback query parameter if present. Also performs some validation: /// We do not allow $callback to be present if the content-type for the response is going to be anythign but JSON or plain text. /// </summary> /// <param name="message">The request message.</param> /// <param name="format">The format we will write the reponse in.</param> /// <returns>Returns the name of the callback function $callback specifies, or null if there isn't one.</returns> internal static string HandleCallbackQueryOption(AstoriaRequestMessage message, ODataFormatWithParameters format) { var functionName = message.GetQueryStringItem(XmlConstants.HttpQueryStringCallback); if (functionName != null) { // The verb must be GET (not IsQuery(), GET specifically) if (message.HttpVerb != HttpVerbs.GET) { throw new DataServiceException(400, Strings.CallbackQueryOptionHandler_GetRequestsOnly); } // If conneg didn't get a format, that means that there was some error that we will fail at later, or // it is going end up being a 204, 304, or something else with no body. For all of these cases we are OK // with 'pretending' that $callback was not there, since the other errors are probably more important than // ours, and in the non-error cases we really should not be throwing (think 204). if (format == null) { return(null); } // JSON and text/plain are the only things allowed with $callback if (format.Format != ODataFormat.Json && format.Format != ODataFormat.RawValue) { throw new DataServiceException(400, Strings.CallbackQueryOptionHandler_UnsupportedContentType(format.Format.ToString())); } } return(functionName); }
/// <summary>Handles an exception when processing a batch response.</summary> /// <param name='service'>Data service doing the processing.</param> /// <param name="requestMessage">requestMessage holding information about the request that caused an error</param> /// <param name="responseMessage">responseMessage to which we need to write the exception message</param> /// <param name='exception'>Exception thrown.</param> /// <param name='batchWriter'>Output writer for the batch.</param> /// <param name="responseStream">Underlying response stream.</param> /// <param name="defaultResponseVersion">The data service version to use for response, if it cannot be computed from the requestMessage.</param> internal static void HandleBatchOperationError(IDataService service, AstoriaRequestMessage requestMessage, IODataResponseMessage responseMessage, Exception exception, ODataBatchWriter batchWriter, Stream responseStream, Version defaultResponseVersion) { Debug.Assert(service != null, "service != null"); Debug.Assert(exception != null, "exception != null"); Debug.Assert(batchWriter != null, "batchWriter != null"); Debug.Assert(service.Configuration != null, "service.Configuration != null"); Debug.Assert(CommonUtil.IsCatchableExceptionType(exception), "CommonUtil.IsCatchableExceptionType(exception)"); ErrorHandler handler = CreateHandler(service, requestMessage, exception, defaultResponseVersion); service.InternalHandleException(handler.exceptionArgs); if (requestMessage != null && responseMessage != null) { responseMessage.SetHeader(XmlConstants.HttpODataVersion, handler.responseVersion.ToString(2) + ";"); requestMessage.ProcessException(handler.exceptionArgs); // if ProcessBenignException returns anything, we can safely not write to the stream. if (ProcessBenignException(exception, service) != null) { return; } } if (requestMessage != null) { responseMessage = requestMessage.BatchServiceHost.GetOperationResponseMessage(); WebUtil.SetResponseHeadersForBatchRequests(responseMessage, requestMessage.BatchServiceHost); } else { responseMessage = batchWriter.CreateOperationResponseMessage(null); responseMessage.StatusCode = handler.exceptionArgs.ResponseStatusCode; } MessageWriterBuilder messageWriterBuilder = MessageWriterBuilder.ForError( null, service, handler.responseVersion, responseMessage, handler.contentType, null /*acceptCharsetHeaderValue*/); using (ODataMessageWriter messageWriter = messageWriterBuilder.CreateWriter()) { ODataError error = handler.exceptionArgs.CreateODataError(); WriteErrorWithFallbackForXml(messageWriter, handler.encoding, responseStream, handler.exceptionArgs, error, messageWriterBuilder); } }
/// <summary> /// Creates a new instance of the RequestMessage and ResponseMessage to cache the request headers and to validate the data from the host interface. /// </summary> /// <remarks> /// Note that this code cannot go in the constructor, because it is possible for a user to attach a host to DataService, /// process a request, changes fields _on the same host_, and then call process request again. So we need to be able to /// create a new AstoriaRequestMessage while still using the same DataServiceOpeationContext. /// </remarks> /// <param name="dataService">The current data service instance.</param> internal void InitializeAndCacheHeaders(IDataService dataService) { Debug.Assert(this.hostInterface != null, "this.hostInterface != null"); this.requestMessage = new AstoriaRequestMessage(this.hostInterface); this.responseMessage = new AstoriaResponseMessage(this.hostInterface); this.CurrentDataService = dataService; // Add a "nosniff" content-type option to instruct IE8/9 not to sniff the content instead of rendering it // according to the specified content-type. This mitigates against XSS attacks such as embedded scripts in // content specified as text/plain. if (this.hostInterface is IDataServiceHost2) { this.ResponseHeaders.Add(XmlConstants.XContentTypeOptions, XmlConstants.XContentTypeOptionNoSniff); } }
/// <summary>Handles an exception before the response has been written out.</summary> /// <param name='exception'>Exception thrown.</param> /// <param name='service'>Data service doing the processing.</param> /// <returns>An action that can serialize the exception into a stream.</returns> internal static Action <Stream> HandleBeforeWritingException(Exception exception, IDataService service) { Debug.Assert(CommonUtil.IsCatchableExceptionType(exception), "CommonUtil.IsCatchableExceptionType(exception)"); Debug.Assert(exception != null, "exception != null"); Debug.Assert(service != null, "service != null"); AstoriaRequestMessage requestMessage = service.OperationContext.RequestMessage; Debug.Assert(requestMessage != null, "requestMessage != null"); ErrorHandler handler = CreateHandler(service, requestMessage, exception, VersionUtil.DataServiceDefaultResponseVersion); service.InternalHandleException(handler.exceptionArgs); service.OperationContext.ResponseMessage.SetHeader(XmlConstants.HttpODataVersion, handler.responseVersion.ToString(2) + ";"); requestMessage.ProcessException(handler.exceptionArgs); Action <Stream> action = ProcessBenignException(exception, service); if (action != null) { return(action); } MessageWriterBuilder messageWriterBuilder = MessageWriterBuilder.ForError( service.OperationContext.RequestMessage.AbsoluteServiceUri, service, handler.responseVersion, service.OperationContext.ResponseMessage, handler.contentType, service.OperationContext.RequestMessage.GetRequestAcceptCharsetHeader()); ODataMessageWriter messageWriter = messageWriterBuilder.CreateWriter(); ODataUtils.SetHeadersForPayload(messageWriter, ODataPayloadKind.Error); return(stream => { service.OperationContext.ResponseMessage.SetStream(stream); ODataError error = handler.exceptionArgs.CreateODataError(); WriteErrorWithFallbackForXml(messageWriter, handler.encoding, stream, handler.exceptionArgs, error, messageWriterBuilder); }); }
/// <summary> /// If the provider implements IConcurrencyProvider, then this method passes the etag values /// to the provider, otherwise compares the etag itself. /// </summary> /// <param name="resourceCookie">etag values for the given resource.</param> /// <param name="container">container for the given resource.</param> internal void SetETagValues(object resourceCookie, ResourceSetWrapper container) { Debug.Assert(resourceCookie != null, "resourceCookie != null"); Debug.Assert(container != null, "container != null"); AstoriaRequestMessage host = this.service.OperationContext.RequestMessage; Debug.Assert(String.IsNullOrEmpty(host.GetRequestIfNoneMatchHeader()), "IfNoneMatch header cannot be specified for Update/Delete operations"); // Resolve the cookie first to the actual resource type object actualEntity = this.ResolveResource(resourceCookie); Debug.Assert(actualEntity != null, "actualEntity != null"); ResourceType resourceType = WebUtil.GetNonPrimitiveResourceType(this.service.Provider, actualEntity); Debug.Assert(resourceType != null, "resourceType != null"); IList <ResourceProperty> etagProperties = this.service.Provider.GetETagProperties(container.Name, resourceType); if (etagProperties.Count == 0) { if (!String.IsNullOrEmpty(host.GetRequestIfMatchHeader())) { throw DataServiceException.CreateBadRequestError(Strings.Serializer_NoETagPropertiesForType); } // If the type has no etag properties, then we do not need to do any etag checks return; } // If the provider implements IConcurrencyProvider, then we need to call the provider // and pass the etag values. Else, we need to compare the etag values ourselves. IDataServiceUpdateProvider concurrencyProvider = this.updateProvider as IDataServiceUpdateProvider; if (concurrencyProvider != null) { bool?checkForEquality = null; IEnumerable <KeyValuePair <string, object> > etagValues; if (!String.IsNullOrEmpty(host.GetRequestIfMatchHeader())) { checkForEquality = true; etagValues = ParseETagValue(etagProperties, host.GetRequestIfMatchHeader()); } else { etagValues = WebUtil.EmptyKeyValuePairStringObject; } concurrencyProvider.SetConcurrencyValues(resourceCookie, checkForEquality, etagValues); } else if (String.IsNullOrEmpty(host.GetRequestIfMatchHeader())) { throw DataServiceException.CreateBadRequestError(Strings.DataService_CannotPerformOperationWithoutETag(resourceType.FullName)); } else if (host.GetRequestIfMatchHeader() != XmlConstants.HttpAnyETag) { // Compare If-Match header value with the current etag value, if the If-Match header value is not equal to '*' string etagValue = WebUtil.GetETagValue(resourceCookie, resourceType, etagProperties, this.service, false /*getMethod*/); Debug.Assert(!String.IsNullOrEmpty(etagValue), "etag value can never be null"); if (etagValue != host.GetRequestIfMatchHeader()) { throw DataServiceException.CreatePreConditionFailedError(Strings.Serializer_ETagValueDoesNotMatch); } } }
/// <summary>Initializes a new <see cref="ResponseBodyWriter"/> that can write the body of a response.</summary> /// <param name="service">Service for the request being processed.</param> /// <param name="queryResults">Enumerator for results.</param> /// <param name="requestDescription">Description of request made to the system.</param> /// <param name="actualResponseMessageWhoseHeadersMayBeOverridden">IODataResponseMessage instance for the response.</param> internal ResponseBodyWriter( IDataService service, QueryResultInfo queryResults, RequestDescription requestDescription, IODataResponseMessage actualResponseMessageWhoseHeadersMayBeOverridden) { Debug.Assert(service != null, "service != null"); Debug.Assert(requestDescription != null, "requestDescription != null"); Debug.Assert(actualResponseMessageWhoseHeadersMayBeOverridden != null, "actualResponseMessageWhoseHeadersMayBeOverridden != null"); this.service = service; this.queryResults = queryResults; this.requestDescription = requestDescription; this.actualResponseMessageWhoseHeadersMayBeOverridden = actualResponseMessageWhoseHeadersMayBeOverridden; Debug.Assert(this.PayloadKind != ODataPayloadKind.Unsupported, "payloadKind != ODataPayloadKind.Unsupported"); this.encoding = ContentTypeUtil.EncodingFromAcceptCharset(this.service.OperationContext.RequestMessage.GetRequestAcceptCharsetHeader()); if (this.PayloadKind == ODataPayloadKind.Entry || this.PayloadKind == ODataPayloadKind.Feed || this.PayloadKind == ODataPayloadKind.Property || this.PayloadKind == ODataPayloadKind.Collection || this.PayloadKind == ODataPayloadKind.EntityReferenceLink || this.PayloadKind == ODataPayloadKind.EntityReferenceLinks || this.PayloadKind == ODataPayloadKind.Error || this.PayloadKind == ODataPayloadKind.ServiceDocument || this.PayloadKind == ODataPayloadKind.Parameter) { AstoriaRequestMessage requestMessage = service.OperationContext.RequestMessage; IODataResponseMessage responseMessageOnOperationContext = service.OperationContext.ResponseMessage; Version effectiveMaxResponseVersion = VersionUtil.GetEffectiveMaxResponseVersion(service.Configuration.DataServiceBehavior.MaxProtocolVersion.ToVersion(), requestMessage.RequestMaxVersion); bool isEntityOrFeed = this.PayloadKind == ODataPayloadKind.Entry || this.PayloadKind == ODataPayloadKind.Feed; if (ContentTypeUtil.IsResponseMediaTypeJsonLight(requestMessage.GetAcceptableContentTypes(), isEntityOrFeed, effectiveMaxResponseVersion)) { // If JSON light 'wins', then bump the version to V3. requestDescription.VerifyAndRaiseResponseVersion(VersionUtil.Version4Dot0, service); responseMessageOnOperationContext.SetHeader(XmlConstants.HttpODataVersion, XmlConstants.ODataVersion4Dot0 + ";"); } } if (this.requestDescription.TargetKind == RequestTargetKind.MediaResource) { Debug.Assert(this.PayloadKind == ODataPayloadKind.BinaryValue, "payloadKind == ODataPayloadKind.BinaryValue"); // Note that GetReadStream will set the ResponseETag before it returns this.mediaResourceStream = service.StreamProvider.GetReadStream( this.queryResults.Current, this.requestDescription.StreamProperty, this.service.OperationContext); } else if (this.PayloadKind != ODataPayloadKind.BinaryValue) { IEdmModel model; if (this.PayloadKind == ODataPayloadKind.MetadataDocument) { model = MetadataSerializer.PrepareModelForSerialization(this.service.Provider, this.service.Configuration); } else { model = this.GetModelFromService(); } // Create the message writer using which the response needs to be written. this.messageWriterBuilder = MessageWriterBuilder.ForNormalRequest( this.service, this.requestDescription, this.actualResponseMessageWhoseHeadersMayBeOverridden, model); this.messageWriter = this.messageWriterBuilder.CreateWriter(); try { // Make sure all the headers are written before the method returns. this.contentFormat = ODataUtils.SetHeadersForPayload(this.messageWriter, this.PayloadKind); } catch (ODataContentTypeException contentTypeException) { throw new DataServiceException(415, null, Strings.DataServiceException_UnsupportedMediaType, null, contentTypeException); } Debug.Assert(requestDescription.ResponseFormat != null, "Response format should already have been determined."); Debug.Assert(ReferenceEquals(this.contentFormat, requestDescription.ResponseFormat.Format), "Response format in request description did not match format when writing."); if (this.PayloadKind == ODataPayloadKind.Value && !String.IsNullOrEmpty(this.requestDescription.MimeType)) { this.actualResponseMessageWhoseHeadersMayBeOverridden.SetHeader(XmlConstants.HttpContentType, this.requestDescription.MimeType); } // EPM is currently removed, but this doesn't seem to be used for EPM only. The old comment was saying: // In astoria, there is a bug in V1/V2 that while computing response version, we did not take // epm into account. Hence while creating the writer, we need to pass the RequestDescription.ActualResponseVersion // so that ODataLib can do the correct payload validation. But we need to write the response version without // the epm into the response headers because of backward-compat issue. Hence over-writing the response version // header with the wrong version value. string responseVersion = this.requestDescription.ResponseVersion.ToString() + ";"; this.actualResponseMessageWhoseHeadersMayBeOverridden.SetHeader(XmlConstants.HttpODataVersion, responseVersion); } }