// .Net StockTrader Sample WCF Application for Benchmarking, Performance Analysis and Design Considerations for Service-Oriented Applications //====================================================================================================== // Code originally contributed by Microsoft Corporation. // This contribution to the Stonehenge project is limited strictly // to the source code that is submitted in this submission. // Any technology, including underlying platform technology, // that is referenced or required by the submitted source code // is not a part of the contribution. // For example and not by way of limitation, // any systems/Windows libraries (WPF, WCF, ASP.NET etc.) // required to run the submitted source code is not a part of the contribution //====================================================================================================== //====================================================================================================== // Contains .NET StockTrader application specific utility methods and constants/statics used by the app. //====================================================================================================== using System; using System.Collections.Generic; using System.Text; using System.Diagnostics; using System.Net.Security; using System.ServiceModel; using System.ServiceModel.Description; using System.ServiceModel.Configuration; using System.IdentityModel.Tokens; using System.IdentityModel.Selectors; using System.Security.Cryptography.X509Certificates; namespace Trade.Utility { /// /// Utility class with constants used in StockTrader app/services. /// public static class StockTraderUtility { //Exception Messages and Strings public static readonly string EXCEPTION_DOTNET_DUPLICATE_PRIMARY_KEY = "Violation of PRIMARY KEY"; public static readonly string EXCEPTION_WEBSPHERE_DUPLICATE_PRIMARY_KEY = "org.omg.CORBA.portable.UnknownException"; public static readonly string EXCEPTION_MESSAGE_INVALID_ORDERMODE_CONFIG = "This 'OrderMode' setting is not a valid setting (settings are case-sensitive)."; public static readonly string EXCEPTION_MESSAGE_INVALID_TXMMODEL_CONFIG = "This 'Use System.Transactions Globally' setting is not a valid setting. Valid settings are 'true' or 'false'. Please fix in: "; public static readonly string EXCEPTION_MESSAGE_VALID_ORDERMODEVALUES = "Valid values are: 'Sync_InProcess', 'ASync_Msmq', 'ASync_Msmq_Volatile', 'ASync_Tcp', 'ASync_Http'."; public static readonly string EXCEPTION_MESSAGE_INVALID_HOLDING_BAD_QUOTE = "Holding with non-valid Quote Symbol: holdingID="; public static readonly string EXCEPTION_MESSAGE_INVALID_HOLDING_NOT_FOUND = "Holding not found!"; public static readonly string EXCEPTION_MESSAGE_INVALID_HOLDING_ZERO_BASIS = "Holding with zero basis!: holdingID="; public static readonly string EXCEPTION_MESSAGE_NULL_DATE = "Data is Null. This method or property cannot be called on Null values."; public static readonly string EXCEPTION_MESSAGE_BAD_ORDER_PARMS = "Your order was not placed becuase the requested quantity was not valid."; public static readonly string EXCEPTION_MESSAGE_BAD_ORDER_RETURN = "We are sorry but your order could not be placed. Please try again later."; public static readonly string EXCEPTION_MESSAGE_ACID_REGISTRATION = "ACID TEST ON INSERT USERID 'ACID': PLANNED EXCEPTION THROWN!"; public static readonly string EXCEPTION_MESSAGE_ACID_BUY = "PLANNED ACID TEST: SYMBOL 'ACIDBUY' PLANNED EXCEPTION THROWN!"; public static readonly string EXCEPTION_MESSAGE_ACID_SELL = "PLANNED ACID TEST: SYMBOL 'ACIDSELL' PLANNED EXCEPTION THROWN!"; public static readonly string EXCEPTION_MESSAGE_BADORDERTYPE = "Sorry, this order type is not allowed."; public static readonly string EXCEPTION_MESSAGE_INVALID_ORDERFORWARDBEHAVIOR_CONFIG = "This 'Order Processing Behavior' setting is not a valid setting (settings are case-sensitive). Please fix in: "; public static readonly string EXCEPTION_MESSAGE_INVALID_ORDERFORWARDMODE_CONFIG = "This 'OrderForwardMode' setting is not a valid setting (settings are case-sensitive). Please fix in: "; public static readonly string EXCEPTION_MESSAGE_VALID_ORDERFORWARDMODEVALUES = "Valid values are: 'ASync_Msmq', 'ASync_Msmq_Volatile', 'ASync_Tcp', 'ASync_Http'."; public static readonly string EXCEPTION_MESSAGE_VALID_ORDERFORWARDBEHAVIORVALUES = "Valid values are: 'Forward' and 'Standard'."; public static readonly string EXCEPTION_WEBSPHERE_USERID_NOTFOUND = "javax.ejb.ObjectNotFoundException"; public static readonly string EXCEPTION_WEBSPHERE_INVALID_PASSWORD = "Incorrect password"; public static readonly string EXCEPTION_MESSAGE_INVALID_ACCESSMODE_CONFIG = "This 'AccessMode' setting is not a valid setting (settings are case-sensitive)."; public static readonly string EXCEPTION_MESSAGE_VALID_ACCESSMODEVALUES = "Valid values are: 'InProcess' (for in-process activation), 'Http_WebService' (for SOA activation via WCF Self-Host/Http), 'Tcp_WebService' (for SOA activation via WCF Self-Host/Tcp), 'Asmx_WebService' (for SOA activation via ASMX) and 'WebSphere_WebService' (for SOA activation via WCF against IBM Trade 6.1 services)."; public static readonly string EXCEPTION_MESSAGE_DUPLICATE_PRIMARY_KEY = "User ID Already Exists! Please Try a Different User ID."; public static readonly string EXCEPTION_MESSAGE_INVALID_LOGIN = "Error Logging In. Invalid Username or Password!"; //Define max lengths for input fields; public static readonly int ADDRESS_MAX_LENGTH = 100; public static readonly int EMAIL_MAX_LENGTH = 100; public static readonly int CREDITCARD_MAX_LENGTH = 100; public static readonly int FULLNAME_MAX_LENGTH = 100; public static readonly int USERID_MAX_LENGTH = 50; public static readonly int PASSWORD_MAX_LENGTH = 100; public static readonly int OPENBALANCE_MAX_LENGTH = 20; public static readonly int QUOTESYMBOL_MAX_LENGTH = 15; public static readonly int SYMBOLSTRING_MAXLENGTH = 30; //Trade Business Logic constants public static readonly decimal BUY_FEE = 15.95m; public static readonly decimal SELL_FEE = 25.95m; public static readonly decimal PENNY_STOCK_P = .1m; public static readonly decimal STOCK_P_HIGH_BAR = 1000m; public static readonly decimal STOCK_P_HIGH_BAR_CRASH = .05m; public static readonly decimal JUNK_STOCK_MIRACLE_MULTIPLIER = 500m; public static readonly int STOCK_CHANGE_MAX_PERCENT = 5; //Valid Config Values for user-set "AccessMode" config setting. Note in all cases, the same //WCF client (that inherits from LoadBalancingClient) is used. Just the binding and service host name varies. public const string ACCESS_STRING_Direct = "InProcess"; public const string ACCESS_STRING_WEB_SERVICE_HTTP = "Http_WebService"; public const string ACCESS_STRING_WEB_SERVICE_HTTP_MESECURITY = "Http_WebService_MSecurity"; public const string ACCESS_STRING_WSO2_WEB_SERVICE_HTTP_MESECURITY = "Http_WSO2_WebService_MSecurity"; //Map string/user-friendly modes to ints for faster lookups public const int ACCESS_Direct = 0; public const int ACCESS_WebService_Http = 1; public const int ACCESS_WebService_WSHttp = 2; public const int ACCESS_WebService_WSHttp_WSO2 = 3; //Valid Config Values for user-set "OrderMode" config setting. public const string ORDER_STRING_Sync = "Sync_InProcess"; public const string ORDER_STRING_ASyncHttp = "ASync_Http"; public const string ORDER_STRING_ASyncHttpMSec = "ASync_Http_MSecurity"; public const string ORDER_STRING_WSO2_HTTP_MSEC = "ASync_WSO2_Java_Http_MSecurity"; //OrderMode constants public const int ORDER_Sync = 0; public const int ORDER_ASync_Http = 1; public const int ORDER_ASync_WSHttp = 2; public const int ORDER_ASync_WSHttp_WSO2 = 3; //map user strings to ints for faster lookups. We want to catch invalid settings //so the user knows what tx model they are running within a service. public const string TRANSACTION_STRING_SYSTEMDOTTRANSACTION_TRANSACTION = "true"; public const string TRANSACTION_STRING_ADONET_TRANSACTION = "false"; public const int TRANSACTION_MODEL_SYSTEMDOTTRANSACTION_TRANSACTION = 1; public const int TRANSACTION_MODEL_ADONET_TRANSACTION = 0; //ACID Test Codes public static readonly string ACID_TEST_USER = "ACIDUSER"; public static readonly string ACID_TEST_BUY = "ACIDBUY"; public static readonly string ACID_TEST_SELL = "ACIDSELL"; public const string REGISTER_USER = "Register"; //Service Host Names. These are good candidates to move into the various repositories to make them more dynamic //than defined in code as constants. Perhaps for next release of StockTrader--easy to do. public const string ORDER_PROCESSOR_SERVICE = "StockTrader Order Processor Service"; public const string WEBAPPLICATION = ".NET StockTrader Web Application"; public const string BUSINESS_SERVICES = "StockTrader Business Services"; //Endpoint - Connected Service Names -- the various modes/bindings we can connect from Web app to Business Services //First for Business Services public const string DOTNET_SELFHOST_BSL_HTTP = ".NET StockTrader Business Services Http/SOAP"; public const string DOTNETSELFHOST_BSL_WSHTTP = ".NET StockTrader Business Services Http/WS-* (M-Security)"; public const string WSO2_BSL_WSHTTP = "WSO2 StockTrader Business Services Http/WS-* (M-Security)"; //Now Endpoint - Connected Service Names for Order Processor public const string DOTNET_SELFHOST_OPS_HTTP = ".NET Order Processor Async-Http (SOAP)"; public const string DOTNET_SELFHOST_OPS_WSHTTP = ".NET Order Processor Async-Http WS-* (M-Security)"; public const string WSO2_OPS_WSHTTP = "WSO2 Order Processor Async-Http WS-* (M-Security)"; //Order Codes public const string ORDER_TYPE_BUY = "buy"; public const string ORDER_TYPE_SELL = "sell"; public const string ORDER_TYPE_SELL_ENHANCED = "sellEnhanced"; public static readonly string ORDER_STATUS_OPEN = "open"; public static readonly string ORDER_STATUS_CLOSED = "closed"; public static readonly string ORDER_STATUS_COMPLETED = "completed"; public const string DAL_SQLSERVER = "Trade.DALSQLServer"; public const string DAL_ORACLE = "Trade.DALOracle"; public const string DAL_DB2 = "Trade.DALDB2"; //Used to cache a Random class used for calculating random price changes vs. creating new //Random class on each buy/sell; private static Random rand = new Random(DateTime.Now.Millisecond); /// /// Writes messages to Main console of Windows self-host base form or console. /// String with message to display/log. /// Event Log entry type code /// Whether to log entry. Entry will be logged if configuration database is set for detailed logging and this parameter is true /// Instance of the Settings class for the service host. Used to determine if detailed logging is on and Event Log Source name. public static void writeConsoleMessage(string message, EventLogEntryType messageType, bool logEntry, string eventLog) { try { LogMessage(message, messageType, logEntry, eventLog); } catch { } return; } /// /// Writes messages to errors console of Windows self-host base form or console. /// /// String with message to display/log. /// Event Log entry type code /// Whether to log entry. Entry will be logged if configuration database is set for detailed logging and this parameter is true /// Instance of the Settings class for the service host. Used to determine if detailed logging is on and Event Log Source name. public static void writeErrorConsoleMessage(string message, EventLogEntryType messageType, bool logEntry, string eventLog) { try { LogMessage(message, messageType, logEntry, eventLog); } catch { } } /// Writes to event log. /// String with message to display/log. /// Event Log entry type code /// Whether to log entry. Entry will be logged if configuration database is set for detailed logging and this parameter is true /// Instance of the Settings class for the service host. Used to determine if detailed logging is on and Event Log Source name. public static void LogMessage(string message, EventLogEntryType messageType, bool logEntry, string eventLog) { if (!logEntry) return; try { EventLog EventLog1 = new EventLog(); EventLog1.Source = eventLog; EventLog1.WriteEntry(message, messageType); } catch (Exception e) { //Choice here: sometimes Web apps will throw a security exception //writing to the event log if the source is not setup properly and the account 'Network Service' //given "Full Control" to the event log to be able to write entries. Really, this exception should //be thrown so user knows they are not getting event log info; however, to avoid frustration //for those that get it, I am simply trapping here. Otherwise, you may wish to uncomment this line: // // throw new Exception("An exception occurred attempting to write to the Windows Application Event Log. Please make sure the 'Network Service' account (the account ASP.NET runs under) has write access to the Application Event Log. You will use RegEdit to add permissions, refer to the StockTrader Readme.html for precise instructions.\nThe specific exception writing to the " + EVENT_LOG + " Application Log event source is: " + e.ToString()); // //So if you catch an exception here, check e to see if a security exception. If so, likely need to add Network //Service to EventLog permissions via regedit, current control set, services, Event Log (select), then //from main menu choose Permissions to add Network Service with Full Control (so ASP.NET/IIS can create/and write to). string throwaway = e.Message; } } //Dermines how much a stock price will change after a trade. public static decimal getRandomPriceChangeFactor(decimal current_price) { if (current_price <= PENNY_STOCK_P) return JUNK_STOCK_MIRACLE_MULTIPLIER; else if (current_price >= STOCK_P_HIGH_BAR) return STOCK_P_HIGH_BAR_CRASH; decimal factor = 0m; int y = rand.Next(1, STOCK_CHANGE_MAX_PERCENT); int x = rand.Next(); if (x % 2 == 0) { factor = 1 - ((decimal)y) / 100m; } else factor = 1 + ((decimal)y) / 100m; return factor; } //How much the brokerage is going to charge! public static decimal getOrderFee(string orderType) { if (orderType.ToLower().Equals(ORDER_TYPE_BUY) || orderType.ToLower().Equals(ORDER_TYPE_SELL)) return BUY_FEE; else return SELL_FEE; } public static void DescribeService(ServiceHost host) { //iterate and display all endpoints via host.Description.Endpoints Console.WriteLine("\n"); Console.WriteLine("Configuration for {0} \n", Console.Title); ServiceDescription svcDesc = host.Description; string configName = svcDesc.ConfigurationName; Console.WriteLine("Configuration name: {0}", configName); // Iterate through the endpoints contained in the ServiceDescription ServiceEndpointCollection sec = svcDesc.Endpoints; foreach (ServiceEndpoint se in sec) { Console.WriteLine("Endpoint:"); Console.WriteLine("\tAddress: {0}", se.Address.ToString()); Console.WriteLine("\tBinding: {0}", se.Binding.ToString()); Console.WriteLine("\tContract: {0}", se.Contract.ToString()); KeyedByTypeCollection behaviors = se.Behaviors; foreach (IEndpointBehavior behavior in behaviors) { Console.WriteLine("Behavior: {0}", behavior.ToString()); } } string name = svcDesc.Name; Console.WriteLine("Service Description name: {0}", name); string namespc = svcDesc.Namespace; Console.WriteLine("Service Description namespace: {0}", namespc); Type serviceType = svcDesc.ServiceType; Console.WriteLine("Service Type: {0}", serviceType.ToString()); Console.WriteLine("\n"); } } //====================================================================================================== //This class contains abstract classes that can be optionally used to authenticate clients when using //advanced Web Services security modes. Three abstract classes are provided, such that any can be //overridden and customized. The first ConfigCertificatePolicy, allows the developer to set a custom //policy for certificates. This is necessary to allow test/dev/self-signed certificates, or else //all WCF operations secured with such a cert would be rejected by WCf clients. Note that the base //SettingsBase class provides a stock instance of this class, which allows all certs if the repository //setting "Accept All Certificates for Development Testing" is set to true. The base instance, which //can be overridden itself within any Settings class (use the new keyword to define the field certificatePolicy //with your implementation class if you want. // //The next two classes are custom validators that are provided. The first class (CustomUserNameValidator) //works with message level security (which always requires a service X.509 certificate) and Username //client credentials. It overrides the default Validate method of the Windows UserNamePassWordValidator to //instead validate against the ConfigService Users table. See StockTrader Business Services for an example with //Message security and Username client credentials. //The second class (CustomCertificateValidator) overrides the Validate method of the Windows X509CertificateValidator //to only allow specified set of client certificates to have access to secured endpoints. //====================================================================================================== /// /// This class is used when repository setting 'ACCEPT_ALL_CERTIFICATES' is set to true, to allow service /// connections via Test (dev-created) certificates. You can override the CheckValidationResult as desired, /// to add a more restrictive/custom policy. /// public abstract class ConfigCertificatePolicy { public ConfigCertificatePolicy() { } /// /// As advertised, always OK. Do not have 'ACCEPT_ALL_CERTIFICATES' set to true for production; or override for more restrictive, /// custom policy. /// /// /// /// /// /// public virtual bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { bool validationResult = true; //Optional add a more restrictive policy here. return validationResult; } } /// /// Note how this class is tied in via a ServiceBehavior, defined in config, to override default Windows auth validation. /// public abstract class CustomUserNameValidator : UserNamePasswordValidator { /// /// Overrides to instead validate the username/password against the Configuration DB Users table. /// /// User id coming in as UserName credentials from client. /// Password coming in as UserName credentials from client. public override void Validate(string userName, string password) { //Add custom user name validation if desired here. Will only be activated if binding security is //set for ClientCredentials = UserName. } } /// /// Provides a base class that allows customization of certificate validation. /// Specifically, enables certificates to be identified specifically based on a list of /// authorized cert thumbprints. See StockTrader Order Processor Service for an example of /// use, as this sample component uses it to ensure only clients using the authorized /// BSLClient certificate are accepted. /// public abstract class CustomCertificateValidator : X509CertificateValidator { /// /// Override with a provided method that returns an array /// of thumbprints as strings. /// /// protected abstract string[] getAllowedThumbprints(); public override void Validate(X509Certificate2 certificate) { // create chain and set validation options X509Chain chain = new X509Chain(); SetValidationSettings(chain); // optional check if cert is valid if (!chain.Build(certificate)) { throw new SecurityTokenValidationException("Client certificate is not valid!"); } // check if cert is from our trusted list if (!isTrusted(chain, getAllowedThumbprints())) { throw new SecurityTokenValidationException("Client certificate is not trusted!"); } } /// /// The base goes with default settings, you could override this method to change them, however. /// /// protected virtual void SetValidationSettings(X509Chain chain) { //override to set customer settings. } /// /// Determines if the end certificate in a chain is in the list of trusted certs. /// You could add logic to perform checks across the whole chain if desired. /// /// /// /// protected virtual bool isTrusted(X509Chain chain, string[] trustedThumbprints) { return CheckThumbprint(chain.ChainElements[0].Certificate, trustedThumbprints); } /// /// Check if a cert is in the trust list. /// /// Cert to check. /// List of authorized certs' thumbprints /// private bool CheckThumbprint(X509Certificate2 certificate, string[] trustedThumbprints) { foreach (string thumbprint in trustedThumbprints) { if (string.Equals(certificate.Thumbprint, thumbprint, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } } }