Monday, February 18, 2008

Use RelayBinding to Expose Your Service to the world through MS Service Bus

To use Microsoft implementation of ESB at http://www.biztalk.net/, We need only use RelayBinding in WCF + Biztalk Service Extension.
In fact Service Config could be as simple as the following:

<configuration>
<system.serviceModel>
<services>
<service name="JQD.HomeLabService">
<endpoint contract="JQD.IHomeLabService"
name="JQD.HomeLabService"
binding="relayBinding" />
...
and the corresponding code are also simple:

static void Main(string[] args)
{
CardSpaceTokenProvider tok = new CardSpaceTokenProvider();
string rHostName = RelayBinding.DefaultRelayHostName;
Uri rHostUri = new Uri(String.Format("sb://{0}/services/{1}/HomeLabService",rHostName,tok.GetUserName()));
ServiceHost rHost = new ServiceHost(typeof(HomeLabService), rHostUri);
rHost.Description.Endpoints[0].Behaviors.Add(tok);
rHost.Open();
Console.ReadLine();
rHost.Close();
}

Clearly, the endpoint "sb://conect.biztalk.net/services/jqd2001/HomeLabService" will be correctly relayed to your server every running code has a correct cardSpace token.

For a piece of code to reach your web services through relay, it only need to know the contract

[ServiceContract()]
public interface IHomeLabService
{
[OperationContract()]
string HeartBeat();
}

and it then need to connect to MS ESB:


static void Main(string[] args)
{
ChannelFactory f = new ChannelFactory(new RelayBinding(),
"sb://connect.biztalk.net/services/jqd2001/HomeLabService");
f.Open();
IHomeLabService ch =f.CreateChannel();
Console.WriteLine( ch.HeartBeat());
f.Close();

Noticeably, RelayBinding is the key to traverse NAT or firewall and without expose your server to the internet directly.

Friday, February 15, 2008

How to correctly use MVP Design pattern

JHA has a presenter implementation in GMWBRiderControl.ascx, which uses GMWBPresenter. This presenter implementation has two problems:
(1) View are updated by function rather than properties, which makes Presenter stateless and unable to utilize overriding, retraction technique commonly seen in OOP/OOD.
(2) Missing commonly seem MVP elements such as IView and Processing() single entry point. This deviation from standard MVP result in a lot of business logic in ASP.Net Code-behind ( such as BindControls function in GMWBRiderControl.ascx.vb) --- completely defeated the purpuse of MVP to separately logic into Presenter.
To correctly this problem, the following changes are needed:

(1) Convert all function into property. e.g


Public Function ShowLifeTimeIncomeAmount() As Boolean
'
If IsGMWBIndicatorA _
OrElse IsGMWBIndicatorH _
Then
Return False
End If
'
Return True
End Function

would be changed to

Private _ShowLifeTimeIncomeAmount As Boolean
Public Property ShowLifeTimeIncomeAmount() As Boolean
Get
Return _ShowLifeTimeIncomeAmount
End Get
Set(ByVal value As Boolean)
_ShowLifeTimeIncomeAmount = value
End Set
End Property

all logic behind those function will be move into the function gettin gdata from Domain Model

Private Sub BindDataFromDomainModelToPresenter()
If IsGMWBIndicatorA _
OrElse IsGMWBIndicatorH _
Then
ShowLifeTimeIncomeAmount = False
End If
'
ShowLifeTimeIncomeAmount = True
' TODO: Change all the other function to property and call here to set it
End Sub


(2) Define "IView" and implement on User Control

Public Interface IGMWBView
Sub UpdateGMWBFields(ByVal IsVantageDown As Boolean)
Sub UpdateWithDrawAllowanceNewLabel(ByVal IsVantageDown As Boolean)
Sub UpdateWithdrawAmountInfo(ByVal IsVantageDown As Boolean)
Sub UpdateBenefitBase(ByVal IsVantageDown As Boolean)
Sub UpdatePaymentStepInfo(ByVal IsVantageDown As Boolean)
Sub MakeAllLIAFieldVisibleForBrokerClient(ByVal IsVantageDown As Boolean)
End Interface


Partial Class GmwbRiderControl
..
Implements IGMWBView


Public Sub UpdateGMWBFields(ByVal _IsVantageDown As Boolean) Implements IGMWBView.UpdateGMWBFields

' Move some code seqment from BindControls to here
End Sub

(3) Implementing business Execution login in Presenter


Public Overridable Sub UpdateGMWBView(ByVal view As IGMWBView)
' Pump Data from DO to P
BindDataFromDomainModelToPresenter()
' Invoke processing function implemented on ASP.Net code-Behind (VIEW)
view.UpdateGMWBFields(_IsVantageDown)
// other logic to change view
End Sub

(4) Call the single entry point on presenter "UpdateGMWBView" in user control in the function "BindControls"

The above steps will result in standard implementation of MVP.

Saturday, February 9, 2008

Using WSE 3.0 X509 Search API to implement RSA Crypto

WSE 3.0 has two functions to retrieve X509 Certificate:

X509Certificate2Collection cert2s = X509Util.FindCertificateBySubjectName(SubjectName, StoreLocation, StoreName.ToString());

X509Certificate2Collection cert2s = X509Util.FindCertificateByKeyIdentifier(ThumbPrint , StoreLocation, StoreName.ToString());

This is much simpler than using COM API or its .Net Wrapper. Using these function, we can write a class encapsulate RSA Crypto with considering of Certification Basic Policy Validation ( such as revocation):


namespace JQD
{
public class X509RSAEncryptor
{
#region Encryption and Decryption
public static string[] EncryptUTF8ToBase64(string ClearTextUTF8, X509Certificate2 cert2ForEncryption, X509Certificate2 cert2ForSignning)
{
if (null == cert2ForEncryption) throw new ApplicationException("null X509 cert for Encryption");
// create Encryption RSA using Public Key only
RSAParameters rsaForEncryptionPublicParam = X509Util.GetKey(cert2ForEncryption).ExportParameters(false);
RSACryptoServiceProvider rsaForEncryption = new RSACryptoServiceProvider();
rsaForEncryption.ImportParameters(rsaForEncryptionPublicParam);
// generate encrypted Base64 string from clear Text
byte[] clearBytes = Encoding.UTF8.GetBytes(ClearTextUTF8);
byte[] cipherBytes = rsaForEncryption.Encrypt(clearBytes, true);
string cipherTextBase64 = Convert.ToBase64String(cipherBytes);
string signatureTextBase64 = "";
if (null != cert2ForSignning)
{
// create Signning RSA using Private Key
RSAParameters rsaForSignningPrivateParam = X509Util.GetKey(cert2ForSignning).ExportParameters(true);
RSACryptoServiceProvider rsaForSignning = new RSACryptoServiceProvider();
rsaForSignning.ImportParameters(rsaForSignningPrivateParam);
// compute Signature using SHA1
byte[] signature = rsaForSignning.SignData(cipherBytes, SHA1.Create());
signatureTextBase64 = Convert.ToBase64String(signature);
rsaForSignning.Clear();
}
rsaForEncryption.Clear();
// send both Data and its singature as Base64 string
return new string[2] { cipherTextBase64, signatureTextBase64 };
}
public static string DecryptBase64ToUTF8(string[] CipherBase64, X509Certificate2 cert2ForDecryption, X509Certificate2 cert2ForVerifySignning)
{
if (null == cert2ForDecryption) throw new ApplicationException("null X509 cert for decryption");
byte[] cipherBytes = Convert.FromBase64String(CipherBase64[0]);
if (null != cert2ForVerifySignning)
{
// create Verify Signning RSA using public Key
RSAParameters rsaForVerifySignningPrivateParam = X509Util.GetKey(cert2ForVerifySignning).ExportParameters(false);
RSACryptoServiceProvider rsaForVerifySignning = new RSACryptoServiceProvider();
rsaForVerifySignning.ImportParameters(rsaForVerifySignningPrivateParam);
// Verify signature
byte[] signature = Convert.FromBase64String(CipherBase64[1]);
try
{
if (!rsaForVerifySignning.VerifyData(cipherBytes, SHA1.Create(), signature)) throw new ApplicationException("Data have been tampered.");
}
finally
{
rsaForVerifySignning.Clear();
}
}
// create Decryption RSA using Private Key
RSAParameters rsaForDecryptionPrivateParam = X509Util.GetKey(cert2ForDecryption).ExportParameters(true);
RSACryptoServiceProvider rsaForDecryption = new RSACryptoServiceProvider();
rsaForDecryption.ImportParameters(rsaForDecryptionPrivateParam);
// Decrypt and convert to Clear Text
byte[] clearBytes = rsaForDecryption.Decrypt(cipherBytes, true);
rsaForDecryption.Clear();
return Encoding.UTF8.GetString(clearBytes);
}
#endregion
#region X509 finder
public static X509Certificate2 FindX509Certificate2(string SubjectName, StoreLocation StoreLocation, StoreName StoreName)
{
X509Certificate2Collection cert2s = X509Util.FindCertificateBySubjectName(SubjectName, StoreLocation, StoreName.ToString());
X509Certificate2Collection validCert2s = Validate(cert2s);
if (validCert2s.Count == 0) return null;
return validCert2s[0];
}
public static X509Certificate2 FindX509Certificate2(byte[] ThumbPrint,StoreLocation StoreLocation, StoreName StoreName)
{
X509Certificate2Collection cert2s = X509Util.FindCertificateByKeyIdentifier(ThumbPrint , StoreLocation, StoreName.ToString());
X509Certificate2Collection validCert2s=Validate(cert2s);
if (validCert2s.Count == 0) return null;
return validCert2s[0];
}
private static X509Certificate2Collection Validate(X509Certificate2Collection cert2s)
{
X509Certificate2Collection validCert2s = new X509Certificate2Collection();
foreach (X509Certificate2 cert2 in cert2s)
{
// applies the base policy to that chain, Note that on Win2k3, the basic policy
// check conformance to RFC3280, which include revocation for X509 Cert2.
bool IsConformedToBasicPolicy = cert2.Verify();
// Some other simple checking
bool IsArchived = cert2.Archived;
bool IsExpired = (DateTime.Now > cert2.NotAfter)
bool IsNotActive =( DateTime.Now < cert2.NotBefore);
if (IsConformedToBasicPolicy && !IsArchived && !IsExpired && !IsNotActive)
{
validCert2s.Add(cert2);
}
}
return validCert2s;
}
#endregion
}
}

The usage is also simple as illustrated by the following:


class Program
{
static void Main(string[] args)
{
string msg="This is a secret.";
Console.WriteLine(msg);
X509Certificate2 cert2Enc;
X509Certificate2 cert2Sig;
string subjectName1="CN=idmcertid, OU=Internet Infrastructure, O=\"Blah Blah, Inc.\", L=Boston, S=massachusetts, C=US";
byte[] thumbPrint1=new byte[] { 0xb7, 0x4a, ....., 0x9c, 0x0c };

cert2Enc = X509RSAEncryptor.FindX509Certificate2(subjectName1,StoreLocation.LocalMachine, StoreName.My);
cert2Sig = X509RSAEncryptor.FindX509Certificate2("CN=XYZ Company", StoreLocation.CurrentUser, StoreName.My);
cert2Sig = X509RSAEncryptor.FindX509Certificate2(subjectName1, StoreLocation.CurrentUser, StoreName.My);
string[] cipherBase64 = X509RSAEncryptor.EncryptUTF8ToBase64(msg, cert2Enc, cert2Sig);
Console.WriteLine(cipherBase64[0]);
Console.WriteLine();
Console.WriteLine(cipherBase64[1]);
Console.WriteLine();
X509Certificate2 cert2Dec=X509RSAEncryptor.FindX509Certificate2(subjectName1,StoreLocation.LocalMachine, StoreName.My);
Console.WriteLine(X509RSAEncryptor.DecryptBase64ToUTF8(cipherBase64, cert2Dec, cert2Sig));
Console.ReadLine();
}

Note that when install Private Key, Must check " Mark this key Exportable" to avoid " Key in invalid state" exception. Also remember RSA encryption requires Receiving party to hold private key for decryption and hold public key for verify signning. Sending party hold just the opposite. So Receiveing party only need sending party's public key, and so on. This means it is better to have two computers with two set of good X509 Cert to run the code. If the cert is really invalid as seen in MMC IDE, IsConformedToBasicPolicy will be false.
Finally, There are no need to install WSE 3.0. You only need to have a copy of Microsoft.Web.Services3.dll and add its reference to your project.

Wednesday, February 6, 2008

Using Ajax Behavior to capture Before Unload event

In my previous post
http://jqdjha.blogspot.com/2008/02/handling-onbeforeunload-event-double.html
I discussed how to use setTimeout to avoid double firing onbeforeunload event in IE6 and IE7.
If the web page support ASP.Net Ajax Library, then we can use Ajax behavior to inject our code from that previous post to window, body, frameset tag, all of which support beforeunload event in IE. Note that we can control whether to attach "NavAway Behavior" or not:

<script>
      $create(JQD.NavAwayWarning,null,null,null,window);
</script>


var globalIsThe2ndNavAway=false;
Type.registerNamespace("JQD");


JQD.NavAwayWarning= function (element) {
      JQD.NavAwayWarning.initializeBase(this,[element]);
      this._navAwayHandler = Function.createDelegate(this,this._onNavAway);
}


JQD.NavAwayWarning.prototype = {

_onNavAway : function (e) {

      if (globalIsThe2ndNavAway) return;
      window.event.returnValue="You are losing changes."
      globalIsThe2ndNavAway=true;
      setTimeout("globalIsThe2ndNavAway=false;",0) ;

},

initialize: function () {
      JQD.NavAwayWarning.callBaseMethod(this,'initialize');
      $addHandler(this.get_element(),'beforeunload',this._navAwayHandler);
},

dispose: function() {
      JQD.NavAwayWarning.callBaseMethod(this,'dispose');
      $removeHandler(this.get_element(),'beforeunload',this._navAwayHandler);
}

}
JQD.NavAwayWarning.registerClass('JQD.NavAwayWarning', Sys.UI.Behavior);

Monday, February 4, 2008

Handling OnBeforeUnload Event Double Firing

IE 6.0 and 7.0 will fire onbeforeunload event twice if there is a link button on the ASP.Net page ( or in general an anchor with href set to a postback javascript). Here is the simple markup that "double firing" when postback happens:

<body onbeforeunload="alert();">
<form id="form1" runat="server">
<asp:LinkButton ID="LinkButton1" runat="server">LinkButton
</form>
</body>

The following javascript will make sure only the 1st Navigate-away event will invoke a message


var IsThe2ndNavAway=false;
function OBUL()
{
if (!IsThe2ndNavAway)
{
event.returnValue="Are you sure you want to lose your changes."
IsThe2ndNavAway=true;
setTimeout("IsThe2ndNavAway=false;",0)
}
}
Note that the 1st Nav-Away will execute the code to show message, etc. At the same time, 2nd will be blocked at function entry point since it is on the same thread as 1st Nav-Away.
As user dismiss the Message Box, the 2nd Nav-Away can no longer get into if-code block.
At the same time setTimeout will execute "IsThe2ndNavAway=false;" on a different thread so that the next round of "1st Nav-Away, 2nd Nav-Away" can contnue.