3

We have a servlet that needs certain variables like passwords, encryption salts, etc., not to be saved on the file system permanently. This is what we do currently (summary):

During initialization,

  1. A Perl script sets ReadMode to 2 to mask stdout echo, prompts user for the variables, filters a known file to put them in and invokes tomcat/bin/startup.sh

  2. The servlet init() method reads the variables from the file and deletes it (the file).

Problem: When the WAR is recompiled, tomcat tries to deploy it (autodeploy=true), which we want. But the data file is no longer there, so a FileNotFoundException is thrown (rightly so).

Question: Is there a property or some HashMap/Table available to servlets where a few variables can be stored during the manual startup? The idea is that init() could check for them if the data file is not there during redeployment. Thank you, - MS.

Donato Szilagyi
  • 4,279
  • 4
  • 36
  • 53
Manidip Sengupta
  • 3,573
  • 5
  • 25
  • 27

5 Answers5

4

How about passing them in as system properties when you call startup.sh, ie. 'bin/startup.sh -DencryptionSalt=foobar'? Then you can call System.getProperty("encryptionSalt") for the duration of the JVM.

Jesse Barnum
  • 6,507
  • 6
  • 40
  • 69
  • The objective here is that foobar as the encryptionSalt should be known only to the group of ppl authorized to run "bin/startup.sh". Tomcat administrator may not be in this group. Now, if we put it on a Unix command line, it becomes available to everybody with a "ps -[augx|ef|somethongElse]", depending on the O/S. But I like the idea of setting up the property at the JVM level, if tomcat cannot do it. Would you know of another method to set this up? May be System.get/setProperty will work (haven't tried that yet)? – Manidip Sengupta Feb 15 '11 at 05:21
  • Yes, you can call System.setProperty() at runtime so that the params will not be visible in the process listing. – Jesse Barnum Feb 15 '11 at 12:49
2

Put your volatile data into JNDI. JNDI isn't cleaned up between redeployments. Your servlet could still do the same thing during init to ensure the data in JNDI is up-to-date.

I can't remember if JNDI reads/writes are thread safe, but it it isn't, you can always put a thread-safe object (e.g. ConcurrentHashMap) and use it without problems.

EDIT by MS:

==========
restart.pl:
==========
ReadMode 2; print "\nEnter spice: "; chomp ($spice = <STDIN>); print "\n";
ReadMode 0;
open (FP, ">$spicefile") or die "$0: Cannot write file $spicefile: $!\n";
print FP "$spice\n"; close (FP);
system "bin/shutdown.sh";       sleep 8;
system "bin/startup.sh";        sleep 8;        system "wget $tomcaturl";
system '/bin/rm -rf *spicefile*';               # delete wget output file
foreach $sCount (1..10) {                       # give it 10 more secs
    sleep 1;
    if (-f $spicefile) {
        print "$0: Waiting on servlet to delete spicefile [$sCount]\n"
    } else {
        print "$0: Successful servlet initialization, no spicefile\n";
        exit 0
}}
print "\n$0: deleting file $spicefile ...\n";   # Error condition
system "unlink $spicefile";
exit 1;

===========
context.xml
===========
    <Resource name="MyServlet/upsBean" auth="Container" type="packageName.UPSBean"
                factory="org.apache.naming.factory.BeanFactory" readOnly="false"/>

===================
app/WEB-INF/web.xml
===================
  <listener>
        <listener-class>packageName.DBInterface</listener-class>
  </listener>
  <resource-env-ref>
    <description>Use spice as transferable during redeployment</description>
    <resource-env-ref-name>MyServlet/upsBean</resource-env-ref-name>
    <resource-env-ref-type>packageName.UPSBean</resource-env-ref-type>
  </resource-env-ref>

==============
MyServlet.java:
==============
init() {
        if (new File (spiceFile).exists())      # Same name in restart.pl
                dbi = new DBInterface (spiceFile);
        else
                dbi = new DBInterface ();       # Redeployment
        otherInitializations (dbi.getSpice());
}

================
DBInterface.java:
================
public DBInterface () {
        // Comment out following block if contextInitialized works
        FileInputStream fin = new FileInputStream (safeDepositBox);
        ObjectInputStream ois = new ObjectInputStream (fin);
        UPSBean upsBean = (UPSBean) ois.readObject();
        ois.close();
        spice = upsBean.getSpice();
        dbiIndex = 2;
        // do stuff with spice
}

public DBInterface (String spiceFileName) {
        File file = new File (spiceFileName);
        BufferedReader br = new BufferedReader (new FileReader (file));
        spice = br.readLine();
        br.close();
        file.delete();
        dbiIndex = 1;

        // Delete following block if contextInitialized works
        UPSBean upsBean = new UPSBean();
        upsBean.setSpice (spice);
        FileOutputStream fout = new FileOutputStream (safeDepositBox);
        ObjectOutputStream oos = new ObjectOutputStream (fout);
        oos.writeObject (upsBean);
        oos.flush();
        oos.close();
        // do stuff with spice and if it works, ...
        // contextInitialized (null);
}

// Above is working currently, would like the following to work

public void contextDestroyed(ServletContextEvent sce) {
        System.setProperty ("spice", spice);
        System.out.println ("[DBInterface" + dbiIndex +
                                        "] Spice saved at " +
                        DateFormat.getDateTimeInstance (DateFormat.SHORT,
                                        DateFormat.LONG).format (new Date()));
}

public void contextInitialized(ServletContextEvent sce) {
        if (sce != null) {
                spice = System.getProperty ("spice");
                System.out.println ("[DBInterface" + dbiIndex +
                                        "] Spice retrieved at " +
                        DateFormat.getDateTimeInstance (DateFormat.SHORT,
                                        DateFormat.LONG).format (new Date()));
        }
        // do stuff with spice
}

============
UPSBean.java:
============
public class UPSBean implements Serializable {
        private String  spice = "parsley, sage, rosemary and thyme";
        public UPSBean() { }
        public String getSpice() {      return spice;   }
        public void setSpice (String s) {       spice = s;      }
}

I am trying to see if get/setProperty works above. Was trying to use JNDI directly, but when I setSpice using the resource MyServlet/upsBean in contextDestroyed() and try to read it in contextInitialized(), I get a null (Sorry, I already deleted that part of the code). Now the declaration in context.xml as well as resource-env-ref has become redundant. Current workaround is to save the serialized instance in a data file (not very good).

Manidip Sengupta
  • 3,573
  • 5
  • 25
  • 27
mindas
  • 26,463
  • 15
  • 97
  • 154
  • I tried this - no luck :( The problem is that the BeanFactory in Tomcat generates an instance of the class as a resource, and it is provided to apps in read-only mode. The bean has getter/setter methods, but I cannot write/update anything. Tomcat documentation confirms this behavior (http://tomcat.apache.org/tomcat-6.0-doc/jndi-resources-howto.html). This means we would have to hard code the passwords, etc. in the context.xml file, which is something we are trying to avoid, the main point of this thread. Please let me know if I missed something. TIA, - MS. – Manidip Sengupta Feb 13 '11 at 08:21
  • I am not sure if I got your comment, but surely you should be able to bind a HashMap to the JNDI and read/write into it whatever is necessary. Also, the same documentation URL you send mentions about readonly attribute which can be set to false. It's best if you post some code as it's hard to guess what has failed for you. – mindas Feb 13 '11 at 13:13
  • There - edited your original answer to add relevant snippets of the source code. Will reply to Tim Funk below on the listener interface. Later, - MS. – Manidip Sengupta Feb 15 '11 at 20:28
  • I approved Manidip's edit, but it's a wall of code...If you don't like it, you can just roll it back to the previous version by clicking here: http://stackoverflow.com/posts/4978472/revisions – Robert Harvey Feb 15 '11 at 20:33
  • @Robert, that's fine, the more the merrier :) If I was him, I would rather edit original question, but whatever... – mindas Feb 15 '11 at 20:41
1

If you want to handle it programmatically, I think what you might be looking for is a ServletContextListener. Create a class that implements the interface and code the needed functionality in the contextInitialized(ServletContextEvent sce) -method, see here for simple example.

esaj
  • 15,875
  • 5
  • 38
  • 52
  • Yes, we do want to handle this programmatically. Now, ServletContext is created during deployment - doesn't that also mean that it is destroyed and recreated when the servlet is redeployed? If so, anything saved using setAttribute() or setInitParameter() will get deleted (wont be there in the new Context to be read). – Manidip Sengupta Feb 11 '11 at 18:18
  • Yes, but the ServletContextListener-interface also has a method (contextDestroyed) to listen for the destruction of the context, and you can implement saving the values into a database or flat file in there, which the listener can then pick back up in the contextInitialized-method once the context is recreated. ServletContextEvent -object is passed into both methods, and you can access the actual ServletContext-object from it. – esaj Feb 11 '11 at 18:21
  • Yes, that would work. Essentially, you are suggesting to save the values in the database or a flat file and read it back the next time init() is invoked. However, we are looking for a solution that can keep the values within the tomcat container. One way could be to write a completely new Servlet just for saving these values and retrieving them on demand using URL's on localhost, but that might be an overkill. So tomcat cannot be persuaded to save a few Strings in its memory for different servlets to access? – Manidip Sengupta Feb 11 '11 at 18:54
  • Yes, that's what I'm suggesting, sorry if it doesn't fit your particular case. I don't know if there's a way to store values straight into Tomcat that could be accessed after the application is redeployed, but that naturally doesn't mean it couldn't be done somehow... – esaj Feb 11 '11 at 19:02
  • There is another problem in the above approach - Lets say tomcat is dumping servlet1 (old war) and deploying servlet2 (new war). This happens in asynchronous threads, and I see init() in servlet2 being called before contextDestoyed() in servlet1 - meaning servlet1 doesnt get a chance to save the variables in a file for servlet2 to read in. – Manidip Sengupta Feb 12 '11 at 05:02
1

Tomcat provides nothing to help you here.

Thats the bad news.

The good news is ... this is a process issue which can be solved using the tools the servlet-api provides as well as some additional items Tomcat can provide. Here are the ingredients ..

  • Tomcat provides on container startup the the ability to have Listener - via the Listener element. Use these to track when the container starts up, shuts down, etc.
  • The servlet api provides ServletContextListener to listen for webapp startup, shutdown
  • When tomcat starts up - you may pass in System properties by first setting JAVA_OPTS. But be careful - these values can be discovered with the ps command.
  • Depending on your deployment method - you can add settings for your configuration via my-app.xml (where my-app is the name of the app for deployment)
  • In CATALINA_BASE/conf/context.xml - you may also set init/env params for the webapp to use for ALL webapps to see.
  • If you are NOT using a security manager - you should be able to set/clear some system properties.

Given all of the above - you could read the "protected" values from a file and write them to a system property. Then for added protection in case you don't want the values always existing as System properties - you can utilize ServletContextListener to read the system property values and delete them from system properties. Then on re-deployment - the listener would write reset the system properties on shutdown - so when the webapp restarts up - they are still there.

So the startup sequence would be like this

  • Admin enters pw (existing process) and the file is saved
  • On webapp startup - ServletContextListner looks for file. If exists - uses those values
  • On webapp shutdown - writes the values to System.properties
  • On webapp restart - ServletContextListner sees the file is missing and uses the System.properties. Then deletes the system.properties

With the above - you can redeploy while not writing the passwords to disk and hopefully minimizing any attack vectors by temporarily storing as system.properties.

Good luck.

Tim Funk
  • 869
  • 7
  • 11
  • Thank you, Tim. I am trying to see if your soultion wil work. The process set up is similar to your suggestion (MyServlet.java), admin enters the pw in the Perl script. The problem I see is that when tomcat tries to call contextDestroyed() the DBInterface object is already gone, and when it tries to call contextInitialized(), DBInterface is not instanciated yet (No sign of the println's in catalina.out). I tried to make MyServlet the ContextListener, and that worked. If I make a synchronization scheme, I am apprehensive of a deadlock between the constructor and the listener. We'll see – Manidip Sengupta Feb 15 '11 at 20:32
  • If all the DB Access (init and destroy) is in just one servlet, then you can use the init() and destroy() methods and avoid using ServletContextListener and worrying about what was init/destroyed first. – Tim Funk Feb 16 '11 at 02:40
1

There is nothing in Tomcat to do what you want. If you move the settings into the tomcat JNDI tree you will have to put the user/name password combo in the server.xml or the context.xml file. Here are a couple of possible solutions for the problem you have.

Option 1: Use a Tomcat Listener

If you look at the top of the tomcat server.xml file you will see several listeners these are java classes that execute when tomcat starts up. You can create you own tomcat listener which reads the password from the file system, deletes the file and stores the username/password Comobo in a way that is accessible to the app. The tomcat listener is tied to the lifecyle of the tomcat server so auto redeploy of the app will not cause your tomcat listener class to be reloaded. The code for your tomcat listener would have to be put into a jar and place in the CATALINA_HOME\lib folder.

The listener could use static variables to store the username/password and have a static methods that return them, this will work because the listener will be in the parent class loader of the tomcat apps. The disadvantages of this approach is that you application code is dependent on the tomcat resource listener implementation, and in your unit tests you might have to do some extra steps to make sure your code can be unit tested.

The listener could also access the tomcat global JNDI tree and put the username and password combo in there, and then your app would have to have context.xml file use a ResourceLink element to make the global jndi entry available to the app. You will also have to do some extra work to make this approach work with unit tests as looking stuff up in JNDI generally complicates unit testing.

If you are using Spring on this project your best bet is to use the static variables in a tomcat listener but then use a custom spring scope to pull the data out of the tomcat listener. that way your app stays testable and you can inject username/password combos into any piece of code that needs them.

Option 2: Use a Servlet Context Listener

In this option you write a Context Listener which will allow your app to be notified whenever the app starts and stops. With this approach on startup the context listener will run and read the password information and delete the file. If the password file is not there on startup then the context listener would have to have a way to get the admin to regenerate the file.

Option 3: Use JMX

Create a JMX MBean register it with the JVM MBeanServer and then use it to store the username/password combo. If you initialize this MBean from a tomcat listener you could then have the perl script call the MBean remotely and pass in the username/password combo.

Hope this helps.

ams
  • 60,316
  • 68
  • 200
  • 288
  • Solved!! Simple POJO solution with no heavy guns like a Listener or JNDI/JMX involved. The solution, for the posterity: (1) Create a single class jar file with static variables and static getter/setter methods (2) Put the jar in TOMCAT_HOME/lib (3) Put the jar in CLASSPATH for servlet to compile (4) During execution, check if data file is there. If it is there, read it, save values using the static setter methods. If not, retrieve them using the getter methods. Thats it, no xml file needs to be changed, testing not a problem with the jar file. Thanks to everybody for a lively discussion. – Manidip Sengupta Feb 17 '11 at 03:46