示例#1
0
    /**
     * Add a named logger.  This does nothing and returns false if a logger
     * with the same name is already registered.
     * <p>
     * The Logger factory methods call this method to register each
     * newly created Logger.
     * <p>
     * The application should retain its own reference to the Logger 
     * object to avoid it being garbage collected.  The LogManager
     * may only retain a weak reference.
     *
     * @param   logger the new logger.
     * @return  true if the argument logger was registered successfully,
     *          false if a logger of that name already exists.
     * @exception NullPointerException if the logger name is null.
     */
    public synchronized boolean addLogger(Logger logger) {
	String name = logger.getName();
	if (name == null) {
	    throw new NullPointerException();
	}

	Logger old = (Logger) loggers.get(name);
	if (old != null) {
	    // We already have a registered logger with the given name.
	    return false;
	}

	// We're adding a new logger.
	// Note that we are creating a strong reference here that will
	// keep the Logger in existence indefinitely.
	loggers.put(name, logger);

	// Apply any initial level defined for the new logger.
	Level level = getLevelProperty(name+".level", null);
	if (level != null) {
	    doSetLevel(logger, level);
	}

	// If any of the logger's parents have levels defined,
	// make sure they are instantiated.
	int ix = 1;
	for (;;) {
	    int ix2 = name.indexOf(".", ix);
	    if (ix2 < 0) {
		break;
	    }
	    String pname = name.substring(0,ix2);
	    if (getProperty(pname+".level") != null) {
		// This pname has a level definition.  Make sure it exists.
		Logger plogger = Logger.getLogger(pname);
	    }
	    ix = ix2+1;
	}

	// Find the new node and its parent.
	LogNode node = findNode(name);
	node.logger = logger;
	Logger parent = null;
	LogNode nodep = node.parent;
	while (nodep != null) {
	    if (nodep.logger != null) {
		parent = nodep.logger;
		break;
	    }
	    nodep = nodep.parent;
	}

	if (parent != null) {
            doSetParent(logger, parent);
	}
	// Walk over the children and tell them we are their new parent.
	node.walkAndSetParent(logger);

	return true;
    }
示例#2
0
/**
 * Implements {@link PacketTransformer} for DTLS-SRTP. It's capable of working in pure DTLS mode if
 * appropriate flag was set in <tt>DtlsControlImpl</tt>.
 *
 * @author Lyubomir Marinov
 */
public class DtlsPacketTransformer extends SinglePacketTransformer {
  private static final long CONNECT_RETRY_INTERVAL = 500;

  /**
   * The maximum number of times that {@link #runInConnectThread(DTLSProtocol, TlsPeer,
   * DatagramTransport)} is to retry the invocations of {@link DTLSClientProtocol#connect(TlsClient,
   * DatagramTransport)} and {@link DTLSServerProtocol#accept(TlsServer, DatagramTransport)} in
   * anticipation of a successful connection.
   */
  private static final int CONNECT_TRIES = 3;

  /**
   * The indicator which determines whether unencrypted packets sent or received through
   * <tt>DtlsPacketTransformer</tt> are to be dropped. The default value is <tt>false</tt>.
   *
   * @see #DROP_UNENCRYPTED_PKTS_PNAME
   */
  private static final boolean DROP_UNENCRYPTED_PKTS;

  /**
   * The name of the <tt>ConfigurationService</tt> and/or <tt>System</tt> property which indicates
   * whether unencrypted packets sent or received through <tt>DtlsPacketTransformer</tt> are to be
   * dropped. The default value is <tt>false</tt>.
   */
  private static final String DROP_UNENCRYPTED_PKTS_PNAME =
      DtlsPacketTransformer.class.getName() + ".dropUnencryptedPkts";

  /** The length of the header of a DTLS record. */
  static final int DTLS_RECORD_HEADER_LENGTH = 13;

  /**
   * The number of milliseconds a <tt>DtlsPacketTransform</tt> is to wait on its {@link
   * #dtlsTransport} in order to receive a packet.
   */
  private static final int DTLS_TRANSPORT_RECEIVE_WAITMILLIS = -1;

  /**
   * The <tt>Logger</tt> used by the <tt>DtlsPacketTransformer</tt> class and its instances to print
   * debug information.
   */
  private static final Logger logger = Logger.getLogger(DtlsPacketTransformer.class);

  static {
    ConfigurationService cfg = LibJitsi.getConfigurationService();
    boolean dropUnencryptedPkts = false;

    if (cfg == null) {
      String s = System.getProperty(DROP_UNENCRYPTED_PKTS_PNAME);

      if (s != null) dropUnencryptedPkts = Boolean.parseBoolean(s);
    } else {
      dropUnencryptedPkts = cfg.getBoolean(DROP_UNENCRYPTED_PKTS_PNAME, dropUnencryptedPkts);
    }
    DROP_UNENCRYPTED_PKTS = dropUnencryptedPkts;
  }

  /**
   * Determines whether a specific array of <tt>byte</tt>s appears to contain a DTLS record.
   *
   * @param buf the array of <tt>byte</tt>s to be analyzed
   * @param off the offset within <tt>buf</tt> at which the analysis is to start
   * @param len the number of bytes within <tt>buf</tt> starting at <tt>off</tt> to be analyzed
   * @return <tt>true</tt> if the specified <tt>buf</tt> appears to contain a DTLS record
   */
  public static boolean isDtlsRecord(byte[] buf, int off, int len) {
    boolean b = false;

    if (len >= DTLS_RECORD_HEADER_LENGTH) {
      short type = TlsUtils.readUint8(buf, off);

      switch (type) {
        case ContentType.alert:
        case ContentType.application_data:
        case ContentType.change_cipher_spec:
        case ContentType.handshake:
          int major = buf[off + 1] & 0xff;
          int minor = buf[off + 2] & 0xff;
          ProtocolVersion version = null;

          if ((major == ProtocolVersion.DTLSv10.getMajorVersion())
              && (minor == ProtocolVersion.DTLSv10.getMinorVersion())) {
            version = ProtocolVersion.DTLSv10;
          }
          if ((version == null)
              && (major == ProtocolVersion.DTLSv12.getMajorVersion())
              && (minor == ProtocolVersion.DTLSv12.getMinorVersion())) {
            version = ProtocolVersion.DTLSv12;
          }
          if (version != null) {
            int length = TlsUtils.readUint16(buf, off + 11);

            if (DTLS_RECORD_HEADER_LENGTH + length <= len) b = true;
          }
          break;
        default:
          // Unless a new ContentType has been defined by the Bouncy
          // Castle Crypto APIs, the specified buf does not represent a
          // DTLS record.
          break;
      }
    }
    return b;
  }

  /** The ID of the component which this instance works for/is associated with. */
  private final int componentID;

  /** The <tt>RTPConnector</tt> which uses this <tt>PacketTransformer</tt>. */
  private AbstractRTPConnector connector;

  /** The background <tt>Thread</tt> which initializes {@link #dtlsTransport}. */
  private Thread connectThread;

  /**
   * The <tt>DatagramTransport</tt> implementation which adapts {@link #connector} and this
   * <tt>PacketTransformer</tt> to the terms of the Bouncy Castle Crypto APIs.
   */
  private DatagramTransportImpl datagramTransport;

  /**
   * The <tt>DTLSTransport</tt> through which the actual packet transformations are being performed
   * by this instance.
   */
  private DTLSTransport dtlsTransport;

  /** The <tt>MediaType</tt> of the stream which this instance works for/is associated with. */
  private MediaType mediaType;

  /**
   * Whether rtcp-mux is in use.
   *
   * <p>If enabled, and this is the transformer for RTCP, it will not establish a DTLS session on
   * its own, but rather wait for the RTP transformer to do so, and reuse it to initialize the SRTP
   * transformer.
   */
  private boolean rtcpmux = false;

  /**
   * The value of the <tt>setup</tt> SDP attribute defined by RFC 4145 &quot;TCP-Based Media
   * Transport in the Session Description Protocol (SDP)&quot; which determines whether this
   * instance acts as a DTLS client or a DTLS server.
   */
  private DtlsControl.Setup setup;

  /** The {@code SRTPTransformer} (to be) used by this instance. */
  private SinglePacketTransformer _srtpTransformer;

  /**
   * The indicator which determines whether the <tt>TlsPeer</tt> employed by this
   * <tt>PacketTransformer</tt> has raised an <tt>AlertDescription.close_notify</tt>
   * <tt>AlertLevel.warning</tt> i.e. the remote DTLS peer has closed the write side of the
   * connection.
   */
  private boolean tlsPeerHasRaisedCloseNotifyWarning;

  /** The <tt>TransformEngine</tt> which has initialized this instance. */
  private final DtlsTransformEngine transformEngine;

  /**
   * Initializes a new <tt>DtlsPacketTransformer</tt> instance.
   *
   * @param transformEngine the <tt>TransformEngine</tt> which is initializing the new instance
   * @param componentID the ID of the component for which the new instance is to work
   */
  public DtlsPacketTransformer(DtlsTransformEngine transformEngine, int componentID) {
    this.transformEngine = transformEngine;
    this.componentID = componentID;
  }

  /** {@inheritDoc} */
  @Override
  public synchronized void close() {
    // SrtpControl.start(MediaType) starts its associated TransformEngine.
    // We will use that mediaType to signal the normal stop then as well
    // i.e. we will call setMediaType(null) first.
    setMediaType(null);
    setConnector(null);
  }

  /**
   * Closes {@link #datagramTransport} if it is non-<tt>null</tt> and logs and swallows any
   * <tt>IOException</tt>.
   */
  private void closeDatagramTransport() {
    if (datagramTransport != null) {
      try {
        datagramTransport.close();
      } catch (IOException ioe) {
        // DatagramTransportImpl has no reason to fail because it is
        // merely an adapter of #connector and this PacketTransformer to
        // the terms of the Bouncy Castle Crypto API.
        logger.error("Failed to (properly) close " + datagramTransport.getClass(), ioe);
      }
      datagramTransport = null;
    }
  }

  /**
   * Determines whether {@link #runInConnectThread(DTLSProtocol, TlsPeer, DatagramTransport)} is to
   * try to establish a DTLS connection.
   *
   * @param i the number of tries remaining after the current one
   * @param datagramTransport
   * @return <tt>true</tt> to try to establish a DTLS connection; otherwise, <tt>false</tt>
   */
  private boolean enterRunInConnectThreadLoop(int i, DatagramTransport datagramTransport) {
    if (i < 0 || i > CONNECT_TRIES) {
      return false;
    } else {
      Thread currentThread = Thread.currentThread();

      synchronized (this) {
        if (i > 0 && i < CONNECT_TRIES - 1) {
          boolean interrupted = false;

          try {
            wait(CONNECT_RETRY_INTERVAL);
          } catch (InterruptedException ie) {
            interrupted = true;
          }
          if (interrupted) currentThread.interrupt();
        }

        return currentThread.equals(this.connectThread)
            && datagramTransport.equals(this.datagramTransport);
      }
    }
  }

  /**
   * Gets the <tt>DtlsControl</tt> implementation associated with this instance.
   *
   * @return the <tt>DtlsControl</tt> implementation associated with this instance
   */
  DtlsControlImpl getDtlsControl() {
    return getTransformEngine().getDtlsControl();
  }

  /**
   * Gets the <tt>TransformEngine</tt> which has initialized this instance.
   *
   * @return the <tt>TransformEngine</tt> which has initialized this instance
   */
  DtlsTransformEngine getTransformEngine() {
    return transformEngine;
  }

  /**
   * Handles a specific <tt>IOException</tt> which was thrown during the execution of {@link
   * #runInConnectThread(DTLSProtocol, TlsPeer, DatagramTransport)} while trying to establish a DTLS
   * connection
   *
   * @param ioe the <tt>IOException</tt> to handle
   * @param msg the human-readable message to log about the specified <tt>ioe</tt>
   * @param i the number of tries remaining after the current one
   * @return <tt>true</tt> if the specified <tt>ioe</tt> was successfully handled; <tt>false</tt>,
   *     otherwise
   */
  private boolean handleRunInConnectThreadException(IOException ioe, String msg, int i) {
    // SrtpControl.start(MediaType) starts its associated TransformEngine.
    // We will use that mediaType to signal the normal stop then as well
    // i.e. we will ignore exception after the procedure to stop this
    // PacketTransformer has begun.
    if (mediaType == null) return false;

    if (ioe instanceof TlsFatalAlert) {
      TlsFatalAlert tfa = (TlsFatalAlert) ioe;
      short alertDescription = tfa.getAlertDescription();

      if (alertDescription == AlertDescription.unexpected_message) {
        msg += " Received fatal unexpected message.";
        if (i == 0
            || !Thread.currentThread().equals(connectThread)
            || connector == null
            || mediaType == null) {
          msg += " Giving up after " + (CONNECT_TRIES - i) + " retries.";
        } else {
          msg += " Will retry.";
          logger.error(msg, ioe);

          return true;
        }
      } else {
        msg += " Received fatal alert " + alertDescription + ".";
      }
    }

    logger.error(msg, ioe);
    return false;
  }

  /**
   * Tries to initialize {@link #_srtpTransformer} by using the <tt>DtlsPacketTransformer</tt> for
   * RTP.
   *
   * @return the (possibly updated) value of {@link #_srtpTransformer}.
   */
  private SinglePacketTransformer initializeSRTCPTransformerFromRtp() {
    DtlsPacketTransformer rtpTransformer =
        (DtlsPacketTransformer) getTransformEngine().getRTPTransformer();

    // Prevent recursion (that is pretty much impossible to ever happen).
    if (rtpTransformer != this) {
      PacketTransformer srtpTransformer = rtpTransformer.waitInitializeAndGetSRTPTransformer();

      if (srtpTransformer != null && srtpTransformer instanceof SRTPTransformer) {
        synchronized (this) {
          if (_srtpTransformer == null) {
            _srtpTransformer = new SRTCPTransformer((SRTPTransformer) srtpTransformer);
            // For the sake of completeness, we notify whenever we
            // assign to _srtpTransformer.
            notifyAll();
          }
        }
      }
    }

    return _srtpTransformer;
  }

  /**
   * Initializes a new <tt>SRTPTransformer</tt> instance with a specific (negotiated)
   * <tt>SRTPProtectionProfile</tt> and the keying material specified by a specific
   * <tt>TlsContext</tt>.
   *
   * @param srtpProtectionProfile the (negotiated) <tt>SRTPProtectionProfile</tt> to initialize the
   *     new instance with
   * @param tlsContext the <tt>TlsContext</tt> which represents the keying material
   * @return a new <tt>SRTPTransformer</tt> instance initialized with <tt>srtpProtectionProfile</tt>
   *     and <tt>tlsContext</tt>
   */
  private SinglePacketTransformer initializeSRTPTransformer(
      int srtpProtectionProfile, TlsContext tlsContext) {
    boolean rtcp;

    switch (componentID) {
      case Component.RTCP:
        rtcp = true;
        break;
      case Component.RTP:
        rtcp = false;
        break;
      default:
        throw new IllegalStateException("componentID");
    }

    int cipher_key_length;
    int cipher_salt_length;
    int cipher;
    int auth_function;
    int auth_key_length;
    int RTCP_auth_tag_length, RTP_auth_tag_length;

    switch (srtpProtectionProfile) {
      case SRTPProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_32:
        cipher_key_length = 128 / 8;
        cipher_salt_length = 112 / 8;
        cipher = SRTPPolicy.AESCM_ENCRYPTION;
        auth_function = SRTPPolicy.HMACSHA1_AUTHENTICATION;
        auth_key_length = 160 / 8;
        RTCP_auth_tag_length = 80 / 8;
        RTP_auth_tag_length = 32 / 8;
        break;
      case SRTPProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_80:
        cipher_key_length = 128 / 8;
        cipher_salt_length = 112 / 8;
        cipher = SRTPPolicy.AESCM_ENCRYPTION;
        auth_function = SRTPPolicy.HMACSHA1_AUTHENTICATION;
        auth_key_length = 160 / 8;
        RTCP_auth_tag_length = RTP_auth_tag_length = 80 / 8;
        break;
      case SRTPProtectionProfile.SRTP_NULL_HMAC_SHA1_32:
        cipher_key_length = 0;
        cipher_salt_length = 0;
        cipher = SRTPPolicy.NULL_ENCRYPTION;
        auth_function = SRTPPolicy.HMACSHA1_AUTHENTICATION;
        auth_key_length = 160 / 8;
        RTCP_auth_tag_length = 80 / 8;
        RTP_auth_tag_length = 32 / 8;
        break;
      case SRTPProtectionProfile.SRTP_NULL_HMAC_SHA1_80:
        cipher_key_length = 0;
        cipher_salt_length = 0;
        cipher = SRTPPolicy.NULL_ENCRYPTION;
        auth_function = SRTPPolicy.HMACSHA1_AUTHENTICATION;
        auth_key_length = 160 / 8;
        RTCP_auth_tag_length = RTP_auth_tag_length = 80 / 8;
        break;
      default:
        throw new IllegalArgumentException("srtpProtectionProfile");
    }

    byte[] keyingMaterial =
        tlsContext.exportKeyingMaterial(
            ExporterLabel.dtls_srtp, null, 2 * (cipher_key_length + cipher_salt_length));
    byte[] client_write_SRTP_master_key = new byte[cipher_key_length];
    byte[] server_write_SRTP_master_key = new byte[cipher_key_length];
    byte[] client_write_SRTP_master_salt = new byte[cipher_salt_length];
    byte[] server_write_SRTP_master_salt = new byte[cipher_salt_length];
    byte[][] keyingMaterialValues = {
      client_write_SRTP_master_key,
      server_write_SRTP_master_key,
      client_write_SRTP_master_salt,
      server_write_SRTP_master_salt
    };

    for (int i = 0, keyingMaterialOffset = 0; i < keyingMaterialValues.length; i++) {
      byte[] keyingMaterialValue = keyingMaterialValues[i];

      System.arraycopy(
          keyingMaterial, keyingMaterialOffset, keyingMaterialValue, 0, keyingMaterialValue.length);
      keyingMaterialOffset += keyingMaterialValue.length;
    }

    SRTPPolicy srtcpPolicy =
        new SRTPPolicy(
            cipher,
            cipher_key_length,
            auth_function,
            auth_key_length,
            RTCP_auth_tag_length,
            cipher_salt_length);
    SRTPPolicy srtpPolicy =
        new SRTPPolicy(
            cipher,
            cipher_key_length,
            auth_function,
            auth_key_length,
            RTP_auth_tag_length,
            cipher_salt_length);
    SRTPContextFactory clientSRTPContextFactory =
        new SRTPContextFactory(
            /* sender */ tlsContext instanceof TlsClientContext,
            client_write_SRTP_master_key,
            client_write_SRTP_master_salt,
            srtpPolicy,
            srtcpPolicy);
    SRTPContextFactory serverSRTPContextFactory =
        new SRTPContextFactory(
            /* sender */ tlsContext instanceof TlsServerContext,
            server_write_SRTP_master_key,
            server_write_SRTP_master_salt,
            srtpPolicy,
            srtcpPolicy);
    SRTPContextFactory forwardSRTPContextFactory;
    SRTPContextFactory reverseSRTPContextFactory;

    if (tlsContext instanceof TlsClientContext) {
      forwardSRTPContextFactory = clientSRTPContextFactory;
      reverseSRTPContextFactory = serverSRTPContextFactory;
    } else if (tlsContext instanceof TlsServerContext) {
      forwardSRTPContextFactory = serverSRTPContextFactory;
      reverseSRTPContextFactory = clientSRTPContextFactory;
    } else {
      throw new IllegalArgumentException("tlsContext");
    }

    SinglePacketTransformer srtpTransformer;

    if (rtcp) {
      srtpTransformer = new SRTCPTransformer(forwardSRTPContextFactory, reverseSRTPContextFactory);
    } else {
      srtpTransformer = new SRTPTransformer(forwardSRTPContextFactory, reverseSRTPContextFactory);
    }
    return srtpTransformer;
  }

  /**
   * Notifies this instance that the DTLS record layer associated with a specific <tt>TlsPeer</tt>
   * has raised an alert.
   *
   * @param tlsPeer the <tt>TlsPeer</tt> whose associated DTLS record layer has raised an alert
   * @param alertLevel {@link AlertLevel}
   * @param alertDescription {@link AlertDescription}
   * @param message a human-readable message explaining what caused the alert. May be <tt>null</tt>.
   * @param cause the exception that caused the alert to be raised. May be <tt>null</tt>.
   */
  void notifyAlertRaised(
      TlsPeer tlsPeer, short alertLevel, short alertDescription, String message, Exception cause) {
    if (AlertLevel.warning == alertLevel && AlertDescription.close_notify == alertDescription) {
      tlsPeerHasRaisedCloseNotifyWarning = true;
    }
  }

  /** {@inheritDoc} */
  @Override
  public RawPacket reverseTransform(RawPacket pkt) {
    byte[] buf = pkt.getBuffer();
    int off = pkt.getOffset();
    int len = pkt.getLength();

    if (isDtlsRecord(buf, off, len)) {
      if (rtcpmux && Component.RTCP == componentID) {
        // This should never happen.
        logger.warn(
            "Dropping a DTLS record, because it was received on the"
                + " RTCP channel while rtcpmux is in use.");
        return null;
      }

      boolean receive;

      synchronized (this) {
        if (datagramTransport == null) {
          receive = false;
        } else {
          datagramTransport.queueReceive(buf, off, len);
          receive = true;
        }
      }
      if (receive) {
        DTLSTransport dtlsTransport = this.dtlsTransport;

        if (dtlsTransport == null) {
          // The specified pkt looks like a DTLS record and it has
          // been consumed for the purposes of the secure channel
          // represented by this PacketTransformer.
          pkt = null;
        } else {
          try {
            int receiveLimit = dtlsTransport.getReceiveLimit();
            int delta = receiveLimit - len;

            if (delta > 0) {
              pkt.grow(delta);
              buf = pkt.getBuffer();
              off = pkt.getOffset();
              len = pkt.getLength();
            } else if (delta < 0) {
              pkt.shrink(-delta);
              buf = pkt.getBuffer();
              off = pkt.getOffset();
              len = pkt.getLength();
            }

            int received = dtlsTransport.receive(buf, off, len, DTLS_TRANSPORT_RECEIVE_WAITMILLIS);

            if (received <= 0) {
              // No application data was decoded.
              pkt = null;
            } else {
              delta = len - received;
              if (delta > 0) pkt.shrink(delta);
            }
          } catch (IOException ioe) {
            pkt = null;
            // SrtpControl.start(MediaType) starts its associated
            // TransformEngine. We will use that mediaType to signal
            // the normal stop then as well i.e. we will ignore
            // exception after the procedure to stop this
            // PacketTransformer has begun.
            if (mediaType != null && !tlsPeerHasRaisedCloseNotifyWarning) {
              logger.error("Failed to decode a DTLS record!", ioe);
            }
          }
        }
      } else {
        // The specified pkt looks like a DTLS record but it is
        // unexpected in the current state of the secure channel
        // represented by this PacketTransformer. This PacketTransformer
        // has not been started (successfully) or has been closed.
        pkt = null;
      }
    } else if (transformEngine.isSrtpDisabled()) {
      // In pure DTLS mode only DTLS records pass through.
      pkt = null;
    } else {
      // DTLS-SRTP has not been initialized yet or has failed to
      // initialize.
      SinglePacketTransformer srtpTransformer = waitInitializeAndGetSRTPTransformer();

      if (srtpTransformer != null) pkt = srtpTransformer.reverseTransform(pkt);
      else if (DROP_UNENCRYPTED_PKTS) pkt = null;
      // XXX Else, it is our explicit policy to let the received packet
      // pass through and rely on the SrtpListener to notify the user that
      // the session is not secured.
    }
    return pkt;
  }

  /**
   * Runs in {@link #connectThread} to initialize {@link #dtlsTransport}.
   *
   * @param dtlsProtocol
   * @param tlsPeer
   * @param datagramTransport
   */
  private void runInConnectThread(
      DTLSProtocol dtlsProtocol, TlsPeer tlsPeer, DatagramTransport datagramTransport) {
    DTLSTransport dtlsTransport = null;
    final boolean srtp = !transformEngine.isSrtpDisabled();
    int srtpProtectionProfile = 0;
    TlsContext tlsContext = null;

    // DTLS client
    if (dtlsProtocol instanceof DTLSClientProtocol) {
      DTLSClientProtocol dtlsClientProtocol = (DTLSClientProtocol) dtlsProtocol;
      TlsClientImpl tlsClient = (TlsClientImpl) tlsPeer;

      for (int i = CONNECT_TRIES - 1; i >= 0; i--) {
        if (!enterRunInConnectThreadLoop(i, datagramTransport)) break;
        try {
          dtlsTransport = dtlsClientProtocol.connect(tlsClient, datagramTransport);
          break;
        } catch (IOException ioe) {
          if (!handleRunInConnectThreadException(
              ioe, "Failed to connect this DTLS client to a DTLS" + " server!", i)) {
            break;
          }
        }
      }
      if (dtlsTransport != null && srtp) {
        srtpProtectionProfile = tlsClient.getChosenProtectionProfile();
        tlsContext = tlsClient.getContext();
      }
    }
    // DTLS server
    else if (dtlsProtocol instanceof DTLSServerProtocol) {
      DTLSServerProtocol dtlsServerProtocol = (DTLSServerProtocol) dtlsProtocol;
      TlsServerImpl tlsServer = (TlsServerImpl) tlsPeer;

      for (int i = CONNECT_TRIES - 1; i >= 0; i--) {
        if (!enterRunInConnectThreadLoop(i, datagramTransport)) break;
        try {
          dtlsTransport = dtlsServerProtocol.accept(tlsServer, datagramTransport);
          break;
        } catch (IOException ioe) {
          if (!handleRunInConnectThreadException(
              ioe, "Failed to accept a connection from a DTLS client!", i)) {
            break;
          }
        }
      }
      if (dtlsTransport != null && srtp) {
        srtpProtectionProfile = tlsServer.getChosenProtectionProfile();
        tlsContext = tlsServer.getContext();
      }
    } else {
      // It MUST be either a DTLS client or a DTLS server.
      throw new IllegalStateException("dtlsProtocol");
    }

    SinglePacketTransformer srtpTransformer =
        (dtlsTransport == null || !srtp)
            ? null
            : initializeSRTPTransformer(srtpProtectionProfile, tlsContext);
    boolean closeSRTPTransformer;

    synchronized (this) {
      if (Thread.currentThread().equals(this.connectThread)
          && datagramTransport.equals(this.datagramTransport)) {
        this.dtlsTransport = dtlsTransport;
        _srtpTransformer = srtpTransformer;
        notifyAll();
      }
      closeSRTPTransformer = (_srtpTransformer != srtpTransformer);
    }
    if (closeSRTPTransformer && srtpTransformer != null) srtpTransformer.close();
  }

  /**
   * Sends the data contained in a specific byte array as application data through the DTLS
   * connection of this <tt>DtlsPacketTransformer</tt>.
   *
   * @param buf the byte array containing data to send.
   * @param off the offset in <tt>buf</tt> where the data begins.
   * @param len the length of data to send.
   */
  public void sendApplicationData(byte[] buf, int off, int len) {
    DTLSTransport dtlsTransport = this.dtlsTransport;
    Throwable throwable = null;

    if (dtlsTransport != null) {
      try {
        dtlsTransport.send(buf, off, len);
      } catch (IOException ioe) {
        throwable = ioe;
      }
    } else {
      throwable = new NullPointerException("dtlsTransport");
    }
    if (throwable != null) {
      // SrtpControl.start(MediaType) starts its associated
      // TransformEngine. We will use that mediaType to signal the normal
      // stop then as well i.e. we will ignore exception after the
      // procedure to stop this PacketTransformer has begun.
      if (mediaType != null && !tlsPeerHasRaisedCloseNotifyWarning) {
        logger.error("Failed to send application data over DTLS transport: ", throwable);
      }
    }
  }

  /**
   * Sets the <tt>RTPConnector</tt> which is to use or uses this <tt>PacketTransformer</tt>.
   *
   * @param connector the <tt>RTPConnector</tt> which is to use or uses this
   *     <tt>PacketTransformer</tt>
   */
  void setConnector(AbstractRTPConnector connector) {
    if (this.connector != connector) {
      this.connector = connector;

      DatagramTransportImpl datagramTransport = this.datagramTransport;

      if (datagramTransport != null) datagramTransport.setConnector(connector);
    }
  }

  /**
   * Sets the <tt>MediaType</tt> of the stream which this instance is to work for/be associated
   * with.
   *
   * @param mediaType the <tt>MediaType</tt> of the stream which this instance is to work for/be
   *     associated with
   */
  synchronized void setMediaType(MediaType mediaType) {
    if (this.mediaType != mediaType) {
      MediaType oldValue = this.mediaType;

      this.mediaType = mediaType;

      if (oldValue != null) stop();
      if (this.mediaType != null) start();
    }
  }

  /**
   * Enables/disables rtcp-mux.
   *
   * @param rtcpmux whether to enable or disable.
   */
  void setRtcpmux(boolean rtcpmux) {
    this.rtcpmux = rtcpmux;
  }

  /**
   * Sets the DTLS protocol according to which this <tt>DtlsPacketTransformer</tt> is to act either
   * as a DTLS server or a DTLS client.
   *
   * @param setup the value of the <tt>setup</tt> SDP attribute to set on this instance in order to
   *     determine whether this instance is to act as a DTLS client or a DTLS server
   */
  void setSetup(DtlsControl.Setup setup) {
    if (this.setup != setup) this.setup = setup;
  }

  /** Starts this <tt>PacketTransformer</tt>. */
  private synchronized void start() {
    if (this.datagramTransport != null) {
      if (this.connectThread == null && dtlsTransport == null) {
        logger.warn(
            getClass().getName()
                + " has been started but has failed to establish"
                + " the DTLS connection!");
      }
      return;
    }

    if (rtcpmux && Component.RTCP == componentID) {
      // In the case of rtcp-mux, the RTCP transformer does not create
      // a DTLS session. The SRTP context (_srtpTransformer) will be
      // initialized on demand using initializeSRTCPTransformerFromRtp().
      return;
    }

    AbstractRTPConnector connector = this.connector;

    if (connector == null) throw new NullPointerException("connector");

    DtlsControl.Setup setup = this.setup;
    SecureRandom secureRandom = DtlsControlImpl.createSecureRandom();
    final DTLSProtocol dtlsProtocolObj;
    final TlsPeer tlsPeer;

    if (DtlsControl.Setup.ACTIVE.equals(setup)) {
      dtlsProtocolObj = new DTLSClientProtocol(secureRandom);
      tlsPeer = new TlsClientImpl(this);
    } else {
      dtlsProtocolObj = new DTLSServerProtocol(secureRandom);
      tlsPeer = new TlsServerImpl(this);
    }
    tlsPeerHasRaisedCloseNotifyWarning = false;

    final DatagramTransportImpl datagramTransport = new DatagramTransportImpl(componentID);

    datagramTransport.setConnector(connector);

    Thread connectThread =
        new Thread() {
          @Override
          public void run() {
            try {
              runInConnectThread(dtlsProtocolObj, tlsPeer, datagramTransport);
            } finally {
              if (Thread.currentThread().equals(DtlsPacketTransformer.this.connectThread)) {
                DtlsPacketTransformer.this.connectThread = null;
              }
            }
          }
        };

    connectThread.setDaemon(true);
    connectThread.setName(DtlsPacketTransformer.class.getName() + ".connectThread");

    this.connectThread = connectThread;
    this.datagramTransport = datagramTransport;

    boolean started = false;

    try {
      connectThread.start();
      started = true;
    } finally {
      if (!started) {
        if (connectThread.equals(this.connectThread)) this.connectThread = null;
        if (datagramTransport.equals(this.datagramTransport)) this.datagramTransport = null;
      }
    }

    notifyAll();
  }

  /** Stops this <tt>PacketTransformer</tt>. */
  private synchronized void stop() {
    if (connectThread != null) connectThread = null;
    try {
      // The dtlsTransport and _srtpTransformer SHOULD be closed, of
      // course. The datagramTransport MUST be closed.
      if (dtlsTransport != null) {
        try {
          dtlsTransport.close();
        } catch (IOException ioe) {
          logger.error("Failed to (properly) close " + dtlsTransport.getClass(), ioe);
        }
        dtlsTransport = null;
      }
      if (_srtpTransformer != null) {
        _srtpTransformer.close();
        _srtpTransformer = null;
      }
    } finally {
      try {
        closeDatagramTransport();
      } finally {
        notifyAll();
      }
    }
  }

  /** {@inheritDoc} */
  @Override
  public RawPacket transform(RawPacket pkt) {
    byte[] buf = pkt.getBuffer();
    int off = pkt.getOffset();
    int len = pkt.getLength();

    // If the specified pkt represents a DTLS record, then it should pass
    // through this PacketTransformer (e.g. it has been sent through
    // DatagramTransportImpl).
    if (isDtlsRecord(buf, off, len)) return pkt;

    // SRTP
    if (!transformEngine.isSrtpDisabled()) {
      // DTLS-SRTP has not been initialized yet or has failed to
      // initialize.
      SinglePacketTransformer srtpTransformer = waitInitializeAndGetSRTPTransformer();

      if (srtpTransformer != null) pkt = srtpTransformer.transform(pkt);
      else if (DROP_UNENCRYPTED_PKTS) pkt = null;
      // XXX Else, it is our explicit policy to let the received packet
      // pass through and rely on the SrtpListener to notify the user that
      // the session is not secured.
    }
    // Pure/non-SRTP DTLS
    else {
      // The specified pkt will pass through this PacketTransformer only
      // if it gets transformed into a DTLS record.
      pkt = null;

      sendApplicationData(buf, off, len);
    }
    return pkt;
  }

  /**
   * Gets the {@code SRTPTransformer} used by this instance. If {@link #_srtpTransformer} does not
   * exist (yet) and the state of this instance indicates that its initialization is in progess,
   * then blocks until {@code _srtpTransformer} is initialized and returns it.
   *
   * @return the {@code SRTPTransformer} used by this instance
   */
  private SinglePacketTransformer waitInitializeAndGetSRTPTransformer() {
    SinglePacketTransformer srtpTransformer = _srtpTransformer;

    if (srtpTransformer != null) return srtpTransformer;

    if (rtcpmux && Component.RTCP == componentID) return initializeSRTCPTransformerFromRtp();

    // XXX It is our explicit policy to rely on the SrtpListener to notify
    // the user that the session is not secure. Unfortunately, (1) the
    // SrtpListener is not supported by this DTLS SrtpControl implementation
    // and (2) encrypted packets may arrive soon enough to be let through
    // while _srtpTransformer is still initializing. Consequently, we will
    // block and wait for _srtpTransformer to initialize.
    boolean interrupted = false;

    try {
      synchronized (this) {
        do {
          srtpTransformer = _srtpTransformer;
          if (srtpTransformer != null) break; // _srtpTransformer is initialized

          if (connectThread == null) {
            // Though _srtpTransformer is NOT initialized, there is
            // no point in waiting because there is no one to
            // initialize it.
            break;
          }

          try {
            // It does not really matter (enough) how much we wait
            // here because we wait in a loop.
            long timeout = CONNECT_TRIES * CONNECT_RETRY_INTERVAL;

            wait(timeout);
          } catch (InterruptedException ie) {
            interrupted = true;
          }
        } while (true);
      }
    } finally {
      if (interrupted) Thread.currentThread().interrupt();
    }

    return srtpTransformer;
  }
}
/** Central repository of loaded keystores */
public class LockssKeyStoreManager extends BaseLockssDaemonManager implements ConfigurableManager {

  protected static Logger log = Logger.getLogger("LockssKeyStoreManager");

  static final String PREFIX = Configuration.PREFIX + "keyMgr.";

  /** Default type for newly created keystores. */
  public static final String PARAM_DEFAULT_KEYSTORE_TYPE = PREFIX + "defaultKeyStoreType";

  public static final String DEFAULT_DEFAULT_KEYSTORE_TYPE = "JCEKS";

  /** Default keystore provider. */
  public static final String PARAM_DEFAULT_KEYSTORE_PROVIDER = PREFIX + "defaultKeyStoreProvider";

  public static final String DEFAULT_DEFAULT_KEYSTORE_PROVIDER = null;

  /**
   * Root of keystore definitions. For each keystore, pick a unique identifier and use it in place
   * of &lt;id&gt; in the following
   */
  public static final String PARAM_KEYSTORE = PREFIX + "keystore";

  /** If true the daemon will exit if a critical keystore is missing. */
  public static final String PARAM_EXIT_IF_MISSING_KEYSTORE = PREFIX + "exitIfMissingKeystore";

  public static final boolean DEFAULT_EXIT_IF_MISSING_KEYSTORE = true;

  /** keystore name, used by clients to refer to it */
  public static final String KEYSTORE_PARAM_NAME = "name";
  /** keystore file. Only one of file, resource or url should be set */
  public static final String KEYSTORE_PARAM_FILE = "file";
  /** keystore resource. Only one of file, resource or url should be set */
  public static final String KEYSTORE_PARAM_RESOURCE = "resource";
  /** keystore url. Only one of file, resource or url should be set */
  public static final String KEYSTORE_PARAM_URL = "url";
  /** keystore type */
  public static final String KEYSTORE_PARAM_TYPE = "type";
  /** keystore provider */
  public static final String KEYSTORE_PARAM_PROVIDER = "provider";
  /** keystore password */
  public static final String KEYSTORE_PARAM_PASSWORD = "******";
  /** private key password */
  public static final String KEYSTORE_PARAM_KEY_PASSWORD = "******";
  /** private key password file */
  public static final String KEYSTORE_PARAM_KEY_PASSWORD_FILE = "keyPasswordFile";
  /**
   * If true, and the keystore doesn't exist, a keystore with a self-signed certificate will be be
   * created.
   */
  public static final String KEYSTORE_PARAM_CREATE = "create";

  protected String defaultKeyStoreType = DEFAULT_DEFAULT_KEYSTORE_TYPE;
  protected String defaultKeyStoreProvider = DEFAULT_DEFAULT_KEYSTORE_PROVIDER;
  protected boolean paramExitIfMissingKeyStore = DEFAULT_EXIT_IF_MISSING_KEYSTORE;

  // Pseudo params for param doc
  public static final String DOC_PREFIX = PARAM_KEYSTORE + ".<id>.";

  /** Name by which daemon component(s) refer to this keystore */
  public static final String PARAM_KEYSTORE_NAME = DOC_PREFIX + KEYSTORE_PARAM_NAME;
  /** Keystore filename. Only one of file, resource or url should be set */
  public static final String PARAM_KEYSTORE_FILE = DOC_PREFIX + KEYSTORE_PARAM_FILE;
  /** Keystore resource. Only one of file, resource or url should be set */
  public static final String PARAM_KEYSTORE_RESOURCE = DOC_PREFIX + KEYSTORE_PARAM_RESOURCE;
  /** Keystore url. Only one of file, resource or url should be set */
  public static final String PARAM_KEYSTORE_URL = DOC_PREFIX + KEYSTORE_PARAM_URL;
  /** Keystore type (JKS, JCEKS, etc.) */
  public static final String PARAM_KEYSTORE_TYPE = DOC_PREFIX + KEYSTORE_PARAM_TYPE;
  /** Keystore provider (SunJCE, etc.) */
  public static final String PARAM_KEYSTORE_PROVIDER = DOC_PREFIX + KEYSTORE_PARAM_PROVIDER;
  /** Keystore password. Default is machine's fqdn */
  public static final String PARAM_KEYSTORE_PASSWORD = DOC_PREFIX + KEYSTORE_PARAM_PASSWORD;
  /** Private key password */
  public static final String PARAM_KEYSTORE_KEY_PASSWORD = DOC_PREFIX + KEYSTORE_PARAM_KEY_PASSWORD;
  /** private key password file */
  public static final String PARAM_KEYSTORE_KEY_PASSWORD_FILE =
      DOC_PREFIX + KEYSTORE_PARAM_KEY_PASSWORD_FILE;
  /**
   * If true, and the keystore doesn't exist, a keystore with a self-signed certificate will be be
   * created.
   */
  public static final String PARAM_KEYSTORE_CREATE = DOC_PREFIX + KEYSTORE_PARAM_CREATE;

  public static boolean DEFAULT_CREATE = false;

  protected Map<String, LockssKeyStore> keystoreMap = new HashMap<String, LockssKeyStore>();

  public void startService() {
    super.startService();
    loadKeyStores();
  }

  public synchronized void setConfig(
      Configuration config, Configuration prevConfig, Configuration.Differences changedKeys) {
    if (changedKeys.contains(PREFIX)) {
      defaultKeyStoreType = config.get(PARAM_DEFAULT_KEYSTORE_TYPE, DEFAULT_DEFAULT_KEYSTORE_TYPE);
      defaultKeyStoreProvider =
          config.get(PARAM_DEFAULT_KEYSTORE_PROVIDER, DEFAULT_DEFAULT_KEYSTORE_PROVIDER);
      paramExitIfMissingKeyStore =
          config.getBoolean(PARAM_EXIT_IF_MISSING_KEYSTORE, DEFAULT_EXIT_IF_MISSING_KEYSTORE);

      if (changedKeys.contains(PARAM_KEYSTORE)) {
        configureKeyStores(config);
        // defer initial set of keystore loading until startService
        if (isInited()) {
          // load any newly added keystores
          loadKeyStores();
        }
      }
    }
  }

  /**
   * Return the named LockssKeyStore or null
   *
   * @param name the keystore name
   */
  public LockssKeyStore getLockssKeyStore(String name) {
    return getLockssKeyStore(name, null);
  }

  /**
   * Return the named LockssKeyStore
   *
   * @param name the keystore name
   * @param criticalServiceName if non-null, this is a criticial keystore whose unavailability
   *     should cause the daemon to exit (if org.lockss.keyMgr.exitIfMissingKeystore is true)
   */
  public LockssKeyStore getLockssKeyStore(String name, String criticalServiceName) {
    LockssKeyStore res = keystoreMap.get(name);
    checkFact(res, name, criticalServiceName, null);
    return res;
  }

  /**
   * Convenience method to return the KeyManagerFactory from the named LockssKeyStore, or null
   *
   * @param name the keystore name
   */
  public KeyManagerFactory getKeyManagerFactory(String name) {
    return getKeyManagerFactory(name, null);
  }

  /**
   * Convenience method to return the KeyManagerFactory from the named LockssKeyStore.
   *
   * @param name the keystore name
   * @param criticalServiceName if non-null, this is a criticial keystore whose unavailability
   *     should cause the daemon to exit (if org.lockss.keyMgr.exitIfMissingKeystore is true)
   */
  public KeyManagerFactory getKeyManagerFactory(String name, String criticalServiceName) {
    LockssKeyStore lk = getLockssKeyStore(name, criticalServiceName);
    if (lk != null) {
      KeyManagerFactory fact = lk.getKeyManagerFactory();
      checkFact(fact, name, criticalServiceName, "found but contains no private keys");

      return fact;
    }
    return null;
  }

  /** Convenience method to return the TrustManagerFactory from the named LockssKeyStore, or null */
  public TrustManagerFactory getTrustManagerFactory(String name) {
    return getTrustManagerFactory(name, null);
  }

  /**
   * Convenience method to return the TrustManagerFactory from the named LockssKeyStore.
   *
   * @param name the keystore name
   * @param criticalServiceName if non-null, this is a criticial keystore whose unavailability
   *     should cause the daemon to exit (if org.lockss.keyMgr.exitIfMissingKeystore is true)
   */
  public TrustManagerFactory getTrustManagerFactory(String name, String criticalServiceName) {
    LockssKeyStore lk = getLockssKeyStore(name, criticalServiceName);
    if (lk != null) {
      TrustManagerFactory fact = lk.getTrustManagerFactory();
      checkFact(fact, name, criticalServiceName, "found but contains no trusted certificates");
      return fact;
    }
    return null;
  }

  private void checkFact(Object fact, String name, String criticalServiceName, String message) {
    if (fact == null && criticalServiceName != null) {
      String msg =
          StringUtil.isNullString(name)
              ? ("No keystore name given for critical keystore"
                  + " needed for service "
                  + criticalServiceName
                  + ", daemon exiting")
              : ("Critical keystore "
                  + name
                  + " "
                  + ((message != null) ? message : "not found or not loadable")
                  + " for service "
                  + criticalServiceName
                  + ", daemon exiting");
      log.critical(msg);

      if (paramExitIfMissingKeyStore) {
        System.exit(Constants.EXIT_CODE_KEYSTORE_MISSING);
      } else {
        throw new IllegalArgumentException(msg);
      }
    }
  }

  /** Create LockssKeystores from config subtree below {@link #PARAM_KEYSTORE} */
  void configureKeyStores(Configuration config) {
    Configuration allKs = config.getConfigTree(PARAM_KEYSTORE);
    for (Iterator iter = allKs.nodeIterator(); iter.hasNext(); ) {
      String id = (String) iter.next();
      Configuration oneKs = allKs.getConfigTree(id);
      try {
        LockssKeyStore lk = createLockssKeyStore(oneKs);
        String name = lk.getName();
        if (name == null) {
          log.error("KeyStore definition missing name: " + oneKs);
          continue;
        }
        LockssKeyStore old = keystoreMap.get(name);
        if (old != null && !lk.equals(old)) {
          log.warning(
              "Keystore "
                  + name
                  + " redefined.  "
                  + "New definition may not take effect until daemon restart");
        }

        log.debug("Adding keystore " + name);
        keystoreMap.put(name, lk);

      } catch (Exception e) {
        log.error("Couldn't create keystore: " + oneKs, e);
      }
    }
  }

  /** Create LockssKeystore from a config subtree */
  LockssKeyStore createLockssKeyStore(Configuration config) {
    log.debug2("Creating LockssKeyStore from config: " + config);
    String name = config.get(KEYSTORE_PARAM_NAME);
    LockssKeyStore lk = new LockssKeyStore(name);

    String file = config.get(KEYSTORE_PARAM_FILE);
    String resource = config.get(KEYSTORE_PARAM_RESOURCE);
    String url = config.get(KEYSTORE_PARAM_URL);

    if (!StringUtil.isNullString(file)) {
      lk.setLocation(file, LocationType.File);
    } else if (!StringUtil.isNullString(resource)) {
      lk.setLocation(resource, LocationType.Resource);
    } else if (!StringUtil.isNullString(url)) {
      lk.setLocation(url, LocationType.Url);
    }

    lk.setType(config.get(KEYSTORE_PARAM_TYPE, defaultKeyStoreType));
    lk.setProvider(config.get(KEYSTORE_PARAM_PROVIDER, defaultKeyStoreProvider));
    lk.setPassword(config.get(KEYSTORE_PARAM_PASSWORD));
    lk.setKeyPassword(config.get(KEYSTORE_PARAM_KEY_PASSWORD));
    lk.setKeyPasswordFile(config.get(KEYSTORE_PARAM_KEY_PASSWORD_FILE));
    lk.setMayCreate(config.getBoolean(KEYSTORE_PARAM_CREATE, DEFAULT_CREATE));
    return lk;
  }

  void loadKeyStores() {
    List<LockssKeyStore> lst = new ArrayList<LockssKeyStore>(keystoreMap.values());
    for (LockssKeyStore lk : lst) {
      try {
        lk.load();
      } catch (Exception e) {
        log.error("Can't load keystore " + lk.getName(), e);
        keystoreMap.remove(lk.getName());
      }
    }
  }
}
/** Functional tests on the simulated content generator. */
public class FuncSimulatedContent extends LockssTestCase {
  static final Logger log = Logger.getLogger("FuncSimulatedContent");

  private PluginManager pluginMgr;
  private Plugin simPlugin;
  private SimulatedArchivalUnit sau1;
  private SimulatedContentGenerator scgen = null;
  private MockLockssDaemon theDaemon;
  String tempDirPath;
  String tempDirPath2;

  private static String DAMAGED_CACHED_URL = "/branch2/branch2/002file.txt";

  public FuncSimulatedContent(String msg) {
    super(msg);
  }

  public void setUp() throws Exception {
    super.setUp();
    tempDirPath = getTempDir().getAbsolutePath() + File.separator;

    theDaemon = getMockLockssDaemon();
    theDaemon.getAlertManager();
    theDaemon.getPluginManager().setLoadablePluginsReady(true);
    theDaemon.getHashService();
    MockSystemMetrics metrics = new MyMockSystemMetrics();
    metrics.initService(theDaemon);
    theDaemon.setSystemMetrics(metrics);

    theDaemon.setDaemonInited(true);

    Properties props = new Properties();
    props.setProperty(SystemMetrics.PARAM_HASH_TEST_DURATION, "1000");
    props.setProperty(SystemMetrics.PARAM_HASH_TEST_BYTE_STEP, "1024");
    props.setProperty(ConfigManager.PARAM_PLATFORM_DISK_SPACE_LIST, tempDirPath);
    ConfigurationUtil.setCurrentConfigFromProps(props);

    pluginMgr = theDaemon.getPluginManager();
    pluginMgr.startService();
    theDaemon.getHashService().startService();
    metrics.startService();
    metrics.setHashSpeed(100);

    simPlugin = PluginTestUtil.findPlugin(SimulatedPlugin.class);
  }

  public void tearDown() throws Exception {
    theDaemon.getLockssRepository(sau1).stopService();
    theDaemon.getNodeManager(sau1).stopService();
    theDaemon.getPluginManager().stopService();
    theDaemon.getHashService().stopService();
    theDaemon.getSystemMetrics().stopService();
    theDaemon.stopDaemon();
    super.tearDown();
  }

  SimulatedArchivalUnit setupSimAu(Configuration auConfig)
      throws ArchivalUnit.ConfigurationException {
    ArchivalUnit au = PluginTestUtil.createAndStartAu(simPlugin, auConfig);
    return (SimulatedArchivalUnit) au;
  }

  Configuration simAuConfig(String rootPath) {
    Configuration conf = ConfigManager.newConfiguration();
    conf.put("root", rootPath);
    conf.put("depth", "2");
    conf.put("branch", "2");
    conf.put("numFiles", "2");
    conf.put("badCachedFileLoc", "2,2");
    conf.put("badCachedFileNum", "2");
    return conf;
  }

  void enableFilter(SimulatedArchivalUnit sau, boolean enable)
      throws ArchivalUnit.ConfigurationException {
    Configuration auConfig = sau.getConfiguration().copy();
    // no bad file when playing with filtering
    auConfig.remove("badCachedFileLoc");
    auConfig.remove("badCachedFileNum");
    if (enable) {
      auConfig.put(SimulatedPlugin.AU_PARAM_HASH_FILTER_SPEC, "true");
    } else {
      auConfig.remove(SimulatedPlugin.AU_PARAM_HASH_FILTER_SPEC);
    }
    sau.setConfiguration(auConfig);
  }

  public void testSimulatedContent() throws Exception {
    sau1 = setupSimAu(simAuConfig(tempDirPath));
    createContent(sau1);
    crawlContent(sau1);
    checkContent(sau1);
    doDamageRemoveTest(sau1); // must be before content read again
    checkFilter(sau1);
    hashContent(sau1);

    // this resets AU's config, do last to avoid messing up toBeDamaged set
  }

  public void testDualContentHash() throws Exception {
    sau1 = setupSimAu(simAuConfig(tempDirPath));
    createContent(sau1);
    crawlContent(sau1);
    CachedUrlSet set = sau1.getAuCachedUrlSet();
    byte[] nameH = getHash(set, true);
    byte[] contentH = getHash(set, false);

    tempDirPath2 = getTempDir().getAbsolutePath() + File.separator;
    SimulatedArchivalUnit sau2 = setupSimAu(simAuConfig(tempDirPath2));

    createContent(sau2);
    crawlContent(sau2);
    set = sau2.getAuCachedUrlSet();
    byte[] nameH2 = getHash(set, true);
    byte[] contentH2 = getHash(set, false);
    assertEquals(nameH, nameH2);
    assertEquals(contentH, contentH2);
  }

  public void testBaseUrl() throws Exception {
    sau1 = setupSimAu(simAuConfig(tempDirPath));
    createContent(sau1);
    crawlContent(sau1);
    CachedUrlSet cus1 = sau1.getAuCachedUrlSet();

    tempDirPath2 = getTempDir().getAbsolutePath() + File.separator;
    Configuration config2 = simAuConfig(tempDirPath2);
    config2.put("base_url", "http://anotherhost.org/");
    SimulatedArchivalUnit sau2 = setupSimAu(config2);
    createContent(sau2);
    crawlContent(sau2);
    CachedUrlSet cus2 = sau1.getAuCachedUrlSet();
    List urls1 = auUrls(sau1);
    List urls2 = auUrls(sau2);

    Pattern pat = Pattern.compile("http://([^/]+)(/.*)$");
    List<String> l1 = auUrls(sau1);
    List<String> l2 = auUrls(sau2);
    assertEquals(l1.size(), l2.size());
    for (int ix = 0; ix < l1.size(); ix++) {
      Matcher m1 = pat.matcher(l1.get(ix));
      assertTrue(m1.matches());
      Matcher m2 = pat.matcher(l2.get(ix));
      assertTrue(m2.matches());
      assertEquals("www.example.com", m1.group(1));
      assertEquals("anotherhost.org", m2.group(1));
      assertEquals(m1.group(2), m2.group(2));
    }
  }

  public void testBaseUrlPath() throws Exception {
    sau1 = setupSimAu(simAuConfig(tempDirPath));
    createContent(sau1);
    crawlContent(sau1);
    CachedUrlSet cus1 = sau1.getAuCachedUrlSet();

    tempDirPath2 = getTempDir().getAbsolutePath() + File.separator;
    Configuration config2 = simAuConfig(tempDirPath2);
    config2.put("base_url", "http://anotherhost.org/some/path/");
    SimulatedArchivalUnit sau2 = setupSimAu(config2);
    createContent(sau2);
    crawlContent(sau2);
    CachedUrlSet cus2 = sau1.getAuCachedUrlSet();
    List urls1 = auUrls(sau1);
    List urls2 = auUrls(sau2);

    Pattern pat1 = Pattern.compile("http://www\\.example\\.com(/.*)$");
    Pattern pat2 = Pattern.compile("http://anotherhost\\.org/some/path(/.*)$");
    List<String> l1 = auUrls(sau1);
    List<String> l2 = auUrls(sau2);
    assertEquals(l1.size(), l2.size());
    for (int ix = 0; ix < l1.size(); ix++) {
      Matcher m1 = pat1.matcher(l1.get(ix));
      assertTrue(m1.matches());
      Matcher m2 = pat2.matcher(l2.get(ix));
      assertTrue(m2.matches());
      assertEquals(m1.group(1), m2.group(1));
    }
  }

  List<String> auUrls(ArchivalUnit au) {
    List<String> res = new ArrayList<String>();
    for (Iterator iter = au.getAuCachedUrlSet().contentHashIterator(); iter.hasNext(); ) {
      CachedUrlSetNode cusn = (CachedUrlSetNode) iter.next();
      if (cusn.hasContent()) {
        res.add(cusn.getUrl());
      }
    }
    return res;
  }

  protected void createContent(SimulatedArchivalUnit sau) {
    log.debug("createContent()");
    scgen = sau.getContentGenerator();
    scgen.setFileTypes(
        SimulatedContentGenerator.FILE_TYPE_HTML + SimulatedContentGenerator.FILE_TYPE_TXT);
    scgen.setAbnormalFile("1,1", 1);
    scgen.setOddBranchesHaveContent(true);

    sau.deleteContentTree();
    sau.generateContentTree();
    assertTrue(scgen.isContentTree());
  }

  protected void crawlContent(SimulatedArchivalUnit sau) {
    log.debug("crawlContent()");
    CrawlSpec spec = new SpiderCrawlSpec(sau.getNewContentCrawlUrls(), null);
    Crawler crawler = new NoCrawlEndActionsNewContentCrawler(sau, spec, new MockAuState());
    crawler.doCrawl();
  }

  protected void checkContent(SimulatedArchivalUnit sau) throws IOException {
    log.debug("checkContent()");
    checkRoot(sau);
    checkLeaf(sau);
    checkStoredContent(sau);
    checkDepth(sau);
  }

  protected void checkFilter(SimulatedArchivalUnit sau) throws Exception {
    log.debug("checkFilter()");
    CachedUrl cu = sau.makeCachedUrl(sau.getUrlRoot() + "/001file.html");

    enableFilter(sau, true);
    InputStream is = cu.openForHashing();
    String expected = "001file.html This is file 1, depth 0, branch 0. foobar ";
    assertEquals(expected, StringUtil.fromInputStream(is));
    is.close();
    enableFilter(sau, false);
    cu = sau.makeCachedUrl(sau.getUrlRoot() + "/001file.html");
    is = cu.openForHashing();
    expected =
        "<HTML><HEAD><TITLE>001file.html</TITLE></HEAD><BODY>\n"
            + "This is file 1, depth 0, branch 0.<br><!-- comment -->    "
            + "Citation String   foobar<br><script>"
            + "(defun fact (n) (cond ((= n 0) 1) (t (fact (sub1 n)))))</script>\n"
            + "</BODY></HTML>";
    assertEquals(expected, StringUtil.fromInputStream(is));
    is.close();
  }

  private byte[] fromHex(String hex) {
    return ByteArray.fromHexString(hex);
  }

  protected void hashContent(SimulatedArchivalUnit sau) throws Exception {
    log.debug("hashContent()");
    measureHashSpeed(sau);

    // If any changes are made to the contents or shape of the simulated
    // content tree, these hash values will have to be changed
    checkHashSet(sau, true, false, fromHex("6AB258B4E1FFD9F9B45316B4F54111FF5E5948D2"));
    checkHashSet(sau, true, true, fromHex("6AB258B4E1FFD9F9B45316B4F54111FF5E5948D2"));
    checkHashSet(sau, false, false, fromHex("409893F1A603F4C276632694DB1621B639BD5164"));
    checkHashSet(sau, false, true, fromHex("85E6213C3771BEAC5A4602CAF7982C6C222800D5"));
  }

  protected void checkDepth(SimulatedArchivalUnit sau) {
    log.debug("checkDepth()");
    String URL_ROOT = sau.getUrlRoot();
    assertEquals(0, sau.getLinkDepth(URL_ROOT + "/index.html"));
    assertEquals(0, sau.getLinkDepth(URL_ROOT + "/"));
    assertEquals(1, sau.getLinkDepth(URL_ROOT + "/001file.html"));
    assertEquals(1, sau.getLinkDepth(URL_ROOT + "/branch1/index.html"));
    assertEquals(1, sau.getLinkDepth(URL_ROOT + "/branch1/"));
    assertEquals(2, sau.getLinkDepth(URL_ROOT + "/branch1/001file.html"));
  }

  protected void checkRoot(SimulatedArchivalUnit sau) {
    log.debug("checkRoot()");
    CachedUrlSet set = sau.getAuCachedUrlSet();
    Iterator setIt = set.flatSetIterator();
    ArrayList childL = new ArrayList(1);
    CachedUrlSet cus = null;
    while (setIt.hasNext()) {
      cus = (CachedUrlSet) setIt.next();
      childL.add(cus.getUrl());
    }

    String urlRoot = sau.getUrlRoot();

    String[] expectedA = new String[1];
    expectedA[0] = urlRoot;
    assertIsomorphic(expectedA, childL);

    setIt = cus.flatSetIterator();
    childL = new ArrayList(7);
    while (setIt.hasNext()) {
      childL.add(((CachedUrlSetNode) setIt.next()).getUrl());
    }

    expectedA =
        new String[] {
          urlRoot + "/001file.html",
          urlRoot + "/001file.txt",
          urlRoot + "/002file.html",
          urlRoot + "/002file.txt",
          urlRoot + "/branch1",
          urlRoot + "/branch2",
          urlRoot + "/index.html"
        };
    assertIsomorphic(expectedA, childL);
  }

  protected void checkLeaf(SimulatedArchivalUnit sau) {
    log.debug("checkLeaf()");
    String parent = sau.getUrlRoot() + "/branch1";
    CachedUrlSetSpec spec = new RangeCachedUrlSetSpec(parent);
    CachedUrlSet set = sau.makeCachedUrlSet(spec);
    Iterator setIt = set.contentHashIterator();
    ArrayList childL = new ArrayList(16);
    while (setIt.hasNext()) {
      childL.add(((CachedUrlSetNode) setIt.next()).getUrl());
    }
    String[] expectedA =
        new String[] {
          parent,
          parent + "/001file.html",
          parent + "/001file.txt",
          parent + "/002file.html",
          parent + "/002file.txt",
          parent + "/branch1",
          parent + "/branch1/001file.html",
          parent + "/branch1/001file.txt",
          parent + "/branch1/002file.html",
          parent + "/branch1/002file.txt",
          parent + "/branch1/index.html",
          parent + "/branch2",
          parent + "/branch2/001file.html",
          parent + "/branch2/001file.txt",
          parent + "/branch2/002file.html",
          parent + "/branch2/002file.txt",
          parent + "/branch2/index.html",
          parent + "/index.html",
        };
    assertIsomorphic(expectedA, childL);
  }

  protected void checkUrlContent(
      SimulatedArchivalUnit sau,
      String path,
      int fileNum,
      int depth,
      int branchNum,
      boolean isAbnormal,
      boolean isDamaged)
      throws IOException {
    String file = sau.getUrlRoot() + path;
    CachedUrl url = sau.makeCachedUrl(file);
    String content = getUrlContent(url);
    String expectedContent;
    if (path.endsWith(".html")) {
      String fn = path.substring(path.lastIndexOf("/") + 1);
      expectedContent = scgen.getHtmlFileContent(fn, fileNum, depth, branchNum, isAbnormal);
    } else {
      expectedContent = scgen.getTxtContent(fileNum, depth, branchNum, isAbnormal);
    }
    if (isDamaged) {
      assertNotEquals(expectedContent, content);
    } else {
      assertEquals(expectedContent, content);
    }
  }

  protected void checkStoredContent(SimulatedArchivalUnit sau) throws IOException {
    checkUrlContent(sau, "/001file.txt", 1, 0, 0, false, false);
    checkUrlContent(sau, "/branch1/branch1/001file.txt", 1, 2, 1, true, false);
    checkUrlContent(sau, DAMAGED_CACHED_URL, 2, 2, 2, false, true);
  }

  protected void doDamageRemoveTest(SimulatedArchivalUnit sau) throws Exception {
    /* Cache the file again; this time the damage should be gone */
    String file = sau.getUrlRoot() + DAMAGED_CACHED_URL;
    UrlCacher uc = sau.makeUrlCacher(file);
    BitSet fetchFlags = new BitSet();
    fetchFlags.set(UrlCacher.REFETCH_FLAG);
    uc.setFetchFlags(fetchFlags);
    uc.cache();
    checkUrlContent(sau, DAMAGED_CACHED_URL, 2, 2, 2, false, false);
  }

  private void measureHashSpeed(SimulatedArchivalUnit sau) throws Exception {
    MessageDigest dig = null;
    try {
      dig = MessageDigest.getInstance("SHA-1");
    } catch (NoSuchAlgorithmException ex) {
      fail("No algorithm.");
    }
    CachedUrlSet set = sau.getAuCachedUrlSet();
    CachedUrlSetHasher hasher = set.getContentHasher(dig);
    SystemMetrics metrics = theDaemon.getSystemMetrics();
    int estimate = metrics.getBytesPerMsHashEstimate(hasher, dig);
    // should be protected against this being zero by MyMockSystemMetrics,
    // but otherwise use the proper calculation.  This avoids test failure
    // due to really slow machines
    assertTrue(estimate > 0);
    long estimatedTime = set.estimatedHashDuration();
    long size = ((Long) PrivilegedAccessor.getValue(set, "totalNodeSize")).longValue();
    assertTrue(size > 0);
    System.out.println("b/ms: " + estimate);
    System.out.println("size: " + size);
    System.out.println("estimate: " + estimatedTime);
    assertEquals(estimatedTime, theDaemon.getHashService().padHashEstimate(size / estimate));
  }

  private void checkHashSet(
      SimulatedArchivalUnit sau, boolean namesOnly, boolean filter, byte[] expected)
      throws Exception {
    enableFilter(sau, filter);
    CachedUrlSet set = sau.getAuCachedUrlSet();
    byte[] hash = getHash(set, namesOnly);
    assertEquals(expected, hash);

    String parent = sau.getUrlRoot() + "/branch1";
    CachedUrlSetSpec spec = new RangeCachedUrlSetSpec(parent);
    set = sau.makeCachedUrlSet(spec);
    byte[] hash2 = getHash(set, namesOnly);
    assertFalse(Arrays.equals(hash, hash2));
  }

  private byte[] getHash(CachedUrlSet set, boolean namesOnly) throws IOException {
    MessageDigest dig = null;
    try {
      dig = MessageDigest.getInstance("SHA-1");
    } catch (NoSuchAlgorithmException ex) {
      fail("No algorithm.");
    }
    hash(set, dig, namesOnly);
    return dig.digest();
  }

  private void hash(CachedUrlSet set, MessageDigest dig, boolean namesOnly) throws IOException {
    CachedUrlSetHasher hasher = null;
    if (namesOnly) {
      hasher = set.getNameHasher(dig);
    } else {
      hasher = set.getContentHasher(dig);
    }
    int bytesHashed = 0;
    long timeTaken = System.currentTimeMillis();
    while (!hasher.finished()) {
      bytesHashed += hasher.hashStep(256);
    }
    timeTaken = System.currentTimeMillis() - timeTaken;
    if ((timeTaken > 0) && (bytesHashed > 500)) {
      System.out.println("Bytes hashed: " + bytesHashed);
      System.out.println("Time taken: " + timeTaken + "ms");
      System.out.println("Bytes/sec: " + (bytesHashed * 1000 / timeTaken));
    } else {
      System.out.println("No time taken, or insufficient bytes hashed.");
      System.out.println("Bytes hashed: " + bytesHashed);
      System.out.println("Time taken: " + timeTaken + "ms");
    }
  }

  private String getUrlContent(CachedUrl url) throws IOException {
    InputStream content = url.getUnfilteredInputStream();
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    StreamUtil.copy(content, baos);
    content.close();
    String contentStr = new String(baos.toByteArray());
    baos.close();
    return contentStr;
  }

  // this version doesn't fully override the 'measureHashSpeed()' function, but
  // protects against it returning '0' by returning the set speed
  private class MyMockSystemMetrics extends MockSystemMetrics {
    public int measureHashSpeed(CachedUrlSetHasher hasher, MessageDigest digest)
        throws IOException {
      int speed = super.measureHashSpeed(hasher, digest);
      if (speed == 0) {
        speed = getHashSpeed();
        if (speed <= 0) {
          throw new RuntimeException("No hash speed set.");
        }
      }
      return speed;
    }
  }

  public static void main(String[] argv) {
    String[] testCaseList = {FuncSimulatedContent.class.getName()};
    junit.swingui.TestRunner.main(testCaseList);
  }

  public static Test suite() {
    return new TestSuite(FuncSimulatedContent.class);
  }
}
public abstract class AbstractCauchoRequest implements CauchoRequest {
  private static final L10N L = new L10N(AbstractCauchoRequest.class);
  private static final Logger log = Logger.getLogger(AbstractCauchoRequest.class.getName());

  private int _sessionGroup = -1;

  private boolean _sessionIsLoaded;
  private SessionImpl _session;

  public abstract CauchoResponse getResponse();

  public RequestDispatcher getRequestDispatcher(String path) {
    if (path == null || path.length() == 0) return null;
    else if (path.charAt(0) == '/') return getWebApp().getRequestDispatcher(path);
    else {
      CharBuffer cb = new CharBuffer();

      WebApp webApp = getWebApp();

      String servletPath = getPageServletPath();
      if (servletPath != null) cb.append(servletPath);
      String pathInfo = getPagePathInfo();
      if (pathInfo != null) cb.append(pathInfo);

      int p = cb.lastIndexOf('/');
      if (p >= 0) cb.setLength(p);
      cb.append('/');
      cb.append(path);

      if (webApp != null) return webApp.getRequestDispatcher(cb.toString());

      return null;
    }
  }

  public String getRealPath(String uri) {
    WebApp webApp = getWebApp();

    return webApp.getRealPath(uri);
  }

  /** Returns the URL for the request */
  public StringBuffer getRequestURL() {
    StringBuffer sb = new StringBuffer();

    sb.append(getScheme());
    sb.append("://");

    sb.append(getServerName());
    int port = getServerPort();

    if (port > 0 && port != 80 && port != 443) {
      sb.append(":");
      sb.append(port);
    }

    sb.append(getRequestURI());

    return sb;
  }

  /** Returns the real path of pathInfo. */
  public String getPathTranslated() {
    // server/106w
    String pathInfo = getPathInfo();

    if (pathInfo == null) return null;
    else return getRealPath(pathInfo);
  }

  public boolean isTop() {
    return false;
  }

  //
  // session management
  //

  public abstract boolean isSessionIdFromCookie();

  public abstract String getSessionId();

  public abstract void setSessionId(String sessionId);

  /** Returns the memory session. */
  public HttpSession getMemorySession() {
    if (_session != null && _session.isValid()) return _session;
    else return null;
  }

  /**
   * Returns the current session, creating one if necessary. Sessions are a convenience for keeping
   * user state across requests.
   */
  public HttpSession getSession() {
    return getSession(true);
  }

  /**
   * Returns the current session.
   *
   * @param create true if a new session should be created
   * @return the current session
   */
  public HttpSession getSession(boolean create) {
    if (_session != null) {
      if (_session.isValid()) return _session;
    } else if (!create && _sessionIsLoaded) return null;

    _sessionIsLoaded = true;

    _session = createSession(create);

    return _session;
  }

  /**
   * Returns the current session.
   *
   * @return the current session
   */
  public HttpSession getLoadedSession() {
    if (_session != null && _session.isValid()) return _session;
    else return null;
  }

  /** Returns true if the HTTP request's session id refers to a valid session. */
  public boolean isRequestedSessionIdValid() {
    String id = getRequestedSessionId();

    if (id == null) return false;

    SessionImpl session = _session;

    if (session == null) session = (SessionImpl) getSession(false);

    return session != null && session.isValid() && session.getId().equals(id);
  }

  /**
   * Returns the current session.
   *
   * <p>XXX: duplicated in RequestAdapter
   *
   * @param create true if a new session should be created
   * @return the current session
   */
  private SessionImpl createSession(boolean create) {
    SessionManager manager = getSessionManager();

    if (manager == null) return null;

    String id = getSessionId();

    long now = Alarm.getCurrentTime();

    SessionImpl session = manager.createSession(create, this, id, now, isSessionIdFromCookie());

    if (session != null
        && (id == null || !session.getId().equals(id))
        && manager.enableSessionCookies()) {
      setSessionId(session.getId());
    }

    // server/0123 vs TCK
    /*
    if (session != null)
      session.setAccessTime(now);
      */

    return session;
  }

  /** Returns the session manager. */
  protected final SessionManager getSessionManager() {
    WebApp webApp = getWebApp();

    if (webApp != null) return webApp.getSessionManager();
    else return null;
  }

  /** Returns the session cookie. */
  protected final String getSessionCookie(SessionManager manager) {
    if (isSecure()) return manager.getSSLCookieName();
    else return manager.getCookieName();
  }

  public int getSessionGroup() {
    return _sessionGroup;
  }

  void saveSession() {
    SessionImpl session = _session;
    if (session != null) session.save();
  }

  //
  // security
  //

  protected String getRunAs() {
    return null;
  }

  protected ServletInvocation getInvocation() {
    return null;
  }

  /** Returns the next request in a chain. */
  protected HttpServletRequest getRequest() {
    return null;
  }

  /** @since Servlet 3.0 */
  @Override
  public void login(String username, String password) throws ServletException {
    WebApp webApp = getWebApp();

    Authenticator auth = webApp.getConfiguredAuthenticator();

    if (auth == null)
      throw new ServletException(
          L.l("No authentication mechanism is configured for '{0}'", getWebApp()));

    // server/1aj0
    Login login = webApp.getLogin();

    if (login == null)
      throw new ServletException(L.l("No login mechanism is configured for '{0}'", getWebApp()));

    if (!login.isPasswordBased())
      throw new ServletException(
          L.l("Authentication mechanism '{0}' does not support password authentication", login));

    removeAttribute(Login.LOGIN_USER);
    removeAttribute(Login.LOGIN_PASSWORD);

    Principal principal = login.getUserPrincipal(this);

    if (principal != null)
      throw new ServletException(L.l("UserPrincipal object has already been established"));

    setAttribute(Login.LOGIN_USER, username);
    setAttribute(Login.LOGIN_PASSWORD, password);

    try {
      login.login(this, getResponse(), false);
    } finally {
      removeAttribute(Login.LOGIN_USER);
      removeAttribute(Login.LOGIN_PASSWORD);
    }

    principal = login.getUserPrincipal(this);

    if (principal == null) throw new ServletException("can't authenticate a user");
  }

  @Override
  public boolean login(boolean isFail) {
    try {
      WebApp webApp = getWebApp();

      if (webApp == null) {
        if (log.isLoggable(Level.FINE)) log.finer("authentication failed, no web-app found");

        getResponse().sendError(HttpServletResponse.SC_FORBIDDEN);

        return false;
      }

      // If the authenticator can find the user, return it.
      Login login = webApp.getLogin();

      if (login != null) {
        Principal user = login.login(this, getResponse(), isFail);

        return user != null;
        /*
        if (user == null)
          return false;

        setAttribute(AbstractLogin.LOGIN_NAME, user);

        return true;
        */
      } else if (isFail) {
        if (log.isLoggable(Level.FINE))
          log.finer("authentication failed, no login module found for " + webApp);

        getResponse().sendError(HttpServletResponse.SC_FORBIDDEN);

        return false;
      } else {
        // if a non-failure, then missing login is fine

        return false;
      }
    } catch (IOException e) {
      log.log(Level.FINE, e.toString(), e);

      return false;
    }
  }

  /** Returns true if any authentication is requested */
  public abstract boolean isLoginRequested();

  public abstract void requestLogin();

  /** @since Servlet 3.0 */
  @Override
  public boolean authenticate(HttpServletResponse response) throws IOException, ServletException {
    WebApp webApp = getWebApp();

    if (webApp == null)
      throw new ServletException(
          L.l("No authentication mechanism is configured for '{0}'", getWebApp()));

    // server/1aj{0,1}
    Authenticator auth = webApp.getConfiguredAuthenticator();

    if (auth == null)
      throw new ServletException(
          L.l("No authentication mechanism is configured for '{0}'", getWebApp()));

    Login login = webApp.getLogin();

    if (login == null)
      throw new ServletException(
          L.l("No authentication mechanism is configured for '{0}'", getWebApp()));

    Principal principal = login.login(this, response, true);

    if (principal != null) return true;

    return false;
  }

  /** Returns the Principal representing the logged in user. */
  public Principal getUserPrincipal() {
    requestLogin();

    Principal user;
    user = (Principal) getAttribute(AbstractLogin.LOGIN_NAME);

    if (user != null) return user;

    WebApp webApp = getWebApp();
    if (webApp == null) return null;

    // If the authenticator can find the user, return it.
    Login login = webApp.getLogin();

    if (login != null) {
      user = login.getUserPrincipal(this);

      if (user != null) {
        getResponse().setPrivateCache(true);
      } else {
        // server/123h, server/1920
        // distinguishes between setPrivateCache and setPrivateOrResinCache
        // _response.setPrivateOrResinCache(true);
      }
    }

    return user;
  }

  /**
   * Returns true if the user represented by the current request plays the named role.
   *
   * @param role the named role to test.
   * @return true if the user plays the role.
   */
  public boolean isUserInRole(String role) {
    ServletInvocation invocation = getInvocation();

    if (invocation == null) {
      if (getRequest() != null) return getRequest().isUserInRole(role);
      else return false;
    }

    HashMap<String, String> roleMap = invocation.getSecurityRoleMap();

    if (roleMap != null) {
      String linkRole = roleMap.get(role);

      if (linkRole != null) role = linkRole;
    }

    String runAs = getRunAs();

    if (runAs != null) return runAs.equals(role);

    WebApp webApp = getWebApp();

    Principal user = getUserPrincipal();

    if (user == null) {
      if (log.isLoggable(Level.FINE)) log.fine(this + " no user for isUserInRole");

      return false;
    }

    RoleMapManager roleManager = webApp != null ? webApp.getRoleMapManager() : null;

    if (roleManager != null) {
      Boolean result = roleManager.isUserInRole(role, user);

      if (result != null) {
        if (log.isLoggable(Level.FINE)) log.fine(this + " userInRole(" + role + ")->" + result);

        return result;
      }
    }

    Login login = webApp == null ? null : webApp.getLogin();

    boolean inRole = login != null && login.isUserInRole(user, role);

    if (log.isLoggable(Level.FINE)) {
      if (login == null) log.fine(this + " no Login for isUserInRole");
      else if (user == null) log.fine(this + " no user for isUserInRole");
      else if (inRole) log.fine(this + " " + user + " is in role: " + role);
      else log.fine(this + " failed " + user + " in role: " + role);
    }

    return inRole;
  }

  //
  // lifecycle
  //

  protected void finishRequest() throws IOException {
    SessionImpl session = _session;
    //
    if (session == null && getSessionId() != null) session = (SessionImpl) getSession(false);

    if (session != null) session.finishRequest();
  }

  @Override
  public String toString() {
    return getClass().getSimpleName() + "[]";
  }
}
示例#6
0
/**
 * Keeps track of entity capabilities.
 *
 * <p>This work is based on Jonas Adahl's smack fork.
 *
 * @author Emil Ivov
 * @author Lyubomir Marinov
 */
public class EntityCapsManager {
  /**
   * The <tt>Logger</tt> used by the <tt>EntityCapsManager</tt> class and its instances for logging
   * output.
   */
  private static final Logger logger = Logger.getLogger(EntityCapsManager.class);

  /** Static OSGi bundle context used by this class. */
  private static BundleContext bundleContext;

  /** Configuration service instance used by this class. */
  private static ConfigurationService configService;

  /**
   * The prefix of the <tt>ConfigurationService</tt> properties which persist {@link
   * #caps2discoverInfo}.
   */
  private static final String CAPS_PROPERTY_NAME_PREFIX =
      "net.java.sip.communicator.impl.protocol.jabber.extensions.caps." + "EntityCapsManager.CAPS.";

  /**
   * An empty array of <tt>UserCapsNodeListener</tt> elements explicitly defined in order to reduce
   * unnecessary allocations.
   */
  private static final UserCapsNodeListener[] NO_USER_CAPS_NODE_LISTENERS =
      new UserCapsNodeListener[0];

  /** The node value to advertise. */
  private static String entityNode =
      OSUtils.IS_ANDROID ? "http://android.jitsi.org" : "http://jitsi.org";

  /**
   * The <tt>Map</tt> of <tt>Caps</tt> to <tt>DiscoverInfo</tt> which associates a node#ver with the
   * entity capabilities so that they don't have to be retrieved every time their necessary. Because
   * ver is constructed from the entity capabilities using a specific hash method, the hash method
   * is also associated with the entity capabilities along with the node and the ver in order to
   * disambiguate cases of equal ver values for different entity capabilities constructed using
   * different hash methods.
   */
  private static final Map<Caps, DiscoverInfo> caps2discoverInfo =
      new ConcurrentHashMap<Caps, DiscoverInfo>();

  /**
   * Map of Full JID -&gt; DiscoverInfo/null. In case of c2s connection the key is formed as
   * user@server/resource (resource is required) In case of link-local connection the key is formed
   * as user@host (no resource)
   */
  private final Map<String, Caps> userCaps = new ConcurrentHashMap<String, Caps>();

  /** CapsVerListeners gets notified when the version string is changed. */
  private final Set<CapsVerListener> capsVerListeners = new CopyOnWriteArraySet<CapsVerListener>();

  /** The current hash of our version and supported features. */
  private String currentCapsVersion = null;

  /**
   * The list of <tt>UserCapsNodeListener</tt>s interested in events notifying about changes in the
   * list of user caps nodes of this <tt>EntityCapsManager</tt>.
   */
  private final List<UserCapsNodeListener> userCapsNodeListeners =
      new LinkedList<UserCapsNodeListener>();

  static {
    ProviderManager.getInstance()
        .addExtensionProvider(
            CapsPacketExtension.ELEMENT_NAME, CapsPacketExtension.NAMESPACE, new CapsProvider());
  }

  /**
   * Add {@link DiscoverInfo} to our caps database.
   *
   * <p><b>Warning</b>: The specified <tt>DiscoverInfo</tt> is trusted to be valid with respect to
   * the specified <tt>Caps</tt> for performance reasons because the <tt>DiscoverInfo</tt> should
   * have already been validated in order to be used elsewhere anyway.
   *
   * @param caps the <tt>Caps<tt/> i.e. the node, the hash and the ver for which a
   *     <tt>DiscoverInfo</tt> is to be added to our caps database.
   * @param info {@link DiscoverInfo} for the specified <tt>Caps</tt>.
   */
  public static void addDiscoverInfoByCaps(Caps caps, DiscoverInfo info) {
    cleanupDiscoverInfo(info);
    /*
     * DiscoverInfo carries the node we're now associating it with a
     * specific node so we'd better keep them in sync.
     */
    info.setNode(caps.getNodeVer());

    synchronized (caps2discoverInfo) {
      DiscoverInfo oldInfo = caps2discoverInfo.put(caps, info);

      /*
       * If the specified info is a new association for the specified
       * node, remember it across application instances in order to not
       * query for it over the network.
       */
      if ((oldInfo == null) || !oldInfo.equals(info)) {
        String xml = info.getChildElementXML();

        if ((xml != null) && (xml.length() != 0)) {
          getConfigService().setProperty(getCapsPropertyName(caps), xml);
        }
      }
    }
  }

  /**
   * Gets the name of the property in the <tt>ConfigurationService</tt> which is or is to be
   * associated with a specific <tt>Caps</tt> value.
   *
   * @param caps the <tt>Caps</tt> value for which the associated <tt>ConfigurationService</tt>
   *     property name is to be returned
   * @return the name of the property in the <tt>ConfigurationService</tt> which is or is to be
   *     associated with a specific <tt>Caps</tt> value
   */
  private static String getCapsPropertyName(Caps caps) {
    return CAPS_PROPERTY_NAME_PREFIX + caps.node + '#' + caps.hash + '#' + caps.ver;
  }

  /** Returns cached instance of {@link ConfigurationService}. */
  private static ConfigurationService getConfigService() {
    if (configService == null) {
      configService = ServiceUtils.getService(bundleContext, ConfigurationService.class);
    }
    return configService;
  }

  /**
   * Sets OSGi bundle context instance that will be used by this class.
   *
   * @param bundleContext the <tt>BundleContext</tt> instance to be used by this class or
   *     <tt>null</tt> to clear the reference.
   */
  public static void setBundleContext(BundleContext bundleContext) {
    if (bundleContext == null) {
      configService = null;
    }
    EntityCapsManager.bundleContext = bundleContext;
  }

  /**
   * Add a record telling what entity caps node a user has.
   *
   * @param user the user (Full JID)
   * @param node the node (of the caps packet extension)
   * @param hash the hashing algorithm used to calculate <tt>ver</tt>
   * @param ver the version (of the caps packet extension)
   * @param ext the ext (of the caps packet extension)
   * @param online indicates if the user is online
   */
  private void addUserCapsNode(
      String user, String node, String hash, String ver, String ext, boolean online) {
    if ((user != null) && (node != null) && (hash != null) && (ver != null)) {
      Caps caps = userCaps.get(user);

      if ((caps == null)
          || !caps.node.equals(node)
          || !caps.hash.equals(hash)
          || !caps.ver.equals(ver)) {
        caps = new Caps(node, hash, ver, ext);

        userCaps.put(user, caps);
      } else return;

      // Fire userCapsNodeAdded.
      UserCapsNodeListener[] listeners;

      synchronized (userCapsNodeListeners) {
        listeners = userCapsNodeListeners.toArray(NO_USER_CAPS_NODE_LISTENERS);
      }
      if (listeners.length != 0) {
        String nodeVer = caps.getNodeVer();

        for (UserCapsNodeListener listener : listeners)
          listener.userCapsNodeAdded(user, nodeVer, online);
      }
    }
  }

  /**
   * Adds a specific <tt>UserCapsNodeListener</tt> to the list of <tt>UserCapsNodeListener</tt>s
   * interested in events notifying about changes in the list of user caps nodes of this
   * <tt>EntityCapsManager</tt>.
   *
   * @param listener the <tt>UserCapsNodeListener</tt> which is interested in events notifying about
   *     changes in the list of user caps nodes of this <tt>EntityCapsManager</tt>
   */
  public void addUserCapsNodeListener(UserCapsNodeListener listener) {
    if (listener == null) throw new NullPointerException("listener");
    synchronized (userCapsNodeListeners) {
      if (!userCapsNodeListeners.contains(listener)) userCapsNodeListeners.add(listener);
    }
  }

  /**
   * Remove records telling what entity caps node a contact has.
   *
   * @param contact the contact
   */
  public void removeContactCapsNode(Contact contact) {
    Caps caps = null;
    String lastRemovedJid = null;

    Iterator<String> iter = userCaps.keySet().iterator();
    while (iter.hasNext()) {
      String jid = iter.next();

      if (StringUtils.parseBareAddress(jid).equals(contact.getAddress())) {
        caps = userCaps.get(jid);
        lastRemovedJid = jid;
        iter.remove();
      }
    }

    // fire only for the last one, at the end the event out
    // of the protocol will be one and for the contact
    if (caps != null) {
      UserCapsNodeListener[] listeners;
      synchronized (userCapsNodeListeners) {
        listeners = userCapsNodeListeners.toArray(NO_USER_CAPS_NODE_LISTENERS);
      }
      if (listeners.length != 0) {
        String nodeVer = caps.getNodeVer();

        for (UserCapsNodeListener listener : listeners)
          listener.userCapsNodeRemoved(lastRemovedJid, nodeVer, false);
      }
    }
  }

  /**
   * Remove a record telling what entity caps node a user has.
   *
   * @param user the user (Full JID)
   */
  public void removeUserCapsNode(String user) {
    Caps caps = userCaps.remove(user);

    // Fire userCapsNodeRemoved.
    if (caps != null) {
      UserCapsNodeListener[] listeners;

      synchronized (userCapsNodeListeners) {
        listeners = userCapsNodeListeners.toArray(NO_USER_CAPS_NODE_LISTENERS);
      }
      if (listeners.length != 0) {
        String nodeVer = caps.getNodeVer();

        for (UserCapsNodeListener listener : listeners)
          listener.userCapsNodeRemoved(user, nodeVer, false);
      }
    }
  }

  /**
   * Removes a specific <tt>UserCapsNodeListener</tt> from the list of
   * <tt>UserCapsNodeListener</tt>s interested in events notifying about changes in the list of user
   * caps nodes of this <tt>EntityCapsManager</tt>.
   *
   * @param listener the <tt>UserCapsNodeListener</tt> which is no longer interested in events
   *     notifying about changes in the list of user caps nodes of this <tt>EntityCapsManager</tt>
   */
  public void removeUserCapsNodeListener(UserCapsNodeListener listener) {
    if (listener != null) {
      synchronized (userCapsNodeListeners) {
        userCapsNodeListeners.remove(listener);
      }
    }
  }

  /**
   * Gets the <tt>Caps</tt> i.e. the node, the hash and the ver of a user.
   *
   * @param user the user (Full JID)
   * @return the <tt>Caps</tt> i.e. the node, the hash and the ver of <tt>user</tt>
   */
  public Caps getCapsByUser(String user) {
    return userCaps.get(user);
  }

  /**
   * Get the discover info given a user name. The discover info is returned if the user has a
   * node#ver associated with it and the node#ver has a discover info associated with it.
   *
   * @param user user name (Full JID)
   * @return the discovered info
   */
  public DiscoverInfo getDiscoverInfoByUser(String user) {
    Caps caps = userCaps.get(user);

    return (caps == null) ? null : getDiscoverInfoByCaps(caps);
  }

  /**
   * Get our own caps version.
   *
   * @return our own caps version
   */
  public String getCapsVersion() {
    return currentCapsVersion;
  }

  /**
   * Get our own entity node.
   *
   * @return our own entity node.
   */
  public String getNode() {
    return entityNode;
  }

  /**
   * Set our own entity node.
   *
   * @param node the new node
   */
  public void setNode(String node) {
    entityNode = node;
  }

  /**
   * Retrieve DiscoverInfo for a specific node.
   *
   * @param caps the <tt>Caps</tt> i.e. the node, the hash and the ver
   * @return The corresponding DiscoverInfo or null if none is known.
   */
  public static DiscoverInfo getDiscoverInfoByCaps(Caps caps) {
    synchronized (caps2discoverInfo) {
      DiscoverInfo discoverInfo = caps2discoverInfo.get(caps);

      /*
       * If we don't have the discoverInfo in the runtime cache yet, we
       * may have it remembered in a previous application instance.
       */
      if (discoverInfo == null) {
        ConfigurationService configurationService = getConfigService();
        String capsPropertyName = getCapsPropertyName(caps);
        String xml = configurationService.getString(capsPropertyName);

        if ((xml != null) && (xml.length() != 0)) {
          IQProvider discoverInfoProvider =
              (IQProvider)
                  ProviderManager.getInstance()
                      .getIQProvider("query", "http://jabber.org/protocol/disco#info");

          if (discoverInfoProvider != null) {
            XmlPullParser parser = new MXParser();

            try {
              parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
              parser.setInput(new StringReader(xml));
              // Start the parser.
              parser.next();
            } catch (XmlPullParserException xppex) {
              parser = null;
            } catch (IOException ioex) {
              parser = null;
            }

            if (parser != null) {
              try {
                discoverInfo = (DiscoverInfo) discoverInfoProvider.parseIQ(parser);
              } catch (Exception ex) {
              }

              if (discoverInfo != null) {
                if (caps.isValid(discoverInfo)) caps2discoverInfo.put(caps, discoverInfo);
                else {
                  logger.error(
                      "Invalid DiscoverInfo for " + caps.getNodeVer() + ": " + discoverInfo);
                  /*
                   * The discoverInfo doesn't seem valid
                   * according to the caps which means that we
                   * must have stored invalid information.
                   * Delete the invalid information in order
                   * to not try to validate it again.
                   */
                  configurationService.removeProperty(capsPropertyName);
                }
              }
            }
          }
        }
      }
      return discoverInfo;
    }
  }

  /**
   * Removes from, to and packet-id from <tt>info</tt>.
   *
   * @param info the {@link DiscoverInfo} that we'd like to cleanup.
   */
  private static void cleanupDiscoverInfo(DiscoverInfo info) {
    info.setFrom(null);
    info.setTo(null);
    info.setPacketID(null);
  }

  /**
   * Gets the features of a specific <tt>DiscoverInfo</tt> in the form of a read-only
   * <tt>Feature</tt> <tt>Iterator<tt/> by calling the internal method {@link
   * DiscoverInfo#getFeatures()}.
   *
   * @param discoverInfo the <tt>DiscoverInfo</tt> the features of which are to be retrieved
   * @return a read-only <tt>Feature</tt> <tt>Iterator</tt> which lists the features of the
   *     specified <tt>discoverInfo</tt>
   */
  @SuppressWarnings("unchecked")
  private static Iterator<DiscoverInfo.Feature> getDiscoverInfoFeatures(DiscoverInfo discoverInfo) {
    Method getFeaturesMethod;

    try {
      getFeaturesMethod = DiscoverInfo.class.getDeclaredMethod("getFeatures");
    } catch (NoSuchMethodException nsmex) {
      throw new UndeclaredThrowableException(nsmex);
    }
    getFeaturesMethod.setAccessible(true);
    try {
      return (Iterator<DiscoverInfo.Feature>) getFeaturesMethod.invoke(discoverInfo);
    } catch (IllegalAccessException iaex) {
      throw new UndeclaredThrowableException(iaex);
    } catch (InvocationTargetException itex) {
      throw new UndeclaredThrowableException(itex);
    }
  }

  /**
   * Registers this Manager's listener with <tt>connection</tt>.
   *
   * @param connection the connection that we'd like this manager to register with.
   */
  public void addPacketListener(XMPPConnection connection) {
    PacketFilter filter =
        new AndFilter(
            new PacketTypeFilter(Presence.class),
            new PacketExtensionFilter(
                CapsPacketExtension.ELEMENT_NAME, CapsPacketExtension.NAMESPACE));

    connection.addPacketListener(new CapsPacketListener(), filter);
  }

  /**
   * Adds <tt>listener</tt> to the list of {@link CapsVerListener}s that we notify when new features
   * occur and the version hash needs to be regenerated. The method would also notify
   * <tt>listener</tt> if our current caps version has been generated and is different than
   * <tt>null</tt>.
   *
   * @param listener the {@link CapsVerListener} we'd like to register.
   */
  public void addCapsVerListener(CapsVerListener listener) {
    synchronized (capsVerListeners) {
      if (capsVerListeners.contains(listener)) return;

      capsVerListeners.add(listener);

      if (currentCapsVersion != null) listener.capsVerUpdated(currentCapsVersion);
    }
  }

  /**
   * Removes <tt>listener</tt> from the list of currently registered {@link CapsVerListener}s.
   *
   * @param listener the {@link CapsVerListener} we'd like to unregister.
   */
  public void removeCapsVerListener(CapsVerListener listener) {
    synchronized (capsVerListeners) {
      capsVerListeners.remove(listener);
    }
  }

  /**
   * Notifies all currently registered {@link CapsVerListener}s that the version hash has changed.
   */
  private void fireCapsVerChanged() {
    List<CapsVerListener> listenersCopy = null;

    synchronized (capsVerListeners) {
      listenersCopy = new ArrayList<CapsVerListener>(capsVerListeners);
    }

    for (CapsVerListener listener : listenersCopy) listener.capsVerUpdated(currentCapsVersion);
  }

  /**
   * Computes and returns the hash of the specified <tt>capsString</tt> using the specified
   * <tt>hashAlgorithm</tt>.
   *
   * @param hashAlgorithm the name of the algorithm to be used to generate the hash
   * @param capsString the capabilities string that we'd like to compute a hash for.
   * @return the hash of <tt>capsString</tt> computed by the specified <tt>hashAlgorithm</tt> or
   *     <tt>null</tt> if generating the hash has failed
   */
  private static String capsToHash(String hashAlgorithm, String capsString) {
    try {
      MessageDigest md = MessageDigest.getInstance(hashAlgorithm);
      byte[] digest = md.digest(capsString.getBytes());

      return Base64.encodeBytes(digest);
    } catch (NoSuchAlgorithmException nsae) {
      logger.error("Unsupported XEP-0115: Entity Capabilities hash algorithm: " + hashAlgorithm);
      return null;
    }
  }

  /**
   * Converts the form field values in the <tt>ffValuesIter</tt> into a caps string.
   *
   * @param ffValuesIter the {@link Iterator} containing the form field values.
   * @param capsBldr a <tt>StringBuilder</tt> to which the caps string representing the form field
   *     values is to be appended
   */
  private static void formFieldValuesToCaps(Iterator<String> ffValuesIter, StringBuilder capsBldr) {
    SortedSet<String> fvs = new TreeSet<String>();

    while (ffValuesIter.hasNext()) fvs.add(ffValuesIter.next());

    for (String fv : fvs) capsBldr.append(fv).append('<');
  }

  /**
   * Calculates the <tt>String</tt> for a specific <tt>DiscoverInfo</tt> which is to be hashed in
   * order to compute the ver string for that <tt>DiscoverInfo</tt>.
   *
   * @param discoverInfo the <tt>DiscoverInfo</tt> for which the <tt>String</tt> to be hashed in
   *     order to compute its ver string is to be calculated
   * @return the <tt>String</tt> for <tt>discoverInfo</tt> which is to be hashed in order to compute
   *     its ver string
   */
  private static String calculateEntityCapsString(DiscoverInfo discoverInfo) {
    StringBuilder bldr = new StringBuilder();

    // Add identities
    {
      Iterator<DiscoverInfo.Identity> identities = discoverInfo.getIdentities();
      SortedSet<DiscoverInfo.Identity> is =
          new TreeSet<DiscoverInfo.Identity>(
              new Comparator<DiscoverInfo.Identity>() {
                public int compare(DiscoverInfo.Identity i1, DiscoverInfo.Identity i2) {
                  int category = i1.getCategory().compareTo(i2.getCategory());

                  if (category != 0) return category;

                  int type = i1.getType().compareTo(i2.getType());

                  if (type != 0) return type;

                  /*
                   * TODO Sort by xml:lang.
                   *
                   * Since sort by xml:lang is currently missing,
                   * use the last supported sort criterion i.e.
                   * type.
                   */
                  return type;
                }
              });

      if (identities != null) while (identities.hasNext()) is.add(identities.next());

      for (DiscoverInfo.Identity i : is) {
        bldr.append(i.getCategory())
            .append('/')
            .append(i.getType())
            .append("//")
            .append(i.getName())
            .append('<');
      }
    }

    // Add features
    {
      Iterator<DiscoverInfo.Feature> features = getDiscoverInfoFeatures(discoverInfo);
      SortedSet<String> fs = new TreeSet<String>();

      if (features != null) while (features.hasNext()) fs.add(features.next().getVar());

      for (String f : fs) bldr.append(f).append('<');
    }

    DataForm extendedInfo = (DataForm) discoverInfo.getExtension("x", "jabber:x:data");

    if (extendedInfo != null) {
      synchronized (extendedInfo) {
        SortedSet<FormField> fs =
            new TreeSet<FormField>(
                new Comparator<FormField>() {
                  public int compare(FormField f1, FormField f2) {
                    return f1.getVariable().compareTo(f2.getVariable());
                  }
                });

        FormField formType = null;

        for (Iterator<FormField> fieldsIter = extendedInfo.getFields(); fieldsIter.hasNext(); ) {
          FormField f = fieldsIter.next();
          if (!f.getVariable().equals("FORM_TYPE")) fs.add(f);
          else formType = f;
        }

        // Add FORM_TYPE values
        if (formType != null) formFieldValuesToCaps(formType.getValues(), bldr);

        // Add the other values
        for (FormField f : fs) {
          bldr.append(f.getVariable()).append('<');
          formFieldValuesToCaps(f.getValues(), bldr);
        }
      }
    }

    return bldr.toString();
  }

  /**
   * Calculates the ver string for the specified <tt>discoverInfo</tt>, identity type, name
   * features, and extendedInfo.
   *
   * @param discoverInfo the {@link DiscoverInfo} we'd be creating a ver <tt>String</tt> for
   */
  public void calculateEntityCapsVersion(DiscoverInfo discoverInfo) {
    setCurrentCapsVersion(
        discoverInfo,
        capsToHash(CapsPacketExtension.HASH_METHOD, calculateEntityCapsString(discoverInfo)));
  }

  /**
   * Set our own caps version.
   *
   * @param discoverInfo the {@link DiscoverInfo} that we'd like to map to the <tt>capsVersion</tt>.
   * @param capsVersion the new caps version
   */
  public void setCurrentCapsVersion(DiscoverInfo discoverInfo, String capsVersion) {
    Caps caps = new Caps(getNode(), CapsPacketExtension.HASH_METHOD, capsVersion, null);

    /*
     * DiscoverInfo carries the node and the ver and we're now setting a new
     * ver so we should update the DiscoveryInfo.
     */
    discoverInfo.setNode(caps.getNodeVer());

    if (!caps.isValid(discoverInfo)) {
      throw new IllegalArgumentException(
          "The specified discoverInfo must be valid with respect"
              + " to the specified capsVersion");
    }

    currentCapsVersion = capsVersion;
    addDiscoverInfoByCaps(caps, discoverInfo);
    fireCapsVerChanged();
  }

  /** The {@link PacketListener} that will be registering incoming caps. */
  private class CapsPacketListener implements PacketListener {
    /**
     * Handles incoming presence packets and maps jids to node#ver strings.
     *
     * @param packet the incoming presence <tt>Packet</tt> to be handled
     * @see PacketListener#processPacket(Packet)
     */
    public void processPacket(Packet packet) {
      CapsPacketExtension ext =
          (CapsPacketExtension)
              packet.getExtension(CapsPacketExtension.ELEMENT_NAME, CapsPacketExtension.NAMESPACE);

      /*
       * Before Version 1.4 of XEP-0115: Entity Capabilities, the 'ver'
       * attribute was generated differently and the 'hash' attribute was
       * absent. The 'ver' attribute in Version 1.3 represents the
       * specific version of the client and thus does not provide a way to
       * validate the DiscoverInfo sent by the client. If
       * EntityCapsManager receives no 'hash' attribute, it will assume
       * the legacy format and will not cache it because the DiscoverInfo
       * to be received from the client later on will not be trustworthy.
       */
      String hash = ext.getHash();

      /* Google Talk web does not set hash but we need it to be cached */
      if (hash == null) hash = "";

      if (hash != null) {
        // Check it the packet indicates  that the user is online. We
        // will use this information to decide if we're going to send
        // the discover info request.
        boolean online = (packet instanceof Presence) && ((Presence) packet).isAvailable();

        if (online) {
          addUserCapsNode(
              packet.getFrom(), ext.getNode(), hash, ext.getVersion(), ext.getExtensions(), online);
        } else {
          removeUserCapsNode(packet.getFrom());
        }
      }
    }
  }

  /**
   * Implements an immutable value which stands for a specific node, a specific hash (algorithm) and
   * a specific ver.
   *
   * @author Lyubomir Marinov
   */
  public static class Caps {
    /** The hash (algorithm) of this <tt>Caps</tt> value. */
    public final String hash;

    /** The node of this <tt>Caps</tt> value. */
    public final String node;

    /** The ext info of this <tt>Caps</tt> value. */
    public String ext;

    /**
     * The String which is the concatenation of {@link #node} and the {@link #ver} separated by the
     * character '#'. Cached for the sake of efficiency.
     */
    private final String nodeVer;

    /** The ver of this <tt>Caps</tt> value. */
    public final String ver;

    /**
     * Initializes a new <tt>Caps</tt> instance which is to represent a specific node, a specific
     * hash (algorithm) and a specific ver.
     *
     * @param node the node to be represented by the new instance
     * @param hash the hash (algorithm) to be represented by the new instance
     * @param ver the ver to be represented by the new instance
     * @param ext the ext to be represented by the new instance
     */
    public Caps(String node, String hash, String ver, String ext) {
      if (node == null) throw new NullPointerException("node");
      if (hash == null) throw new NullPointerException("hash");
      if (ver == null) throw new NullPointerException("ver");

      this.node = node;
      this.hash = hash;
      this.ver = ver;
      this.ext = ext;

      this.nodeVer = this.node + '#' + this.ver;
    }

    /**
     * Gets a <tt>String</tt> which represents the concatenation of the <tt>node</tt> property of
     * this instance, the character '#' and the <tt>ver</tt> property of this instance.
     *
     * @return a <tt>String</tt> which represents the concatenation of the <tt>node</tt> property of
     *     this instance, the character '#' and the <tt>ver</tt> property of this instance
     */
    public final String getNodeVer() {
      return nodeVer;
    }

    /**
     * Determines whether a specific <tt>DiscoverInfo</tt> is valid according to this <tt>Caps</tt>
     * i.e. whether the <tt>discoverInfo</tt> has the node and the ver of this <tt>Caps</tt> and the
     * ver calculated from the <tt>discoverInfo</tt> using the hash (algorithm) of this
     * <tt>Caps</tt> is equal to the ver of this <tt>Caps</tt>.
     *
     * @param discoverInfo the <tt>DiscoverInfo</tt> to be validated by this <tt>Caps</tt>
     * @return <tt>true</tt> if the specified <tt>DiscoverInfo</tt> has the node and the ver of this
     *     <tt>Caps</tt> and the ver calculated from the <tt>discoverInfo</tt> using the hash
     *     (algorithm) of this <tt>Caps</tt> is equal to the ver of this <tt>Caps</tt>; otherwise,
     *     <tt>false</tt>
     */
    public boolean isValid(DiscoverInfo discoverInfo) {
      if (discoverInfo != null) {
        // The "node" attribute is not necessary in the query element.
        // For example, Swift does not send back the "node" attribute in
        // the Disco#info response. Thus, if the node of the IQ response
        // is null, then we set it to the request one.
        if (discoverInfo.getNode() == null) {
          discoverInfo.setNode(getNodeVer());
        }

        if (getNodeVer().equals(discoverInfo.getNode())
            && !hash.equals("")
            && ver.equals(capsToHash(hash, calculateEntityCapsString(discoverInfo)))) {
          return true;
        }
      }
      return false;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      Caps caps = (Caps) o;

      if (!hash.equals(caps.hash)) return false;
      if (!node.equals(caps.node)) return false;
      if (!ver.equals(caps.ver)) return false;

      return true;
    }

    @Override
    public int hashCode() {
      int result = hash.hashCode();
      result = 31 * result + node.hashCode();
      result = 31 * result + ver.hashCode();
      return result;
    }
  }
}
示例#7
0
/**
 * A {@link AbstractPluginComponent} that registers the Off-the-Record button in the main chat
 * toolbar.
 *
 * @author George Politis
 * @author Marin Dzhigarov
 */
public class OtrMetaContactButton extends AbstractPluginComponent
    implements ScOtrEngineListener, ScOtrKeyManagerListener {
  /** The logger */
  private final Logger logger = Logger.getLogger(OtrMetaContactButton.class);

  private SIPCommButton button;

  private OtrContact otrContact;

  private AnimatedImage animatedPadlockImage;

  private Image finishedPadlockImage;

  private Image verifiedLockedPadlockImage;

  private Image unverifiedLockedPadlockImage;

  private Image unlockedPadlockImage;

  private Image timedoutPadlockImage;

  public void sessionStatusChanged(OtrContact otrContact) {
    // OtrMetaContactButton.this.contact can be null.
    if (otrContact.equals(OtrMetaContactButton.this.otrContact)) {
      setStatus(OtrActivator.scOtrEngine.getSessionStatus(otrContact));
    }
  }

  public void contactPolicyChanged(Contact contact) {
    // OtrMetaContactButton.this.contact can be null.
    if (OtrMetaContactButton.this.otrContact != null
        && contact.equals(OtrMetaContactButton.this.otrContact.contact)) {
      setPolicy(OtrActivator.scOtrEngine.getContactPolicy(contact));
    }
  }

  public void globalPolicyChanged() {
    if (OtrMetaContactButton.this.otrContact != null)
      setPolicy(OtrActivator.scOtrEngine.getContactPolicy(otrContact.contact));
  }

  public void contactVerificationStatusChanged(OtrContact otrContact) {
    // OtrMetaContactButton.this.contact can be null.
    if (otrContact.equals(OtrMetaContactButton.this.otrContact)) {
      setStatus(OtrActivator.scOtrEngine.getSessionStatus(otrContact));
    }
  }

  public OtrMetaContactButton(Container container, PluginComponentFactory parentFactory) {
    super(container, parentFactory);

    /*
     * XXX This OtrMetaContactButton instance cannot be added as a listener
     * to scOtrEngine and scOtrKeyManager without being removed later on
     * because the latter live forever. Unfortunately, the dispose() method
     * of this instance is never executed. OtrWeakListener will keep this
     * instance as a listener of scOtrEngine and scOtrKeyManager for as long
     * as this instance is necessary. And this instance will be strongly
     * referenced by the JMenuItems which depict it. So when the JMenuItems
     * are gone, this instance will become obsolete and OtrWeakListener will
     * remove it as a listener of scOtrEngine and scOtrKeyManager.
     */
    new OtrWeakListener<OtrMetaContactButton>(
        this, OtrActivator.scOtrEngine, OtrActivator.scOtrKeyManager);
  }

  /**
   * Gets the <code>SIPCommButton</code> which is the component of this plugin. If the button
   * doesn't exist, it's created.
   *
   * @return the <code>SIPCommButton</code> which is the component of this plugin
   */
  @SuppressWarnings("fallthrough")
  private SIPCommButton getButton() {
    if (button == null) {
      button = new SIPCommButton(null, null);
      button.setEnabled(false);
      button.setPreferredSize(new Dimension(25, 25));

      button.setToolTipText(
          OtrActivator.resourceService.getI18NString("plugin.otr.menu.OTR_TOOLTIP"));

      Image i1 = null, i2 = null, i3 = null;
      try {
        i1 =
            ImageIO.read(
                OtrActivator.resourceService.getImageURL("plugin.otr.LOADING_ICON1_22x22"));
        i2 =
            ImageIO.read(
                OtrActivator.resourceService.getImageURL("plugin.otr.LOADING_ICON2_22x22"));
        i3 =
            ImageIO.read(
                OtrActivator.resourceService.getImageURL("plugin.otr.LOADING_ICON3_22x22"));
        finishedPadlockImage =
            ImageIO.read(
                OtrActivator.resourceService.getImageURL("plugin.otr.FINISHED_ICON_22x22"));
        verifiedLockedPadlockImage =
            ImageIO.read(
                OtrActivator.resourceService.getImageURL("plugin.otr.ENCRYPTED_ICON_22x22"));
        unverifiedLockedPadlockImage =
            ImageIO.read(
                OtrActivator.resourceService.getImageURL(
                    "plugin.otr.ENCRYPTED_UNVERIFIED_ICON_22x22"));
        unlockedPadlockImage =
            ImageIO.read(
                OtrActivator.resourceService.getImageURL("plugin.otr.PLAINTEXT_ICON_22x22"));
        timedoutPadlockImage =
            ImageIO.read(OtrActivator.resourceService.getImageURL("plugin.otr.BROKEN_ICON_22x22"));
      } catch (IOException e) {
        logger.debug("Failed to load padlock image");
      }

      animatedPadlockImage = new AnimatedImage(button, i1, i2, i3);

      button.addActionListener(
          new ActionListener() {
            public void actionPerformed(ActionEvent e) {
              if (otrContact == null) return;

              switch (OtrActivator.scOtrEngine.getSessionStatus(otrContact)) {
                case ENCRYPTED:
                  OtrPolicy policy = OtrActivator.scOtrEngine.getContactPolicy(otrContact.contact);
                  policy.setSendWhitespaceTag(false);
                  OtrActivator.scOtrEngine.setContactPolicy(otrContact.contact, policy);
                case FINISHED:
                case LOADING:
                  // Default action for finished, encrypted and loading
                  // sessions is end session.
                  OtrActivator.scOtrEngine.endSession(otrContact);
                  break;
                case TIMED_OUT:
                case PLAINTEXT:
                  policy = OtrActivator.scOtrEngine.getContactPolicy(otrContact.contact);
                  OtrPolicy globalPolicy = OtrActivator.scOtrEngine.getGlobalPolicy();
                  policy.setSendWhitespaceTag(globalPolicy.getSendWhitespaceTag());
                  OtrActivator.scOtrEngine.setContactPolicy(otrContact.contact, policy);
                  // Default action for timed_out and plaintext sessions
                  // is start session.
                  OtrActivator.scOtrEngine.startSession(otrContact);
                  break;
              }
            }
          });
    }
    return button;
  }

  /*
   * Implements PluginComponent#getComponent(). Returns the SIPCommButton
   * which is the component of this plugin creating it first if it doesn't
   * exist.
   */
  public Object getComponent() {
    return getButton();
  }

  /*
   * Implements PluginComponent#getName().
   */
  public String getName() {
    return "";
  }

  /*
   * Implements PluginComponent#setCurrentContact(Contact).
   */
  @Override
  public void setCurrentContact(Contact contact) {
    setCurrentContact(contact, null);
  }

  public void setCurrentContact(Contact contact, String resourceName) {
    if (contact == null) {
      this.otrContact = null;
      this.setPolicy(null);
      this.setStatus(ScSessionStatus.PLAINTEXT);
      return;
    }

    if (resourceName == null) {
      OtrContact otrContact = OtrContactManager.getOtrContact(contact, null);
      if (this.otrContact == otrContact) return;
      this.otrContact = otrContact;
      this.setStatus(OtrActivator.scOtrEngine.getSessionStatus(otrContact));
      this.setPolicy(OtrActivator.scOtrEngine.getContactPolicy(contact));
      return;
    }
    for (ContactResource resource : contact.getResources()) {
      if (resource.getResourceName().equals(resourceName)) {
        OtrContact otrContact = OtrContactManager.getOtrContact(contact, resource);
        if (this.otrContact == otrContact) return;
        this.otrContact = otrContact;
        this.setStatus(OtrActivator.scOtrEngine.getSessionStatus(otrContact));
        this.setPolicy(OtrActivator.scOtrEngine.getContactPolicy(contact));
        return;
      }
    }
    logger.debug("Could not find resource for contact " + contact);
  }

  /*
   * Implements PluginComponent#setCurrentContact(MetaContact).
   */
  @Override
  public void setCurrentContact(MetaContact metaContact) {
    setCurrentContact((metaContact == null) ? null : metaContact.getDefaultContact());
  }

  /**
   * Sets the button enabled status according to the passed in {@link OtrPolicy}.
   *
   * @param contactPolicy the {@link OtrPolicy}.
   */
  private void setPolicy(OtrPolicy contactPolicy) {
    getButton().setEnabled(contactPolicy != null && contactPolicy.getEnableManual());
  }

  /**
   * Sets the button icon according to the passed in {@link SessionStatus}.
   *
   * @param status the {@link SessionStatus}.
   */
  private void setStatus(ScSessionStatus status) {
    animatedPadlockImage.pause();
    Image image;
    String tipKey;
    switch (status) {
      case ENCRYPTED:
        PublicKey pubKey = OtrActivator.scOtrEngine.getRemotePublicKey(otrContact);
        String fingerprint = OtrActivator.scOtrKeyManager.getFingerprintFromPublicKey(pubKey);
        image =
            OtrActivator.scOtrKeyManager.isVerified(otrContact.contact, fingerprint)
                ? verifiedLockedPadlockImage
                : unverifiedLockedPadlockImage;
        tipKey =
            OtrActivator.scOtrKeyManager.isVerified(otrContact.contact, fingerprint)
                ? "plugin.otr.menu.VERIFIED"
                : "plugin.otr.menu.UNVERIFIED";
        break;
      case FINISHED:
        image = finishedPadlockImage;
        tipKey = "plugin.otr.menu.FINISHED";
        break;
      case PLAINTEXT:
        image = unlockedPadlockImage;
        tipKey = "plugin.otr.menu.START_OTR";
        break;
      case LOADING:
        image = animatedPadlockImage;
        animatedPadlockImage.start();
        tipKey = "plugin.otr.menu.LOADING_OTR";
        break;
      case TIMED_OUT:
        image = timedoutPadlockImage;
        tipKey = "plugin.otr.menu.TIMED_OUT";
        break;
      default:
        return;
    }

    SIPCommButton button = getButton();
    button.setIconImage(image);
    button.setToolTipText(OtrActivator.resourceService.getI18NString(tipKey));
    button.repaint();
  }

  @Override
  public void multipleInstancesDetected(OtrContact contact) {}

  @Override
  public void outgoingSessionChanged(OtrContact otrContact) {
    // OtrMetaContactButton.this.contact can be null.
    if (otrContact.equals(OtrMetaContactButton.this.otrContact)) {
      setStatus(OtrActivator.scOtrEngine.getSessionStatus(otrContact));
    }
  }
}