/* This code is part of Freenet. It is distributed under the GNU General
* Public License, version 2 (or at your option any later version). See
* http://www.gnu.org/ for further details of the GPL. */
package freenet.keys;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import freenet.client.InsertException;
import freenet.client.InsertException.InsertExceptionMode;
import freenet.support.Base64;
import freenet.support.Fields;
import freenet.support.HexUtil;
import freenet.support.IllegalBase64Exception;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.URLDecoder;
import freenet.support.URLEncodedFormatException;
import freenet.support.URLEncoder;
import freenet.support.io.FileUtil;
/**
* Note that the metadata pairs below are not presently supported. They are supported
* by the old (0.5) code however.
*
* FreenetURI handles parsing and creation of the Freenet URI format, defined
* as follows:
* <p>
* <code>freenet:[KeyType@]RoutingKey,CryptoKey[,n1=v1,n2=v2,...][/docname][/metastring]</code>
* </p>
* <p>
* where KeyType is the TLA of the key (currently USK, SSK, KSK, or CHK). If
* omitted, KeyType defaults to KSK.
* BUT: CHKs don't support or require a docname.
* KSKs and SSKs do.
* Therefore CHKs go straight into metastrings.
* </p>
* <p>
* For KSKs, the string keyword (docname) takes the RoutingKey position and the
* remainder of the fields are inapplicable (except metastring). Examples:
* <coe>freenet:KSK@foo/bar freenet:KSK@test.html freenet:test.html</code>.
* </p>
* <p>
* RoutingKey is the modified Base64 encoded key value. CryptoKey is the
* modified Base64 encoded decryption key.
* </p>
* <p>
* Following the RoutingKey and CryptoKey there may be a series of <code>
* name=value</code> pairs representing URI meta-information.
* </p>
* <p>
* The docname is only meaningful for SSKs, and is hashed with the PK
* fingerprint to get the key value.
* </p>
* <p>
* The metastring is meant to be passed to the metadata processing systems that
* act on the retrieved document.
* </p>
* <p>
* When constructing a FreenetURI with a String argument, it is now legal for CHK keys
* to have a '.extension' tail, eg 'CHK@blahblahblah.html'. The constructor will simply
* chop off at the first dot.
* </p>
* REDFLAG: Old code has a FieldSet, and the ability to put arbitrary metadata
* in through name/value pairs. Do we want this?
*
* WARNING: Changing non-transient members on classes that are Serializable can result in
* restarting downloads or losing uploads.
*/
public class FreenetURI implements Cloneable, Comparable<FreenetURI>, Serializable {
/**
* For Serializable.
*/
private static transient final long serialVersionUID = 1L;
private static volatile boolean logMINOR;
private static volatile boolean logDEBUG;
static {
Logger.registerLogThresholdCallback(new LogThresholdCallback() {
@Override
public void shouldUpdate() {
logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this);
}
});
}
private final String keyType, docName;
/** The meta-strings, in the order they are given. Typically we will
* construct the base key from the key type, routing key, extra, and
* document name (SSK@blah,blah,blah/filename, CHK@blah,blah,blah,
* KSK@filename or USK@blah,blah,blah/filename/20), fetch it, discover
* that it is a manifest, and look up the first meta-string. If this is
* the final data, we use that (and complain if there are meta-strings
* left), else we look up the next meta-string in the manifest, and so
* on. This is executed by SingleFileFetcher. */
private final String[] metaStr;
/* for SSKs, routingKey is actually the pkHash. the actual routing key is
* calculated in NodeSSK and is a function of pkHash and the docName
*/
private final byte[] routingKey, cryptoKey, extra;
private final long suggestedEdition; // for USKs
private boolean hasHashCode;
private int hashCode;
// private final int uniqueHashCode;
static final String[] VALID_KEY_TYPES =
new String[]{"CHK", "SSK", "KSK", "USK"};
@Override
public synchronized int hashCode() {
if(hasHashCode)
return hashCode;
int x = keyType.hashCode();
if(docName != null)
x ^= docName.hashCode();
if(metaStr != null)
for(int i = 0; i < metaStr.length; i++)
x ^= metaStr[i].hashCode();
if(routingKey != null)
x ^= Fields.hashCode(routingKey);
if(cryptoKey != null)
x ^= Fields.hashCode(cryptoKey);
if(extra != null)
x ^= Fields.hashCode(extra);
if(keyType.equals("USK"))
x ^= suggestedEdition;
hashCode = x;
hasHashCode = true;
return x;
}
@Override
public boolean equals(Object o) {
if(o == this) return true;
if(!(o instanceof FreenetURI))
return false;
else {
FreenetURI f = (FreenetURI) o;
if(!keyType.equals(f.keyType))
return false;
if(keyType.equals("USK"))
if(!(suggestedEdition == f.suggestedEdition))
return false;
if((docName == null) ^ (f.docName == null))
return false;
if((metaStr == null || metaStr.length == 0) ^ (f.metaStr == null || f.metaStr.length == 0))
return false;
if((routingKey == null) ^ (f.routingKey == null))
return false;
if((cryptoKey == null) ^ (f.cryptoKey == null))
return false;
if((extra == null) ^ (f.extra == null))
return false;
if((docName != null) && !docName.equals(f.docName))
return false;
if((metaStr != null) && !Arrays.equals(metaStr, f.metaStr))
return false;
if((routingKey != null) && !Arrays.equals(routingKey, f.routingKey))
return false;
if((cryptoKey != null) && !Arrays.equals(cryptoKey, f.cryptoKey))
return false;
if((extra != null) && !Arrays.equals(extra, f.extra))
return false;
return true;
}
}
/** Is the keypair (the routing key and crypto key) the same as the
* given key?
* @return False if there is no routing key or no crypto key (CHKs,
* SSKs, USKs have them, KSKs don't), or if the keys don't have the
* same crypto key and routing key.
*/
public boolean equalsKeypair(FreenetURI u2) {
if((routingKey != null) && (cryptoKey != null))
return Arrays.equals(routingKey, u2.routingKey) && Arrays.equals(cryptoKey, u2.cryptoKey);
return false;
}
@Override
public final FreenetURI clone() {
return new FreenetURI(this);
}
public FreenetURI(FreenetURI uri) {
// this.uniqueHashCode = super.hashCode();
if(uri.keyType == null) throw new NullPointerException();
keyType = uri.keyType;
docName = uri.docName;
if(uri.metaStr != null) {
metaStr = uri.metaStr.clone();
} else metaStr = null;
if(uri.routingKey != null) {
routingKey = uri.routingKey.clone();
} else
routingKey = null;
if(uri.cryptoKey != null) {
cryptoKey = uri.cryptoKey.clone();
} else
cryptoKey = null;
if(uri.extra != null) {
extra = uri.extra.clone();
} else
extra = null;
this.suggestedEdition = uri.suggestedEdition;
if(logDEBUG) Logger.debug(this, "Copied: "+toString()+" from "+uri.toString(), new Exception("debug"));
}
boolean noCacheURI = false;
/** Optimize for memory. */
public FreenetURI intern() {
boolean changedAnything = false;
byte[] x = extra;
if(keyType.equals("CHK"))
x = ClientCHK.internExtra(x);
else
x = ClientSSK.internExtra(x);
if(x != extra) changedAnything = true;
String[] newMetaStr = null;
if(metaStr != null) {
newMetaStr = new String[metaStr.length];
for(int i=0;i<metaStr.length;i++) {
newMetaStr[i] = metaStr[i].intern();
if(metaStr[i] != newMetaStr[i]) changedAnything = true;
}
}
String dn = docName == null ? null : docName.intern();
if(dn != docName) changedAnything = true;
if(!changedAnything) {
noCacheURI = true;
return this;
}
FreenetURI u = new FreenetURI(keyType, dn, newMetaStr, routingKey, cryptoKey, extra);
u.noCacheURI = true;
return u;
}
public FreenetURI(String keyType, String docName) {
this(keyType, docName, (String[]) null, null, null, null);
}
public static final FreenetURI EMPTY_CHK_URI = new FreenetURI("CHK", null, null, null, null, null);
public FreenetURI(
String keyType,
String docName,
byte[] routingKey,
byte[] cryptoKey, byte[] extra2) {
this(keyType, docName, (String[]) null, routingKey, cryptoKey, extra2);
}
public FreenetURI(
String keyType,
String docName,
String metaStr,
byte[] routingKey,
byte[] cryptoKey) {
this(
keyType,
docName,
(metaStr == null ? (String[]) null : new String[]{metaStr}),
routingKey,
cryptoKey,
null);
}
public FreenetURI(
String keyType,
String docName,
String[] metaStr,
byte[] routingKey,
byte[] cryptoKey, byte[] extra2) {
// this.uniqueHashCode = super.hashCode();
this.keyType = keyType.trim().toUpperCase().intern();
this.docName = docName;
this.metaStr = metaStr;
this.routingKey = routingKey;
if(routingKey != null && keyType.equals("CHK") && routingKey.length != 32)
throw new IllegalArgumentException("Bad URI: Routing key should be 32 bytes");
this.cryptoKey = cryptoKey;
if(cryptoKey != null && cryptoKey.length != 32)
throw new IllegalArgumentException("Bad URI: Crypto key should be 32 bytes");
this.extra = extra2;
this.suggestedEdition = -1;
if (logDEBUG) Logger.minor(this, "Created from components: "+toString(), new Exception("debug"));
}
public FreenetURI(
String keyType,
String docName,
String[] metaStr,
byte[] routingKey,
byte[] cryptoKey, byte[] extra2,
long suggestedEdition) {
// this.uniqueHashCode = super.hashCode();
this.keyType = keyType.trim().toUpperCase().intern();
this.docName = docName;
this.metaStr = metaStr;
this.routingKey = routingKey;
if(routingKey != null && keyType.equals("CHK") && routingKey.length != 32)
throw new IllegalArgumentException("Bad URI: Routing key should be 32 bytes");
this.cryptoKey = cryptoKey;
if(cryptoKey != null && cryptoKey.length != 32)
throw new IllegalArgumentException("Bad URI: Crypto key should be 32 bytes");
this.extra = extra2;
this.suggestedEdition = suggestedEdition;
if (logDEBUG) Logger.minor(this, "Created from components (B): "+toString(), new Exception("debug"));
}
// Strip http:// and freenet: prefix
protected final static Pattern URI_PREFIX = Pattern.compile("^(http://[^/]+/+)?(freenet:)?");
public FreenetURI(String URI) throws MalformedURLException {
this(URI, false);
}
/**
* Create a FreenetURI from its string form. May or may not have a
* freenet: prefix.
* @throws MalformedURLException If the string could not be parsed.
*/
public FreenetURI(String URI, boolean noTrim) throws MalformedURLException {
// this.uniqueHashCode = super.hashCode();
if(URI == null)
throw new MalformedURLException("No URI specified");
if(!noTrim)
URI = URI.trim();
// Strip ?max-size, ?type etc.
// Un-encoded ?'s are illegal.
int x = URI.indexOf('?');
if(x > -1)
URI = URI.substring(0, x);
if(URI.indexOf('@') < 0 || URI.indexOf('/') < 0)
// Encoded URL?
try {
URI = URLDecoder.decode(URI, false);
} catch(URLEncodedFormatException e) {
throw new MalformedURLException("Invalid URI: no @ or /, or @ or / is escaped but there are invalid escapes");
}
URI = URI_PREFIX.matcher(URI).replaceFirst("");
// decode keyType
int atchar = URI.indexOf('@');
if(atchar == -1)
throw new MalformedURLException("There is no @ in that URI! (" + URI + ')');
String _keyType = URI.substring(0, atchar).toUpperCase();
URI = URI.substring(atchar + 1);
boolean validKeyType = false;
for(int i = 0; i < VALID_KEY_TYPES.length; i++) {
if (_keyType.equals(VALID_KEY_TYPES[i])) {
validKeyType = true;
_keyType = VALID_KEY_TYPES[i];
break;
}
}
keyType = _keyType;
if(!validKeyType)
throw new MalformedURLException("Invalid key type: " + keyType);
boolean isSSK = "SSK".equals(keyType);
boolean isUSK = "USK".equals(keyType);
boolean isKSK = "KSK".equals(keyType);
// decode metaString
ArrayList<String> sv = null;
int slash2;
sv = new ArrayList<String>();
if (isKSK) URI = "/" + URI; // ensure that KSK docNames are decoded
while ((slash2 = URI.lastIndexOf('/')) != -1) {
String s;
try {
s = URLDecoder.decode(URI.substring(slash2 + 1 /* "/".length() */), true);
} catch(URLEncodedFormatException e) {
throw (MalformedURLException)new MalformedURLException(e.toString()).initCause(e);
}
if(s != null)
sv.add(s);
URI = URI.substring(0, slash2);
}
// sv is *backwards*
// this makes for more efficient handling
// SSK@ = create a random SSK
if(sv.isEmpty() && (isUSK || isKSK))
throw new MalformedURLException("No docname for " + keyType);
if((isSSK || isUSK || isKSK) && !sv.isEmpty()) {
docName = sv.remove(sv.size() - 1);
if(isUSK) {
if(sv.isEmpty())
throw new MalformedURLException("No suggested edition number for USK");
try {
suggestedEdition = Long.parseLong(sv.remove(sv.size() - 1));
} catch(NumberFormatException e) {
throw (MalformedURLException)new MalformedURLException("Invalid suggested edition: " + e).initCause(e);
}
} else
suggestedEdition = -1;
} else {
// docName not necessary, nor is it supported, for CHKs.
docName = null;
suggestedEdition = -1;
}
if(!sv.isEmpty()) {
metaStr = new String[sv.size()];
for(int i = 0; i < metaStr.length; i++) {
metaStr[i] = sv.get(metaStr.length - 1 - i).intern();
if(metaStr[i] == null)
throw new NullPointerException();
}
} else
metaStr = null;
if(isKSK) {
routingKey = extra = cryptoKey = null;
return;
}
// strip 'file extensions' from CHKs
// added by aum (david@rebirthing.co.nz)
if("CHK".equals(keyType)) {
int idx = URI.lastIndexOf('.');
if(idx != -1)
URI = URI.substring(0, idx);
}
// URI now contains: routingKey[,cryptoKey][,metaInfo]
StringTokenizer st = new StringTokenizer(URI, ",");
try {
if(st.hasMoreTokens()) {
routingKey = Base64.decode(st.nextToken());
if(routingKey.length != 32 && keyType.equals("CHK"))
throw new MalformedURLException("Bad URI: Routing key should be 32 bytes long");
} else {
routingKey = cryptoKey = extra = null;
return;
}
if(!st.hasMoreTokens()) {
cryptoKey = extra = null;
return;
}
// Can be cryptokey or name-value pair.
String t = st.nextToken();
cryptoKey = Base64.decode(t);
if(cryptoKey.length != 32)
throw new MalformedURLException("Bad URI: Routing key should be 32 bytes long");
if(!st.hasMoreTokens()) {
extra = null;
return;
}
extra = Base64.decode(st.nextToken());
} catch(IllegalBase64Exception e) {
throw new MalformedURLException("Invalid Base64 quantity: " + e);
}
if (logDEBUG) Logger.debug(this, "Created from parse: "+toString()+" from "+URI, new Exception("debug"));
}
/** USK constructor from components. */
public FreenetURI(byte[] pubKeyHash, byte[] cryptoKey, byte[] extra, String siteName, long suggestedEdition2) {
// this.uniqueHashCode = super.hashCode();
this.keyType = "USK";
this.routingKey = pubKeyHash;
// Don't check routingKey as it could be an insertable USK
this.cryptoKey = cryptoKey;
if(cryptoKey != null && cryptoKey.length != 32)
throw new IllegalArgumentException("Bad URI: Crypto key should be 32 bytes");
this.extra = extra;
this.docName = siteName;
this.suggestedEdition = suggestedEdition2;
metaStr = null;
if (logDEBUG) Logger.minor(this, "Created from components (USK): "+toString(), new Exception("debug"));
}
protected FreenetURI() {
// For serialization only!
this.metaStr = null;
this.keyType = null;
this.routingKey = null;
this.cryptoKey = null;
this.extra = null;
this.docName = null;
this.suggestedEdition = 0;
}
/** Dump the individual components of the key to System.out. */
public void decompose() {
String r = routingKey == null ? "none" : HexUtil.bytesToHex(routingKey);
String k = cryptoKey == null ? "none" : HexUtil.bytesToHex(cryptoKey);
String e = extra == null ? "none" : HexUtil.bytesToHex(extra);
System.out.println("FreenetURI" + this);
System.out.println("Key type : " + keyType);
System.out.println("Routing key: " + r);
System.out.println("Crypto key : " + k);
System.out.println("Extra : " + e);
System.out.println(
"Doc name : " + (docName == null ? "none" : docName));
System.out.print("Meta strings: ");
if(metaStr == null)
System.out.println("none");
else
System.out.println(Arrays.asList(metaStr).toString());
}
public String getGuessableKey() {
return getDocName();
}
/** Get the document name. For a KSK this is everything from the @ to
* the first slash or the end of the key. For an SSK this is everything
* from the slash to the next slash or the end of the key. CHKs don't
* have a doc name, they only have meta strings. */
public String getDocName() {
return docName;
}
/** Get the first meta-string. This is just after the main part of the
* key and the doc name. Meta-strings are directory (manifest) lookups
* delimited by /'es after the main key and the doc name if any.
*/
public String getMetaString() {
return ((metaStr == null) || (metaStr.length == 0)) ? null : metaStr[0];
}
/** Get the last meta string. Meta-strings are directory (manifest)
* lookups after the main key and the doc name if any. So the last meta
* string, if there is one, is from the last / to the end of the uri
* i.e. usually the filename. */
public String lastMetaString() {
return ((metaStr == null) || (metaStr.length == 0)) ? null : metaStr[metaStr.length - 1];
}
/** Get all the meta strings. Meta strings are directory (manifest)
* lookups after the main key and the doc name if any. Examples:
*
* CHK@blah,blah,blah/filename
*
* This has a routing key, a crypto key, extra bytes, no document name,
* and one meta string "filename"
*
* SSK@blah,blah,blah/docname/dir/subdir/filename
*
* This has a routing key, a crypto key, extra bytes, a document name,
* and three meta strings "dir", "subdir" and "filename". The SSK
* including the docname is turned into a low level Freenet key, which
* we fetch. This will produce a metadata document containing a
* manifest, within which we look up "dir". This either gives us
* another metadata document directly, or a redirect if the dir is
* inserted separately. And so on. If it's a container, the files will
* be stored, with the metadata, in the container (tar.bz2 or
* whatever); the metadata fetched by SSK@blah,blah,blah/docname will
* say that there is a container and explain how to fetch it.
*
* KSK@gpl.txt
*
* This has no routing key, no crypto key, and no meta strings (but
* KSKs *can* have meta strings), but it has a document name.
*/
public String[] getAllMetaStrings() {
return metaStr;
}
/** Are there any meta-strings? */
public boolean hasMetaStrings() {
return !(metaStr == null || metaStr.length == 0);
}
/** Get the routing key. This is the first part of the key after the @
* for CHKs, SSKs and USKs. For purposes of FreenetURI, KSKs do not
* have a routing key. For CHKs, this is ultimately derived from the
* hash of the encrypted data; for SSKs it is the hash of the public
* key.
*/
public byte[] getRoutingKey() {
return routingKey;
}
/** Get the crypto key. This is the second part of the key after the @
* for CHKs, SSKs and USKs. For purposes of FreenetURI, KSKs do not
* have a crypto key. For CHKs, this is derived from the hash of the
* *original* plaintext data; for SSKs it is a separate key for
* decryption. The crypto key is kept on the requesting node and is not
* sent over the network - but of course many freesites and other
* documents on the network include URIs which do include crypto keys.
*/
public byte[] getCryptoKey() {
return cryptoKey;
}
/** Get the key type. CHK, SSK, KSK or USK. Upper case, we normally
* use the constants. */
public String getKeyType() {
return keyType;
}
/**
* Returns a copy of this URI with the first meta string removed.
*/
public FreenetURI popMetaString() {
String[] newMetaStr = null;
if (metaStr != null) {
final int metaStrLength = metaStr.length;
if (metaStrLength > 1) {
newMetaStr = Arrays.copyOf(metaStr, metaStr.length-1);
}
}
return setMetaString(newMetaStr);
}
/** Create a new URI with the last few meta-strings dropped.
* @param i The number of meta-strings to drop.
* @return A new FreenetURI with the specified number of meta-strings
* removed from the end.
*/
public FreenetURI dropLastMetaStrings(int i) {
String[] newMetaStr = null;
if((metaStr != null) && (metaStr.length > i)) {
newMetaStr = Arrays.copyOf(metaStr, metaStr.length - i);
}
return setMetaString(newMetaStr);
}
/**
* Returns a copy of this URI with the given string appended as a
* meta-string.
*/
public FreenetURI pushMetaString(String name) {
String[] newMetaStr;
if(name == null)
throw new NullPointerException();
if(metaStr == null)
newMetaStr = new String[]{name};
else {
newMetaStr = Arrays.copyOf(metaStr, metaStr.length + 1);
newMetaStr[metaStr.length] = name.intern();
}
return setMetaString(newMetaStr);
}
/**
* Returns a copy of this URI with these meta strings appended.
*/
public FreenetURI addMetaStrings(String[] strs) {
if(strs == null)
return this; // legal noop, since getMetaStrings can return null
for(int i = 0; i < strs.length; i++)
if(strs[i] == null)
throw new NullPointerException("element " + i + " of " + strs.length + " is null");
String[] newMetaStr;
if(metaStr == null)
return setMetaString(strs);
else {
newMetaStr = Arrays.copyOf(metaStr, metaStr.length + strs.length);
System.arraycopy(strs, 0, newMetaStr, metaStr.length, strs.length);
return setMetaString(newMetaStr);
}
}
/**
* Returns a copy of this URI with these meta strings appended.
*/
public FreenetURI addMetaStrings(List<String> metaStrings) {
return addMetaStrings(metaStrings.toArray(new String[metaStrings.size()]));
}
/**
* Returns a copy of this URI with a new Document name set.
*/
public FreenetURI setDocName(String name) {
return new FreenetURI(
keyType,
name,
metaStr,
routingKey,
cryptoKey,
extra,
suggestedEdition);
}
/** Returns a copy of this URI with new meta-strings. */
public FreenetURI setMetaString(String[] newMetaStr) {
return new FreenetURI(
keyType,
docName,
newMetaStr,
routingKey,
cryptoKey,
extra,
suggestedEdition);
}
protected String toStringCache;
/** toString() is equivalent to toString(false, false) but is cached. */
@Override
public String toString() {
if (toStringCache == null)
toStringCache = toString(false, false)/* + "#"+super.toString()+"#"+uniqueHashCode*/;
return toStringCache;
}
/**
* @deprecated Use {@link #toASCIIString()} instead
*/
@Deprecated
public String toACIIString() {
return toASCIIString();
}
/**
* Get the FreenetURI as a pure ASCII string, any non-english
* characters as well as any dangerous characters are encoded.
* @return
*/
public String toASCIIString() {
return toString(true, true);
}
/**
* Get the FreenetURI as a string.
* @param prefix Whether to include the freenet: prefix.
* @param pureAscii If true, encode any non-english characters. If
* false, only encode dangerous characters (slashes e.g.).
*/
public String toString(boolean prefix, boolean pureAscii) {
if(keyType == null) {
// Not activated or something...
if(logMINOR) Logger.minor(this, "Not activated?? in toString("+prefix+","+pureAscii+")");
return null;
}
StringBuilder b;
if(prefix)
b = new StringBuilder("freenet:");
else
b = new StringBuilder();
b.append(keyType).append('@');
if(!"KSK".equals(keyType)) {
if(routingKey != null)
b.append(Base64.encode(routingKey));
if(cryptoKey != null)
b.append(',').append(Base64.encode(cryptoKey));
if(extra != null)
b.append(',').append(Base64.encode(extra));
if(docName != null)
b.append('/');
}
if(docName != null)
b.append(URLEncoder.encode(docName, "/", pureAscii));
if(keyType.equals("USK")) {
b.append('/');
b.append(suggestedEdition);
}
if(metaStr != null)
for(int i = 0; i < metaStr.length; i++) {
b.append('/').append(URLEncoder.encode(metaStr[i], "/", pureAscii));
}
return b.toString();
}
/** Encode to a user-friendly, incomplete string with ... replacing some of
* the base64. Allow spaces, foreign chars etc. */
public String toShortString() {
StringBuilder b = new StringBuilder();
b.append(keyType).append('@');
if(!"KSK".equals(keyType)) {
b.append("...");
if(docName != null)
b.append('/');
}
if(docName != null)
b.append(URLEncoder.encode(docName, "/", false, " "));
if(keyType.equals("USK")) {
b.append('/');
b.append(suggestedEdition);
}
if(metaStr != null)
for(int i = 0; i < metaStr.length; i++) {
b.append('/').append(URLEncoder.encode(metaStr[i], "/", false, " "));
}
return b.toString();
}
/** Run this class to decompose the argument. */
public static void main(String[] args) throws Exception {
(new FreenetURI(args[0])).decompose();
}
/** Get the extra bytes. SSKs and CHKs have extra bytes, these come
* after the second comma, and specify encryption and hashing
* algorithms etc.
*/
public byte[] getExtra() {
return extra;
}
/** Get the meta strings as an ArrayList. */
public ArrayList<String> listMetaStrings() {
if(metaStr != null) {
ArrayList<String> l = new ArrayList<String>(metaStr.length);
for(int i = 0; i < metaStr.length; i++)
l.add(metaStr[i]);
return l;
} else return new ArrayList<String>(0);
}
static final byte CHK = 1;
static final byte SSK = 2;
static final byte KSK = 3;
static final byte USK = 4;
/** Read the binary form of a key, preceded by a short for its length. */
public static FreenetURI readFullBinaryKeyWithLength(DataInputStream dis) throws IOException {
int len = dis.readShort();
byte[] buf = new byte[len];
dis.readFully(buf);
if(logMINOR) Logger.minor(FreenetURI.class, "Read " + len + " bytes for key");
return fromFullBinaryKey(buf);
}
/** Create a FreenetURI from the binary form of the key. */
public static FreenetURI fromFullBinaryKey(byte[] buf) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(buf);
DataInputStream dis = new DataInputStream(bais);
return readFullBinaryKey(dis);
}
/** Create a FreenetURI from the binary form of the key, read from a
* stream, with no length.
* @throws MalformedURLException If there was a format error in the data.
* @throws IOException If a read error occurred */
public static FreenetURI readFullBinaryKey(DataInputStream dis) throws IOException {
byte type = dis.readByte();
String keyType;
if(type == CHK)
keyType = "CHK";
else if(type == SSK)
keyType = "SSK";
else if(type == KSK)
keyType = "KSK";
else
throw new MalformedURLException("Unrecognized type " + type);
byte[] routingKey = null;
byte[] cryptoKey = null;
byte[] extra = null;
if((type == CHK) || (type == SSK)) {
// routingKey is a hash, so is exactly 32 bytes
routingKey = new byte[32];
dis.readFully(routingKey);
// cryptoKey is a 256 bit AES key, so likewise
cryptoKey = new byte[32];
dis.readFully(cryptoKey);
// Number of bytes of extra depends on key type
int extraLen;
extraLen = (type == CHK ? ClientCHK.EXTRA_LENGTH : ClientSSK.EXTRA_LENGTH);
extra = new byte[extraLen];
dis.readFully(extra);
}
String docName = null;
if(type != CHK)
docName = dis.readUTF();
int count = dis.readInt();
String[] metaStrings = new String[count];
for(int i = 0; i < metaStrings.length; i++)
metaStrings[i] = dis.readUTF();
return new FreenetURI(keyType, docName, metaStrings, routingKey, cryptoKey, extra);
}
/** Write either a null or a FreenetURI. */
public static void writeFullBinaryKeyWithLength(FreenetURI uri, DataOutputStream dos)
throws IOException {
if(uri == null)
dos.writeShort((short)0);
else
uri.writeFullBinaryKeyWithLength(dos);
}
/**
* Write a binary representation of this URI, with a short length, so it can be passed over if necessary.
* @param dos The stream to write to.
* @throws MalformedURLException If the key could not be written because of inconsistencies or other
* problems in the key itself.
* @throws IOException If an error occurred while writing the key.
*/
public void writeFullBinaryKeyWithLength(DataOutputStream dos) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream ndos = new DataOutputStream(baos);
writeFullBinaryKey(ndos);
ndos.close();
byte[] data = baos.toByteArray();
if(data.length > Short.MAX_VALUE)
throw new MalformedURLException("Full key too long: " + data.length + " - " + this);
dos.writeShort((short) data.length);
if(logMINOR)
Logger.minor(this, "Written " + data.length + " bytes");
dos.write(data);
}
/**
* Write a binary representation of this URI.
* @param dos The stream to write to.
* @throws MalformedURLException If the key could not be written because of inconsistencies or other
* problems in the key itself.
* @throws IOException If an error occurred while writing the key.
*/
private void writeFullBinaryKey(DataOutputStream dos) throws IOException {
if(keyType.equals("CHK"))
dos.writeByte(CHK);
else if(keyType.equals("SSK"))
dos.writeByte(SSK);
else if(keyType.equals("KSK"))
dos.writeByte(KSK);
else if(keyType.equals("USK"))
throw new MalformedURLException("Cannot write USKs as binary keys");
else
throw new MalformedURLException("Cannot write key of type " + keyType + " - do not know how");
if(!keyType.equals("KSK")) {
if(routingKey.length != 32)
throw new MalformedURLException("Routing key must be of length 32");
dos.write(routingKey);
if(cryptoKey.length != 32)
throw new MalformedURLException("Crypto key must be of length 32");
dos.write(cryptoKey);
if(keyType.equals("CHK") && (extra.length != ClientCHK.EXTRA_LENGTH))
throw new MalformedURLException("Wrong number of extra bytes for CHK");
if(keyType.equals("SSK") && (extra.length != ClientSSK.EXTRA_LENGTH))
throw new MalformedURLException("Wrong number of extra bytes for SSK");
dos.write(extra);
}
if(!keyType.equals("CHK"))
dos.writeUTF(docName);
if(metaStr != null) {
dos.writeInt(metaStr.length);
for(int i = 0; i < metaStr.length; i++)
dos.writeUTF(metaStr[i]);
} else
dos.writeInt(0);
}
/** Get suggested edition. Only valid for USKs. */
public long getSuggestedEdition() {
if(keyType.equals("USK"))
return suggestedEdition;
else
throw new IllegalArgumentException("Not a USK requesting suggested edition");
}
/** Generate a suggested filename for the URI. This may be constructed
* from more than one part of the URI e.g. SSK@blah,blah,blah/sitename/
* might return sitename. The returned string will already have been
* through FileUtil.sanitize(). */
public String getPreferredFilename() {
if (logMINOR)
Logger.minor(this, "Getting preferred filename for " + this);
ArrayList<String> names = new ArrayList<String>();
if(keyType != null && (keyType.equals("KSK") || keyType.equals("SSK") || keyType.equals("USK"))) {
if(logMINOR)
Logger.minor(this, "Adding docName: " + docName);
if(docName != null) {
names.add(docName);
if(keyType.equals("USK"))
names.add(Long.toString(suggestedEdition));
} else if(!keyType.equals("SSK")) {
// "SSK@" is legal for an upload.
throw new IllegalStateException("No docName for key of type "+keyType);
}
}
if(metaStr != null)
for(String s : metaStr) {
if(s == null || s.equals("")) {
if(logMINOR)
Logger.minor(this, "metaString \"" + s + "\": was null or empty");
continue;
}
if(logMINOR)
Logger.minor(this, "Adding metaString \"" + s + "\"");
names.add(s);
}
StringBuilder out = new StringBuilder();
for(int i = 0; i < names.size(); i++) {
String s = names.get(i);
if(logMINOR)
Logger.minor(this, "name " + i + " = " + s);
s = FileUtil.sanitize(s);
if(logMINOR)
Logger.minor(this, "Sanitized name " + i + " = " + s);
if(s.length() > 0) {
if(out.length() > 0)
out.append('-');
out.append(s);
}
}
if(logMINOR)
Logger.minor(this, "out = " + out.toString());
if(out.length() == 0) {
if(routingKey != null) {
if(logMINOR)
Logger.minor(this, "Returning base64 encoded routing key");
return Base64.encode(routingKey);
}
// FIXME return null in this case, localise in a wrapper.
return "unknown";
}
assert out.toString().equals(FileUtil.sanitize(out.toString())) : ("Not sanitized? \""+out.toString()+"\" -> \""+FileUtil.sanitize(out.toString()))+"\"";
return out.toString();
}
/** Returns a <b>new</b> FreenetURI with a new suggested edition number.
* Note that the suggested edition number is only valid for USKs. */
public FreenetURI setSuggestedEdition(long newEdition) {
return new FreenetURI(
keyType,
docName,
metaStr,
routingKey,
cryptoKey,
extra,
newEdition);
}
/** Returns a <b>new</b> FreenetURI with a new key type. Usually this
* will be invalid!
*/
public FreenetURI setKeyType(String newKeyType) {
return new FreenetURI(
newKeyType,
docName,
metaStr,
routingKey,
cryptoKey,
extra,
suggestedEdition);
}
/** Returns a <b>new</b> FreenetURI with a new routing key. KSKs do not
* have a routing key. */
public FreenetURI setRoutingKey(byte[] newRoutingKey) {
return new FreenetURI(
keyType,
docName,
metaStr,
newRoutingKey,
cryptoKey,
extra,
suggestedEdition);
}
/** Throw an InsertException if we have any meta-strings. They are not
* valid for inserts, you must insert a directory to create a directory
* structure. */
public void checkInsertURI() throws InsertException {
if(metaStr != null && metaStr.length > 0)
throw new InsertException(InsertExceptionMode.META_STRINGS_NOT_SUPPORTED, this);
}
/** Throw an InsertException if the argument has any meta-strings. They
* are not valid for inserts, you must insert a directory to create a
* directory structure. */
public static void checkInsertURI(FreenetURI uri) throws InsertException {
uri.checkInsertURI();
}
/** Convert to a relative URI in the form of a URI (/KSK@gpl.txt etc). */
public URI toRelativeURI() throws URISyntaxException {
// Single-argument constructor used because it preserves encoded /'es in path.
// Hence we can have slashes, question marks etc in the path, but they are encoded.
return new URI('/' + toString(false, false));
}
/** Convert to a relative URI in the form of a URI, with the base path
* not necessarily /. */
public URI toURI(String basePath) throws URISyntaxException {
return new URI(basePath + toString(false, false));
}
/** Is this key an SSK? */
public boolean isSSK() {
return "SSK".equals(keyType);
}
/** Is this key a USK? */
public boolean isUSK() {
return "USK".equals(keyType);
}
/** Is this key a CHK? */
public boolean isCHK() {
return "CHK".equals(keyType);
}
/** Is this key a KSK? */
public boolean isKSK() {
return "KSK".equals(keyType);
}
/** Convert a USK into an SSK by appending "-" and the suggested edition
* to the document name and changing the key type. */
public FreenetURI sskForUSK() {
if(!keyType.equalsIgnoreCase("USK")) throw new IllegalStateException();
return new FreenetURI("SSK", docName+"-"+suggestedEdition, metaStr, routingKey, cryptoKey, extra, 0);
}
private static final Pattern docNameWithEditionPattern;
static {
docNameWithEditionPattern = Pattern.compile(".*\\-([0-9]+)");
}
/** Could this SSK be the result of sskForUSK()? */
public boolean isSSKForUSK() {
return keyType.equalsIgnoreCase("SSK") && docName != null && docNameWithEditionPattern.matcher(docName).matches();
}
/** Convert an SSK into a USK, if possible. */
public FreenetURI uskForSSK() {
if(!keyType.equalsIgnoreCase("SSK")) throw new IllegalStateException();
Matcher matcher = docNameWithEditionPattern.matcher(docName);
if (!matcher.matches())
throw new IllegalStateException();
int offset = matcher.start(1) - 1;
String siteName = docName.substring(0, offset);
long edition = Long.parseLong(docName.substring(offset + 1, docName.length()));
return new FreenetURI("USK", siteName, metaStr, routingKey, cryptoKey, extra, edition);
}
/**
* Get the edition number, if the key is a USK or a USK converted to an
* SSK.
*/
public long getEdition() {
if(keyType.equalsIgnoreCase("USK"))
return suggestedEdition;
else if(keyType.equalsIgnoreCase("SSK")) {
if(docName == null)
throw new IllegalStateException();
Matcher matcher = docNameWithEditionPattern.matcher(docName);
if (!matcher.matches()) /* Taken from uskForSSK, also modify there if necessary; TODO just use isSSKForUSK() here?! */
throw new IllegalStateException();
return Long.parseLong(docName.substring(matcher.start(1), docName.length()));
} else
throw new IllegalStateException();
}
@Override
/** This looks expensive, but 99% of the time it will quit out pretty
* early on: Either a different key type or a different routing key. The
* worst case cost is relatively bad though. Unfortunately we can't use
* a HashMap if an attacker might be able to influence the keys and
* create a hash collision DoS, so we *do* need this. */
public int compareTo(FreenetURI o) {
if(this == o) return 0;
int cmp = keyType.compareTo(o.keyType);
if(cmp != 0) return cmp;
if(routingKey != null) {
// Same type will have same routingKey != null
cmp = Fields.compareBytes(routingKey, o.routingKey);
if(cmp != 0) return cmp;
}
if(cryptoKey != null) {
// Same type will have same cryptoKey != null
cmp = Fields.compareBytes(cryptoKey, o.cryptoKey);
if(cmp != 0) return cmp;
}
if(docName == null && o.docName != null) return -1;
if(docName != null && o.docName == null) return 1;
if(docName != null && o.docName != null) {
cmp = docName.compareTo(o.docName);
if(cmp != 0) return cmp;
}
if(extra != null) {
// Same type will have same cryptoKey != null
cmp = Fields.compareBytes(extra, o.extra);
if(cmp != 0) return cmp;
}
if(metaStr != null && o.metaStr == null) return 1;
if(metaStr == null && o.metaStr != null) return -1;
if(metaStr != null && o.metaStr != null) {
if(metaStr.length > o.metaStr.length) return 1;
if(metaStr.length < o.metaStr.length) return -1;
for(int i=0;i<metaStr.length;i++) {
cmp = metaStr[i].compareTo(o.metaStr[i]);
if(cmp != 0) return cmp;
}
}
if(suggestedEdition > o.suggestedEdition) return 1;
if(suggestedEdition < o.suggestedEdition) return -1;
return 0;
}
/**
* If this object is a USK/SSK insert URI, this function computes the request URI which belongs to it.
* If it is a CHK/KSK, the original URI is returned as CHK/KSK do not have a private insert URI, they are their own "insert URI".
*
* If you want to give people access to content at an URI, you should always publish only the request URI.
* Never give away the insert URI, this allows anyone to insert under your URI!
*
* @return The request URI which belongs to this insert URI.
* @throws MalformedURLException If this object is a USK/SSK request URI already. NOT thrown for CHK/KSK URIs!
*/
public FreenetURI deriveRequestURIFromInsertURI() throws MalformedURLException {
final FreenetURI originalURI = this;
if(originalURI.isCHK()) {
return originalURI;
} else if(originalURI.isSSK() || originalURI.isUSK()) {
FreenetURI newURI = originalURI;
if(originalURI.isUSK())
newURI = newURI.sskForUSK();
InsertableClientSSK issk = InsertableClientSSK.create(newURI);
newURI = issk.getURI();
if(originalURI.isUSK()) {
newURI = newURI.uskForSSK();
newURI = newURI.setSuggestedEdition(originalURI.getSuggestedEdition());
}
// docName will be preserved.
// Any meta strings *should not* be preserved.
return newURI;
} else if(originalURI.isKSK()) {
return originalURI;
} else {
throw new IllegalArgumentException("Not implemented yet for this key type: " + getKeyType());
}
}
public static final Comparator<FreenetURI> FAST_COMPARATOR = new Comparator<FreenetURI>() {
@Override
public int compare(FreenetURI uri0, FreenetURI uri1) {
// Unfortunately the hashCode's may not have been computed yet.
// But it's still cheaper to recompute them in the long run.
int hash0 = uri0.hashCode();
int hash1 = uri1.hashCode();
if(hash0 > hash1) return 1;
else if(hash1 > hash0) return -1;
return uri0.compareTo(uri1);
}
};
public static FreenetURI generateRandomCHK(Random rand) {
byte[] rkey = new byte[32];
rand.nextBytes(rkey);
byte[] ckey = new byte[32];
rand.nextBytes(ckey);
byte[] extra = ClientCHK.getExtra(Key.ALGO_AES_CTR_256_SHA256, (short)-1, false);
return new FreenetURI("CHK", null, rkey, ckey, extra);
}
// TODO add something like the following?
// public boolean isUpdatable() { return isUSK() || isSSKForUSK() }
}