//------------------------------------------------------------ // Copyright (c) Microsoft Corporation. All rights reserved. //------------------------------------------------------------ namespace System.ServiceModel.Activation { using System.CodeDom; using System.CodeDom.Compiler; using System.Collections; using System.Collections.Specialized; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Text.RegularExpressions; using System.Web; using System.Web.Hosting; using System.Web.Compilation; using System.Web.RegularExpressions; using System.ServiceModel.Activation.Diagnostics; using System.Security; using System.Runtime.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime; /// /// This class will parse the .svc file and maintains a list of useful information that the build /// provider needs in order to compile the file. The parser creates a list of dependent assemblies, /// understands the compiler that we need to use, fully parses all the supported directives etc. /// /// /// The class is not thread-safe. /// #pragma warning disable 618 // have not moved to the v4 security model yet [SecurityCritical(SecurityCriticalScope.Everything)] #pragma warning restore 618 class ServiceParser { // the delimiter for the compiled custom string const string Delimiter = ServiceHostingEnvironment.ServiceParserDelimiter; // attribute names const string DefaultDirectiveName = "ServiceHost"; const string FactoryAttributeName = "Factory"; const string ServiceAttributeName = "Service"; // regular exression for the directive readonly static SimpleDirectiveRegex directiveRegex; // the build provider we will work with ServiceBuildProvider buildProvider; // text for the file string serviceText; // the class attribute value string factoryAttributeValue = string.Empty; // the constructorstring string serviceAttributeValue = string.Empty; // the line number in file currently being parsed int lineNumber; // the column number in file currently being parsed int startColumn; // the main directive was found or not bool foundMainDirective; // the type of the compiler (i.e C#) CompilerType compilerType; // the string containing the code to be compiled, // it will be null when all the code is "behind" string sourceString; // assemblies to be linked with, we need a unique list // of them and we maintain a Dictionary for it. HybridDictionary linkedAssemblies; // the set of assemblies that the build system is // telling us we will be linked with. There is no unique // requirement for them. ICollection referencedAssemblies; // used to figure out where the new lines start static char[] newlineChars = new char[] { '\r', '\n' }; // source file dependencies HybridDictionary sourceDependencies; // virtual path for the file that we are parsing string virtualPath; [SuppressMessage(FxCop.Category.Security, FxCop.Rule.AptcaMethodsShouldOnlyCallAptcaMethods, Justification = "Users cannot pass arbitrary data to this code.")] static ServiceParser() { directiveRegex = new SimpleDirectiveRegex(); } /// /// The Contructor needs the path to the file that it will parse and a reference to /// the build provider that we are using. This is necessary because there are things that /// need to be set on the build provider directly as we are parsing... /// internal ServiceParser(string virtualPath, ServiceBuildProvider buildProvider) { if (DiagnosticUtility.ShouldTraceInformation) { TraceUtility.TraceEvent(TraceEventType.Information, TraceCode.WebHostCompilation, SR.TraceCodeWebHostCompilation, new StringTraceRecord("VirtualPath", virtualPath), this, (Exception)null); } this.virtualPath = virtualPath; this.buildProvider = buildProvider; } /// /// Constructor that is used when the whole svc file content is provided. This is the case /// when the COM+ Admin tool calls into it. /// ServiceParser(string serviceText) { this.serviceText = serviceText; this.buildProvider = new ServiceBuildProvider(); } /// /// Parsing the content of the service file and retrieve the serviceAttributeValue attribute for ComPlus. /// /// The content of the service file. /// The "serviceAttributeValue" attribute of the Service directive. /// internal static IDictionary ParseServiceDirective(string serviceText) { ServiceParser parser = new ServiceParser(serviceText); parser.ParseString(); // the list of valid attributes for ComPlus for Service Directive IDictionary attributeTable = new Dictionary( StringComparer.OrdinalIgnoreCase); if (!string.IsNullOrEmpty(parser.factoryAttributeValue)) attributeTable.Add(FactoryAttributeName, parser.factoryAttributeValue); if (!string.IsNullOrEmpty(parser.serviceAttributeValue)) attributeTable.Add(ServiceAttributeName, parser.serviceAttributeValue); return attributeTable; } /// /// // various getters for private objects that the build // provider will need // internal CompilerType CompilerType { get { return compilerType; } } internal ICollection AssemblyDependencies { get { if (linkedAssemblies == null) { return null; } return linkedAssemblies.Keys; } } internal ICollection SourceDependencies { get { if (sourceDependencies == null) { return null; } return sourceDependencies.Keys; } } internal bool HasInlineCode { get { return (sourceString != null); } } /// /// Parses the code file appropriately. This method is used by the /// build provider. /// internal void Parse(ICollection referencedAssemblies) { if (referencedAssemblies == null) { throw FxTrace.Exception.ArgumentNull("referencedAssemblies"); } this.referencedAssemblies = referencedAssemblies; AddSourceDependency(virtualPath); using (TextReader reader = buildProvider.OpenReaderInternal()) { this.serviceText = reader.ReadToEnd(); ParseString(); } } /// /// This method returns a code compile unit that will be added /// to the other depdnecies in order to compile /// internal CodeCompileUnit GetCodeModel() { // Do we have something to compile? // if (sourceString == null || sourceString.Length == 0) return null; CodeSnippetCompileUnit snippetCompileUnit = new CodeSnippetCompileUnit(sourceString); // Put in some context so that the file can be debugged. // string pragmaFile = HostingEnvironmentWrapper.MapPath(virtualPath); snippetCompileUnit.LinePragma = new CodeLinePragma(pragmaFile, lineNumber); return snippetCompileUnit; } Exception CreateParseException(string message, string sourceCode) { return CreateParseException(message, null, sourceCode); } Exception CreateParseException(Exception innerException, string sourceCode) { return CreateParseException(innerException.Message, innerException, sourceCode); } Exception CreateParseException(string message, Exception innerException, string sourceCode) { return new HttpParseException(message, innerException, this.virtualPath, sourceCode, this.lineNumber); } /// /// This method returns the custom string that is to be passed to ServiceHostingEnvironment from BuildManager. /// /// The full name of the built assembly for inline code. internal string CreateParseString(Assembly compiledAssembly) { Type typeToPreserve = this.GetCompiledType(compiledAssembly); string typeToPreserveName = string.Empty; if (typeToPreserve != null) typeToPreserveName = typeToPreserve.AssemblyQualifiedName; System.Text.StringBuilder builder = new System.Text.StringBuilder(); if (compiledAssembly != null) { builder.Append(Delimiter); builder.Append(compiledAssembly.FullName); } if (this.referencedAssemblies != null) { // CSDMain #192135 // Minimize code change by doing 2 passes to have assembly containing type at the top of the list. // As a result, this assembly will get loaded first in ServiceHostFactory.CreateServiceHost. // In the multi-targetting scenario this prevents the runtime from trying to load a newer CLR assembly // and failing. In the happy case, duplicate assembly references may occur (no effect on runtime). // Note that if the service type is contained in a framework assembly, this does not fix the problem. // Future improvement is to write fully qualified type name and let CLR handle load/search. if (!string.IsNullOrEmpty(serviceAttributeValue)) { foreach (Assembly assembly in this.referencedAssemblies) { Type serviceType; try { serviceType = assembly.GetType(serviceAttributeValue, false); } catch (Exception e) { if (System.Runtime.Fx.IsFatal(e)) { throw; } // log exception, but do not rethrow DiagnosticUtility.TraceHandledException(e, TraceEventType.Warning); break; } if (serviceType != null) { builder.Append(Delimiter); builder.Append(assembly.FullName); break; } } } foreach (Assembly assembly in this.referencedAssemblies) { builder.Append(Delimiter); builder.Append(assembly.FullName); } } if (this.AssemblyDependencies != null) { foreach (Assembly assembly in this.AssemblyDependencies) { builder.Append(Delimiter); builder.Append(assembly.FullName); } } // use application relative virtualpath instead of the absolute path // so that the compliedcustomstring is applicationame independent return string.Concat(VirtualPathUtility.ToAppRelative(virtualPath), Delimiter, typeToPreserveName, Delimiter, serviceAttributeValue, builder.ToString()); } void AddSourceDependency(string fileName) { if (sourceDependencies == null) sourceDependencies = new HybridDictionary(true); sourceDependencies.Add(fileName, fileName); } Type GetCompiledType(Assembly compiledAssembly) { if (string.IsNullOrEmpty(factoryAttributeValue)) { return null; } Type type = null; // First, try to get the type from the assembly that has been built (if any) if (this.HasInlineCode && (compiledAssembly != null)) { type = compiledAssembly.GetType(factoryAttributeValue); } // If not, try to get it from other assemblies if (type == null) { type = GetType(factoryAttributeValue); } return type; } internal IDictionary GetLinePragmasTable() { LinePragmaCodeInfo info = new LinePragmaCodeInfo(this.lineNumber, this.startColumn, 1, -1, false); IDictionary dictionary = new Hashtable(); dictionary[this.lineNumber] = info; return dictionary; } /// /// Parses the content of the svc file for each directive line /// void ParseString() { try { int textPos = 0; Match match; lineNumber = 1; // Check for ending bracket first, MB 45013. if (this.serviceText.IndexOf('>') == -1) { throw FxTrace.Exception.AsError(new HttpException(SR.Hosting_BuildProviderDirectiveEndBracketMissing(ServiceParser.DefaultDirectiveName))); } // First, parse all the <%@ ... %> directives // for (;;) { match = directiveRegex.Match(this.serviceText, textPos); // Done with the directives? // if (!match.Success) break; lineNumber += ServiceParserUtilities.LineCount(this.serviceText, textPos, match.Index); textPos = match.Index; // Get all the directives into a bag // IDictionary directive = CollectionsUtil.CreateCaseInsensitiveSortedList(); string directiveName = ProcessAttributes(match, directive); // Understand the directive // ProcessDirective(directiveName, directive); lineNumber += ServiceParserUtilities.LineCount(this.serviceText, textPos, match.Index + match.Length); textPos = match.Index + match.Length; // Fixup line and column numbers to have meaninglful errors // int newlineIndex = this.serviceText.LastIndexOfAny(newlineChars, textPos - 1); startColumn = textPos - newlineIndex; } if (!foundMainDirective) { throw FxTrace.Exception.AsError(new HttpException(SR.Hosting_BuildProviderDirectiveMissing(ServiceParser.DefaultDirectiveName))); } // skip the directives chunk // string remainingText = this.serviceText.Substring(textPos); // If there is something else in the file, it needs to be compiled // if (!ServiceParserUtilities.IsWhiteSpaceString(remainingText)) { sourceString = remainingText; } } catch (HttpException e) { // the string is set in the internal exception, no need to set it again. // Exception parseException = CreateParseException(e, this.serviceText); throw FxTrace.Exception.AsError( new HttpCompileException(parseException.Message, parseException)); } } /// /// Return the directive if it exists or an empty string /// string ProcessAttributes(Match match, IDictionary attribs) { // creates 3 parallel capture collections // for the attribute names, the attribute values and the // equal signs // string ret = String.Empty; CaptureCollection attrnames = match.Groups["attrname"].Captures; CaptureCollection attrvalues = match.Groups["attrval"].Captures; CaptureCollection equalsign = match.Groups["equal"].Captures; // Iterate through all of them and add then to // the dictionary of attributes // for (int i = 0; i < attrnames.Count; i++) { string attribName = attrnames[i].ToString(); string attribValue = attrvalues[i].ToString(); // Check if there is an equal sign. // bool fHasEqual = (equalsign[i].ToString().Length > 0); if (attribName != null) { // A <%@ %> block can have two formats: // <%@ directive foo=1 bar=hello %> // <%@ foo=1 bar=hello %> // Check if we have the first format // if (!fHasEqual && i == 0) { // return the main directive // ret = attribName; continue; } try { if (attribs != null) attribs.Add(attribName, attribValue); } catch (ArgumentException) { throw FxTrace.Exception.AsError(new HttpException(SR.Hosting_BuildProviderDuplicateAttribute(attribName))); } } } return ret; } /// /// This method understands the compilation parameters if any ... /// [SuppressMessage(FxCop.Category.Security, FxCop.Rule.DoNotIndirectlyExposeMethodsWithLinkDemands, Justification = "This method doesn't allow callers to access sensitive information, operations, or resources that can be used in a destructive manner.")] void ProcessCompilationParams(IDictionary directive, CompilerParameters compilParams) { bool debug = false; if (ServiceParserUtilities.GetAndRemoveBooleanAttribute(directive, "debug", ref debug)) { compilParams.IncludeDebugInformation = debug; } int warningLevel = 0; if (ServiceParserUtilities.GetAndRemoveNonNegativeIntegerAttribute(directive, "warninglevel", ref warningLevel)) { compilParams.WarningLevel = warningLevel; if (warningLevel > 0) compilParams.TreatWarningsAsErrors = true; } string compilerOptions = ServiceParserUtilities.GetAndRemoveNonEmptyAttribute(directive, "compileroptions"); if (compilerOptions != null) { compilParams.CompilerOptions = compilerOptions; } } /// /// Processes a directive block /// void ProcessDirective(string directiveName, IDictionary directive) { // Throw on empy, no directive specified // if (directiveName.Length == 0) { throw FxTrace.Exception.AsError(new HttpException(SR.Hosting_BuildProviderDirectiveNameMissing)); } // Check for the main directive // if (string.Compare(directiveName, ServiceParser.DefaultDirectiveName, StringComparison.OrdinalIgnoreCase) == 0) { // Make sure the main directive was not already specified // if (foundMainDirective) { throw FxTrace.Exception.AsError(new HttpException(SR.Hosting_BuildProviderDuplicateDirective(ServiceParser.DefaultDirectiveName))); } foundMainDirective = true; // Ignore 'codebehind' attribute (ASURT 4591) // directive.Remove("codebehind"); string language = ServiceParserUtilities.GetAndRemoveNonEmptyAttribute(directive, "language"); // Get the compiler for the specified language (if any) // or get the one from config // if (language != null) { compilerType = buildProvider.GetDefaultCompilerTypeForLanguageInternal(language); } else { compilerType = buildProvider.GetDefaultCompilerTypeInternal(); } if (directive.Contains(FactoryAttributeName)) { factoryAttributeValue = ServiceParserUtilities.GetAndRemoveNonEmptyAttribute(directive, FactoryAttributeName); serviceAttributeValue = ServiceParserUtilities.GetAndRemoveNonEmptyAttribute(directive, ServiceAttributeName); } else if (directive.Contains(ServiceAttributeName)) { serviceAttributeValue = ServiceParserUtilities.GetAndRemoveNonEmptyAttribute(directive, ServiceAttributeName); } else { throw FxTrace.Exception.AsError(new HttpException(SR.Hosting_BuildProviderMainAttributeMissing)); } // parse the parameters that are related to the compiler // ProcessCompilationParams(directive, compilerType.CompilerParameters); } else if (string.Compare(directiveName, "assembly", StringComparison.OrdinalIgnoreCase) == 0) { if (directive.Contains("name") && directive.Contains("src")) { throw FxTrace.Exception.AsError(new HttpException(SR.Hosting_BuildProviderMutualExclusiveAttributes("src", "name"))); } else if (directive.Contains("name")) { string assemblyName = ServiceParserUtilities.GetAndRemoveNonEmptyAttribute(directive, "name"); if (assemblyName != null) { AddAssemblyDependency(assemblyName); } else throw FxTrace.Exception.AsError(new HttpException(SR.Hosting_BuildProviderAttributeEmpty("name"))); } else if (directive.Contains("src")) { string srcPath = ServiceParserUtilities.GetAndRemoveNonEmptyAttribute(directive, "src"); if (srcPath != null) { ImportSourceFile(srcPath); } else throw FxTrace.Exception.AsError(new HttpException(SR.Hosting_BuildProviderAttributeEmpty("src"))); } else { // if (!directive.Contains("name") && !directive.Contains("src")) throw FxTrace.Exception.AsError(new HttpException(SR.Hosting_BuildProviderRequiredAttributesMissing("src", "name"))); } } else { throw FxTrace.Exception.AsError(new HttpException(SR.Hosting_BuildProviderUnknownDirective(directiveName))); } // check if there are any directives that you did not process // if (directive.Count > 0) throw FxTrace.Exception.AsError(new HttpException(SR.Hosting_BuildProviderUnknownAttribute(ServiceParserUtilities.FirstDictionaryKey(directive)))); } void ImportSourceFile(string path) { // Get a full path to the source file, compile it to an assembly // add the depedency to the assembly // string baseVirtualDir = VirtualPathUtility.GetDirectory(virtualPath); string fullVirtualPath = VirtualPathUtility.Combine(baseVirtualDir, path); AddSourceDependency(fullVirtualPath); Assembly a = BuildManager.GetCompiledAssembly(fullVirtualPath); AddAssemblyDependency(a); } void AddAssemblyDependency(string assemblyName) { // Load and keep track of the assembly // Assembly a = Assembly.Load(assemblyName); AddAssemblyDependency(a); } void AddAssemblyDependency(Assembly assembly) { if (linkedAssemblies == null) linkedAssemblies = new HybridDictionary(false); linkedAssemblies.Add(assembly, null); } /// /// Look for a type by name in the assemblies available to this page /// Type GetType(string typeName) { Type type; // If it contains an assembly name, just call Type.GetType (ASURT 53589) // if (ServiceParserUtilities.TypeNameIncludesAssembly(typeName)) { try { type = Type.GetType(typeName, true); } catch (ArgumentException e) { Exception parseException = CreateParseException(e, this.sourceString); throw FxTrace.Exception.AsError( new HttpCompileException(parseException.Message, parseException)); } catch (TargetInvocationException e) { Exception parseException = CreateParseException(e, this.sourceString); throw FxTrace.Exception.AsError( new HttpCompileException(parseException.Message, parseException)); } catch (TypeLoadException e) { Exception parseException = CreateParseException(SR.Hosting_BuildProviderCouldNotCreateType(typeName), e, this.sourceString); throw FxTrace.Exception.AsError( new HttpCompileException(parseException.Message, parseException)); } return type; } try { type = ServiceParserUtilities.GetTypeFromAssemblies(referencedAssemblies, typeName, false /*ignoreCase*/); if (type != null) return type; type = ServiceParserUtilities.GetTypeFromAssemblies(AssemblyDependencies, typeName, false /*ignoreCase*/); if (type != null) return type; } catch (HttpException e) { Exception parseException = CreateParseException(SR.Hosting_BuildProviderCouldNotCreateType(typeName), e, this.sourceString); throw FxTrace.Exception.AsError( new HttpCompileException(parseException.Message, parseException)); } Exception exception = CreateParseException(SR.Hosting_BuildProviderCouldNotCreateType(typeName), this.sourceString); throw FxTrace.Exception.AsError( new HttpCompileException(exception.Message, exception)); } /// /// This class contains static methods that are necessary to manipulate the /// structures that contain the directives. The logic assumes that the parser will /// create a dictionary that contains all the directives and we can pull certain directives as /// necessary while processing/compiling the page. The directives are strings. /// /// static class ServiceParserUtilities { /// /// Return the first key of the dictionary as a string. Throws if it's /// empty or if the key is not a string. /// internal static string FirstDictionaryKey(IDictionary dictionary) { // assume that the caller has checked the dictionary before calling // IDictionaryEnumerator e = dictionary.GetEnumerator(); e.MoveNext(); return (string)e.Key; } /// /// Get a string value from a dictionary, and remove /// it from the dictionary of attributes if it exists. /// /// Returns null if the value was not there ... static string GetAndRemove(IDictionary dictionary, string key) { string val = (string)dictionary[key]; if (val != null) { dictionary.Remove(key); val = val.Trim(); } else return string.Empty; return val; } /// /// Get a value from a dictionary, and remove it from the dictionary if /// it exists. Throw an exception if the value is a whitespace string. /// However, don't complain about null, which simply means the value is not /// in the dictionary. /// internal static string GetAndRemoveNonEmptyAttribute(IDictionary directives, string key, bool required) { string val = ServiceParserUtilities.GetAndRemove(directives, key); if (val.Length == 0) { if (required) { throw FxTrace.Exception.AsError(new HttpException(SR.Hosting_BuildProviderAttributeMissing(key))); } return null; } return val; } internal static string GetAndRemoveNonEmptyAttribute(IDictionary directives, string key) { return GetAndRemoveNonEmptyAttribute(directives, key, false /*required*/); } /// /// Get a string value from a dictionary, and convert it to bool. Throw an /// exception if it's not a valid bool string. /// However, don't complain about null, which simply means the value is not /// in the dictionary. /// The value is returned through a REF param (unchanged if null) /// /// True if attrib exists, false otherwise internal static bool GetAndRemoveBooleanAttribute(IDictionary directives, string key, ref bool val) { string s = ServiceParserUtilities.GetAndRemove(directives, key); if (s.Length == 0) return false; try { val = bool.Parse(s); } catch (FormatException) { throw FxTrace.Exception.AsError(new HttpException(SR.Hosting_BuildProviderInvalidValueForBooleanAttribute(s, key))); } return true; } /// /// Get a string value from a dictionary, and convert it to integer. Throw an /// exception if it's not a valid positive integer string. /// However, don't complain about null, which simply means the value is not /// in the dictionary. /// The value is returned through a REF param (unchanged if null) /// /// True if attrib exists, false otherwise internal static bool GetAndRemoveNonNegativeIntegerAttribute(IDictionary directives, string key, ref int val) { string s = ServiceParserUtilities.GetAndRemove(directives, key); if (s.Length == 0) return false; val = GetNonNegativeIntegerAttribute(key, s); return true; } /// /// Parse a string attribute into a non-negative integer /// /// Name of the attribute, used only for the error messages /// Value to convert to int static int GetNonNegativeIntegerAttribute(string name, string value) { int ret; try { ret = int.Parse(value, CultureInfo.InvariantCulture); } catch (FormatException) { throw FxTrace.Exception.AsError(new HttpException(SR.Hosting_BuildProviderInvalidValueForNonNegativeIntegerAttribute(value, name))); } // Make sure it's not negative // if (ret < 0) { throw FxTrace.Exception.AsError(new HttpException(SR.Hosting_BuildProviderInvalidValueForNonNegativeIntegerAttribute(value, name))); } return ret; } internal static bool IsWhiteSpaceString(string s) { return (s.Trim().Length == 0); } /// /// This method takes the code that will be compiled as a string and it /// will count how many lines exist between the given offset and the final /// offset. /// /// The text that contains the source code /// Starting offset for lookup /// Ending offset /// The number of lines internal static int LineCount(string text, int offset, int newoffset) { int linecount = 0; while (offset < newoffset) { if (text[offset] == '\r' || (text[offset] == '\n' && (offset == 0 || text[offset - 1] != '\r'))) linecount++; offset++; } return linecount; } /// /// Parses a string that contains a type trying to figure out if the assembly info is there. /// /// The string to search internal static bool TypeNameIncludesAssembly(string typeName) { return (typeName.IndexOf(",", StringComparison.Ordinal) >= 0); } /// /// Loops through a list of assemblies that are already collected by the parser/provider and /// looks for the specified type. /// /// The collection of assemblies /// The type name /// Case sensitivity knob /// internal static Type GetTypeFromAssemblies(ICollection assemblies, string typeName, bool ignoreCase) { if (assemblies == null) return null; Type type = null; foreach (Assembly assembly in assemblies) { Type t = assembly.GetType(typeName, false /*throwOnError*/, ignoreCase); if (t == null) continue; // If we had already found a different one, it's an ambiguous type reference // if (type != null && t != type) { throw FxTrace.Exception.AsError(new HttpException( SR.Hosting_BuildProviderAmbiguousType(typeName, type.Assembly.FullName, t.Assembly.FullName))); } // Keep track of it // type = t; } return type; } } } }