/** * 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; }
/** * 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 "TCP-Based Media * Transport in the Session Description Protocol (SDP)" 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 <id> 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() + "[]"; } }
/** * 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 -> 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; } } }
/** * 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)); } } }