|
//------------------------------------------------------------------------------
// <copyright file="RestHandler.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
namespace System.Web.Script.Services {
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Net;
using System.Reflection;
using System.Security;
using System.Text;
using System.Web;
using System.Web.Resources;
using System.Web.Script.Serialization;
using System.Web.SessionState;
internal class RestHandler : IHttpHandler {
private WebServiceMethodData _webServiceMethodData;
internal static IHttpHandler CreateHandler(HttpContext context) {
// Expectation is that we got a PathInfo of form /MethodName
if (context.Request.PathInfo.Length < 2 || context.Request.PathInfo[0] != '/') {
throw new InvalidOperationException(AtlasWeb.WebService_InvalidWebServiceCall);
}
// Get the data about the web service being invoked
WebServiceData webServiceData = WebServiceData.GetWebServiceData(context, context.Request.FilePath);
string methodName = context.Request.PathInfo.Substring(1);
return CreateHandler(webServiceData, methodName);
}
private static IHttpHandler CreateHandler(WebServiceData webServiceData, string methodName) {
// Get the data about the method being called
WebServiceMethodData methodData = webServiceData.GetMethodData(methodName);
// Create the proper handler, depending on whether we need session state
RestHandler handler;
if (methodData.RequiresSession)
handler = new RestHandlerWithSession();
else
handler = new RestHandler();
// Save the method data in the handler
handler._webServiceMethodData = methodData;
return handler;
}
// This is very similar to WebService caching, the differences are
// 1) Here we explicitely SetValidUntilExpires(true) because in an XmlHttp there is
// "pragma:no-cache" in header which would result in cache miss on the server.
// 2) Here we don't vary on header "Content-type" or "SOAPAction" because the former
// is specific to soap 1.2, which puts action in the content-type param; and the
// later is used by soap calls.
private static void InitializeCachePolicy(WebServiceMethodData methodData, HttpContext context) {
int cacheDuration = methodData.CacheDuration;
if (cacheDuration > 0) {
context.Response.Cache.SetCacheability(HttpCacheability.Server);
context.Response.Cache.SetExpires(DateTime.Now.AddSeconds(cacheDuration));
context.Response.Cache.SetSlidingExpiration(false);
context.Response.Cache.SetValidUntilExpires(true);
// DevDiv 23596: Don't set VaryBy* if the method takes no parameters
if (methodData.ParameterDatas.Count > 0) {
context.Response.Cache.VaryByParams["*"] = true;
}
else {
context.Response.Cache.VaryByParams.IgnoreParams = true;
}
}
else {
context.Response.Cache.SetNoServerCaching();
context.Response.Cache.SetMaxAge(TimeSpan.Zero);
}
}
private static IDictionary<string, object> GetRawParamsFromGetRequest(HttpContext context, JavaScriptSerializer serializer, WebServiceMethodData methodData) {
// Get all the parameters from the query string
NameValueCollection queryString = context.Request.QueryString;
Dictionary<string, object> rawParams = new Dictionary<string, object>();
foreach (WebServiceParameterData param in methodData.ParameterDatas) {
string name = param.ParameterInfo.Name;
string val = queryString[name];
if (val != null) {
rawParams.Add(name, serializer.DeserializeObject(val));
}
}
return rawParams;
}
private static IDictionary<string, object> GetRawParamsFromPostRequest(HttpContext context, JavaScriptSerializer serializer) {
// Read the entire body as a string
TextReader reader = new StreamReader(context.Request.InputStream);
string bodyString = reader.ReadToEnd();
// If there is no body, treat it as an empty object
if (String.IsNullOrEmpty(bodyString)) {
return new Dictionary<string, object>();
}
// Deserialize the javascript request body
return serializer.Deserialize<IDictionary<string, object>>(bodyString);
}
private static IDictionary<string, object> GetRawParams(WebServiceMethodData methodData, HttpContext context) {
if (methodData.UseGet) {
if (context.Request.HttpMethod == "GET") {
return GetRawParamsFromGetRequest(context, methodData.Owner.Serializer, methodData);
}
else {
throw new InvalidOperationException(
String.Format(CultureInfo.CurrentCulture, AtlasWeb.WebService_InvalidVerbRequest,
methodData.MethodName, "POST"));
}
}
else if (context.Request.HttpMethod == "POST") {
return GetRawParamsFromPostRequest(context, methodData.Owner.Serializer);
} else {
throw new InvalidOperationException(
String.Format(CultureInfo.CurrentCulture, AtlasWeb.WebService_InvalidVerbRequest,
methodData.MethodName, "GET"));
}
}
private static void InvokeMethod(HttpContext context, WebServiceMethodData methodData, IDictionary<string, object> rawParams) {
// Initialize HttpCachePolicy
InitializeCachePolicy(methodData, context);
// Create an new instance of the class
object target = null;
if (!methodData.IsStatic) target = Activator.CreateInstance(methodData.Owner.TypeData.Type);
// Make the actual method call on it
object retVal = methodData.CallMethodFromRawParams(target, rawParams);
string contentType;
string responseString = null;
if (methodData.UseXmlResponse) {
responseString = retVal as string;
// If it's a string, output it as is unless XmlSerializeString is set
if (responseString == null || methodData.XmlSerializeString) {
// Use the Xml Serializer
try {
responseString = ServicesUtilities.XmlSerializeObjectToString(retVal);
}
catch (Exception e) {
// Throw a better error if Xml serialization fails
throw new InvalidOperationException(
String.Format(CultureInfo.CurrentCulture, AtlasWeb.WebService_InvalidXmlReturnType,
methodData.MethodName, retVal.GetType().FullName, e.Message));
}
}
contentType = "text/xml";
}
else {
// Convert the result to a JSON string
// DevDiv 88409:Change JSON wire format to prevent CSRF attack
// We wrap the returned value inside an object , and assign the returned value
// to member "d" of the object. We do so as JSOM for object will never be parsed
// as valid Javascript , unlike arrays.
responseString =@"{""d"":" + methodData.Owner.Serializer.Serialize(retVal) + "}";
contentType = "application/json";
}
// Set the response content-type
context.Response.ContentType = contentType;
// Write the string to the response
if (responseString != null)
context.Response.Write(responseString);
}
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
Justification="All exceptions need to be reported to the client")]
[SuppressMessage("Microsoft.Security", "CA2107:ReviewDenyAndPermitOnlyUsage",
Justification = "Fix for DevDiv 39162 GAC'd non-APTCA types can instantiate in networking stack in Medium trust")]
internal static void ExecuteWebServiceCall(HttpContext context, WebServiceMethodData methodData) {
try {
NamedPermissionSet s_permissionSet = HttpRuntime.NamedPermissionSet;
if (s_permissionSet != null) {
s_permissionSet.PermitOnly();
}
// Deserialize the javascript request body
IDictionary<string, object> rawParams = GetRawParams(methodData, context);
InvokeMethod(context, methodData, rawParams);
}
catch (Exception ex) {
WriteExceptionJsonString(context, ex);
}
}
private static object BuildWebServiceError(string msg, string stack, string type) {
var result = new OrderedDictionary();
result["Message"] = msg;
result["StackTrace"] = stack;
result["ExceptionType"] = type;
return result;
}
internal static void WriteExceptionJsonString(HttpContext context, Exception ex) {
WriteExceptionJsonString(context, ex, (int)HttpStatusCode.InternalServerError);
}
internal static void WriteExceptionJsonString(HttpContext context, Exception ex, int statusCode) {
// Record the charset before we call ClearHeaders(). (DevDiv Bugs 158401)
string charset = context.Response.Charset;
context.Response.ClearHeaders();
context.Response.ClearContent();
context.Response.Clear();
context.Response.StatusCode = statusCode;
context.Response.StatusDescription = HttpWorkerRequest.GetStatusDescription(statusCode);
context.Response.ContentType = "application/json";
context.Response.AddHeader("jsonerror", "true");
// Maintain the Charset from before. (DevDiv Bugs 158401)
context.Response.Charset = charset;
//Devdiv Bug: 118619:When accessed remotely, an Ajax web service that throws an error doesn't return the error string in the proper format on IIS7
//For IIS 7.0 integrated mode we need to set TrySkipIisCustomErrors to override IIS custom error handling. This has no functional/perf impact on
//IIS 7.0 classic mode or earlier versions.
context.Response.TrySkipIisCustomErrors = true;
using (StreamWriter writer = new StreamWriter(context.Response.OutputStream, new UTF8Encoding(false))) {
if (ex is TargetInvocationException) {
ex = ex.InnerException;
}
// Don't show any error stack or sensitive info when custom error is enabled.
if (context.IsCustomErrorEnabled) {
writer.Write(JavaScriptSerializer.SerializeInternal(BuildWebServiceError(AtlasWeb.WebService_Error, String.Empty, String.Empty)));
}
else {
writer.Write(JavaScriptSerializer.SerializeInternal(BuildWebServiceError(ex.Message, ex.StackTrace, ex.GetType().FullName)));
}
writer.Flush();
}
}
public void ProcessRequest(HttpContext context) {
ExecuteWebServiceCall(context, _webServiceMethodData);
}
public bool IsReusable {
get {
return false;
}
}
}
// Same handler, but implementing IRequiresSessionState to allow session state use
internal class RestHandlerWithSession: RestHandler, IRequiresSessionState {
}
}
|