Compare commits

..

1 Commits

Author SHA1 Message Date
zzz
68fbd09c0c I2CP: Add support for Datagram 2/3 (proposal 163)
Some checks failed
Java CI / build (push) Has been cancelled
Java CI / javadoc-latest (push) Has been cancelled
Java CI / build-java7 (push) Has been cancelled
Java with IzPack Snapshot Setup / setup (push) Has been cancelled
Prep for UDP announces (proposal 160)
Base for future changes in i2psnark and zzzot
Not hooked in to anything
2025-04-26 14:08:55 -04:00
6 changed files with 926 additions and 129 deletions

View File

@@ -489,14 +489,27 @@ public interface I2PSession {
public static final int PROTO_STREAMING = 6;
/**
* Generally a signed datagram, but could
* also be a raw datagram, depending on the application
* A repliable and signed datagram
*/
public static final int PROTO_DATAGRAM = 17;
/**
* A raw (unsigned) datagram
* A raw (unsigned, unrepliable) datagram
* @since 0.9.2
*/
public static final int PROTO_DATAGRAM_RAW = 18;
/**
* A repliable and signed datagram.
* See Proposal 163 and datagrams/Datagram2
* @since 0.9.66
*/
public static final int PROTO_DATAGRAM2 = 19;
/**
* A repliable but unsigned datagram.
* See Proposal 163 and datagrams/Datagram3
* @since 0.9.66
*/
public static final int PROTO_DATAGRAM3 = 20;
}

View File

@@ -0,0 +1,355 @@
package net.i2p.client.datagram;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Properties;
import net.i2p.I2PAppContext;
import net.i2p.client.I2PClient;
import net.i2p.client.I2PClientFactory;
import net.i2p.client.I2PSession;
import net.i2p.crypto.KeyGenerator;
import net.i2p.crypto.SigType;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.data.Signature;
import net.i2p.data.SigningPrivateKey;
import net.i2p.data.SigningPublicKey;
import net.i2p.data.SimpleDataStructure;
import net.i2p.util.ByteArrayStream;
import net.i2p.util.HexDump;
/**
* Class for creating and loading I2P repliable datagrams version 2.
* Ref: Proposal 163
*
*<pre>
+----+----+----+----+----+----+----+----+
| |
~ from ~
~ ~
| |
+----+----+----+----+----+----+----+----+
| flags | options (optional) |
+----+----+ +
~ ~
~ ~
+----+----+----+----+----+----+----+----+
| |
~ offline_signature (optional) ~
~ expires, sigtype, pubkey, offsig ~
| |
+----+----+----+----+----+----+----+----+
| |
~ payload ~
~ ~
| |
+----+----+----+----+----+----+----+----+
| |
~ signature ~
~ ~
| |
+----+----+----+----+----+----+----+----+
*</pre>
*
* @since 0.9.66
*/
public class Datagram2 {
private final Destination _from;
private final byte[] _payload;
private final Properties _options;
private static final int INIT_DGRAM_BUFSIZE = 2*1024;
private static final int MIN_DGRAM_SIZE = 387 + 2 + 40;
private static final int MAX_DGRAM_BUFSIZE = 61*1024;
private static final byte VERSION_MASK = 0x0f;
private static final byte OPTIONS = 0x10;
private static final byte OFFLINE = 0x20;
private static final byte VERSION = 2;
/**
* As returned from load()
*/
private Datagram2(Destination dest, byte[] data, Properties options) {
_from = dest;
_payload = data;
_options = options;
}
/**
* Make a repliable I2P datagram2 containing the specified payload.
*
* @param payload non-null Bytes to be contained in the I2P datagram.
* @return non-null, throws on all errors
* @throws DataFormatException if payload is too big
*/
public static byte[] make(I2PAppContext ctx, I2PSession session, byte[] payload, Hash tohash) throws DataFormatException {
return make(ctx, session, payload, tohash, null);
}
/**
* Make a repliable I2P datagram2 containing the specified payload.
*
* @param payload non-null Bytes to be contained in the I2P datagram.
* @param options may be null
* @return non-null, throws on all errors
* @throws DataFormatException if payload is too big
*/
public static byte[] make(I2PAppContext ctx, I2PSession session, byte[] payload,
Hash tohash, Properties options) throws DataFormatException {
ByteArrayOutputStream out = new ByteArrayOutputStream(payload.length + 512);
try {
Destination dest = session.getMyDestination();
dest.writeBytes(out);
// start of signed data
int off = out.size();
out.write(tohash.getData());
out.write((byte) 0); // high byte flags
byte flags = VERSION;
if (options != null && !options.isEmpty())
flags |= OPTIONS;
if (session.isOffline())
flags |= OFFLINE;
out.write(flags); // low byte flags
if (options != null && !options.isEmpty())
DataHelper.writeProperties(out, options);
if (session.isOffline()) {
DataHelper.writeLong(out, 4, session.getOfflineExpiration() / 1000);
SigningPublicKey tspk = session.getTransientSigningPublicKey();
DataHelper.writeLong(out, 2, tspk.getType().getCode());
tspk.writeBytes(out);
Signature tsig = session.getOfflineSignature();
tsig.writeBytes(out);
}
out.write(payload);
// end of signed data
byte[] data = out.toByteArray();
SigningPrivateKey sxPrivKey = session.getPrivateKey();
Signature sig = ctx.dsa().sign(data, off, out.size() - off, sxPrivKey);
if (sig == null)
throw new IllegalArgumentException("Sig fail");
sig.writeBytes(out);
if (out.size() - Hash.HASH_LENGTH > MAX_DGRAM_BUFSIZE)
throw new DataFormatException("Too big");
byte[] rv = out.toByteArray();
// remove hash
System.arraycopy(rv, off + Hash.HASH_LENGTH, rv, off, rv.length - (off + Hash.HASH_LENGTH));
return Arrays.copyOfRange(rv, 0, rv.length - Hash.HASH_LENGTH);
} catch (IOException e) {
throw new DataFormatException("DG2 maker error", e);
}
}
/**
* Load an I2P repliable datagram and verify the signature.
*
* @param dgram non-null I2P repliable datagram to be loaded
* @return non-null, throws on all errors
* @throws DataFormatException If there is an error in the datagram format
* @throws I2PInvalidDatagramException If the signature fails
*/
public static Datagram2 load(I2PAppContext ctx, I2PSession session, byte[] dgram) throws DataFormatException, I2PInvalidDatagramException {
if (dgram.length < MIN_DGRAM_SIZE)
throw new DataFormatException("Datagram2 too small: " + dgram.length);
ByteArrayInputStream in = new ByteArrayInputStream(dgram);
try {
Destination rxDest = Destination.create(in);
// start of signed data
int off = dgram.length - in.available();
in.read(); // ignore high byte of flags
int flags = in.read();
int version = flags & VERSION_MASK;
if (version != VERSION)
throw new DataFormatException("Bad version " + version);
SigningPublicKey spk = rxDest.getSigningPublicKey();
SigType type = spk.getType();
if (type == null)
throw new DataFormatException("unsupported sig type");
int optlen = 0;
Properties options = null;
if ((flags & OPTIONS) != 0) {
in.mark(0);
optlen = (int) DataHelper.readLong(in, 2);
if (optlen > 0) {
in.reset();
if (in.available() < optlen)
throw new DataFormatException("too small for options: " + dgram.length);
options = DataHelper.readProperties(in, null, true);
}
optlen += 2;
}
int offlinelen = 0;
if ((flags & OFFLINE) != 0) {
offlinelen = 6;
int off2 = dgram.length - in.available();
long transientExpires = DataHelper.readLong(in, 4) * 1000;
if (transientExpires < ctx.clock().now())
throw new I2PInvalidDatagramException("Offline signature expired");
int itype = (int) DataHelper.readLong(in, 2);
SigType ttype = SigType.getByCode(itype);
if (ttype == null || !ttype.isAvailable())
throw new I2PInvalidDatagramException("Unsupported transient sig type: " + itype);
SigningPublicKey transientSigningPublicKey = new SigningPublicKey(type);
byte[] buf = new byte[transientSigningPublicKey.length()];
offlinelen += buf.length;
in.read(buf);
transientSigningPublicKey.setData(buf);
SigType otype = rxDest.getSigningPublicKey().getType();
Signature offlineSignature = new Signature(otype);
buf = new byte[offlineSignature.length()];
offlinelen += buf.length;
in.read(buf);
offlineSignature.setData(buf);
byte[] data = new byte[0]; // fixme
if (!ctx.dsa().verifySignature(offlineSignature, dgram, off2, 6 + transientSigningPublicKey.length(), spk))
throw new I2PInvalidDatagramException("Bad offline signature");
}
int siglen = type.getSigLen();
in.skip(in.available() - siglen);
// end of signed data
Signature sig = new Signature(type);
sig.readBytes(in);
byte[] buf = new byte[dgram.length + Hash.HASH_LENGTH - (off + siglen)];
System.arraycopy(session.getMyDestination().calculateHash().getData(), 0, buf, 0, Hash.HASH_LENGTH);
System.arraycopy(dgram, off, buf, Hash.HASH_LENGTH, dgram.length - (off + siglen));
if (!ctx.dsa().verifySignature(sig, buf, spk)) {
throw new I2PInvalidDatagramException("Bad signature " + type);
}
if (offlinelen > 0)
off += offlinelen;
int datalen = dgram.length - (off + 2 + optlen + siglen);
byte[] payload = new byte[datalen];
System.arraycopy(dgram, off + 2 + optlen, payload, 0, datalen);
return new Datagram2(rxDest, payload, options);
} catch (IOException e) {
throw new DataFormatException("Error loading datagram", e);
}
}
/**
* Get the payload carried by an I2P repliable datagram (previously loaded
* with the load() method)
*
* @return A byte array containing the datagram payload
*/
public byte[] getPayload() {
return _payload;
}
/**
* Get the sender of an I2P repliable datagram (previously loaded with the
* load() method)
*
* @return The Destination of the I2P repliable datagram sender
*/
public Destination getSender() {
return _from;
}
/**
* Get the options of an I2P repliable datagram (previously loaded with the
* load() method), if any
*
* @return options or null
*/
public Properties getOptions() {
return _options;
}
/*
public static void main(String[] args) throws Exception {
I2PAppContext ctx = I2PAppContext.getGlobalContext();
// 1 is normal dest, 2 is offline
I2PClient cl = I2PClientFactory.createClient();
ByteArrayStream bas1 = new ByteArrayStream(800);
ByteArrayStream bas2 = new ByteArrayStream(800);
// sess 1
cl.createDestination(bas1, SigType.EdDSA_SHA512_Ed25519);
// sess 2 offline
cl.createDestination(bas2, SigType.EdDSA_SHA512_Ed25519);
// sess 2 transient keys
SimpleDataStructure[] tr = ctx.keyGenerator().generateSigningKeys(SigType.EdDSA_SHA512_Ed25519);
SigningPublicKey tpub = (SigningPublicKey) tr[0];
SigningPrivateKey tpriv = (SigningPrivateKey) tr[1];
ByteArrayStream bas3 = new ByteArrayStream(800);
byte[] b2 = bas2.toByteArray();
int pklen = SigType.EdDSA_SHA512_Ed25519.getPrivkeyLen();
int tocopy = b2.length - pklen;
bas3.write(b2, 0, tocopy);
// zero out privkey
for (int i = 0; i < pklen; i++) {
bas3.write((byte) 0);
}
byte[] oprivb = new byte[pklen];
System.arraycopy(b2, tocopy, oprivb, 0, pklen);
SigningPrivateKey opriv = new SigningPrivateKey(SigType.EdDSA_SHA512_Ed25519, oprivb);
// offline section
ByteArrayOutputStream baos = new ByteArrayOutputStream(70);
// expires
DataHelper.writeLong(bas3, 4, 0x7fffffff);
DataHelper.writeLong(baos, 4, 0x7fffffff);
// type
DataHelper.writeLong(bas3, 2, SigType.EdDSA_SHA512_Ed25519.getCode());
DataHelper.writeLong(baos, 2, SigType.EdDSA_SHA512_Ed25519.getCode());
// transient pubkey
byte[] tpubb = tpub.getData();
bas3.write(tpubb);
baos.write(tpubb);
// sig
Signature sig = ctx.dsa().sign(baos.toByteArray(), opriv);
if (sig == null)
throw new IllegalArgumentException("Sig fail");
bas3.write(sig.getData());
// transient privkey
bas3.write(oprivb);
Properties p = new Properties();
I2PSession s1 = cl.createSession(bas1.asInputStream(), p);
I2PSession s2 = cl.createSession(bas3.asInputStream(), p);
Destination d1 = s1.getMyDestination();
Destination d2 = s2.getMyDestination();
Properties opts = new Properties();
opts.setProperty("foooooooooooo", "bar");
opts.setProperty("a", "b");
// test 1: 1 to 2, normal
byte[] data1 = new byte[1024];
ctx.random().nextBytes(data1);
byte[] dg1 = Datagram2.make(ctx, s1, data1, d2.calculateHash(), opts);
Datagram2 datag = Datagram2.load(ctx, s2, dg1);
byte[] data2 = datag.getPayload();
Destination dr = datag.getSender();
if (!dr.equals(d1)) {
System.out.println("ONLINE FAIL sender mismatch");
} else if (!DataHelper.eq(data1, data2)) {
System.out.println("ONLINE FAIL data mismatch");
System.out.println("Send Payload:\n" + HexDump.dump(data1));
System.out.println("Rcv Payload:\n" + HexDump.dump(data2));
} else {
System.out.println("ONLINE PASS");
}
// test 2: 2 to 1, offline
dg1 = Datagram2.make(ctx, s2, data1, d1.calculateHash(), opts);
datag = Datagram2.load(ctx, s1, dg1);
data2 = datag.getPayload();
dr = datag.getSender();
if (!dr.equals(d2)) {
System.out.println("OFFLINE FAIL sender mismatch");
} else if (!DataHelper.eq(data1, data2)) {
System.out.println("OFFLINE FAIL data mismatch");
System.out.println("Send Payload:\n" + HexDump.dump(data1));
System.out.println("Rcv Payload:\n" + HexDump.dump(data2));
} else {
System.out.println("OFFLINE PASS");
}
}
*/
}

View File

@@ -0,0 +1,217 @@
package net.i2p.client.datagram;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Properties;
import net.i2p.I2PAppContext;
import net.i2p.client.I2PClient;
import net.i2p.client.I2PClientFactory;
import net.i2p.client.I2PSession;
import net.i2p.crypto.KeyGenerator;
import net.i2p.crypto.SigType;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.util.ByteArrayStream;
import net.i2p.util.HexDump;
/**
* Class for creating and loading I2P repliable datagrams version 3.
* Ref: Proposal 163
*
*<pre>
+----+----+----+----+----+----+----+----+
| |
+ fromHash +
| |
+ +
| |
+----+----+----+----+----+----+----+----+
| flags | options (optional) |
+----+----+ +
~ ~
~ ~
+----+----+----+----+----+----+----+----+
| |
~ payload ~
~ ~
| |
+----+----+----+----+----+----+----+----+
*</pre>
*
* @since 0.9.66
*/
public class Datagram3 {
private final Hash _from;
private final byte[] _payload;
private final Properties _options;
private static final int INIT_DGRAM_BUFSIZE = 2*1024;
private static final int MIN_DGRAM_SIZE = 32 + 2;
private static final int MAX_DGRAM_BUFSIZE = 61*1024;
private static final byte VERSION_MASK = 0x0f;
private static final byte OPTIONS = 0x10;
private static final byte VERSION = 3;
/**
* As returned from load()
*/
private Datagram3(Hash dest, byte[] data, Properties options) {
_from = dest;
_payload = data;
_options = options;
}
/**
* Make a repliable I2P datagram3 containing the specified payload.
*
* @param payload non-null Bytes to be contained in the I2P datagram.
* @return non-null, throws on all errors
* @throws DataFormatException if payload is too big
*/
public static byte[] make(I2PAppContext ctx, I2PSession session, byte[] payload) throws DataFormatException {
return make(ctx, session, payload, null);
}
/**
* Make a repliable I2P datagram3 containing the specified payload.
*
* @param payload non-null Bytes to be contained in the I2P datagram.
* @param options may be null
* @return non-null, throws on all errors
* @throws DataFormatException if payload is too big
*/
public static byte[] make(I2PAppContext ctx, I2PSession session, byte[] payload,
Properties options) throws DataFormatException {
ByteArrayOutputStream out = new ByteArrayOutputStream(payload.length + 34);
try {
out.write(session.getMyDestination().calculateHash().getData());
out.write((byte) 0); // high byte flags
byte flags = VERSION;
if (options != null && !options.isEmpty())
flags |= OPTIONS;
out.write(flags); // low byte flags
if (options != null && !options.isEmpty())
DataHelper.writeProperties(out, options);
out.write(payload);
if (out.size() > MAX_DGRAM_BUFSIZE)
throw new DataFormatException("Too big");
} catch (IOException ioe) {}
return out.toByteArray();
}
/**
* Load an I2P repliable datagram3.
*
* @param dgram non-null I2P repliable datagram to be loaded
* @return non-null, throws on all errors
* @throws DataFormatException If there is an error in the datagram format
*/
public static Datagram3 load(I2PAppContext ctx, I2PSession session, byte[] dgram) throws DataFormatException {
if (dgram.length < MIN_DGRAM_SIZE)
throw new DataFormatException("Datagram3 too small: " + dgram.length);
ByteArrayInputStream in = new ByteArrayInputStream(dgram);
try {
Hash rxDest = Hash.create(in);
in.read(); // ignore high byte of flags
int flags = in.read();
int version = flags & VERSION_MASK;
if (version != VERSION)
throw new DataFormatException("Bad version " + version);
int optlen = 0;
Properties options = null;
if ((flags & OPTIONS) != 0) {
in.mark(0);
optlen = (int) DataHelper.readLong(in, 2);
if (optlen > 0) {
in.reset();
if (in.available() < optlen)
throw new DataFormatException("too small for options: " + dgram.length);
options = DataHelper.readProperties(in, null, true);
}
optlen += 2;
}
int datalen = dgram.length - (Hash.HASH_LENGTH + 2 + optlen);
byte[] payload = new byte[datalen];
System.arraycopy(dgram, Hash.HASH_LENGTH + 2 + optlen, payload, 0, datalen);
return new Datagram3(rxDest, payload, options);
} catch (IOException e) {
throw new DataFormatException("Error loading datagram", e);
}
}
/**
* Get the payload carried by an I2P repliable datagram (previously loaded
* with the load() method)
*
* @return A byte array containing the datagram payload
*/
public byte[] getPayload() {
return _payload;
}
/**
* Get the sender of an I2P repliable datagram (previously loaded with the
* load() method)
*
* @return The Hash of the Destination of the I2P repliable datagram sender
*/
public Hash getSender() {
return _from;
}
/**
* Get the options of an I2P repliable datagram (previously loaded with the
* load() method), if any
*
* @return options or null
*/
public Properties getOptions() {
return _options;
}
/*
public static void main(String[] args) throws Exception {
I2PAppContext ctx = I2PAppContext.getGlobalContext();
I2PClient cl = I2PClientFactory.createClient();
ByteArrayStream bas1 = new ByteArrayStream(800);
ByteArrayStream bas2 = new ByteArrayStream(800);
// sess 1
cl.createDestination(bas1, SigType.EdDSA_SHA512_Ed25519);
// sess 2
cl.createDestination(bas2, SigType.EdDSA_SHA512_Ed25519);
Properties p = new Properties();
I2PSession s1 = cl.createSession(bas1.asInputStream(), p);
I2PSession s2 = cl.createSession(bas2.asInputStream(), p);
Destination d1 = s1.getMyDestination();
Destination d2 = s2.getMyDestination();
Properties opts = new Properties();
opts.setProperty("foooooooooooo", "bar");
opts.setProperty("a", "b");
byte[] data1 = new byte[1024];
ctx.random().nextBytes(data1);
byte[] dg1 = Datagram3.make(ctx, s1, data1, opts);
Datagram3 datag = Datagram3.load(ctx, s2, dg1);
byte[] data2 = datag.getPayload();
Hash dr = datag.getSender();
if (!dr.equals(d1.calculateHash())) {
System.out.println("FAIL sender mismatch");
} else if (!DataHelper.eq(data1, data2)) {
System.out.println("FAIL data mismatch");
System.out.println("Send Payload:\n" + HexDump.dump(data1));
System.out.println("Rcv Payload:\n" + HexDump.dump(data2));
} else {
System.out.println("PASS");
}
}
*/
}

View File

@@ -154,10 +154,8 @@ public class ClientAppConfig {
try {
List<ClientAppConfig> cacs = getClientApps(cf);
if (!cacs.isEmpty()) {
// Jetty 5/6/7/8 to 9 migration
if (!SystemVersion.isAndroid())
MigrateJetty.migrate(ctx, cacs);
// clients.config to clients.config.d migration
boolean ok = migrate(ctx, cacs, cf, dir);
if (!ok)
rv.addAll(cacs);
@@ -190,9 +188,6 @@ public class ClientAppConfig {
System.out.println("Error loading the client app properties from " + f + ' ' + ioe);
}
}
// Jetty id to refid migration
if (!rv.isEmpty() && !SystemVersion.isAndroid())
MigrateJetty.migrate(ctx, rv);
}
}
return rv;

View File

@@ -22,16 +22,6 @@ import net.i2p.util.SecureFileOutputStream;
import net.i2p.util.VersionComparator;
/**
* Second migration, as of 2.9.0:
*<p>
* Migrate all the jetty*.xml files to change
* Ref id= to Ref refid= because dup ids is a fatal error.
* Also migrate the old configure.dtd to configure_9_3.dtd.
* Reference: https://github.com/jetty/jetty.project/issues/12881
*</p>
*
* First migration, as of 0.9.30:
*<p>
* Migrate the clients.config and jetty.xml files
* from Jetty 5/6 to Jetty 7/8.
* Also migrate jetty.xml from Jetty 7/8 to Jetty 9.
@@ -53,7 +43,6 @@ import net.i2p.util.VersionComparator;
*
* Does NOT preserve port number, thread counts, etc. in the migration to 7/8.
* DOES preserve everything in the migration to 9.
*</p>
*
* @since Jetty 6
*/
@@ -67,57 +56,41 @@ abstract class MigrateJetty {
private static final String TEST_CLASS = "org.eclipse.jetty.server.Server";
private static final String BACKUP_SUFFIX = ".jetty6";
private static final String BACKUP_SUFFIX_8 = ".jetty8";
private static final String BACKUP_SUFFIX_9 = ".jetty9-id";
private static final String JETTY_TEMPLATE_DIR = "eepsite-jetty9";
private static final String JETTY_TEMPLATE_PKGDIR = "eepsite";
private static final String BASE_CONTEXT = "contexts/base-context.xml";
private static final String CGI_CONTEXT = "contexts/cgi-context.xml";
private static final String PROP_JETTY9_MIGRATED = "router.startup.jetty9.migrated";
private static final String PROP_JETTY9_MIGRATED_2 = "router.startup.jetty-ids.migrated";
/**
* For each entry in apps, if the main class is an old Jetty class,
* migrate it to the new Jetty class, and update the Jetty config files.
*/
public static void migrate(RouterContext ctx, List<ClientAppConfig> apps) {
if (ctx.getBooleanProperty(PROP_JETTY9_MIGRATED_2))
if (ctx.getBooleanProperty(PROP_JETTY9_MIGRATED))
return;
String installed = ctx.getProperty("router.firstVersion");
if (installed != null && VersionComparator.comp(installed, "2.9.0") >= 0) {
ctx.router().saveConfig(PROP_JETTY9_MIGRATED_2, "true");
if (installed != null && VersionComparator.comp(installed, "0.9.30") >= 0) {
ctx.router().saveConfig(PROP_JETTY9_MIGRATED, "true");
return;
}
boolean migrated1 = ctx.getBooleanProperty(PROP_JETTY9_MIGRATED);
if (!migrated1 && installed != null && VersionComparator.comp(installed, "0.9.30") >= 0) {
ctx.router().saveConfig(PROP_JETTY9_MIGRATED, "true");
migrated1 = true;
}
boolean migration2success = false;
boolean shouldSave = false;
boolean jetty9success = false;
for (int i = 0; i < apps.size(); i++) {
ClientAppConfig app = apps.get(i);
String client;
String backupSuffix;
if (migrated1) {
if (app.className.equals(NEW_CLASS)) {
client = "client application " + i + " [" + app.clientName +
"] to fix DTDs and duplicate ids";
backupSuffix = BACKUP_SUFFIX_9;
} else {
continue;
}
if (app.className.equals(NEW_CLASS)) {
client = "client application " + i + " [" + app.clientName +
"] from Jetty 7/8 to Jetty 9";
backupSuffix = BACKUP_SUFFIX_8;
} else if (app.className.equals(OLD_CLASS) || app.className.equals(OLD_CLASS_6)) {
client = "client application " + i + " [" + app.clientName +
"] from Jetty 5/6 " + app.className +
" to Jetty 9 " + NEW_CLASS;
backupSuffix = BACKUP_SUFFIX;
} else {
if (app.className.equals(NEW_CLASS)) {
client = "client application " + i + " [" + app.clientName +
"] from Jetty 7/8 to Jetty 9";
backupSuffix = BACKUP_SUFFIX_8;
} else if (app.className.equals(OLD_CLASS) || app.className.equals(OLD_CLASS_6)) {
client = "client application " + i + " [" + app.clientName +
"] from Jetty 5/6 " + app.className +
" to Jetty 9 " + NEW_CLASS;
backupSuffix = BACKUP_SUFFIX;
} else {
continue;
}
continue;
}
if (!hasLatestJetty()) {
System.err.println("WARNING: Jetty 7 unavailable, cannot migrate " + client);
@@ -129,62 +102,333 @@ abstract class MigrateJetty {
String args[] = LoadClientAppsJob.parseArgs(app.args);
if (args.length == 0)
continue;
if (!migrated1) {
// migration from 0.9.29 or earlier (2017-02-27) straight to 2.9.0 or later
System.err.println("WARNING: Unable to migrate " + client +
", delete client or uninstall and reinstall I2P");
app.disabled = true;
String xml = args[0];
File xmlFile = new File(xml);
if (!xmlFile.isAbsolute())
xmlFile = new File(ctx.getAppDir(), xml);
if (!xmlFile.exists()) {
System.err.println("WARNING: XML file " + xmlFile +
" not found, cannot migrate " + client);
continue;
}
File eepsite = xmlFile.getParentFile();
boolean ok = backupFile(xmlFile, backupSuffix);
if (!ok) {
System.err.println("WARNING: Failed to backup up XML file " + xmlFile +
", cannot migrate " + client);
continue;
}
if (app.className.equals(NEW_CLASS)) {
// Do the migration of 8 to 9, handle additional command-line xml files too
for (int j = 0; j < args.length; j++) {
if (j > 0) {
// probably jetty-ssl.xml
xmlFile = new File(args[j]);
ok = backupFile(xmlFile, backupSuffix);
if (!ok) {
System.err.println("WARNING: Failed to backup up XML file " + xmlFile +
", cannot migrate " + client);
continue;
}
}
boolean ok9 = migrateToJetty9(xmlFile);
if (ok9) {
System.err.println("WARNING: Migrated " + client + ".\n" +
"Check the " + xmlFile.getName() + " file in " + eepsite + ".\n" +
"Your old " + xmlFile.getName() + " file was backed up to " + xmlFile.getAbsolutePath() + BACKUP_SUFFIX_8);
jetty9success = true;
}
}
continue;
}
System.err.println("Migrating " + client);
// Below here is migration of 5/6 to 9
// migration 2 below here
// Note that JettyStart automatically copies and adds jetty-gzip.xml
// to the command line, not in the arg list here,
// but it does not contain anything we need to fix.
for (String xml : args) {
if (!xml.endsWith(".xml"))
continue;
File xmlFile = new File(xml);
if (!xmlFile.isAbsolute())
xmlFile = new File(ctx.getAppDir(), xml);
if (!xmlFile.exists()) {
System.err.println("WARNING: XML file " + xmlFile +
" not found, cannot migrate " + client);
continue;
}
boolean ok = backupFile(xmlFile, backupSuffix);
if (!ok) {
System.err.println("WARNING: Failed to backup up XML file " + xmlFile +
", cannot migrate " + client);
continue;
}
File tmpFile = new File(xmlFile + ".tmp");
try {
WorkingDir.migrateFileXML(xmlFile, tmpFile,
"<Ref id=", "<Ref refid=",
"/jetty/configure.dtd", "/jetty/configure_9_3.dtd");
ok = FileUtil.rename(tmpFile, xmlFile);
if (!ok)
throw new IOException();
} catch (IOException ioe) {
System.err.println("WARNING: Failed to migrate XML file " + xmlFile +
", cannot migrate " + client);
continue;
}
migration2success = true;
File baseEep = new File(ctx.getBaseDir(), JETTY_TEMPLATE_DIR);
// in packages, or perhaps on an uninstall/reinstall, the files are in eepsite/
if (!baseEep.exists())
baseEep = new File(ctx.getBaseDir(), JETTY_TEMPLATE_PKGDIR);
if (baseEep.equals(eepsite)) {
// non-split directory yet not an upgrade? shouldn't happen
System.err.println("Eepsite in non-split directory " + eepsite +
", cannot migrate " + client);
continue;
}
System.err.println("Migrated " + client);
// jetty.xml existed before in jetty 5 version, so check this new file
// and if it doesn't exist we can't continue
File baseContext = new File(baseEep, BASE_CONTEXT);
if (!baseContext.exists()) {
System.err.println("WARNING: Cannot find new XML file template " + baseContext +
", cannot migrate " + client);
continue;
}
String newPath = eepsite.getAbsolutePath() + File.separatorChar;
ok = WorkingDir.migrateJettyXml(baseEep, eepsite, "jetty.xml", "./eepsite/", newPath);
if (!ok) {
System.err.println("WARNING: Failed to modify XML file " + xmlFile +
", cannot migrate " + client);
continue;
}
// now we're committed, so don't check any more failure codes
backupAndMigrateFile(baseEep, eepsite, "jetty-ssl.xml", "./eepsite/", newPath);
(new File(eepsite, "contexts")).mkdir();
// ContextProvider scanner only looks for files ending in .xml so we can
// back up to the same directory
backupAndMigrateFile(baseEep, eepsite, BASE_CONTEXT, "./eepsite/", newPath);
backupAndMigrateFile(baseEep, eepsite, CGI_CONTEXT, "./eepsite/", newPath);
backupAndCopyFile(baseEep, eepsite, "jetty-rewrite.xml");
(new File(eepsite, "etc")).mkdir();
// realm.properties: No change from 6 to 7
File to = new File(eepsite, "etc/realm.properties");
if (!to.exists())
WorkingDir.copyFile(new File(baseEep, "etc/realm.properties"), to);
backupAndCopyFile(baseEep, eepsite, "etc/webdefault.xml");
app.className = NEW_CLASS;
shouldSave = true;
System.err.println("WARNING: Migrated " + client + '\n' +
"Check the following files in " + eepsite +
": jetty.xml, " + BASE_CONTEXT + ", and " + CGI_CONTEXT + "\n" +
"Your old jetty.xml was backed up." + '\n' +
"If you modified your jetty.xml to change ports, thread limits, etc, you MUST\n" +
"edit it to change them again. Your port was reset to 7658.");
}
if (migration2success)
ctx.router().saveConfig(PROP_JETTY9_MIGRATED_2, "true");
if (shouldSave) {
File cfgFile = ClientAppConfig.configFile(ctx);
boolean ok = backupFile(cfgFile);
if (ok) {
try {
ClientAppConfig.writeClientAppConfig(ctx, apps);
System.err.println("WARNING: Migrated clients config file " + cfgFile +
" from Jetty 5/6 " + OLD_CLASS + '/' + OLD_CLASS_6 +
" to Jetty 9 " + NEW_CLASS);
} catch (IOException ioe) {
ok = false;
}
}
if (!ok) {
System.err.println("WARNING: Failed to migrate clients config file " + cfgFile +
" from Jetty 5/6 " + OLD_CLASS + '/' + OLD_CLASS_6 +
" to Jetty 9 " + NEW_CLASS);
}
}
if (jetty9success)
ctx.router().saveConfig(PROP_JETTY9_MIGRATED, "true");
}
/**
* Migrate a jetty.xml file to Jetty 9.
* Unlike above, where we just migrate the new install file over for Jetty 9,
* here we modify the xml file in-place to preserve settings where possible.
*
* @return success
* @since Jetty 9
*/
private static boolean migrateToJetty9(File xmlFile) {
if (xmlFile.getName().equals("jetty-jmx.xml")) {
// This is lazy but nobody's using jmx, not worth the trouble
System.err.println("ERROR: Migration of " + xmlFile +
" file is not supported. Copy new file from $I2P/eepsite-jetty9/jetty-jmx.xml");
return false;
}
// we don't re-migrate from the template, we just add the
// necessary args for the QueuedThreadPool constructor in-place
// and fixup the renamed set call
boolean modified = false;
File eepsite = xmlFile.getParentFile();
File newFile = new File(eepsite, xmlFile.getName() + System.currentTimeMillis() + ".tmp");
FileInputStream in = null;
PrintWriter out = null;
try {
in = new FileInputStream(xmlFile);
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(newFile), "UTF-8")));
String s;
boolean foundQTP = false;
boolean foundSTP = false;
boolean foundETP = false;
boolean foundSCC = false;
boolean foundHC = false;
boolean foundSSCC = false;
while ((s = DataHelper.readLine(in)) != null) {
// readLine() doesn't strip \r
if (s.endsWith("\r"))
s = s.substring(0, s.length() - 1);
if (s.contains("Modified by I2P migration script for Jetty 9.") ||
s.contains("This configuration supports Jetty 9.") ||
s.contains("http://www.eclipse.org/jetty/configure_9_0.dtd")) {
if (!modified)
break;
// else we've modified it twice?
} else if (s.contains("org.eclipse.jetty.util.thread.QueuedThreadPool")) {
foundQTP = true;
} else if (foundQTP) {
if (!(s.contains("Modified by") || s.contains("<Arg type=\"int\">"))) {
out.println(" <!-- Modified by I2P migration script for Jetty 9. Do not remove this line -->");
out.println(" <Arg type=\"int\">20</Arg> <!-- maxThreads, overridden below -->");
out.println(" <Arg type=\"int\">3</Arg> <!-- minThreads, overridden below -->");
out.println(" <Arg type=\"int\">60000</Arg> <!-- maxIdleTimeMs, overridden below -->");
modified = true;
}
foundQTP = false;
}
if (s.contains("<Set name=\"maxIdleTimeMs\">")) {
// <Set name="maxIdleTimeMs">60000</Set>
s = s.replace("<Set name=\"maxIdleTimeMs\">", "<Set name=\"idleTimeout\">");
modified = true;
} else if (s.contains("<Set name=\"ThreadPool\">")) {
// <Set name="ThreadPool">, must be changed to constructor arg
out.println(" <!-- Modified by I2P migration script for Jetty 9. Do not remove this line -->");
s = s.replace("<Set name=\"ThreadPool\">", "<Arg>");
foundSTP = true;
modified = true;
} else if (foundSTP && !foundETP && s.contains("</Set>") && !s.contains("<Set")) {
// </Set> (close of <Set name="ThreadPool">)
// All the lines above have <Set>...</Set> on the same line, if they don't, this will break.
s = s.replace("</Set>", "</Arg>");
foundETP = true;
} else if (s.contains("org.eclipse.jetty.server.nio.SelectChannelConnector")) {
s = s.replace("org.eclipse.jetty.server.nio.SelectChannelConnector", "org.eclipse.jetty.server.ServerConnector");
out.println(" <!-- Modified by I2P migration script for Jetty 9. Do not remove this line -->");
out.println(s);
out.println(" <Arg><Ref id=\"Server\" /></Arg>");
out.println(" <Arg type=\"int\">1</Arg> <!-- number of acceptors -->");
out.println(" <Arg type=\"int\">0</Arg> <!-- default number of selectors -->");
out.println(" <Arg>");
out.println(" <Array type=\"org.eclipse.jetty.server.ConnectionFactory\"> <!-- varargs so we need an array -->");
out.println(" <Item>");
out.println(" <New class=\"org.eclipse.jetty.server.HttpConnectionFactory\">");
out.println(" <Arg>");
out.println(" <New class=\"org.eclipse.jetty.server.HttpConfiguration\">");
out.println(" <Set name=\"sendServerVersion\">false</Set>");
out.println(" <Set name=\"sendDateHeader\">true</Set>");
out.println(" </New>");
out.println(" </Arg>");
out.println(" </New>");
out.println(" </Item>");
out.println(" </Array>");
out.println(" </Arg>");
modified = true;
continue;
// SSL starts here
} else if (s.contains("org.eclipse.jetty.http.ssl.SslContextFactory")) {
s = s.replace("org.eclipse.jetty.http.ssl.SslContextFactory", "org.eclipse.jetty.util.ssl.SslContextFactory");
out.println(" <!-- Modified by I2P migration script for Jetty 9. Do not remove this line -->");
out.println(s);
// don't try to migrate from below, just generate a new list
out.println(" <Set name=\"ExcludeCipherSuites\">");
out.println(" <Array type=\"java.lang.String\">");
for (String ss : I2PSSLSocketFactory.EXCLUDE_CIPHERS) {
out.println(" <Item>" + ss + "</Item>");
}
out.println(" </Array>");
out.println(" </Set>");
out.println(" <Set name=\"ExcludeProtocols\">");
out.println(" <Array type=\"java.lang.String\">");
for (String ss : I2PSSLSocketFactory.EXCLUDE_PROTOCOLS) {
out.println(" <Item>" + ss + "</Item>");
}
out.println(" </Array>");
out.println(" </Set>");
modified = true;
continue;
} else if (s.contains("org.eclipse.jetty.server.ssl.SslSelectChannelConnector")) {
s = s.replace("org.eclipse.jetty.server.ssl.SslSelectChannelConnector", "org.eclipse.jetty.server.ServerConnector");
out.println(" <!-- Modified by I2P migration script for Jetty 9. Do not remove this line -->");
out.println(s);
out.println(" <Arg><Ref id=\"Server\" /></Arg>");
out.println(" <Arg type=\"int\">1</Arg> <!-- number of acceptors -->");
out.println(" <Arg type=\"int\">0</Arg> <!-- default number of selectors -->");
out.println(" <Arg>");
out.println(" <Array type=\"org.eclipse.jetty.server.ConnectionFactory\"> <!-- varargs so we need an array -->");
out.println(" <Item>");
out.println(" <New class=\"org.eclipse.jetty.server.SslConnectionFactory\">");
out.println(" <Arg><Ref id=\"sslContextFactory\" /></Arg>");
out.println(" <Arg>http/1.1</Arg>");
out.println(" </New>");
out.println(" </Item>");
out.println(" <Item>");
out.println(" <New class=\"org.eclipse.jetty.server.HttpConnectionFactory\">");
out.println(" <Arg>");
out.println(" <New class=\"org.eclipse.jetty.server.HttpConfiguration\">");
out.println(" <Set name=\"sendServerVersion\">false</Set>");
out.println(" <Set name=\"sendDateHeader\">true</Set>");
out.println(" </New>");
out.println(" </Arg>");
out.println(" </New>");
out.println(" </Item>");
out.println(" </Array>");
out.println(" </Arg>");
foundSSCC = true;
modified = true;
continue;
} else if (foundSSCC && s.contains("<Set name=\"ExcludeCipherSuites\">")) {
// delete the old ExcludeCipherSuites in this section
do {
s = DataHelper.readLine(in);
} while(s != null && !s.contains("</Set>"));
modified = true;
continue;
} else if (foundSSCC &&
s.contains("<Ref id=\"sslContextFactory\"")) {
// delete old one in this section, replaced above
modified = true;
continue;
} else if (s.contains("<Set name=\"KeyStore\">")) {
s = s.replace("<Set name=\"KeyStore\">", "<Set name=\"KeyStorePath\">");
modified = true;
} else if (s.contains("<Set name=\"TrustStore\">")) {
s = s.replace("<Set name=\"TrustStore\">", "<Set name=\"TrustStorePath\">");
modified = true;
// SSL ends here
} else if (s.contains("class=\"org.eclipse.jetty.deploy.providers.ContextProvider\">")) {
// WebAppProvider now also does what ContextProvider used to do
out.println(" <!-- Modified by I2P migration script for Jetty 9. Do not remove this line -->");
s = s.replace("class=\"org.eclipse.jetty.deploy.providers.ContextProvider\">", "class=\"org.eclipse.jetty.deploy.providers.WebAppProvider\">");
modified = true;
} else if (s.contains("<Set name=\"maxIdleTime\">")) {
s = s.replace("<Set name=\"maxIdleTime\">", "<Set name=\"idleTimeout\">");
modified = true;
} else if (s.contains("<Set name=\"gracefulShutdown\">")) {
s = s.replace("<Set name=\"gracefulShutdown\">", "<Set name=\"stopTimeout\">");
modified = true;
} else if (s.contains("org.eclipse.jetty.server.HttpConfiguration")) {
foundHC = true;
} else if (!foundHC &&
(s.contains("<Set name=\"sendServerVersion\">") ||
s.contains("<Set name=\"sendDateHeader\">"))) {
// old ones for Server, not in HTTPConfiguration section, delete
modified = true;
continue;
} else if (s.contains("<Set name=\"Acceptors\">") ||
s.contains("<Set name=\"acceptors\">") ||
s.contains("<Set name=\"statsOn\">") ||
s.contains("<Set name=\"confidentialPort\">") ||
s.contains("<Set name=\"lowResourcesConnections\">") ||
s.contains("<Set name=\"lowResourcesMaxIdleTime\">") ||
s.contains("<Set name=\"useDirectBuffers\">")) {
// delete
modified = true;
continue;
}
out.println(s);
}
} catch (IOException ioe) {
if (in != null) {
System.err.println("FAILED migration of " + xmlFile + ": " + ioe);
}
return false;
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
if (out != null) out.close();
}
if (modified) {
return FileUtil.rename(newFile, xmlFile);
} else {
newFile.delete();
return true;
}
}
/** do we have Jetty 7/8/9? */
private static boolean hasLatestJetty() {
if (!_wasChecked) {

View File

@@ -409,8 +409,6 @@ public class WorkingDir {
* 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.
*
* @return success
*/
static boolean migrateJettyXml(File olddir, File todir, String filename, String oldString, String newString) {
File oldFile = new File(olddir, filename);
@@ -419,32 +417,6 @@ public class WorkingDir {
File newFile = new File(todir, filename);
FileInputStream in = null;
PrintWriter out = null;
try {
migrateFileXML(oldFile, newFile, oldString, newString, null, null);
System.err.println("Copied " + oldFile + " with modifications");
return true;
} catch (IOException ioe) {
System.err.println("FAILED copy " + oldFile + ": " + ioe);
return false;
}
}
/**
* Copy over a XML file with modifications.
* Will overwrite any existing newFile.
*
* @param oldString to replace
* @param newString replacement
* @param oldString2 to replace, or null
* @param newString2 replacement, or null
* @throws IOException on all errors
* @since 0.9.66
*/
static void migrateFileXML(File oldFile, File newFile, String oldString, String newString,
String oldString2, String newString2) throws IOException {
FileInputStream in = null;
PrintWriter out = null;
try {
in = new FileInputStream(oldFile);
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(newFile), "UTF-8")));
@@ -456,13 +428,14 @@ public class WorkingDir {
if (s.indexOf(oldString) >= 0) {
s = s.replace(oldString, newString);
}
if (oldString2 != null && s.indexOf(oldString2) >= 0) {
s = s.replace(oldString2, newString2);
}
out.println(s);
}
out.println("<!-- Modified by I2P User dir migration script -->");
System.err.println("Copied " + oldFile + " with modifications");
return true;
} catch (IOException ioe) {
System.err.println("FAILED copy " + oldFile + ": " + ioe);
return false;
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
if (out != null) out.close();