Package freenet.keys

Source Code of freenet.keys.FreenetURI

/* 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() }
}
TOP

Related Classes of freenet.keys.FreenetURI

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.