/*
* 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.IOException;
import java.io.InterruptedIOException;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.sshd.agent.common.AbstractAgentProxy;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
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;
/**
* A client for a remote SSH agent
*/
public class AgentClient extends AbstractAgentProxy implements Runnable {
private final String authSocket;
private final long pool;
private final long handle;
private final Buffer receiveBuffer;
private final Queue<Buffer> messages;
private Future<?> pumper;
private final AtomicBoolean open = new AtomicBoolean(true);
public AgentClient(String authSocket) throws IOException {
this(authSocket, null, false);
}
public AgentClient(String authSocket, ExecutorService executor, boolean shutdownOnExit) throws IOException {
this.authSocket = authSocket;
setExecutorService((executor == null) ? ThreadUtils.newSingleThreadExecutor("AgentClient[" + authSocket + "]") : executor);
setShutdownOnExit((executor == null) || shutdownOnExit);
try {
pool = Pool.create(AprLibrary.getInstance().getRootPool());
handle = Local.create(authSocket, pool);
int result = Local.connect(handle, 0);
if (result != Status.APR_SUCCESS) {
throwException(result);
}
receiveBuffer = new ByteArrayBuffer();
messages = new ArrayBlockingQueue<>(10);
ExecutorService service = getExecutorService();
pumper = service.submit(this);
} catch (IOException e) {
throw e;
} catch (Exception e) {
throw new SshException(e);
}
}
@Override
public boolean isOpen() {
return open.get();
}
@Override
public void run() {
try {
byte[] buf = new byte[1024];
while (isOpen()) {
int result = Socket.recv(handle, buf, 0, buf.length);
if (result < Status.APR_SUCCESS) {
throwException(result);
}
messageReceived(new ByteArrayBuffer(buf, 0, result));
}
} catch (Exception e) {
if (isOpen()) {
log.warn(e.getClass().getSimpleName() + " while still open: " + e.getMessage());
} else {
if (log.isDebugEnabled()) {
log.debug("Closed client loop exception", e);
}
}
} finally {
try {
close();
} catch (IOException e) {
if (log.isDebugEnabled()) {
log.debug(e.getClass().getSimpleName() + " while closing: " + e.getMessage());
}
}
}
}
protected void messageReceived(Buffer buffer) throws Exception {
Buffer message = null;
synchronized (receiveBuffer) {
receiveBuffer.putBuffer(buffer);
if (receiveBuffer.available() >= 4) {
int rpos = receiveBuffer.rpos();
int len = receiveBuffer.getInt();
receiveBuffer.rpos(rpos);
if (receiveBuffer.available() >= 4 + len) {
message = new ByteArrayBuffer(receiveBuffer.getBytes());
receiveBuffer.compact();
}
}
}
if (message != null) {
synchronized (messages) {
messages.offer(message);
messages.notifyAll();
}
}
}
@Override
public void close() throws IOException {
if (open.getAndSet(false)) {
Socket.close(handle);
}
if ((pumper != null) && isShutdownOnExit() && (!pumper.isDone())) {
pumper.cancel(true);
}
super.close();
}
@Override
protected synchronized Buffer request(Buffer buffer) throws IOException {
int wpos = buffer.wpos();
buffer.wpos(0);
buffer.putInt(wpos - 4);
buffer.wpos(wpos);
synchronized (messages) {
try {
int result = Socket.send(handle, buffer.array(), buffer.rpos(), buffer.available());
if (result < Status.APR_SUCCESS) {
throwException(result);
}
if (messages.isEmpty()) {
messages.wait();
}
return messages.poll();
} catch (InterruptedException e) {
throw (IOException) new InterruptedIOException(authSocket + ": Interrupted while polling for messages").initCause(e);
}
}
}
/**
* 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
*/
private void throwException(int code) throws IOException {
throw new IOException(
org.apache.tomcat.jni.Error.strerror(-code)
+ " (code: " + code + ")");
}
}