Hacking Into Equinox Logging

If you develop product on top of Eclipse RCP platform or use Equinox as OSGI framework then, probably, you’ll need to customize the way logging happening from your application. Below we will extend default Eclipse logger to prevent sensitive information being logged.

Warning: this will work only for ancient version of Equinox, the version which comes with Eclipse Ganymede platform, I believe it is org.eclipse.osgi_3.4.x.

If you look inside log file created by Eclipse you will see something like this on every application startup or log rotation occured:

!SESSION 2017-02-19 14:13:57.695 -----------------------------------------------
eclipse.buildId=unknown
java.version=1.8.0_31
java.vendor=Oracle Corporation
BootLoader constants: OS=linux, ARCH=x86, WS=gtk, NL=ru_RU
Framework arguments:  -sourcepassword pass1 -targetpassword pass2
Command-line arguments:  -dev file:/home/alt/workspaces/metalogix/.metadata/.plugins/org.eclipse.pde.core/com.example.equinox.logging/dev.properties -os linux -ws gtk -arch x86 -consoleLog -console -sourcepassword pass1 -targetpassword pass2

Sometimes you need to pass some sensitive data to your application as command line arguments. It could be encrypted or clear text passwords or credit card numbers or anything else you consider as sensitive data. By default Eclipse writes log file using:

org.eclipse.core.runtime.adaptor.EclipseLog

class. If you look inside you’ll notice that this class already supports hiding -password argument value by replacing it with (omitted) text. But we need to hide values of our custom arguments. After some debugging I realized we can hook up into framework startup and pass our own logging implementation.

After reading Eclipse Adaptor Hooks article you will be able to do what you need. Below I explain key moments. The main issue is all this hookups happening at the framework startup level, there is no bundles loaded except org.eclipse.osgi itself. To hook up with our own logging implementation we need to create bundle fragment for org.eclipse.osgi bundle. And because host bundle is a framework itself, we going to create not simply fragment but framework extension. Framework extension is a bundle fragment in first place, and what makes it framework extension is declaring framework property that list our bundle fragment.

osgi.framework.extensions=com.example.equinox.logging

Roadmap

  • We will create framework extension that will contain modified EclipseLogHook class and our own log implementation by subclassing EclipseLog class.
  • Next we create eclipse plugin to produce some logs.
  • We setup launch configuration and will see everything in action.

Creating Framework Extension

If you using Eclipse IDE you could processed to next steps to create Plug-in Fragment. I’ll be using Eclipse Luna.

  1. Navigate to menu File->New->Other or press Ctrl-N from IDE;
  2. Select Plug-in Development->Fragment Project and press Next;
    Select Fragment Project
  3. Enter “com.example.equinox.logging” as project name and press Next;eclipse-new-fragment-2
  4. Now you need to specify host bundle for our fragment. Enter “org.eclipse.osgi” as value of “Plug-In ID” field in “Host Plug-In” group, or click “Browse” and search for required bundle;eclipse-new-fragment-3
  5. Change required minimum and maximum versions if you need or simply press Finish.

For other IDE’s you can just create a project and change META-INF/MANIFEST.MF by yourself to look like this:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Logging
Bundle-SymbolicName: com.example.equinox.logging
Bundle-Version: 1.0.0.qualifier
Bundle-Vendor: EXAMPLE
Fragment-Host: org.eclipse.osgi;bundle-version="3.4.0"

Writing some code

Now we going to subclass EclipseLog class to extend program arguments filtering. There is a method called writeArgs that do the job:

        /**
	 * Helper method for writing out argument arrays.
	 * @param header the header
	 * @param args the list of arguments
	 */
	protected void writeArgs(String header, String[] args) throws IOException {
		if (args == null || args.length == 0)
			return;
		write(header);
		for (int i = 0; i < args.length; i++) {
			//mask out the password argument for security
			if (i > 0 && PASSWORD.equals(args[i - 1]))
				write(" (omitted)"); //$NON-NLS-1$
			else
				write(" " + args[i]); //$NON-NLS-1$
		}
		writeln();
	}

To make things happen lets extend EclipseLog and override writeArgs:

package com.example.equinox.logging;

import java.io.File;
import java.io.IOException;
import java.io.Writer;

import org.eclipse.core.runtime.adaptor.EclipseLog;

public class PasswordFilterLog extends EclipseLog {

	private static final String[] PASSWORD_ARGS = {"-sourcepassword", "-targetpassword"};

	public PasswordFilterLog() {
		super();
	}

	public PasswordFilterLog(File outFile) {
		super(outFile);
	}

	public PasswordFilterLog(Writer writer) {
		super(writer);
	}

	@Override
	protected void writeArgs(String header, String[] args) throws IOException {
		if (args == null || args.length == 0)
			return;
		write(header);
		for (int i = 0; i < args.length; i++) {
			//mask out the password argument for security
			if (i > 0 && isPasswordArgument(args[i - 1]))
				write(" (omitted)"); //$NON-NLS-1$
			else
				write(" " + args[i]); //$NON-NLS-1$
		}
		writeln();
	}
	
	private boolean isPasswordArgument(String arg) {
		for(String p_arg : PASSWORD_ARGS) {
			if(p_arg.equals(arg)) {
				return true;
			}
		}
		
		return false;
	}
}

To inject our own logger implementation first we need to do is create hook for it. In Eclipse implementation there is class for it named EclipseLogHook. I’ve just copy-pasted it into my framework extension project and simply changed new EclipseLog() calls to new PasswordFilterLog(). That’s it.

/*
 * Copied from {@link org.eclipse.core.runtime.internal.adaptor.EclipseLogHook}.
 */

package com.example.equinox.logging;

import java.io.File;

public class PasswordFilterLogHook implements HookConfigurator, AdaptorHook {
	// The eclipse log file extension */
	private static final String LOG_EXT = ".log"; //$NON-NLS-1$
	
	BaseAdaptor adaptor;

	private PasswordFilterLog frameworkLog;

	public void addHooks(HookRegistry hookRegistry) {
		hookRegistry.addAdaptorHook(this);
	}

	public void initialize(BaseAdaptor adaptor) {
		this.adaptor = adaptor;
	}

	public void frameworkStart(BundleContext context) throws BundleException {
		AdaptorUtil.register(FrameworkLog.class.getName(), adaptor.getFrameworkLog(), context);
		registerPerformanceLog(context);
	}

	public void frameworkStop(BundleContext context) throws BundleException {
		// TODO should unregister service registered a frameworkStart
	}

	public void frameworkStopping(BundleContext context) {
		// do nothing

	}

	public void addProperties(Properties properties) {
		// do nothing
	}

	public URLConnection mapLocationToURLConnection(String location) throws IOException {
		// do nothing
		return null;
	}

	public void handleRuntimeError(Throwable error) {
		// TODO Auto-generated method stub

	}

	public boolean matchDNChain(String pattern, String[] dnChain) {
		// do nothing
		return false;
	}

	public FrameworkLog createFrameworkLog() {
		String logFileProp = FrameworkProperties.getProperty(EclipseStarter.PROP_LOGFILE);
		if (logFileProp != null) {
			frameworkLog = new PasswordFilterLog(new File(logFileProp));
		} else {
			Location location = LocationManager.getConfigurationLocation();
			File configAreaDirectory = null;
			if (location != null)
				// TODO assumes the URL is a file: url
				configAreaDirectory = new File(location.getURL().getFile());

			if (configAreaDirectory != null) {
				String logFileName = Long.toString(System.currentTimeMillis()) + PasswordFilterLogHook.LOG_EXT;
				File logFile = new File(configAreaDirectory, logFileName);
				FrameworkProperties.setProperty(EclipseStarter.PROP_LOGFILE, logFile.getAbsolutePath());
				frameworkLog = new PasswordFilterLog(logFile);
			} else
				frameworkLog = new PasswordFilterLog();
		}
		if ("true".equals(FrameworkProperties.getProperty(EclipseStarter.PROP_CONSOLE_LOG))) //$NON-NLS-1$
			frameworkLog.setConsoleLog(true);
		
		return frameworkLog;
	}

	private void registerPerformanceLog(BundleContext context) {
		Object service = createPerformanceLog();
		String serviceName = FrameworkLog.class.getName();
		Hashtable serviceProperties = new Hashtable(7);
		Dictionary headers = context.getBundle().getHeaders();

		serviceProperties.put(Constants.SERVICE_VENDOR, headers.get(Constants.BUNDLE_VENDOR));
		serviceProperties.put(Constants.SERVICE_RANKING, new Integer(Integer.MIN_VALUE));
		serviceProperties.put(Constants.SERVICE_PID, context.getBundle().getBundleId() + '.' + service.getClass().getName());
		serviceProperties.put(FrameworkLog.SERVICE_PERFORMANCE, Boolean.TRUE.toString());

		context.registerService(serviceName, service, serviceProperties);
	}

	private FrameworkLog createPerformanceLog() {
		String logFileProp = FrameworkProperties.getProperty(EclipseStarter.PROP_LOGFILE);
		if (logFileProp != null) {
			int lastSlash = logFileProp.lastIndexOf(File.separatorChar);
			if (lastSlash > 0) {
				String logFile = logFileProp.substring(0, lastSlash + 1) + "performance.log"; //$NON-NLS-1$
				return new PasswordFilterLog(new File(logFile));
			}
		}
		//if all else fails, write to std err
		return new PasswordFilterLog(new PrintWriter(System.err));
	}
}

Creating test plugin

To see out implementation in action we need to create test eclipse plugin and do some logging from it.

  1. Navigate to menu File->New->Other or press Ctrl-N;
  2. Select Plug-in Development->Plug-in Project from list and press Next;
  3. Enter com.example.logging.test as Project name, select Eclipse version: 3.5 or greater and press Next;
    New Plug-in wizard first page
  4. Check Generate an activator…, do not create rich client application and press Finish;
    New Plug-in wizard second page

Next open your Activator class and put logging code to start method:

	public void start(BundleContext bundleContext) throws Exception {
		Activator.context = bundleContext;
		
		getLog().log(new Status(IStatus.INFO, "com.example.logging.test", "Starting..."));
	}

Make sure your Activator extends Plugin class! That’s it.

Setting up launch configuration

Prepare

As I mentioned earlier to make Equinox OSGI framework recognize our extension we need to set framework property to specific value. While development time it’s enough to set java system property and for exported eclipse product config.ini required to be modified to contain that property. Also you need to be aware of one more trick – when org.eclipse.osgi launches it expecting to find framework extensions in same directory placed next to it. That means in development time you required to import org.eclipse.osgi to your workspace as plug-in project. To do this you need to follow next steps:

  1. Navigate to menu Window->Show View->Other;
  2. Search for Plug-ins view, select it and press OK.

Now, when Plug-ins view opened, search it for plugin named org.eclipse.osgi, and select Import As submenu from context menu. There are several options to import project, any Binary Project or Source Project will work. I imported it as Binary Project with Linked Content myself, to prevent copying extra files to workspace.

Lets create launch configuration

  1. Navigate to menu Run->Run Configurations;
  2. Select OSGI Framework from left side tree and press New toolbar button;
  3. Rename newly created configuration to something like com.example.equinox.logging;
  4. Make sure Bundles tab is open and check com.example.equinox.logging, com.example.logging.test and org.eclipse.osgi bundles. Note that org.eclipse.osgi must be selected from Workspace and not from Target Platform;
  5. Press Add Required Bundles button, result should be like:
    Bundles
  6. Switch to Arguments tab and add -sourcepassword and -targetpassword arguments as Program Arguments;
  7. Add folowing lines as VM Arguments:
    -Dosgi.framework.extensions=com.example.equinox.logging
    -Dosgi.hook.configurators.exclude=org.eclipse.core.runtime.internal.adaptor.EclipseLogHook
    -Dosgi.hook.configurators.include=com.example.equinox.logging.PasswordFilterLogHook
    

    Arguments

There are two extra system properties, first one disables internal EclipseLogHook and second one enables our implementation hook. These extra properties should be placed to config.ini at production as well.

Running

Time to test it – press Run button from Run Configurations dialog now.

Console output should be like this:

osgi> 
!SESSION 2017-02-19 13:57:21.941 -----------------------------------------------
eclipse.buildId=unknown
java.version=1.8.0_31
java.vendor=Oracle Corporation
BootLoader constants: OS=linux, ARCH=x86, WS=gtk, NL=ru_RU
Framework arguments:  -sourcepassword (omitted) -targetpassword (omitted)
Command-line arguments:  -dev file:/home/alt/workspaces/metalogix/.metadata/.plugins/org.eclipse.pde.core/com.example.equinox.logging/dev.properties -os linux -ws gtk -arch x86 -consoleLog -console -sourcepassword (omitted) -targetpassword (omitted)

!ENTRY com.example.logging.test 1 0 2017-02-19 13:57:22.240
!MESSAGE Starting...

As you can see our sensitive data filtered out from framework log.

All sources you can find on GitHub

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s