/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.sshd.agent.unix; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.sshd.agent.SshAgentServer; import org.apache.sshd.common.SshException; import org.apache.sshd.common.session.ConnectionService; import org.apache.sshd.common.session.Session; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.OsUtils; import org.apache.sshd.common.util.logging.AbstractLoggingBean; import org.apache.sshd.common.util.threads.ExecutorServiceCarrier; import org.apache.sshd.common.util.threads.ThreadUtils; import org.apache.tomcat.jni.Local; import org.apache.tomcat.jni.Pool; import org.apache.tomcat.jni.Socket; import org.apache.tomcat.jni.Status; /** * The server side fake agent, acting as an agent, but actually forwarding the requests to the auth channel on the client side. */ public class AgentServerProxy extends AbstractLoggingBean implements SshAgentServer, ExecutorServiceCarrier { /** * Property that can be set on the {@link Session} in order to control * the authentication timeout (millis). If not specified then * {@link #DEFAULT_AUTH_SOCKET_TIMEOUT} is used */ public static final String AUTH_SOCKET_TIMEOUT = "ssh-agent-server-proxy-auth-socket-timeout"; public static final int DEFAULT_AUTH_SOCKET_TIMEOUT = 10000000; //used to wake the Local.listen() JNI call private static final byte[] END_OF_STREAM_MESSAGE = new byte[]{"END_OF_STREAM".getBytes(StandardCharsets.UTF_8)[0]}; private final ConnectionService service; private final String authSocket; private final long pool; private final long handle; private Future<?> piper; private final ExecutorService pipeService; private final boolean pipeCloseOnExit; private final AtomicBoolean open = new AtomicBoolean(true); private final AtomicBoolean innerFinished = new AtomicBoolean(false); public AgentServerProxy(ConnectionService service) throws IOException { this(service, null, false); } public AgentServerProxy(ConnectionService service, ExecutorService executor, boolean shutdownOnExit) throws IOException { this.service = service; try { String authSocket = AprLibrary.createLocalSocketAddress(); pool = Pool.create(AprLibrary.getInstance().getRootPool()); handle = Local.create(authSocket, pool); this.authSocket = authSocket; int result = Local.bind(handle, 0); if (result != Status.APR_SUCCESS) { throwException(result); } AprLibrary.secureLocalSocket(authSocket, handle); result = Local.listen(handle, 0); if (result != Status.APR_SUCCESS) { throwException(result); } pipeService = (executor == null) ? ThreadUtils.newSingleThreadExecutor("sshd-AgentServerProxy-PIPE-" + authSocket) : executor; pipeCloseOnExit = executor != pipeService || shutdownOnExit; piper = pipeService.submit(() -> { try { while (isOpen()) { try { long clientSock = Local.accept(handle); if (!isOpen()) { break; } Session session = AgentServerProxy.this.service.getSession(); Socket.timeoutSet(clientSock, session.getIntProperty(AUTH_SOCKET_TIMEOUT, DEFAULT_AUTH_SOCKET_TIMEOUT)); String channelType = session.getStringProperty(PROXY_CHANNEL_TYPE, DEFAULT_PROXY_CHANNEL_TYPE); AgentForwardedChannel channel = new AgentForwardedChannel(clientSock, channelType); AgentServerProxy.this.service.registerChannel(channel); channel.open().verify(session.getLongProperty(CHANNEL_OPEN_TIMEOUT_PROP, DEFAULT_CHANNEL_OPEN_TIMEOUT)); } catch (Exception e) { if (log.isDebugEnabled()) { log.debug("run(open={}) {} while authentication forwarding: {}", isOpen(), e.getClass().getSimpleName(), e.getMessage()); } if (log.isTraceEnabled()) { log.trace("run(open=" + isOpen() + ") authentication forwarding failure details", e); } } } } finally { innerFinished.set(true); } }); } catch (IOException e) { throw e; } catch (Exception e) { throw new SshException(e); } } @Override public boolean isOpen() { return open.get(); } @Override public ExecutorService getExecutorService() { return pipeService; } @Override public boolean isShutdownOnExit() { return pipeCloseOnExit; } @Override public String getId() { return authSocket; } @Override public synchronized void close() throws IOException { if (!open.getAndSet(false)) { return; // already closed (or closing) } final boolean isDebug = log.isDebugEnabled(); if (handle != 0) { if (!innerFinished.get()) { try { final long tmpPool = Pool.create(AprLibrary.getInstance().getRootPool()); final long tmpSocket = Local.create(authSocket, tmpPool); long connectResult = Local.connect(tmpSocket, 0L); if (connectResult != Status.APR_SUCCESS) { if (isDebug) { log.debug("Unable to connect to socket PIPE {}. APR errcode {}", authSocket, connectResult); } } //write a single byte -- just wake up the accept() int sendResult = Socket.send(tmpSocket, END_OF_STREAM_MESSAGE, 0, 1); if (sendResult != 1) { if (isDebug) { log.debug("Unable to send signal the EOS for {}. APR retcode {} != 1", authSocket, sendResult); } } } catch (Exception e) { //log eventual exceptions in debug mode if (isDebug) { log.debug("Exception connecting to the PIPE socket: " + authSocket, e); } } } final int closeCode = Socket.close(handle); if (closeCode != Status.APR_SUCCESS) { log.warn("Exceptions closing the PIPE: {}. APR error code: {} ", authSocket, closeCode); } } try { if (authSocket != null) { final File socketFile = new File(authSocket); if (socketFile.exists()) { deleteFile(socketFile, "Deleted PIPE socket {}"); if (OsUtils.isUNIX()) { deleteFile(socketFile.getParentFile(), "Deleted parent PIPE socket {}"); } } } } catch (Exception e) { //log eventual exceptions in debug mode if (isDebug) { log.debug("Exception deleting the PIPE socket: " + authSocket, e); } } try { if ((piper != null) && (!piper.isDone())) { piper.cancel(true); } } finally { piper = null; } ExecutorService executor = getExecutorService(); if ((executor != null) && isShutdownOnExit() && (!executor.isShutdown())) { Collection<?> runners = executor.shutdownNow(); if (log.isDebugEnabled()) { log.debug("Shut down runners count=" + GenericUtils.size(runners)); } } } private void deleteFile(File file, String msg) { if (file.delete()) { if (log.isDebugEnabled()) { log.debug(msg, file); } } } /** * transform an APR error number in a more fancy exception * * @param code APR error code * @throws java.io.IOException the produced exception for the given APR error number */ static void throwException(int code) throws IOException { throw new IOException(org.apache.tomcat.jni.Error.strerror(-code) + " (code: " + code + ")"); } }