All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.io.ssl.SslConnection Maven / Gradle / Ivy

There is a newer version: 7.4.3.112-ga112
Show newest version
//
//  ========================================================================
//  Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.io.ssl;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;

import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLEngineResult.Status;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;

import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.AbstractEndPoint;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.WriteFlusher;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.Invocable;

/**
 * A Connection that acts as an interceptor between an EndPoint providing SSL encrypted data
 * and another consumer of an EndPoint (typically an {@link Connection} like HttpConnection) that
 * wants unencrypted data.
 * 

* The connector uses an {@link EndPoint} (typically SocketChannelEndPoint) as * it's source/sink of encrypted data. It then provides an endpoint via {@link #getDecryptedEndPoint()} to * expose a source/sink of unencrypted data to another connection (eg HttpConnection). *

* The design of this class is based on a clear separation between the passive methods, which do not block nor schedule any * asynchronous callbacks, and active methods that do schedule asynchronous callbacks. *

* The passive methods are {@link DecryptedEndPoint#fill(ByteBuffer)} and {@link DecryptedEndPoint#flush(ByteBuffer...)}. They make best * effort attempts to progress the connection using only calls to the encrypted {@link EndPoint#fill(ByteBuffer)} and {@link EndPoint#flush(ByteBuffer...)} * methods. They will never block nor schedule any readInterest or write callbacks. If a fill/flush cannot progress either because * of network congestion or waiting for an SSL handshake message, then the fill/flush will simply return with zero bytes filled/flushed. * Specifically, if a flush cannot proceed because it needs to receive a handshake message, then the flush will attempt to fill bytes from the * encrypted endpoint, but if insufficient bytes are read it will NOT call {@link EndPoint#fillInterested(Callback)}. *

* It is only the active methods : {@link DecryptedEndPoint#fillInterested(Callback)} and * {@link DecryptedEndPoint#write(Callback, ByteBuffer...)} that may schedule callbacks by calling the encrypted * {@link EndPoint#fillInterested(Callback)} and {@link EndPoint#write(Callback, ByteBuffer...)} * methods. For normal data handling, the decrypted fillInterest method will result in an encrypted fillInterest and a decrypted * write will result in an encrypted write. However, due to SSL handshaking requirements, it is also possible for a decrypted fill * to call the encrypted write and for the decrypted flush to call the encrypted fillInterested methods. *

* MOST IMPORTANTLY, the encrypted callbacks from the active methods (#onFillable() and WriteFlusher#completeWrite()) do no filling or flushing * themselves. Instead they simple make the callbacks to the decrypted callbacks, so that the passive encrypted fill/flush will * be called again and make another best effort attempt to progress the connection. * */ public class SslConnection extends AbstractConnection { private static final Logger LOG = Log.getLogger(SslConnection.class); private static final String TLS_1_3 = "TLSv1.3"; private enum Handshake { INITIAL, SUCCEEDED, FAILED } private enum FillState { IDLE, // Not Filling any data INTERESTED, // We have a pending read interest WAIT_FOR_FLUSH // Waiting for a flush to happen } private enum FlushState { IDLE, // Not flushing any data WRITING, // We have a pending write of encrypted data WAIT_FOR_FILL // Waiting for a fill to happen } private final List handshakeListeners = new ArrayList<>(); private final ByteBufferPool _bufferPool; private final SSLEngine _sslEngine; private final DecryptedEndPoint _decryptedEndPoint; private ByteBuffer _decryptedInput; private ByteBuffer _encryptedInput; private ByteBuffer _encryptedOutput; private final boolean _encryptedDirectBuffers; private final boolean _decryptedDirectBuffers; private boolean _renegotiationAllowed; private int _renegotiationLimit = -1; private boolean _closedOutbound; private boolean _allowMissingCloseMessage = true; private FlushState _flushState = FlushState.IDLE; private FillState _fillState = FillState.IDLE; private AtomicReference _handshake = new AtomicReference<>(Handshake.INITIAL); private boolean _underflown; private abstract class RunnableTask implements Runnable, Invocable { private final String _operation; protected RunnableTask(String op) { _operation=op; } @Override public String toString() { return String.format("SSL:%s:%s:%s",SslConnection.this,_operation,getInvocationType()); } } private final Runnable _runFillable = new RunnableTask("runFillable") { @Override public void run() { _decryptedEndPoint.getFillInterest().fillable(); } @Override public InvocationType getInvocationType() { return getDecryptedEndPoint().getFillInterest().getCallbackInvocationType(); } }; private final Callback _sslReadCallback = new Callback() { @Override public void succeeded() { onFillable(); } @Override public void failed(final Throwable x) { onFillInterestedFailed(x); } @Override public InvocationType getInvocationType() { return getDecryptedEndPoint().getFillInterest().getCallbackInvocationType(); } @Override public String toString() { return String.format("SSLC.NBReadCB@%x{%s}", SslConnection.this.hashCode(),SslConnection.this); } }; public SslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine sslEngine) { this(byteBufferPool, executor, endPoint, sslEngine, false, false); } public SslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine sslEngine, boolean useDirectBuffersForEncryption, boolean useDirectBuffersForDecryption) { // This connection does not execute calls to onFillable(), so they will be called by the selector thread. // onFillable() does not block and will only wakeup another thread to do the actual reading and handling. super(endPoint, executor); this._bufferPool = byteBufferPool; this._sslEngine = sslEngine; this._decryptedEndPoint = newDecryptedEndPoint(); this._encryptedDirectBuffers = useDirectBuffersForEncryption; this._decryptedDirectBuffers = useDirectBuffersForDecryption; } public void addHandshakeListener(SslHandshakeListener listener) { handshakeListeners.add(listener); } public boolean removeHandshakeListener(SslHandshakeListener listener) { return handshakeListeners.remove(listener); } protected DecryptedEndPoint newDecryptedEndPoint() { return new DecryptedEndPoint(); } public SSLEngine getSSLEngine() { return _sslEngine; } public DecryptedEndPoint getDecryptedEndPoint() { return _decryptedEndPoint; } public boolean isRenegotiationAllowed() { return _renegotiationAllowed; } public void setRenegotiationAllowed(boolean renegotiationAllowed) { _renegotiationAllowed = renegotiationAllowed; } /** * @return The number of renegotions allowed for this connection. When the limit * is 0 renegotiation will be denied. If the limit is less than 0 then no limit is applied. */ public int getRenegotiationLimit() { return _renegotiationLimit; } /** * @param renegotiationLimit The number of renegotions allowed for this connection. * When the limit is 0 renegotiation will be denied. If the limit is less than 0 then no limit is applied. * Default -1. */ public void setRenegotiationLimit(int renegotiationLimit) { _renegotiationLimit = renegotiationLimit; } public boolean isAllowMissingCloseMessage() { return _allowMissingCloseMessage; } public void setAllowMissingCloseMessage(boolean allowMissingCloseMessage) { this._allowMissingCloseMessage = allowMissingCloseMessage; } @Override public void onOpen() { super.onOpen(); getDecryptedEndPoint().getConnection().onOpen(); } @Override public void onClose() { _decryptedEndPoint.getConnection().onClose(); super.onClose(); } @Override public void close() { getDecryptedEndPoint().getConnection().close(); } @Override public boolean onIdleExpired() { return getDecryptedEndPoint().getConnection().onIdleExpired(); } @Override public void onFillable() { // onFillable means that there are encrypted bytes ready to be filled. // however we do not fill them here on this callback, but instead wakeup // the decrypted readInterest and/or writeFlusher so that they will attempt // to do the fill and/or flush again and these calls will do the actually // filling. if (LOG.isDebugEnabled()) LOG.debug(">c.onFillable {}", SslConnection.this); // We have received a close handshake, close the end point to send FIN. if (_decryptedEndPoint.isInputShutdown()) _decryptedEndPoint.close(); _decryptedEndPoint.onFillable(); if (LOG.isDebugEnabled()) LOG.debug("%s=>%s", getClass().getSimpleName(), hashCode(), _sslEngine.getHandshakeStatus(), ei,eo,di, _fillState,_flushState, _decryptedEndPoint.toEndPointString(), connection instanceof AbstractConnection ? ((AbstractConnection)connection).toConnectionString() : connection); } private void releaseEncryptedOutputBuffer() { if (!Thread.holdsLock(_decryptedEndPoint)) throw new IllegalStateException(); if (_encryptedOutput != null && !_encryptedOutput.hasRemaining()) { _bufferPool.release(_encryptedOutput); _encryptedOutput = null; } } public class DecryptedEndPoint extends AbstractEndPoint { private final Callback _incompleteWriteCallback = new IncompleteWriteCallback(); public DecryptedEndPoint() { // Disable idle timeout checking: no scheduler and -1 timeout for this instance. super(null); super.setIdleTimeout(-1); } @Override public long getIdleTimeout() { return getEndPoint().getIdleTimeout(); } @Override public void setIdleTimeout(long idleTimeout) { getEndPoint().setIdleTimeout(idleTimeout); } @Override public boolean isOpen() { return getEndPoint().isOpen(); } @Override public InetSocketAddress getLocalAddress() { return getEndPoint().getLocalAddress(); } @Override public InetSocketAddress getRemoteAddress() { return getEndPoint().getRemoteAddress(); } @Override protected WriteFlusher getWriteFlusher() { return super.getWriteFlusher(); } protected void onFillable() { try { // If we are handshaking, then wake up any waiting write as well as it may have been blocked on the read boolean waiting_for_fill; synchronized(_decryptedEndPoint) { if (LOG.isDebugEnabled()) LOG.debug("onFillable {}", SslConnection.this); _fillState = FillState.IDLE; waiting_for_fill = _flushState==FlushState.WAIT_FOR_FILL; } getFillInterest().fillable(); if (waiting_for_fill) { synchronized(_decryptedEndPoint) { waiting_for_fill = _flushState==FlushState.WAIT_FOR_FILL; } if (waiting_for_fill) fill(BufferUtil.EMPTY_BUFFER); } } catch (Throwable e) { close(e); } } protected void onFillableFail(Throwable failure) { // If we are handshaking, then wake up any waiting write as well as it may have been blocked on the read boolean fail = false; synchronized(_decryptedEndPoint) { if (LOG.isDebugEnabled()) LOG.debug("onFillableFail {}", SslConnection.this, failure); _fillState = FillState.IDLE; switch(_flushState) { case WAIT_FOR_FILL: _flushState = FlushState.IDLE; fail = true; break; default: break; } } // wake up whoever is doing the fill getFillInterest().onFail(failure); // Try to complete the write if (fail) { if (!getWriteFlusher().onFail(failure)) close(failure); } } @Override public void setConnection(Connection connection) { if (connection instanceof AbstractConnection) { AbstractConnection a = (AbstractConnection)connection; if (a.getInputBufferSize()<_sslEngine.getSession().getApplicationBufferSize()) a.setInputBufferSize(_sslEngine.getSession().getApplicationBufferSize()); } super.setConnection(connection); } public SslConnection getSslConnection() { return SslConnection.this; } @Override public int fill(ByteBuffer buffer) throws IOException { try { synchronized(_decryptedEndPoint) { if (LOG.isDebugEnabled()) LOG.debug(">fill {}", SslConnection.this); int filled = -2; try { if (_fillState!=FillState.IDLE) return filled = 0; // Do we already have some decrypted data? if (BufferUtil.hasContent(_decryptedInput)) return filled = BufferUtil.append(buffer,_decryptedInput); // loop filling and unwrapping until we have something while (true) { HandshakeStatus status = _sslEngine.getHandshakeStatus(); if (LOG.isDebugEnabled()) LOG.debug("fill {}", status); switch(status) { case NEED_UNWRAP: case NOT_HANDSHAKING: break; case NEED_TASK: _sslEngine.getDelegatedTask().run(); continue; case NEED_WRAP: if (_flushState==FlushState.IDLE && flush(BufferUtil.EMPTY_BUFFER)) continue; // handle in needsFillInterest return filled = 0; default: throw new IllegalStateException("Unexpected HandshakeStatus " + status); } if (_encryptedInput==null) _encryptedInput = _bufferPool.acquire(_sslEngine.getSession().getPacketBufferSize(), _encryptedDirectBuffers); // can we use the passed buffer if it is big enough ByteBuffer app_in; if (_decryptedInput == null) { if (BufferUtil.space(buffer) > _sslEngine.getSession().getApplicationBufferSize()) app_in = buffer; else app_in = _decryptedInput = _bufferPool.acquire(_sslEngine.getSession().getApplicationBufferSize(), _decryptedDirectBuffers); } else { app_in = _decryptedInput; BufferUtil.compact(_encryptedInput); } // Let's try reading some encrypted data... even if we have some already. int net_filled = getEndPoint().fill(_encryptedInput); if (LOG.isDebugEnabled()) LOG.debug("net filled={}", net_filled); if (net_filled > 0 && _handshake.get() == Handshake.INITIAL && isOutboundDone()) throw new SSLHandshakeException("Closed during handshake"); // Let's unwrap even if we have no net data because in that // case we want to fall through to the handshake handling int pos = BufferUtil.flipToFill(app_in); SSLEngineResult unwrapResult; try { _underflown = false; unwrapResult = _sslEngine.unwrap(_encryptedInput, app_in); } finally { BufferUtil.flipToFlush(app_in, pos); } if (LOG.isDebugEnabled()) LOG.debug("unwrap {} {} unwrapBuffer={} appBuffer={}", net_filled, unwrapResult.toString().replace('\n',' '), BufferUtil.toDetailString(app_in), BufferUtil.toDetailString(buffer)); SSLEngineResult.Status unwrap = unwrapResult.getStatus(); // Extra check on unwrapResultStatus == OK with zero bytes consumed // or produced is due to an SSL client on Android (see bug #454773). if (unwrap==Status.OK && unwrapResult.bytesConsumed() == 0 && unwrapResult.bytesProduced() == 0) unwrap = Status.BUFFER_UNDERFLOW; switch (unwrap) { case CLOSED: return filled = -1; case BUFFER_UNDERFLOW: if (net_filled > 0) continue; // try filling some more _underflown = true; if (net_filled < 0 && _sslEngine.getUseClientMode()) { closeInbound(); return filled = -1; } return filled = net_filled; case OK: { if (unwrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED) handshakeSucceeded(); if (isRenegotiating() && !allowRenegotiate()) return filled = -1; // If bytes were produced, don't bother with the handshake status; // pass the decrypted data to the application, which will perform // another call to fill() or flush(). if (unwrapResult.bytesProduced() > 0) { if (app_in==buffer) return filled = unwrapResult.bytesProduced(); return filled = BufferUtil.append(buffer,_decryptedInput); } break; } default: throw new IllegalStateException("Unexpected unwrap result " + unwrap); } } } catch (Throwable x) { handshakeFailed(x); if (_flushState==FlushState.WAIT_FOR_FILL) { _flushState=FlushState.IDLE; getExecutor().execute(()->_decryptedEndPoint.getWriteFlusher().onFail(x)); } throw x; } finally { if (_encryptedInput != null && !_encryptedInput.hasRemaining()) { _bufferPool.release(_encryptedInput); _encryptedInput = null; } if (_decryptedInput != null && !_decryptedInput.hasRemaining()) { _bufferPool.release(_decryptedInput); _decryptedInput = null; } if (_flushState==FlushState.WAIT_FOR_FILL) { _flushState=FlushState.IDLE; getExecutor().execute(()->_decryptedEndPoint.getWriteFlusher().completeWrite()); } if (LOG.isDebugEnabled()) LOG.debug("needFillInterest uf={} {}", _underflown, SslConnection.this); LOG.debug("ei={} di={}",BufferUtil.toDetailString(_encryptedInput),BufferUtil.toDetailString(_decryptedInput)); } if (_fillState!=FillState.IDLE) return; // Fillable if we have decrypted Input OR encrypted input that has not yet been underflown. fillable = BufferUtil.hasContent(_decryptedInput) || (BufferUtil.hasContent(_encryptedInput) && !_underflown); HandshakeStatus status = _sslEngine.getHandshakeStatus(); switch (status) { case NEED_TASK: // Pretend we are fillable fillable = true; break; case NEED_UNWRAP: case NOT_HANDSHAKING: if (!fillable) { interest = true; _fillState = FillState.INTERESTED; } break; case NEED_WRAP: if (!fillable) { _fillState = FillState.WAIT_FOR_FLUSH; if (_flushState == FlushState.IDLE) { _flushState = FlushState.WRITING; write = BufferUtil.hasContent(_encryptedOutput) ? _encryptedOutput : BufferUtil.EMPTY_BUFFER; } } break; default: throw new IllegalStateException("Unexpected HandshakeStatus " + status); } if (LOG.isDebugEnabled()) LOG.debug("0) _renegotiationLimit--; } } private void handshakeFailed(Throwable failure) { if (_handshake.compareAndSet(Handshake.INITIAL, Handshake.FAILED)) { if (LOG.isDebugEnabled()) LOG.debug("handshake failed {} {}", SslConnection.this, failure); if (!(failure instanceof SSLHandshakeException)) failure = new SSLHandshakeException(failure.getMessage()).initCause(failure); notifyHandshakeFailed(_sslEngine, failure); } } private void terminateInput() { try { _sslEngine.closeInbound(); } catch (Throwable x) { LOG.ignore(x); } } private void closeInbound() throws SSLException { HandshakeStatus handshakeStatus = _sslEngine.getHandshakeStatus(); try { _sslEngine.closeInbound(); } catch (SSLException x) { if (handshakeStatus == HandshakeStatus.NOT_HANDSHAKING && !isAllowMissingCloseMessage()) throw x; else LOG.ignore(x); } catch (Throwable x) { LOG.ignore(x); } } @Override public boolean flush(ByteBuffer... appOuts) throws IOException { try { synchronized(_decryptedEndPoint) { if (LOG.isDebugEnabled()) { LOG.debug(">flush {}", SslConnection.this); int i=0; for (ByteBuffer b : appOuts) LOG.debug("flush b[{}]={}", i++, BufferUtil.toDetailString(b)); } Boolean result = null; try { if (_flushState!=FlushState.IDLE) return result = false; // Keep going while we can make progress or until we are done while (true) { HandshakeStatus status = _sslEngine.getHandshakeStatus(); if (LOG.isDebugEnabled()) LOG.debug("flush {}",status); switch(status) { case NEED_WRAP: case NOT_HANDSHAKING: break; case NEED_TASK: _sslEngine.getDelegatedTask().run(); continue; case NEED_UNWRAP: if (_fillState==FillState.IDLE) { int filled = fill(BufferUtil.EMPTY_BUFFER); if (_sslEngine.getHandshakeStatus()!=status) continue; if (filled < 0) throw new IOException("Broken pipe"); } return result = false; default: throw new IllegalStateException("Unexpected HandshakeStatus " + status); } if (_encryptedOutput == null) _encryptedOutput = _bufferPool.acquire(_sslEngine.getSession().getPacketBufferSize(), _encryptedDirectBuffers); // We call sslEngine.wrap to try to take bytes from appOut buffers and encrypt them into the _netOut buffer BufferUtil.compact(_encryptedOutput); int pos = BufferUtil.flipToFill(_encryptedOutput); SSLEngineResult wrapResult; try { wrapResult = _sslEngine.wrap(appOuts, _encryptedOutput); if (LOG.isDebugEnabled()) LOG.debug("wrap {} {}", wrapResult.toString().replace('\n',' '), BufferUtil.toHexSummary(_encryptedOutput)); } finally { BufferUtil.flipToFlush(_encryptedOutput, pos); } // Was all the data consumed? boolean allConsumed=true; for (ByteBuffer b : appOuts) if (BufferUtil.hasContent(b)) allConsumed=false; // if we have net bytes, let's try to flush them boolean flushed = true; if (BufferUtil.hasContent(_encryptedOutput)) flushed = getEndPoint().flush(_encryptedOutput); if (LOG.isDebugEnabled()) LOG.debug("net flushed={}, ac={}", flushed, allConsumed); // Now deal with the results returned from the wrap Status wrap = wrapResult.getStatus(); switch (wrap) { case CLOSED: { // TODO: do we need to remember the CLOSED state or SSLEngine // TODO: will produce CLOSED again if wrap() is called again? if (!flushed) return result = false; getEndPoint().shutdownOutput(); if (allConsumed) return result = true; throw new IOException("Broken pipe"); } case BUFFER_OVERFLOW: if (!flushed) return result = false; continue; case OK: if (wrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED) handshakeSucceeded(); if (isRenegotiating() && !allowRenegotiate()) { getEndPoint().shutdownOutput(); if (allConsumed && BufferUtil.isEmpty(_encryptedOutput)) return result = true; throw new IOException("Broken pipe"); } if (!flushed) return result = false; if (allConsumed) return result = true; break; default: throw new IllegalStateException("Unexpected wrap result " + wrap); } if (getEndPoint().isOutputShutdown()) return false; } } catch (Throwable x) { handshakeFailed(x); throw x; } finally { releaseEncryptedOutputBuffer(); if (LOG.isDebugEnabled()) LOG.debug("onIncompleteFlush {} {}", SslConnection.this, BufferUtil.toDetailString(_encryptedOutput)); if (_flushState!=FlushState.IDLE) return; while(true) { HandshakeStatus status = _sslEngine.getHandshakeStatus(); switch(status) { case NEED_TASK: case NEED_WRAP: case NOT_HANDSHAKING: // write what we have or an empty buffer to reschedule a call to flush write = BufferUtil.hasContent(_encryptedOutput)?_encryptedOutput:BufferUtil.EMPTY_BUFFER; _flushState = FlushState.WRITING; break; case NEED_UNWRAP: // If we have something to write, then write it and ignore the needed unwrap for now. if (BufferUtil.hasContent(_encryptedOutput)) { write = _encryptedOutput; _flushState = FlushState.WRITING; break; } if (_fillState!=FillState.IDLE) { // Wait for a fill that is happening anyway _flushState = FlushState.WAIT_FOR_FILL; break; } // Try filling ourselves try { int filled = fill(BufferUtil.EMPTY_BUFFER); // If this changed the status, let's try again if (_sslEngine.getHandshakeStatus()!=status) continue; if (filled < 0) throw new IOException("Broken pipe"); } catch(IOException e) { LOG.debug(e); close(e); write = BufferUtil.EMPTY_BUFFER; _flushState = FlushState.WRITING; break; } // Make sure we are fill interested. fillInterest = true; _fillState = FillState.INTERESTED; _flushState = FlushState.WAIT_FOR_FILL; break; default: throw new IllegalStateException("Unexpected HandshakeStatus " + status); } break; } if (LOG.isDebugEnabled()) LOG.debug(" { if (fail_fill_interest) _decryptedEndPoint.getFillInterest().onFail(x); _decryptedEndPoint.getWriteFlusher().onFail(x); }); } @Override public InvocationType getInvocationType() { return _decryptedEndPoint.getWriteFlusher().getCallbackInvocationType(); } @Override public String toString() { return String.format("SSL@%h.DEP.writeCallback",SslConnection.this); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy