// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; using System.Web.Http.Properties; namespace System.Web.Http.Routing { /// /// Route class for self-host (i.e. hosted outside of ASP.NET). This class is mostly the /// same as the System.Web.Routing.Route implementation. /// This class has the same URL matching functionality as System.Web.Routing.Route. However, /// in order for this route to match when generating URLs, a special "httproute" key must be /// specified when generating the URL. /// public class HttpRoute : IHttpRoute { /// /// Key used to signify that a route URL generation request should include HTTP routes (e.g. Web API). /// If this key is not specified then no HTTP routes will match. /// public static readonly string HttpRouteKey = "httproute"; internal const string RoutingContextKey = "MS_RoutingContext"; private string _routeTemplate; private HttpRouteValueDictionary _defaults; private HttpRouteValueDictionary _constraints; private HttpRouteValueDictionary _dataTokens; public HttpRoute() : this(routeTemplate: null, defaults: null, constraints: null, dataTokens: null, handler: null, parsedRoute: null) { } public HttpRoute(string routeTemplate) : this(routeTemplate, defaults: null, constraints: null, dataTokens: null, handler: null, parsedRoute: null) { } public HttpRoute(string routeTemplate, HttpRouteValueDictionary defaults) : this(routeTemplate, defaults, constraints: null, dataTokens: null, handler: null, parsedRoute: null) { } public HttpRoute(string routeTemplate, HttpRouteValueDictionary defaults, HttpRouteValueDictionary constraints) : this(routeTemplate, defaults, constraints, dataTokens: null, handler: null, parsedRoute: null) { } public HttpRoute(string routeTemplate, HttpRouteValueDictionary defaults, HttpRouteValueDictionary constraints, HttpRouteValueDictionary dataTokens) : this(routeTemplate, defaults, constraints, dataTokens, handler: null, parsedRoute: null) { } public HttpRoute(string routeTemplate, HttpRouteValueDictionary defaults, HttpRouteValueDictionary constraints, HttpRouteValueDictionary dataTokens, HttpMessageHandler handler) : this(routeTemplate, defaults, constraints, dataTokens, handler, parsedRoute: null) { } internal HttpRoute(string routeTemplate, HttpRouteValueDictionary defaults, HttpRouteValueDictionary constraints, HttpRouteValueDictionary dataTokens, HttpMessageHandler handler, HttpParsedRoute parsedRoute) { _routeTemplate = routeTemplate == null ? String.Empty : routeTemplate; _defaults = defaults ?? new HttpRouteValueDictionary(); _constraints = constraints ?? new HttpRouteValueDictionary(); _dataTokens = dataTokens ?? new HttpRouteValueDictionary(); Handler = handler; if (parsedRoute == null) { // The parser will throw for invalid routes. ParsedRoute = RouteParser.Parse(routeTemplate); } else { ParsedRoute = parsedRoute; } } public IDictionary Defaults { get { return _defaults; } } public IDictionary Constraints { get { return _constraints; } } public IDictionary DataTokens { get { return _dataTokens; } } public HttpMessageHandler Handler { get; private set; } public string RouteTemplate { get { return _routeTemplate; } } internal HttpParsedRoute ParsedRoute { get; private set; } public virtual IHttpRouteData GetRouteData(string virtualPathRoot, HttpRequestMessage request) { if (virtualPathRoot == null) { throw Error.ArgumentNull("virtualPathRoot"); } if (request == null) { throw Error.ArgumentNull("request"); } RoutingContext context = GetOrCreateRoutingContext(virtualPathRoot, request); if (!context.IsValid) { return null; } HttpRouteValueDictionary values = ParsedRoute.Match(context, _defaults); if (values == null) { // If we got back a null value set, that means the URI did not match return null; } // Validate the values if (!ProcessConstraints(request, values, HttpRouteDirection.UriResolution)) { return null; } return new HttpRouteData(this, values); } private static RoutingContext GetOrCreateRoutingContext(string virtualPathRoot, HttpRequestMessage request) { RoutingContext context; if (!request.Properties.TryGetValue(RoutingContextKey, out context)) { context = CreateRoutingContext(virtualPathRoot, request); request.Properties[RoutingContextKey] = context; } return context; } private static RoutingContext CreateRoutingContext(string virtualPathRoot, HttpRequestMessage request) { // Note: we don't validate host/port as this is expected to be done at the host level string requestPath = "/" + request.RequestUri.GetComponents(UriComponents.Path, UriFormat.Unescaped); // This code is optimized for the common path being an exact case match on the virtual path string. // An Ordinal (case-sensitive) comparison is significantly faster than OrdinalIgnoreCase. if (!requestPath.StartsWith(virtualPathRoot, StringComparison.Ordinal)) { if (!requestPath.StartsWith(virtualPathRoot, StringComparison.OrdinalIgnoreCase)) { return RoutingContext.Invalid(); } } string relativeRequestPath = null; int virtualPathLength = virtualPathRoot.Length; if (requestPath.Length > virtualPathLength && requestPath[virtualPathLength] == '/') { relativeRequestPath = requestPath.Substring(virtualPathLength + 1); } else { relativeRequestPath = requestPath.Substring(virtualPathLength); } return RoutingContext.Valid(RouteParser.SplitUriToPathSegmentStrings(relativeRequestPath)); } /// /// Attempt to generate a URI that represents the values passed in based on current /// values from the and new values using the specified . /// /// The HTTP request message. /// The route values. /// A instance or null if URI cannot be generated. public virtual IHttpVirtualPathData GetVirtualPath(HttpRequestMessage request, IDictionary values) { if (request == null) { throw Error.ArgumentNull("request"); } // Only perform URL generation if the "httproute" key was specified. This allows these // routes to be ignored when a regular MVC app tries to generate URLs. Without this special // key an HTTP route used for Web API would normally take over almost all the routes in a // typical app. if (values != null && !values.Keys.Contains(HttpRouteKey, StringComparer.OrdinalIgnoreCase)) { return null; } // Remove the value from the collection so that it doesn't affect the generated URL var newValues = GetRouteDictionaryWithoutHttpRouteKey(values); IHttpRouteData routeData = request.GetRouteData(); IDictionary requestValues = routeData == null ? null : routeData.Values; BoundRouteTemplate result = ParsedRoute.Bind(requestValues, newValues, _defaults, _constraints); if (result == null) { return null; } // Verify that the route matches the validation rules if (!ProcessConstraints(request, result.Values, HttpRouteDirection.UriGeneration)) { return null; } return new HttpVirtualPathData(this, result.BoundTemplate); } private static IDictionary GetRouteDictionaryWithoutHttpRouteKey(IDictionary routeValues) { var newRouteValues = new HttpRouteValueDictionary(); if (routeValues != null) { foreach (var routeValue in routeValues) { if (!String.Equals(routeValue.Key, HttpRouteKey, StringComparison.OrdinalIgnoreCase)) { newRouteValues.Add(routeValue.Key, routeValue.Value); } } } return newRouteValues; } protected virtual bool ProcessConstraint(HttpRequestMessage request, object constraint, string parameterName, HttpRouteValueDictionary values, HttpRouteDirection routeDirection) { IHttpRouteConstraint customConstraint = constraint as IHttpRouteConstraint; if (customConstraint != null) { return customConstraint.Match(request, this, parameterName, values, routeDirection); } // If there was no custom constraint, then treat the constraint as a string which represents a Regex. string constraintsRule = constraint as string; if (constraintsRule == null) { throw Error.InvalidOperation(SRResources.Route_ValidationMustBeStringOrCustomConstraint, parameterName, RouteTemplate, typeof(IHttpRouteConstraint).Name); } object parameterValue; values.TryGetValue(parameterName, out parameterValue); string parameterValueString = Convert.ToString(parameterValue, CultureInfo.InvariantCulture); string constraintsRegEx = "^(" + constraintsRule + ")$"; return Regex.IsMatch(parameterValueString, constraintsRegEx, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); } private bool ProcessConstraints(HttpRequestMessage request, HttpRouteValueDictionary values, HttpRouteDirection routeDirection) { if (Constraints != null) { foreach (KeyValuePair constraintsItem in Constraints) { if (!ProcessConstraint(request, constraintsItem.Value, constraintsItem.Key, values, routeDirection)) { return false; } } } return true; } // Validates that a constraint is of a type that HttpRoute can process. This is not valid to // call when a route implements IHttpRoute or inherits from HttpRoute - as the derived class can handle // any types of constraints it wants to support. internal static void ValidateConstraint(string routeTemplate, string name, object constraint) { if (constraint is IHttpRouteConstraint) { return; } if (constraint is string) { return; } throw CreateInvalidConstraintTypeException(routeTemplate, name); } private static Exception CreateInvalidConstraintTypeException(string routeTemplate, string name) { return Error.InvalidOperation( SRResources.Route_ValidationMustBeStringOrCustomConstraint, name, routeTemplate, typeof(IHttpRouteConstraint).FullName); } } }