Saturday, October 12, 2013

Cross-Platform Java Details on Compiling and JNA - Getting Extended User Name, Windows and gecos


One of the great things about Java is cross-platform runtime execution of code .  For example, we can compile web applications to be run in Tomcat and can run them on both Windows and UNIX servers.  In addition to cross-platform web applications, I write utilities in Java, and I benefit from cross-platform runtime compatibility.
However, there are some pitfalls to watch out for!  For one thing, directory structures are different across platforms.  On UNIX systems, directory structures start at the root “/” and get assembled at mount points.  For example “/” might be a mount point for one drive, and “/usr” might be a mount point for a separate drive.  On Windows, the directory structure takes a different form with drive letters mapped to each drive, for example “C:” is usually the boot drive.  These generalizations are standard and typical; although, there’s always a new mousetrap, for better or worse.

Other areas of difference between Windows and UNIX systems are worth mentioning.  (1) The classpath separator character on UNIX is “:” while the classpath separator on Windows is “;”.  This is usually not a factor in Java code, but is a concern for getting your java runtime started.  (2) The directory (path) separator is forward slash (“/”) on UNIX and backslash (“\”) on Windows.  In Java code, it is always good to use the forward slash (UNIX style) path separator.  This will be handled correctly on both Windows and UNIX.  Because the backslash character has dual meaning (it’s a character and an “escape” character), to use it as a path separator, you need to double it, like “\\Windows\\System32”.  (3) The line separator character is different between UNIX and Windows.  Windows uses two characters: carriage return (CR, 0x0D, “\r”) and line feed (LF, 0x0A, “\n”); while UNIX only uses line feed.  The IO and NIO classes in Java which read lines (like java.io.BufferedReader.readLine()) will accommodate either line separator syntax, but if you intend to read lines manually (byte by byte) then you will need to be aware of this difference and handle it accordingly.
Now to the heart of this discussion...  There are certain aspects of Java that are not cross-platform capable.  These are few, and are not included in the standard Java library packages.  One thing that is done differently on different platforms is acquiring the user id and name.  You can imagine that Windows and UNIX keep user information in different formats.

A simple effort can be made to acquire the current userID from the Java system properties with a simple call like this:
String userID = System.getProperties().getProperty( "user.name" );

This approach is much better than reading the operating system environment, and I’m almost embarrassed to say that in my almost 2 decades of writing Java, I have done that more than once.  Here’s an example from code I wrote in 2008:
Process proc;
Runtime rt = Runtime.getRuntime();
if( props.getProperty( "os.arch" ).equals( "x86" ) &&
    props.getProperty( "os.name" ).startsWith( "Windows" ) )
{
    proc = rt.exec( "cmd /c set" );
} else {
    proc = rt.exec( "ksh -c set" );
}
InputStream is = proc.getInputStream();
int in;
while( ( in = is.read() ) != -1 ) {
// ... parse for User ID
 
But now I know that reading the Java system properties is cross-platform compatible; whereas, executing a Runtime and getting a Process to do work or get information is simply a way to turn Java into a scripting language – it is a role reversal, to be done only when the same job cannot be done in native Java.  With that philosophy, we will promote pure Java solutions for cross-platform tasks.

So, we are able to get the userID from the Java system properties, but how good is that?  It is not good at all!  If I take a second before running the Java code, and set the USERNAME environment variable to something else (“set USERNAME=fake” on Windows, “export USER=fake” in most UNIX shells), then the Java system properties will present this miss-set userID as if that were the real userID.  Note that this same frailty (security vulnerability) exists when reading User ID from the operating system environment settings.
A better way to do this would be to talk directly to the operating system (Windows or UNIX).  However, with the Java Virtual Machine (JVM) between your Java code and the native operating system, this becomes a bit more difficult.  One way to work around this difficulty in an enterprise environment is to call on a separate service, like an LDAP directory service, to provide the information.  And Java can readily do that, with sufficient credentials and access, and with the service available.  For cross-platform applications, there will probably be separate directory services available for each client platform.

But shouldn’t the local computer be able to tell me what I want?  The local machine knows my userID and already knows or can find out more details about me.  So our next job will be to have Java talk to the native operating system.
As I have said, the platform-specific sections of Java are kept outside of the standard Java library packages, but some are nevertheless included in the standard distribution library jar files.  For example, in the rt.jar file that comes with every Java Runtime Environment (JRE) there is a package named “com.sun.security.auth.module”.  You can tell this package is not a standard Java library, because the name does not start with “java”, like “java.lang” or “java.util”.  And it is for this very reason that certain Integrated Development Environments (IDEs) like Eclipse do not give ready access to classes in the “com.sun.*” packages, even though they are part of the standard distribution.  We will deal with that.

Let me assume that you are developing code using Eclipse on a Windows computer.  The standard JRE rt.jar library will include a class named “com.sun.security.auth.module.NTSystem”.  We will use it to find the current user’s identity like this:
String userName = (new com.sun.security.auth.module.NTSystem()).getName();

This will generate an error indicator in Eclipse, and will not be able to be compiled.  The error text is “Access restriction: The method getName() from the type NTSystem is not accessible due to restriction on required library”.  Basically, the class and method exist, but you’re not encouraged (or readily permitted) to use them.  To immediately resolve this problem, you can go to your Eclipse project Properties and add the local rt.jar file as an External Jar to your Java Build Path.  Note that rt.jar is in the default classpath, so once we get Eclipse to accept and compile the code, there is no problem running it.  However, as we shall see, that is true only on a Windows platform.
On a UNIX platform, the standard JRE distribution of rt.jar does not have the NTSystem.class file at all!  That makes sense, come to think of it, since NTSystem is not functional on UNIX (it is not cross-platform compatible).  As an alternative, in the UNIX JRE, we find this class that did not exist in the standard Windows JRE, “com.sun.security.auth.module.UnixSystem”.  We will use it like this:

String userName = (new com.sun.security.auth.module.UnixSystem()).getUsername();

However, now we are faced with another difficulty – that class doesn’t even exist on the Windows workstation where we are running Eclipse!  To remedy this, we need to copy the rt.jar file from a UNIX computer to our Windows workstation.  I recommend you rename it on the Windows box to something like UNIXrt.jar, so it’s clear what it is.  Then you can add that file as an External Jar to your Java Build Path in your project Properties.  Now that’s all well and good, and your code should be free from reported errors, but there are precautions we need to take.
Unless you are willing to always and only run your code from your Eclipse environment (not a very cross-platform approach), then you need to avoid mentioning classes that don’t exist.  If you run your code on a different Windows machine, or at the command prompt (without manipulating the classpath to point at the UNIXrt.jar file) then you will not have access to UnixSystem.class, and it is unmentionable (else runtime exceptions will ensue.)  And on a UNIX platform, NTSystem.class is unmentionable.  So how can you run this code cross-platform?  These are the steps we must take:

1)      Do not import these classes, rather refer to them with their complete package names, as shown in the code above.

2)      Determine what platform you are running on, and only attempt to execute code specific to that platform.
Here is a model I like to use for separating platform-specific code.  I use the Java System Properties to determine if I’m executing on a Windows box or not, and call the platform-specific code in an If/else block:

Properties props = System.getProperties();
String userName;
if( ( props.getProperty( "os.arch" ).equals( "x86" ) ||
    props.getProperty( "os.arch" ).equals( "amd64" ) ) &&
    props.getProperty( "os.name" ).startsWith( "Windows" ) )
{
    userName = (new com.sun.security.auth.module.NTSystem()).getName();
} else {
    userName = (new com.sun.security.auth.module.UnixSystem()).getUsername();
}

That code model examines two properties, operating system architecture and name.  The architecture can be either “x86” for 32-bit or “amd64” for 64-bit.  In this case, that is an indication of 32-bit or 64-bit implementation of Java, not an indication of the CPU type.  Also, the OS name, for Windows machines, can have several appearances, but they all start with “Windows”.
I spend a bit of text describing how NTSystem, UnixSystem and the Java Authentication and Authorization System (JAAS) package operates in the chapter on Single Sign-On in my book, Expert Oracle and Java Security, published by Apress.  Of course, I recommend that book.  Also see my current blogs at http://oraclejavasecure.blogspot.com/

So what have we done so far?  We have achieved cross-platform determination of current user identity from the operating system.  However, what if we are really after the user’s name, like “John Doe”?  Is that a value we can acquire from the local machine?  I should hope so, but for this effort, we will need to talk a bit more intimately with the operating system.   We will use Java Native Interface (JNI) programming to accomplish this, and we will use some existing utility code that was developed to make this effort easy, the Java Native Access (JNA) packages.  JNI is used, not to talk to operating systems, but rather to talk to other programming languages.  In this case, we will be talking to C language libraries that have the intimate kind of access to the operating system that we want to tap into.
To use JNA, you will need to browse to jna.java.net and download jna-3.3.0.jar and jna-3.3.0-platforms.jar.  Add these two files as External JAR files to your Java Build Path in your project Properties.  These will also need to be included in the classpath you configure for your java runtime from the command line or a script.

As is frequently the case when trying to work well with other computers or languages, a lowest-common-denominator (LCD) for data exchange must be established.  In JNI, the LCD data exchange is byte arrays.  Passing arrays of bytes can be easily done, even when both parties do not have a common understanding of a String.  So before getting the Windows extended user name via JNA, we define a byte array (char array) to hold it.  The following code passes our char array as the second parameter to the GetUserNameEx() method of the JNA class, Secur32.  That method returns the Windows extended user name in our char array.  For local use, we create a String from the char array and trim it to its significant characters.
char[] name = new char[100];
// Gets current user extended name
com.sun.jna.platform.win32.Secur32.INSTANCE.GetUserNameEx(
    com.sun.jna.platform.win32.Secur32.EXTENDED_NAME_FORMAT.NameDisplay,
    name, new com.sun.jna.ptr.IntByReference(name.length) );
userName = new String(name).trim();

I wish it were as easy as this to get the extended user name (“John Doe”) from UNIX and Linux computers, but the classes in the com.sun.jna.platform package that are specific for UNIX variants do not include that ability – probably because of the many implementations that would be required.
However, do not fret.  Similar C libraries that provide intimate access to UNIX platforms do exist, and with a little Java tailoring, we can acquire the same level of data as on Windows.  Here we will be using closer-to-the-bone JNI programming, using existing native libraries.   The JNA package gives us a friendly interface into those libraries.

Let’s start with the interface between Java and the specific C Library we want to address.  This example configures the interface that is specific to the Oracle Solaris platform.  Likely the only change you will have to make to accommodate other UNIX / Linux variants is in the list of fields included in the data structure returned from the library.
interface CLibrary extends Library {
    // Method to shadow C library getpwnam function
    passwd getpwnam(String username);
 
    // This matches the struct for passwd on Solaris
    // Modify to match passwd struct on other platforms
    public class passwd extends Structure {
        public String pw_name;
        public String pw_passwd;
        public int pw_uid;
        public int pw_gid;
        public String pw_age;
        public String pw_comment;
        public String pw_gecos;
        public String pw_dir;
        public String pw_shell;
    }
}

Notice that this interface extends the Library interface, included in the JNA package.  In our extension, we define one method, getpwnam() which shadows a function in the native C library which returns a data structure representing an entry in the system passwd datastore.  On Solaris, there are nine fields returned in this structure, and the “gecos” field contains data that is often the user’s name, but can be any text data specific to the user.  In my limited research, I’ve seen that “gecos” stands for “General Electric” so-and-so.  That’s hard for me to believe, but with other examples of UNIX utilities named for the initials of their authors, I guess I shouldn’t be surprised.  The gecos field is returned in the seventh field of our passwd.class, which extends the JNA Structure class.
To use our CLibrary interface, we instantiate a class based on it by calling the JNA Native.loadLibrary() method.  We tell that method to load the “c” native library, which includes the getpwnam function, and to wrap it in our CLibrary interface.  At that point we can call the getpwnam() method we defined in CLibrary.  Then we can access the pw_gecos member of the passwd instance returned by that method.  Note that this function reads the passwd datastore for a specified userID, so we are passing in the userID we acquired earlier.

CLibrary libc = (CLibrary)Native.loadLibrary("c", CLibrary.class);
// Gets named user gecos field from passwd struct
userName = libc.getpwnam(userName).pw_gecos;

I am including the complete code, below, for a class that executes cross-platform and acquires the extended username from the native platform.  Of course, all the significant code is shown previously in this article, along with instructions on how to set up your environment to edit and compile it.

package dac;

import java.util.Properties;

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Structure;

public class CrossPlatformUserName {

    public static void main(String[] args) {
        String userName = "";
        Properties props = System.getProperties();
        userName = props.getProperty( "user.name" );
        System.out.println( "A: " + userName );

        System.out.println( "Arch: " + props.getProperty( "os.arch" ) +
            ", OS: " + props.getProperty( "os.name" ) );
        // Some example reports of Arch and OS:
        //  Arch: amd64, OS: Windows 7
        //  Arch: x86, OS: Windows Vista
        //  Arch: sparc, OS: SunOS

        if( ( props.getProperty( "os.arch" ).equals( "x86" ) ||
            (props.getProperty( "os.arch" ).equals( "amd64" ) ) &&
            props.getProperty( "os.name" ).startsWith( "Windows" ) ) )
        {
            try {
                // Eclipse complains:
                // Access restriction: The method getName() from the type NTSystem
                //  is not accessible due to restriction on required library
                //  C:\dac\java\jdk1.6\jre\lib\rt.jar
                // Add that standard rt.jar to project properties compile build path
                //  to remove the restriction
                userName = (new com.sun.security.auth.module.NTSystem()).getName();
                System.out.println( "B: " + userName );

                // Certain Java Native Access (JNA) applications are available for
                //  various platforms.
                // One will allow us to read the Users full name from Windows
                // Include 2 jar files, download from jna.java.net, jna-3.3.0.jar
                //  and jna-3.3.0-platform.jar
                //  as external jars in your project properties, compile build path
                char[] name = new char[100];
                // Gets current user extended name
                com.sun.jna.platform.win32.Secur32.INSTANCE.GetUserNameEx(
                com.sun.jna.platform.win32.Secur32.EXTENDED_NAME_FORMAT.NameDisplay,
                name,new com.sun.jna.ptr.IntByReference(name.length) );
                userName = new String(name).trim();
                System.out.println( "C: " + userName );
            } catch( Throwable t ) {
                System.out.println( t.toString() );
            }
        }
        else {
            // ldaplist -l -v passwd $USER | grep gecos:
            // Eclipse on Windows machine, accessing Windows JDK/JRE does not find
            //  UnixSystem.class
            // Copy rt.jar from a UNIX machine, which will include UnixSystem
            //  then add that rt.jar as an External Jar to your project properties
            // Now you will be able to compile on Windows, even with this reference
            //  to a UNIX-specific class. Alternatively, you could create a
            //  local project, with empty UnixSystem class in the same package
            //  with an empty getUsername() method
            try {
                userName =
                    (new com.sun.security.auth.module.UnixSystem()).getUsername();
                System.out.println( "B: " + userName );
 
                CLibrary libc = (CLibrary)Native.loadLibrary("c", CLibrary.class);
                // Gets named user gecos field from passwd struct
                userName = libc.getpwnam(userName).pw_gecos;

                System.out.println( "C: " + userName );
            } catch( Throwable t ) {
                t.printStackTrace();
            }
        }
        System.exit(0);
    }
}

// Create our own platform-specific library
interface CLibrary extends Library {
    // Method to shadow C library getpwname function
    passwd getpwnam(String username);

    // This matches the struct for passwd on Solaris
    // Modify to match passwd struct on other platforms
    public class passwd extends Structure {
        public String pw_name;
        public String pw_passwd;
        public int pw_uid;
        public int pw_gid;
        public String pw_age;
        public String pw_comment;
        public String pw_gecos;
        public String pw_dir;
        public String pw_shell;
    }
}

3 comments:

  1. Please reference the current JNA, which is hosted on github at https://github.com/twall/jna, which is at version 4.0. The version 3.3 hosted on java.net is over two years old.

    ReplyDelete
  2. Hello,
    The Article on Cross-Platform Java Details on Compiling and JNA.It give detail information about it.Thanks for sharing this valuable information. Its really useful for me.Xamarin Consultant

    ReplyDelete
  3. Wow, What a Excellent post. I really found this to much informatics. It is what i was searching for.I would like to suggest you that please keep sharing such type of info.Visit here for Penetration testing services and Software testing services

    ReplyDelete