package net.i2p.router.startup;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.Properties;
import net.i2p.data.DataHelper;
import net.i2p.util.SecureDirectory;
import net.i2p.util.SecureFileOutputStream;
import net.i2p.util.SystemVersion;
/**
* Get a working directory for i2p.
*
* For the location, first try the system property i2p.dir.config
* Next try $HOME/.i2p on linux or %APPDATA%\I2P on Windows.
*
* If the dir exists, return it.
* Otherwise, attempt to create it, and copy files from the base directory.
* To successfully copy, the base install dir must be the system property i2p.dir.base
* or else must be in $CWD.
*
* If I2P was run from the install directory in the past,
* and migrateOldData = true, copy the
* necessary data files (except i2psnark/) over to the new working directory.
*
* Otherwise, just copy over a limited number of files over.
*
* Do not ever copy or move the old i2psnark/ directory, as if the
* old and new locations are on different file systems, this could
* be quite slow.
*
* Modify some files while copying, see methods below.
*
* After migration, the router will run using the new directory.
* The wrapper, however, must be stopped and restarted from the new script - until then,
* it will continue to write to wrapper.log* in the old directory.
*/
public class WorkingDir {
private final static String PROP_BASE_DIR = "i2p.dir.base";
private final static String PROP_WORKING_DIR = "i2p.dir.config";
private final static String WORKING_DIR_DEFAULT_WINDOWS = "I2P";
private final static String WORKING_DIR_DEFAULT_MAC = "i2p";
private final static String WORKING_DIR_DEFAULT = ".i2p";
private final static String WORKING_DIR_DEFAULT_DAEMON = "i2p-config";
/** we do a couple of things differently if this is the username */
private static final String PROP_WRAPPER_LOG = "wrapper.logfile";
private static final String DEFAULT_WRAPPER_LOG = "wrapper.log";
/** Feb 16 2006 */
private static final long EEPSITE_TIMESTAMP = 1140048000000l;
/**
* Only call this once on router invocation.
* Caller should store the return value for future reference.
*
* This also redirects stdout and stderr to a wrapper.log file if there is no wrapper present.
*
* @param migrateOldConfig whether to copy all data over from an existing install
*/
public static String getWorkingDir(Properties envProps, boolean migrateOldConfig) {
String dir = null;
if (envProps != null)
dir = envProps.getProperty(PROP_WORKING_DIR);
if (dir == null)
dir = System.getProperty(PROP_WORKING_DIR);
boolean isWindows = SystemVersion.isWindows();
File dirf = null;
String gentooWarning = null;
if (dir != null) {
dirf = new SecureDirectory(dir);
} else {
String home = System.getProperty("user.home");
if (isWindows) {
String appdata = System.getenv("APPDATA");
if (appdata != null)
home = appdata;
dirf = new SecureDirectory(home, WORKING_DIR_DEFAULT_WINDOWS);
} else if (SystemVersion.isMac()) {
String appdata = "/Library/Application Support/";
File old = new File(home,WORKING_DIR_DEFAULT);
if (old.exists() && old.isDirectory())
dirf = new SecureDirectory(home, WORKING_DIR_DEFAULT);
else {
home = home+appdata;
dirf = new SecureDirectory(home, WORKING_DIR_DEFAULT_MAC);
}
} else {
if (SystemVersion.isLinuxService()) {
if (SystemVersion.isGentoo() &&
SystemVersion.GENTOO_USER.equals(System.getProperty("user.name"))) {
// whoops, we didn't recognize Gentoo as a service until 0.9.29,
// so the config dir was /var/lib/i2p/.i2p through 0.9.28
// and changed to /var/lib/i2p/i2p-config in 0.9.29.
// Look for both to decide which to use.
// We prefer .i2p if neither exists.
// We prefer the newer if both exist.
File d1 = new SecureDirectory(home, WORKING_DIR_DEFAULT);
File d2 = new SecureDirectory(home, WORKING_DIR_DEFAULT_DAEMON);
boolean e1 = isSetup(d1);
boolean e2 = isSetup(d2);
if (e1 && e2) {
// d1 is probably older. Switch if it isn't.
if (d2.lastModified() < d1.lastModified()) {
File tmp = d2;
d2 = d1;
d1 = tmp;
// d1 now is the older one
}
dirf = d2;
gentooWarning = "Warning - Found both an old configuration directory " + d1.getAbsolutePath() +
" and new configuration directory " + d2.getAbsolutePath() +
" created due to a bug in release 0.9.29\n. Using the new configuration" +
" directory. To use the old directory instead, stop i2p," +
" delete the new directory, and restart.";
} else if (e1 && !e2) {
dirf = d1;
} else if (!e1 && e2) {
dirf = d2;
} else {
dirf = d1;
}
} else {
dirf = new SecureDirectory(home, WORKING_DIR_DEFAULT_DAEMON);
}
} else {
dirf = new SecureDirectory(home, WORKING_DIR_DEFAULT);
}
}
}
// where we are now
String cwd = null;
if (envProps != null)
cwd = envProps.getProperty(PROP_BASE_DIR);
if (cwd == null) {
cwd = System.getProperty(PROP_BASE_DIR);
if (cwd == null)
cwd = System.getProperty("user.dir");
}
// Check for a hosts.txt file, if it exists then I2P is there
File oldDirf = new File(cwd);
File test = new File(oldDirf, "hosts.txt");
if (!test.exists()) {
setupSystemOut(cwd);
System.err.println("ERROR - Cannot find I2P installation in " + cwd +
" - Will probably be just a router with no apps or console at all!");
// we are probably doomed...
return cwd;
}
// apparently configured for "portable" ?
try {
if (oldDirf.getCanonicalPath().equals(dirf.getCanonicalPath())) {
setupSystemOut(cwd);
return cwd;
}
} catch (IOException ioe) {}
// where we want to go
String rv = dirf.getAbsolutePath();
if (dirf.exists()) {
if (dirf.isDirectory()) {
if (isSetup(dirf)) {
setupSystemOut(rv);
// see above for why
if (gentooWarning != null)
System.err.println(gentooWarning);
return rv; // all is good, we found the user directory
}
}
else {
setupSystemOut(null);
System.err.println("Wanted to use " + rv + " for a working directory but it is not a directory");
return cwd;
}
}
// Check for a router.keys file or logs dir, if either exists it's an old install,
// and only migrate the data files if told to do so
// (router.keys could be deleted later by a killkeys())
test = new File(oldDirf, CreateRouterInfoJob.KEYS_FILENAME);
boolean oldInstall = test.exists();
if (!oldInstall) {
test = new File(oldDirf, "logs");
oldInstall = test.exists();
}
// keep everything where it is, in one place...
if (oldInstall && !migrateOldConfig) {
setupSystemOut(cwd);
return cwd;
}
boolean migrateOldData = false; // this is a terrible idea
if (!dirf.exists() && !dirf.mkdir()) {
setupSystemOut(null);
System.err.println("Wanted to use " + rv + " for a working directory but could not create it");
return cwd;
}
setupSystemOut(dirf.getAbsolutePath());
// Do the copying
if (migrateOldData)
System.err.println("Migrating data files to new user directory " + rv);
else
System.err.println("Setting up new user directory " + rv);
boolean success = migrate(MIGRATE_BASE, oldDirf, dirf);
// this one must be after MIGRATE_BASE
File oldEep = new File(oldDirf, "eepsite");
File newEep = new File(dirf, "eepsite");
String newPath = newEep.getAbsolutePath() + File.separatorChar;
success &= migrateJettyXml(oldEep, newEep, "jetty.xml", "./eepsite/", newPath);
success &= migrateJettyXml(oldEep, newEep, "jetty-ssl.xml", "./eepsite/", newPath);
success &= migrateJettyXml(oldEep, newEep, "contexts/base-context.xml", "./eepsite/", newPath);
success &= migrateJettyXml(oldEep, newEep, "contexts/cgi-context.xml", "./eepsite/", newPath);
success &= migrateClientsConfig(oldDirf, dirf);
// for later news.xml updates (we don't copy initialNews.xml over anymore)
success &= (new SecureDirectory(dirf, "docs")).mkdir();
// prevent correlation of eepsite timestamps with router first-seen time
touchRecursive(new File(dirf, "eepsite/docroot"), EEPSITE_TIMESTAMP);
// Report success or failure
if (success) {
System.err.println("Successfully copied data files to new user directory " + rv);
return rv;
} else {
System.err.println("FAILED copy of some or all data files to new directory " + rv);
System.err.println("Check logs for details");
System.err.println("Continung to use data files in old directory " + cwd);
return cwd;
}
}
/**
* Tests if <code>dir</code> has been set up as a I2P working directory.<br/>
* Returns <code>false</code> if a directory is empty, or contains nothing that
* is usually migrated from the base install.
* This allows to pre-install plugins before the first router start.
* @return true if already set up
*/
private static boolean isSetup(File dir) {
if (dir.isDirectory()) {
String[] files = dir.list();
if (files == null)
return false;
String migrated[] = DataHelper.split(MIGRATE_BASE, ",");
for (String file: files) {
for (int i = 0; i < migrated.length; i++) {
if (file.equals(migrated[i]))
return true;
}
}
}
return false;
}
/**
* Redirect stdout and stderr to a wrapper.log file if there is no wrapper.
*
* If there is no -Dwrapper.log=/path/to/wrapper.log on the java command line
* to specify a log file, check for existence of wrapper.log in CWD,
* for backward compatibility in old installations (don't move it).
* Otherwise, use (system temp dir)/wrapper.log.
* Create if it doesn't exist, and append to it if it does.
* Put the location in the environment as an absolute path, so logs.jsp can find it.
*
* @param dir if null, use Java temp dir; System property wrapper.logfile overrides
* @since 0.8.13
*/
private static void setupSystemOut(String dir) {
if (System.getProperty("wrapper.version") != null)
return;
String path = System.getProperty(PROP_WRAPPER_LOG);
File logfile;
if (path != null) {
logfile = new File(path);
} else {
logfile = new File(DEFAULT_WRAPPER_LOG);
if (!logfile.exists()) {
if (dir == null)
dir = System.getProperty("java.io.tmpdir");
logfile = new File(dir, DEFAULT_WRAPPER_LOG);
}
}
System.setProperty(PROP_WRAPPER_LOG, logfile.getAbsolutePath());
try {
PrintStream ps = new PrintStream(new SecureFileOutputStream(logfile, true), true, "UTF-8");
System.setOut(ps);
System.setErr(ps);
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
/**
* files and directories from the base install to copy over
* None of these should be included in i2pupdate.zip
*
* The user should not delete these in the old location, leave them as templates for new users
*/
private static final String MIGRATE_BASE =
// base install - dirs
// We don't currently have a default addressbook/ in the base distribution,
// but distros might put one in
"addressbook,eepsite," +
// 0.9.15 support bundled router infos
"netDb," +
// base install - files
// We don't currently have a default router.config, logger.config, susimail.config, or webapps.config in the base distribution,
// but distros might put one in
// blocklist.txt now accessed in base dir, user can add another in config dir if desired
"hosts.txt,i2psnark.config,i2ptunnel.config,jetty-i2psnark.xml," +
"logger.config,router.config,susimail.config,systray.config,webapps.config";
private static boolean migrate(String list, File olddir, File todir) {
boolean rv = true;
String files[] = DataHelper.split(list, ",");
for (int i = 0; i < files.length; i++) {
File from = new File(olddir, files[i]);
if (!copy(from, todir)) {
System.err.println("Error copying " + from.getAbsolutePath());
rv = false;
}
}
return rv;
}
/**
* Copy over the clients.config file with modifications
*/
private static boolean migrateClientsConfig(File olddir, File todir) {
File oldFile = new File(olddir, "clients.config");
File newFile = new File(todir, "clients.config");
FileInputStream in = null;
PrintWriter out = null;
try {
in = new FileInputStream(oldFile);
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(newFile), "UTF-8")));
out.println("# Modified by I2P User dir migration script");
String s = null;
boolean isDaemon = SystemVersion.isLinuxService();
while ((s = DataHelper.readLine(in)) != null) {
// readLine() doesn't strip \r
if (s.endsWith("\r"))
s = s.substring(0, s.length() - 1);
if (s.endsWith("=\"eepsite/jetty.xml\"")) {
s = s.replace("=\"eepsite/jetty.xml\"", "=\"" + todir.getAbsolutePath() +
File.separatorChar + "eepsite" +
File.separatorChar + "jetty.xml\"");
} else if (isDaemon && s.equals("clientApp.4.startOnLoad=true")) {
// disable browser launch for daemon
s = "clientApp.4.startOnLoad=false";
}
out.println(s);
}
System.err.println("Copied " + oldFile + " with modifications");
if (out.checkError())
throw new IOException("Failed write to " + newFile);
return true;
} catch (IOException ioe) {
if (in != null) {
System.err.println("FAILED copy " + oldFile + ": " + ioe);
}
return false;
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
if (out != null) out.close();
}
}
/**
* Copy over the jetty.xml file with modifications
* It was already copied over once in migrate(), throw that out and
* do it again with modifications.
*/
static boolean migrateJettyXml(File olddir, File todir, String filename, String oldString, String newString) {
File oldFile = new File(olddir, filename);
File newFile = new File(todir, filename);
FileInputStream in = null;
PrintWriter out = null;
try {
in = new FileInputStream(oldFile);
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(newFile), "UTF-8")));
String s = null;
while ((s = DataHelper.readLine(in)) != null) {
// readLine() doesn't strip \r
if (s.endsWith("\r"))
s = s.substring(0, s.length() - 1);
if (s.indexOf(oldString) >= 0) {
s = s.replace(oldString, newString);
}
out.println(s);
}
out.println("<!-- Modified by I2P User dir migration script -->");
System.err.println("Copied " + oldFile + " with modifications");
return true;
} catch (IOException ioe) {
if (in != null) {
System.err.println("FAILED copy " + oldFile + ": " + ioe);
}
return false;
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
if (out != null) out.close();
}
}
/**
* Recursive copy a file or dir to a dir
*
* @param src file or directory, need not exist
* @param targetDir the directory to copy to, will be created if it doesn't exist
* @return true for success OR if src does not exist
*/
private static boolean copy(File src, File targetDir) {
if (!src.exists())
return true;
if (!targetDir.exists()) {
if (!targetDir.mkdir()) {
System.err.println("FAILED copy " + src.getPath());
return false;
}
System.err.println("Created " + targetDir.getPath());
}
// SecureDirectory is a File so this works for non-directories too
File targetFile = new SecureDirectory(targetDir, src.getName());
if (!src.isDirectory())
return copyFile(src, targetFile);
File children[] = src.listFiles();
if (children == null) {
System.err.println("FAILED copy " + src.getPath());
return false;
}
// make it here so even empty dirs get copied
if (!targetFile.exists()) {
if (!targetFile.mkdir()) {
System.err.println("FAILED copy " + src.getPath());
return false;
}
System.err.println("Created " + targetFile.getPath());
}
boolean rv = true;
for (int i = 0; i < children.length; i++) {
rv &= copy(children[i], targetFile);
}
return rv;
}
/**
* @param src not a directory, must exist
* @param dst not a directory, will be overwritten if existing, will be mode 600
* @return true if it was copied successfully
*/
static boolean copyFile(File src, File dst) {
if (!src.exists()) return false;
boolean rv = true;
byte buf[] = new byte[4096];
FileInputStream in = null;
FileOutputStream out = null;
try {
in = new FileInputStream(src);
out = new SecureFileOutputStream(dst);
DataHelper.copy(in, out);
System.err.println("Copied " + src.getPath());
} catch (IOException ioe) {
System.err.println("FAILED copy " + src.getPath() + ": " + ioe);
rv = false;
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
if (out != null) try { out.close(); } catch (IOException ioe) {}
}
if (rv)
dst.setLastModified(src.lastModified());
return rv;
}
/**
* Recursive touch all files in a dir to a given time
*
* @param target the directory or file to touch, must exist
* @param time the timestamp
* @since 0.8.13
*/
private static void touchRecursive(File target, long time) {
if (!target.exists())
return;
if (target.isFile()) {
target.setLastModified(time);
return;
}
if (!target.isDirectory())
return;
File children[] = target.listFiles();
if (children == null)
return;
for (int i = 0; i < children.length; i++) {
touchRecursive(children[i], time);
}
}
}