Writing my own JavaProxy for ColdFusion 8 using onMissingMethod

First of all, you may be wondering 'what on earth is a JavaProxy?', well,
to answer that question, it is the Java class that does all the work behind the
scenes in ColdFusion to allow you to be able to write all that Java code in-line
in your ColdFusion CFCs and CFM pages by taking the Coldfusion invocations you
have implemented, and passed them to the native Java objects that you want to
use.

To further your understanding, if you are at all interested, you can also read
up on the
proxy
design pattern here
.

Now, what some people may or may not realise, is that inside
JavaLoader,
I create an instance of the coldfusion.runtime.java.JavaProxy class, so it
becomes really easy for developers to create and use instances of Java objects
that are loaded from external .jar files within their applications.  I have
a good blog post on doing this
here.

Now just the other day, I became aware of a new setting in ColdFusion 8 entitled
'Disable Access to internal ColdFusion Java components', that really threw me
for a bend.

For people who run shared hosts, they probably think of this as a g-d send, in
that it will disable access to coldfusion.runtime.ServiceFactory – and for that,
I totally understand, however, it completely locks down access to any Java
object that sits under the coldfusion.* package space.

What does this mean? It means that JavaLoader, no longer works, along with
any
other
project
that also
uses JavaLoader could quite potentially not work on some shared hosts providers
that have upgraded to ColdFusion 8!

Why did Adobe decided to do this? Not so sure! However, with the power that we have
in ColdFusion 8, we are able to implement our own JavaProxy, that should be able
to be seamlessly interchanged with the ColdFusion native JavaProxy!

(Disclaimer: This is the first run at this code, and it works in the given tests
I have tried on it.  I will be running it against all the unit tests on
Transfer, and when they all work perfectly, and Transfer can run with this CF8
restriction in place, I will release the full code as part of a new version of
JavaLoader)

There are two things that allow us to do this -

  1. Nothing stopping us from using Java Reflection to dynamically call methods
    on a Java Class or Instance.
  2. In CF8 we got 'onMissingMethod()' – so we have a hook into every method that
    is fired on a CFC, regardless of whether or not it has been implemented.

For those of you who aren't that familiar with the term
reflection – it essentially means that we
are able to introspect a Java Class or Object, determine what methods and/or
properties it has, and dynamically call them at run-time.  If you want to
read more, the Java site has a whole
tutorial
on reflection
.

So, first of all, let's look at the few different ways you can instantiate and
use ColdFusion Java Objects, we'll use an
ArrayList
as an example, so that we know what we need to support in our own
JavaProxy. 
I will use 'createObject' here, but it could also just as easily be
JavaLoader.create(className) to create an instance of the JavaProxy.


  1. array = createObject("java", "java.util.ArrayList").init();
    array.add(obj);

    – This would be the most common, and generally the 'best' way of
    instantiating a Java Object in CF, as it calls the constructor straight
    away, with the appropriate arguments, and is the closest you will get, in
    style, to implementing a real constructor.

  2. array = createObject("java", "java.util.ArrayList");
    array.init();
    array.add(obj);

    – I've seen this sort of code before… to me, it seems a bit weird to split
    out the constructor, but it works, and you always know that the object has
    been instantiated.

  3. array = createObject("java", "java.util.ArrayList");
    array.add(obj);

    – This is what I feel is the 'worst' way of using Java objects in CF as the
    no argument default constructor is called implicitly, which means you really
    have no control, and it can lead to all sorts of weirdness in your
    application if you don't track what has been actually instantiated and what
    hasn't.

  4. Collections = createObject("java", "java.util.Collections");
    sortedArray = Collections.sort(array);

    – This case shows where static methods are called, in which case, there is
    no constructor.

  5. Color = createObject("java", "java.awt.Color");
    black = Color.black;

    – This is where we want to be able to retrieve a static property.

Okay, so we have our work cut out for us! But all this is very much possible!

So, the first decision to make, is that all of our internal methods on the
JavaProxy.cfc are going to start with an underscore.  This is so that any
method that we write, doesn't interfere with the onMissingMethod's we want to be
able to pick up.  Also as we need to implement a special 'init' method,
that isn't the constructor for the JavaProxy, but instead is a constructor for
the Java object the JavaProxy represents, so we will have a _init(class)
method that instantiates the JavaProxy.

I'm not going to show all the code here, just the relevant parts, but don't
worry, you will be able to see it in the next version of JavaLoader.

So the _init method will do the following things -

  1. Take a Java
    Class
    as an argument, and store it in state
  2. Store some helpful Java Objects in some setters.
  3. Set the Static
    Fields
    of the Class the JavaProxy represents to the this scope
  4. Store all the
    Method
    objects of the Class in a struct of arrays for easy lookup (more on this
    later).

First of all, we'll set the Static Fields to the this scope.  It is
actually very straight forward -


<cffunction name="_setStaticFields" hint="loops around all the fields and
sets the static one to this scope" access="private" returntype="void"
output="false">
    <cfscript>
        var fields = _getClass().getFields();
        var counter = 1;
        var len = ArrayLen(fields);
        var field = 0;

        for(; counter <= len; counter++)
        {
            field =
fields[counter];
            if(_getModifier().isStatic(field.getModifiers()))
            {
               
this[field.getName()] = field.get(JavaCast("null", 0));
            }
        }
    </cfscript>
</cffunction>

For reference

So what are we doing here? Well, we ask the Class for all of it's Fields, then
we look around them, and then interrogate into whether or not they are static.

Once we know they are static, we retrieve their value, using field.get(), and
set them to the same place in the this
scope as they would have been in the Java Object.

We are able to use 'null' on the field.get() because the values are static, and
are not tied to any actual instance of the Class.

So, what would be nice now, is to be actually be able to instantiate an
object!  So let's look at implementing our own 'init' method, so that we
can instantiate the Java Class that the JavaProxy represents.


<cffunction name="init" hint="create an instance of this object"
access="public" returntype="any" output="false">
    <cfscript>
        var constructor = 0;
        var instance = 0;

        //make sure we only ever have one instance
        if(_hasClassInstance())
        {
            return _getClassInstance();
        }

        constructor =
_resolveMethodByParams("Constructor", _getClass().getConstructors(), arguments);

        instance =
constructor.newInstance(_buildArgumentArray(arguments));

        _setClassInstance(instance);

        return _getClassInstance();
    </cfscript>
</cffunction>

For reference

  • _buildArgumentArray() simply takes the argument struct, and turns it into an
    actual Java Array of the same objects.

So first off, we check to see if we already have created an instance – because
we only want one, otherwise weird stuff could happen.  If we do, just give
back the Java instance we already have.

The next line, is a little bit more complicated, so we'll break it down.

The _getClass().getConstructors() returns an array of all the possible
Constructors
that are available for this given class.

The _resolveMethodByParams() method takes a array of Method/Constructor objects,
the arguments that have been passed through to the given method, in this case
'init', and find the best match that it can, and returns it.  We'll go into
the details of that in a minute.

Once we have the right Constructor object, to get an instance of the Object that our JavaProxy represents, we call
'newInstance' on it, and pass in an array of the objects that make up the
arguments that the Constructor needs.

And Preso! We have an instance of our Class! We set it to the state of
the Class Instance, and return the newly created instance back out.

Wait! What? Return the new created instance? Why aren't we returning this, that doesn't make sense?  Well actually, if you think about it, it does.

We
really would prefer it if ColdFusion did all the heavy lifting when it
comes to the bridge between Java and ColdFusion, not only is it more
performant, but it also provides a greater deal of consistency across
the code base.

So we have code that is:
obj = JavaProxy.init();
obj
is actually an instance of the ColdFusion JavaProxy, and then there is
a much more seamless line between the new CFC JavaProxy, and the use of
the ColdFusion one, which is a very good thing.

That being said,
we need to provide support for all the different types of ways that
Java Objects can be used and created, so we have to also cater for the
other aspects as well.

So without further ado, let's actually fire off some methods!  This is where onMissingMethod really comes into it's power!


<cffunction name="onMissingMethod" access="public" returntype="any"
output="false" hint="wires the coldfusion invocation to the Java
Object">
    <cfargument name="missingMethodName" type="string" required="true" />
    <cfargument name="missingMethodArguments" type="struct" required="true" />
    <cfscript>
        var method = _findMethod(arguments.missingMethodName, arguments.missingMethodArguments);

        if(_getModifier().isStatic(method.getModifiers()))
        {
            return method.invoke(JavaCast("null", 0), _buildArgumentArray(arguments.missingMethodArguments));
        }
        else
        {
            if(NOT _hasClassInstance())
            {
                //run the default constructor, just like in normal CF, if there is no instance
                init();
            }

            return method.invoke(_getClassInstance(), _buildArgumentArray(arguments.missingMethodArguments));
        }
    </cfscript>
</cffunction>


Okay, so this is the code that actually takes the methods that are
called on the JavaProxy, and passes them to the Java instance or Class
as appropriate.

The _findMethod() method, returns the Method
that best matches the name of the method that was called, and the
arguments that it has.  I will go into detail on that in just a second.

Once we have the correct Method, if it is static, we can then invoke it against 'null', and return it's value.

If
it isn't static, then we check to see if we have a instance of the Java
Class yet, if not, we create one using the default Constructor, which
is the same way that ColdFusion does it.

From here, we are able to invoke the method against the class instance, and return any results that we may get.

Now we can look at the logic that allows us to work out which method matches what in ColdFusion.

Our first step, is to look at the _findMethod method, which is actually pretty simple:


<cffunction name="_findMethod" hint="finds the method that
closest matches the signature" access="public" returntype="any"
output="false">
    <cfargument name="methodName" hint="the name of the method" type="string" required="Yes">
    <cfargument name="methodArgs" hint="the arguments to look for" type="struct" required="Yes">
    <cfscript>
        var decision = 0;

        if(StructKeyExists(_getMethodCollection(), arguments.methodName))
        {
            decision = StructFind(_getMethodCollection(), arguments.methodName);

            //if there is only one option, try it, it's only going to throw a runtime exception if it doesn't work.
            if(ArrayLen(decision) == 1)
            {
                return decision[1];
            }
            else
            {
                return _resolveMethodByParams(arguments.methodName, decision, arguments.methodArgs);
            }
        }

   
    throw("JavaProxy.MethodNotFoundException", "Could not find the
designated method", "Could not find the method '#arguments.methodName#'
in the class #_getClass().getName()#");
    </cfscript>
</cffunction>


The first thing to know is, that _getMethodCollection() returns a
struct of arrays that was set up in our _init(), the key of which is
the name of the methods found in the class.  The arrays contained in
the struct have all the Methods that have that name, as there may be
more than one.

So, the first thing we do, is check to see if the
name of the method we need is in the collection of methods we have, if
it is we go and grab the array of methods this invocation could
possibly be.

You will notice that I have written code that
states 'if you only have one option for the method, just return that'. 
You may be wondering why, as the parameters of that method may not
match what has been passed in.  Well, if that is the case, we will get
a runtime error, which is the same as what we would get otherwise, so
there is not a huge difference here to just say 'let's give this a
shot, if it doesn't work, no big deal', and we save the performance hit
of comparing parameters.

If there are more than one option
available, then we have to start comparing parameters, and this is
where the _resolveMethodByParams() method that we saw earlier does it's
hard work.

<cffunction name="_resolveMethodByParams"
hint="resolves the method to use by the parameters provided"
access="private" returntype="any" output="false">
    <cfargument name="methodName" hint="the name of the method" type="string" required="Yes">
    <cfargument name="decision" hint="the array of methods to decide from" type="array" required="Yes">
    <cfargument name="methodArgs" hint="the arguments to look for" type="struct" required="Yes">
    <cfscript>
        var decisionLen = ArrayLen(arguments.decision);
        var method = 0;
        var counter = 1;
        var argLen = ArrayLen(arguments.methodArgs);
        var paremeters = 0;
        var paramLen = 0;
        var pCounter = 0;
        var param = 0;
        var class = 0;
        var found = true;

        for(; counter <= decisionLen; counter++)
        {
            method = arguments.decision[counter];
            parameters = method.getParameterTypes();
            paramLen = ArrayLen(parameters);

            found = true;

            if(argLen eq paramLen)
            {
                for(pCounter = 1; pCounter <= paramLen AND found; pCounter++)
                {
                    param = parameters[pCounter];
                    class = _getClassMethod().invoke(arguments.methodArgs[pCounter], JavaCast("null", 0));

                    if(param.isAssignableFrom(class))
                    {
                        found = true;
                    }
                    else if(param.isPrimitive()) //if it's a primitive, it can be mapped to object primtive classes
                    {
                        if(param.getName() eq "boolean" AND class.getName() eq "java.lang.Boolean")
                        {
                            found = true;
                        }
                        else if(param.getName() eq "int" AND class.getName() eq "java.lang.Integer")
                        {
                            found = true;
                        }
                        ...
                        else
                        {
                            throw("Ack", "Cannot match this primitive type", "'#param.getName()#' is just not matching");
                        }
                    }
                    else
                    {
                        found = false;
                    }
                }

                if(found)
                {
                    return method;
                }
            }
        }

   
    throw("JavaProxy.MethodNotFoundException", "Could not find the
designated method", "Could not find the method '#arguments.methodName#'
in the class #_getClass().getName()#");
    </cfscript>
</cffunction>

Woah! That's a lot of crazy code… well, it's not too bad once you break it down.

What we are doing is looping around all the possible methods we have available in the decision array, and trying to see if they match the parameters that we have in our argument struct.

First test says, 'if the number of parameters is different, well, we can't invoke this method', and simply passes it by.

From
there, we need to loop around each of the parameters, and see if it can
work with the class that the corresponding argument that matches it's
place.

You're probably looking at the line that reads '_getClassMethod().invoke(arguments.methodArgs[pCounter], JavaCast("null", 0));' and thinking… what on earth does that do?  Well, it allows us to get to the Class object of that argument.

This
is very similar to doing a obj.getClass(), however, I can't do that in
all instances.  If an argument is a CFC, then it will try and resolve
the method 'getClasss' against the CFC, and most likely throw me an
error.  So I have to use reflection!

the _getClassMethod()
(this was setup in our _init()) is the actually Method class that
represents the 'getClass()' method aforementioned, what I then do is
invoke it on the required argument, and this gives me back the Class
Object I need for comparison.

Now, a Class object has
a great method called 'isAssignableFrom', basically, this says 'If the
object is a the same as this Class, or a subclass, or implements this
interface, return true'.  This means that if the argument in the method
that have been invoked on the CF side isAssignable to the parameter
that is required for this method, then this method can be invoked
successfully.

The other thing we need to do is map primitive
parameters, such as int, char, boolean, etc back to their Object
representations.  The nice thing is that Java will map this back and
forth at runtime for us, so as long as the values match up, we're good
to go.

Once the parameters have all been resolved, we can return the method we have found that works, otherwise, we throw an exception.

That
pretty much covers the JavaProxy.CFC.  As I said before, you will be
able to see the code in action once I release the new version of
JavaLoader, but feel free to ask any questions you may have, I will be
happy to answer them.

Leave a Comment

Comments

  • Toby Reiter | August 29, 2007

    Simply incredible. Can’t wait to try it!

  • MrBuzzy | September 3, 2007

    Whoa that’s a big one :-O

    Some CF Hosts disable the use of java or worse cfobject/createobject.
    I assume the ‘Disable Access to internal ColdFusion Java components’ option is there so that CF hosters will go ahead and re-enable java usage.

    Also with these limitations some (have to) choose Reactor over Transfer ;)

    Nice job, looking forward to the next JavaLoader.

  • Mark | September 3, 2007

    Well, if shared hosts refuse to turn on createObject, even in an sandboxed environment, then I suggest finding a new, decent, shared host!

    I’ve actually implemented the new JavaProxy into Transfer and all the unit tests work perfectly with the cf8 Java object restriction turned on, which is great.

  • MrBuzzy | September 3, 2007

    Just to be clear, I have access tp createbject, but not type=java. My point was CF8 will allow this ‘safely’, which means (in theory) more people can use transfer & javaloader.

  • Russ Michaels | September 30, 2007

    In answer to your question of why Adobe added ‘Disable Access to internal ColdFusion Java components’.
    Probably because hsared hosts like myself have been asking for this for years. On a shared hsoting server allowing customers access to java makes all other steps you haven to lock down the server essentially pointless. Any client who knows JAVA can get right into the filesystem and do whatever they like.
    Unfortunating completely denying access to createObject(java) with no exceptions is really not very viable as so many customers need this because they use 3rd party frameworks like yours that need it and they simply don’t care about the security implications it involves or are not prepared to get their own server.

    So it means that all hosts have 2 choices. Have completely secure coldfusion servers and turn away lots of customers, or bite the bullet and allow createobject(java) and rely on the fact that most customers have no clue about Java and do not know what security holes it opens and how to take advantage of them.

    We disallow createObject(java) by default so that customers who don’t need it don’t have it, and those who do want it have to ask for it and tell us why they need it.
    The big problem however is the very fact that most of the time customers are using 3rd party code that they have no idea how it works or what it does, they just upload it and run it, and this is where the problems begin when the code starts using the serviceFactory to alter cfadmin settings and break other customers sites. For example, FCKeditor adds a "/" mapping, or at least it used to, this causes total mayhelm as any customers who uses "/" to reference the root of their site now has a broken site as "/" now points somewhere else, it also breaks CFC’s

  • Russ Michaels | September 30, 2007

    MArk, You are basically arbitrarily advising people that if they have a secure host, go find one that is not secure. Perhaps you do not mind of your site gets hacked or your databases are stolen and your customers data sold to spammers, but not everyone would want to take that risk, especially as it would lead them open to criminal charges under the data protection act.
    I know I certainly would not be happy if I had a site developed by a company and discovered that they chose an insecure host just because they wanted to use a specific tag and would not compromise and as a result our ecommerce database had been stolen containing all our customers personal details, card details etc.
    You really should be careful before making such sweeping statements.

  • Mark | September 30, 2007

    @Russ

    I actually think that you’re not reading what I was writing.

    Use of createObject() does not make a host insecure, especially if you set up sandboxing.

    If a host turns off createObject, it is not a decent host, because it does not know how to set up a ColdFusion box properly, and therefore really don’t know what they are doing, while at the same time seriously crippling ColdFusion as a product.

    Now, if you want to talk specifically about createObject(‘java’) I could at least understand, as there are possible implications there, however, if you have clients that have sensitive data, what are they doing on a shared host that they, or you, have no complete control over.

    At the very least they should only reside on a VPS that you have complete control over, or they do. Putting sensitive data on a shared host is just a crazy idea.

  • Russ michaels | October 11, 2007

    Mark,
    Security sandboxes only sandbox coldfusion tags and fucntions. They have absolutely effect whatsoever on Java.
    If CreateObject(java) is enabled then you can do whatever you like.
    With just CreateObject() enabled (without the java) does not allow access to Java classes or the coldfusion runtime, it only allows you instantiate web services, cfc’s and COM
    Putting sensitive data on a shared host may be a crazy idea, but as you must know 99% of people do it either through lack of understanding, lack of information, lack of money or lack of common sense.

    A VPS may be cheap these days, but this is still not a viable option for a lot of clients as they do not have the technical ability to manage their own server, VPS or otherwise and this is where the extra cost come sin, paying someone to manage it.

  • Mark | October 11, 2007

    @Russ

    Again, I don’t believe you actually read what I wrote.

    I will handily admit that if you are allowing createObject(‘java’) on a CF 7 or below host, it can be an issue.

    That being said, it has always been an issue, as by using the ServiceFactory you are able to get at people’s sessions and application scopes – which can lead to access to sensitive data. But this has always been the case on shared servers, and is something that is well known.

    I would much rather have a fully featured shared host that allows for Java object creation, while encrypting and otherwise managing my data and sensitive information properly such that other people on the same host can’t access it, and if they do, they get no information that is valuable to them.

    If someone is serious about hacking a site – it is more likely that the attack will try and reach the web server, or the database server, or FTP or something similar.

    Signing up to a shared hosting account with a credit card, and very traceable information on the off chance you may end up on the same server as your target is a pretty high risk / low probability of success endeavour.

    At the end of the day, if you are losing sleep about this issue – upgrade to a CF8 server, and make sure they enable the setting to remove access to ColdFusion based Java objects. After that, you have nothing more to worry about, and you can use regular ol’ Java objects as you see fit.

  • Russ michaels | October 11, 2007

    yes I did read what you wrote, I tyink yout misisng the point of my comment and seeing it as a dig at you rather than being informative to people using a shared host.
    Your attitude is probably like most client, they would prefer to have all the features enabled regardless. They do change their tune when something happens though and are first to start blaming the host. See my simple example above of adding a mapping can break sites. These issues need to be considered by people using shared hosts. Telling people to tell their host to upgrade to CF8 probably wont get them a very nice response either.

  • Toby Tremayne | October 12, 2007

    I have to agree with Mark here – this has long been a known thing with coldfusion. A host needs to make a decision on whether to allow coldfusion or not based on this, and if they decide to offer shared coldfusion hosting then that’s the business decision they’re taking.

    Clients and developers on the other hand (developers particularly) have exactly zero excuse for not making themselves aware of the very publicly available information on potential security risks involved in the software they choose to use, and quite frankly anyone hosting financial data unencrypted in a shared environment deserves everything they get.

    A conscientious development group will ensure that their client is made aware of any risks to data in an application and should be quoting to include whatever obfuscation and encryption is required, or should be advising the client on the requirement for a dedicated secure server.

    I agree also that if someone’s going to hack the site, an attack through coldfusion would be the least of your concerns, and a very random chance approach to an attack. There *is* of course a valid concern that one client could do something that causes problems for another client, but again – If you think this is a big enough issue, don’t host coldfusion. Or set things up in such a way that it’s not a problem. It’s down to clients and developers to make sure apps are secure, and that includes taking into account any perceived deficiencies of the platform.

  • Russ | September 3, 2010

    Toby,
    I accept the fact it is very hard for a developer like yourself to understand the semantics of the hosting side of things and why hosts have to take certain measures. However both of you seem to have not actually read what I said properly.

    As I have said, clients like yourself want everything enabled/insecure until something bad happens, then they blame the host for having everything enabled accusing you of being incompetent, insecure, yada yada even though that is what they wanted.

    You are focussing on the hacking part, but again as I said, the biggest problem is clients using 3rd party code/frameworks that do things they shouldn’t on a shared server. Assuming that all developers actually know what they are doing or know the risks is a very poor assumption to make and a dangerous one, as most simply do not have a clue what happens on a server after they have uploaded their files via FTP. I can tell you this is an absolute concrete fact based on 10 years in the hosting business.
    You are also making the huge assumption that every customer on the server is honest, again a dangerous assumption to make.

    You can’t really have your cake and eat it. If you want the freedom to do as you wish then don’t use shared hosting.

    If your bank asked you to deposit cash by simply leaving it in a cardboard box on the counter, and told you it was safe as it is really unlikely that anyone will take it, would you do it? Of course not, as common sense would tell you it is a bad idea. Sadly when it comes to hosting, common sense takes a back seat.