| /* |
| * 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.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.CloseableExecutorService; |
| import org.apache.sshd.common.util.threads.ThreadUtils; |
| import org.apache.sshd.core.CoreModuleProperties; |
| 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 { |
| |
| // 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 CloseableExecutorService pipeService; |
| private final AtomicBoolean open = new AtomicBoolean(true); |
| private final AtomicBoolean innerFinished = new AtomicBoolean(false); |
| |
| public AgentServerProxy(ConnectionService service) throws IOException { |
| this(service, null); |
| } |
| |
| public AgentServerProxy(ConnectionService service, CloseableExecutorService executor) 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) { |
| throw toIOException(result); |
| } |
| AprLibrary.secureLocalSocket(authSocket, handle); |
| result = Local.listen(handle, 0); |
| if (result != Status.APR_SUCCESS) { |
| throw toIOException(result); |
| } |
| |
| pipeService = (executor == null) |
| ? ThreadUtils.newSingleThreadExecutor("AgentServerProxy-PIPE-" + authSocket) |
| : ThreadUtils.noClose(executor); |
| piper = pipeService.submit(() -> { |
| try { |
| while (isOpen()) { |
| try { |
| long clientSock = Local.accept(handle); |
| if (!isOpen()) { |
| break; |
| } |
| |
| Session session = AgentServerProxy.this.service.getSession(); |
| Socket.timeoutSet(clientSock, |
| CoreModuleProperties.AUTH_SOCKET_TIMEOUT.getRequired(session).toMillis() * 1000); |
| String channelType = CoreModuleProperties.PROXY_CHANNEL_TYPE.getRequired(session); |
| AgentForwardedChannel channel = new AgentForwardedChannel(clientSock, channelType); |
| AgentServerProxy.this.service.registerChannel(channel); |
| channel.open().verify(CoreModuleProperties.CHANNEL_OPEN_TIMEOUT.getRequired(session)); |
| } catch (Exception e) { |
| debug("run(open={}) {} while authentication forwarding: {}", |
| isOpen(), e.getClass().getSimpleName(), e.getMessage(), e); |
| } |
| } |
| } finally { |
| innerFinished.set(true); |
| } |
| }); |
| } catch (IOException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new SshException(e); |
| } |
| } |
| |
| @Override |
| public boolean isOpen() { |
| return open.get(); |
| } |
| |
| public CloseableExecutorService getExecutorService() { |
| return pipeService; |
| } |
| |
| @Override |
| public String getId() { |
| return authSocket; |
| } |
| |
| @Override |
| public synchronized void close() throws IOException { |
| if (!open.getAndSet(false)) { |
| return; // already closed (or closing) |
| } |
| |
| boolean debugEnabled = log.isDebugEnabled(); |
| if (handle != 0) { |
| if (!innerFinished.get()) { |
| try { |
| signalEOS(AprLibrary.getInstance(), debugEnabled); |
| } catch (Exception e) { |
| // log eventual exceptions in debug mode |
| if (debugEnabled) { |
| log.debug("Exception signalling EOS to the PIPE socket: " + authSocket, e); |
| } |
| } |
| } |
| |
| 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) { |
| removeSocketFile(authSocket, debugEnabled); |
| } |
| } catch (Exception e) { |
| // log eventual exceptions in debug mode |
| if (debugEnabled) { |
| log.debug("Exception deleting the PIPE socket: " + authSocket, e); |
| } |
| } |
| |
| try { |
| if ((piper != null) && (!piper.isDone())) { |
| piper.cancel(true); |
| } |
| } finally { |
| piper = null; |
| } |
| |
| CloseableExecutorService executor = getExecutorService(); |
| if ((executor != null) && (!executor.isShutdown())) { |
| Collection<?> runners = executor.shutdownNow(); |
| if (debugEnabled) { |
| log.debug("Shut down runners count={}", GenericUtils.size(runners)); |
| } |
| } |
| } |
| |
| protected File removeSocketFile(String socketPath, boolean debugEnabled) throws Exception { |
| File socketFile = new File(socketPath); |
| if (socketFile.exists()) { |
| deleteFile(socketFile, "Deleted PIPE socket {}", debugEnabled); |
| |
| if (OsUtils.isUNIX()) { |
| deleteFile(socketFile.getParentFile(), "Deleted parent PIPE socket {}", debugEnabled); |
| } |
| } |
| |
| return socketFile; |
| } |
| |
| protected void signalEOS(AprLibrary libInstance, boolean debugEnabled) throws Exception { |
| long tmpPool = Pool.create(libInstance.getRootPool()); |
| long tmpSocket = Local.create(authSocket, tmpPool); |
| long connectResult = Local.connect(tmpSocket, 0L); |
| |
| if (connectResult != Status.APR_SUCCESS) { |
| if (debugEnabled) { |
| 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 (debugEnabled) { |
| log.debug("Unable to send signal the EOS for {}. APR retcode {} != 1", authSocket, sendResult); |
| } |
| } |
| } |
| |
| protected boolean deleteFile(File file, String msg, boolean debugEnabled) { |
| boolean success = file.delete(); |
| if (success) { |
| if (debugEnabled) { |
| log.debug(msg, file); |
| } |
| } |
| |
| return success; |
| } |
| |
| /** |
| * transform an APR error number in a more fancy exception |
| * |
| * @param code APR error code |
| * @return {@link IOException} with the exception details for the given APR error number |
| */ |
| public static IOException toIOException(int code) { |
| return new IOException(org.apache.tomcat.jni.Error.strerror(-code) + " (code: " + code + ")"); |
| } |
| } |