/*
* 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.server.x11;
import java.io.IOException;
import java.io.OutputStream;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Collection;
import java.util.Objects;
import org.apache.sshd.common.Closeable;
import org.apache.sshd.common.FactoryManager;
import org.apache.sshd.common.io.IoAcceptor;
import org.apache.sshd.common.io.IoServiceFactory;
import org.apache.sshd.common.io.IoSession;
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.Readable;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
import org.apache.sshd.common.util.closeable.AbstractInnerCloseable;
/**
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class DefaultX11ForwardSupport extends AbstractInnerCloseable implements X11ForwardSupport {
private final ConnectionService service;
private IoAcceptor acceptor;
public DefaultX11ForwardSupport(ConnectionService service) {
this.service = Objects.requireNonNull(service, "No connection service");
}
@Override
public void close() throws IOException {
close(true);
}
@Override
protected Closeable getInnerCloseable() {
return builder().close(acceptor).build();
}
// TODO consider reducing the 'synchronized' section to specific code locations rather than entire method
@Override
public synchronized String createDisplay(
boolean singleConnection, String authenticationProtocol, String authenticationCookie, int screen)
throws IOException {
if (isClosed()) {
throw new IllegalStateException("X11ForwardSupport is closed");
}
if (isClosing()) {
throw new IllegalStateException("X11ForwardSupport is closing");
}
// only support non windows systems
if (OsUtils.isWin32()) {
if (log.isDebugEnabled()) {
log.debug("createDisplay(auth={}, cookie={}, screen={}) Windows O/S N/A",
authenticationProtocol, authenticationCookie, screen);
}
return null;
}
Session session = Objects.requireNonNull(service.getSession(), "No session");
if (acceptor == null) {
FactoryManager manager = Objects.requireNonNull(session.getFactoryManager(), "No factory manager");
IoServiceFactory factory = Objects.requireNonNull(manager.getIoServiceFactory(), "No I/O service factory");
acceptor = factory.createAcceptor(this);
}
int minDisplayNumber = session.getIntProperty(X11_DISPLAY_OFFSET, DEFAULT_X11_DISPLAY_OFFSET);
int maxDisplayNumber = session.getIntProperty(X11_MAX_DISPLAYS, DEFAULT_X11_MAX_DISPLAYS);
int basePort = session.getIntProperty(X11_BASE_PORT, DEFAULT_X11_BASE_PORT);
String bindHost = session.getStringProperty(X11_BIND_HOST, DEFAULT_X11_BIND_HOST);
InetSocketAddress addr = null;
// try until bind successful or max is reached
for (int displayNumber = minDisplayNumber; displayNumber < maxDisplayNumber; displayNumber++) {
int port = basePort + displayNumber;
addr = new InetSocketAddress(bindHost, port);
try {
acceptor.bind(addr);
break;
} catch (BindException bindErr) {
if (log.isDebugEnabled()) {
log.debug("createDisplay(auth={}, cookie={}, screen={}) failed ({}) to bind to address={}: {}",
authenticationProtocol, authenticationCookie, screen,
bindErr.getClass().getSimpleName(), addr, bindErr.getMessage());
}
addr = null;
}
}
if (addr == null) {
log.warn("createDisplay(auth={}, cookie={}, screen={})"
+ " failed to allocate internet-domain X11 display socket in range {}-{}",
authenticationProtocol, authenticationCookie, screen,
minDisplayNumber, maxDisplayNumber);
Collection<SocketAddress> boundAddresses = acceptor.getBoundAddresses();
if (GenericUtils.isEmpty(boundAddresses)) {
if (log.isDebugEnabled()) {
log.debug("createDisplay(auth={}, cookie={}, screen={}) closing - no more bound addresses",
authenticationProtocol, authenticationCookie, screen);
}
close();
} else {
if (log.isDebugEnabled()) {
log.debug("createDisplay(auth={}, cookie={}, screen={}) closing - remaining bound addresses: {}",
authenticationProtocol, authenticationCookie, screen, boundAddresses);
}
}
return null;
}
int port = addr.getPort();
int displayNumber = port - basePort;
String authDisplay = "unix:" + displayNumber + "." + screen;
try {
Process p = new ProcessBuilder(XAUTH_COMMAND, "remove", authDisplay).start();
int result = p.waitFor();
if (log.isDebugEnabled()) {
log.debug("createDisplay({}) {} remove result={}", authDisplay, XAUTH_COMMAND, result);
}
if (result == 0) {
p = new ProcessBuilder(XAUTH_COMMAND, "add", authDisplay, authenticationProtocol, authenticationCookie).start();
result = p.waitFor();
if (log.isDebugEnabled()) {
log.debug("createDisplay({}) {} add result={}", authDisplay, XAUTH_COMMAND, result);
}
}
if (result != 0) {
throw new IllegalStateException("Bad " + XAUTH_COMMAND + " invocation result: " + result);
}
return bindHost + ":" + displayNumber + "." + screen;
} catch (Throwable e) {
log.warn("createDisplay({}) failed ({}) run xauth: {}",
authDisplay, e.getClass().getSimpleName(), e.getMessage());
if (log.isDebugEnabled()) {
log.debug("createDisplay(" + authDisplay + ") xauth failure details", e);
}
return null;
}
}
@Override
public void sessionCreated(IoSession session) throws Exception {
ChannelForwardedX11 channel = new ChannelForwardedX11(session);
session.setAttribute(ChannelForwardedX11.class, channel);
if (log.isDebugEnabled()) {
log.debug("sessionCreated({}) channel{}", session, channel);
}
this.service.registerChannel(channel);
channel.open().verify(channel.getLongProperty(CHANNEL_OPEN_TIMEOUT_PROP, DEFAULT_CHANNEL_OPEN_TIMEOUT));
}
@Override
public void sessionClosed(IoSession session) throws Exception {
ChannelForwardedX11 channel = (ChannelForwardedX11) session.getAttribute(ChannelForwardedX11.class);
if (channel != null) {
if (log.isDebugEnabled()) {
log.debug("sessionClosed({}) close channel={}", session, channel);
}
channel.close(false);
}
}
@Override
public void messageReceived(IoSession session, Readable message) throws Exception {
ChannelForwardedX11 channel = (ChannelForwardedX11) session.getAttribute(ChannelForwardedX11.class);
Buffer buffer = new ByteArrayBuffer(message.available() + Long.SIZE, false);
buffer.putBuffer(message);
if (log.isTraceEnabled()) {
log.trace("messageReceived({}) channel={}, len={}", session, channel, buffer.available());
}
OutputStream outputStream = channel.getInvertedIn();
outputStream.write(buffer.array(), buffer.rpos(), buffer.available());
outputStream.flush();
}
@Override
public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
if (log.isDebugEnabled()) {
log.debug("exceptionCaught({}) {}: {}", session, cause.getClass().getSimpleName(), cause.getMessage());
}
if (log.isTraceEnabled()) {
log.trace("exceptionCaught(" + session + ") caught exception details", cause);
}
session.close(false);
}
@Override
public String toString() {
return getClass().getSimpleName() + ": " + service.getClass();
}
}