We have been using an SAP "COM License Bridge" to access their license server and query the hardware key of a system programatically (to reuse with out own licensing). This worked fine on SAP Business one Versions 2007A, 2007B and 8.8, but in 8.81 they seem to have updated their CORBA interface without updating the COM wrapper because we now get memory exceptions when attempting to call the GetHardwareKey function.
So I downloaded IIOP.NET and started trying to write my own interface. I never liked that COM wrapper anyway. But I encountered my old nemesis of .NET remoting -- the inability to invoke a remote method without having a common interface defined in both the server and the client. I tried using the IDL to CLS compiler included with IIOP.NET, but I continue to get errors about the interface beign incompatible (SAP did not provide an IDL file). I don't know how IIOP and CORBA determine whether an interface is compatible. But I even tried debugging into IIOP.NET code and forcing it to execute the method despite the incompatibility, but received an empty string back instead of the hardware key I wanted.
My next step is to try to implement a fake license server and examine the requests coming in from the production client in the hopes of identifying what they should look like, but I'm not counting on much success considering the difficulty I've had in peering into the innards of .NET remoting already.
My real problem is how to get or generate the SAP Business One hardware key, but questions stemming from that include:
Edit: I managed to make IIOP.NET / CORBA believe that I had a compatible interface by applying the RepositoryID attribute:
[Ch.Elca.Iiop.Idl.InterfaceType(Ch.Elca.Iiop.Idl.IdlTypeInterface.ConcreteInterface)]
[Ch.Elca.Iiop.Idl.RepositoryID("IDL:LicenseInfo:1.0")]
public interface ILicenseInfo : Ch.Elca.Iiop.Idl.IIdlEntity
{
void GetHardwareKey(out string hwKey);
}
But I am still getting an empty string result.
Edit 2: After some more experimentation and debugging, I have found that the response messages do contain the data I'm looking for, but are not being parsed into the client values properly, probably because of my bad interface definition. Hoping that debugging into the response processing further will help me figure out how to correct my interface. Strangely the first thing it's parsing from the response is a null boxed value, which doesn't seem right for an "out string" parameter.
Edit 3: I have found that I need to apply string attributes to the parameters like this to prevent them from being treated as boxed values:
void GetHardwareKey([StringValue(), WideChar(true)] out string hwKey);
But despite the WideChar attribute, I am getting en error about the CodeSet not supporting WChar or something. I'm getting really close to figuring this out.
Edit 4: I am stumped at how to set the codeset for WChar. If I do not set it, I receive an error: "WChar Codeset either not specified or not supported." because the server has returned a unicode string without overriding the default character set. I can't find any way to override that from the client. I tried calling:
omg.org.CORBA.OrbServices.GetSingleton().OverrideDefaultCharSets(
CharSet.UTF8, WCharSet.UTF16);
But that does not seem to have any effect on the client end. The example code shows calling that on the server end. But I didn't write the server, so I can't control that. Is my only option to rewrite the IIOP.NET code for my own purposes forcing a default WChar CodeSet to go into effect?
After 3 days of debugging into IIOP to track down its behavior and inspect the data coming back in the response, I have settled on this solution.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using omg.org.CosNaming;
using Ch.Elca.Iiop;
using Ch.Elca.Iiop.Services;
using System.Runtime.Remoting.Channels;
using Ch.Elca.Iiop.Idl;
[RepositoryID("IDL:LicenseInfo:1.0")]
public interface ILicenseInfo
{
Int32 GetHardwareKey([IdlSequence(0)] out byte[] hwKey);
Int32 GetInstallationNumberList([IdlSequence(0)] out byte[] instNum);
}
class Program
{
static void Main(string[] args)
{
IiopClientChannel channel = new IiopClientChannel();
ChannelServices.RegisterChannel(channel, false);
CorbaInit init = CorbaInit.GetInit();
NamingContext context = init.GetNameService("MYLICSRV", 30000);
NameComponent[] names = new NameComponent[] { new NameComponent("B1LicenseInfo") };
ILicenseInfo li = (ILicenseInfo)context.resolve(names);
byte[] hwKey;
byte[] instNum;
li.GetHardwareKey(out hwKey);
li.GetInstallationNumberList(out instNum);
Encoding encoding = new System.Text.UnicodeEncoding(false, false, true);
Console.WriteLine(encoding.GetString(hwKey));
Console.WriteLine(encoding.GetString(instNum));
}
}
I temporarily was also using this in an attempt to make IIOP give me back the right strings. If it had occurred to me that I could simply accept the reply as a byte array and perform the decoding myself, I wouldn't have wasted half the time trying to work out how to get IIOP to understand how I wanted my string back:
class MyOrbInitializer : omg.org.PortableInterceptor.ORBInitializer
{
public void post_init(omg.org.PortableInterceptor.ORBInitInfo info)
{
// Nothing to do
}
public void pre_init(omg.org.PortableInterceptor.ORBInitInfo info)
{
omg.org.IOP.Codec codec = info.codec_factory.create_codec(
new omg.org.IOP.Encoding(omg.org.IOP.ENCODING_CDR_ENCAPS.ConstVal, 1, 2));
Program.m_codec = codec;
}
}
class Program
{
public static omg.org.IOP.Codec m_codec;
static void Main(string[] args)
{
IOrbServices orb = OrbServices.GetSingleton();
orb.OverrideDefaultCharSets(CharSet.UTF8, WCharSet.UTF16);
orb.RegisterPortableInterceptorInitalizer(new MyOrbInitializer());
orb.CompleteInterceptorRegistration();
...
MarshalByRefObject objRef = context.resolve(names);
string origObjData = orb.object_to_string(objRef);
Ch.Elca.Iiop.CorbaObjRef.Ior iorObj = new Ch.Elca.Iiop.CorbaObjRef.Ior(origObjData);
CodeSetComponentData cscd = new CodeSetComponentData(
(int)Ch.Elca.Iiop.Services.CharSet.UTF8,
new int[] { (int)Ch.Elca.Iiop.Services.CharSet.UTF8 },
(int)Ch.Elca.Iiop.Services.WCharSet.UTF16,
new int[] { (int)Ch.Elca.Iiop.Services.WCharSet.UTF16 });
omg.org.IOP.TaggedComponent codesetcomp = new omg.org.IOP.TaggedComponent(
omg.org.IOP.TAG_CODE_SETS.ConstVal, m_codec.encode_value(cscd));
iorObj.Profiles[0].TaggedComponents.AddComponent(codesetcomp);
string newObjData = iorObj.ToString();
MarshalByRefObject newObj = (MarshalByRefObject)orb.string_to_object(newObjData);
ILicenseInfo li = (ILicenseInfo)newObj;
...
}
After that much code ran, I had an object that would define the WChar CodeSet so it would parse the return strings properly, avoiding the "WChar CodeSet either not specified or not supported" error. But after all that, the Unicode byte ordering was backwards too! And the only way to fix that, as far as I could tell, was to re-parse the string into bytes and then back into a Unicode string. But that's when it occurred to me, why even ask for the result as a string!? I could just take the bytes directly and avoid so much of this complication. I wish I had thought of that earlier.