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
structuserName = 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;
}
}
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.
ReplyDeleteHello,
ReplyDeleteThe 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
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