org.xenmaster.web.VNCHook.java Source code

Java tutorial

Introduction

Here is the source code for org.xenmaster.web.VNCHook.java

Source

/*
 * VNCHook.java
 * Copyright (C) 2011,2012 Wannes De Smet
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.xenmaster.web;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import net.wgr.server.web.handling.WebCommandHandler;
import net.wgr.utility.GlobalExecutorService;
import net.wgr.wcp.Commander;
import net.wgr.wcp.Scope;
import net.wgr.wcp.command.Command;
import net.wgr.wcp.command.CommandException;
import org.apache.commons.codec.binary.Base64;
import org.apache.log4j.Logger;
import org.xenmaster.api.entity.Console;
import org.xenmaster.api.entity.VM;
import org.xenmaster.connectivity.ConnectionMultiplexer;
import org.xenmaster.controller.Controller;

/**
 *
 * @created Dec 15, 2011
 * @author double-u
 */
public class VNCHook extends WebCommandHandler {

    protected static ConnectionMultiplexer cm;
    protected static ConcurrentHashMap<String, Connection> connections;
    protected ConnectionMultiplexer.ActivityListener al;
    protected static int connectionSlots, available = -1;
    protected static Gson gson;

    public VNCHook() {
        super("vnc");

        if (cm == null) {
            gson = new Gson();
            setupInfrastructure();
        }
    }

    protected static String buildHttpConnect(URI uri) {
        StringBuilder sb = new StringBuilder();
        sb.append("CONNECT ");
        sb.append(uri.getPath()).append('?').append(uri.getQuery());
        sb.append(" HTTP/1.1").append("\r\n");
        sb.append("Cookie: session_id=").append(Controller.getSession().getReference());
        sb.append("\r\n\r\n");
        return sb.toString();
    }

    @Override
    public Object execute(Command cmd) {
        try {
            switch (cmd.getName()) {
            case "openConnection":
                if (!cmd.getData().isJsonObject() || !cmd.getData().getAsJsonObject().has("ref")) {
                    throw new IllegalArgumentException("No VM reference parameter given");
                }
                Connection conn = new Connection(cmd.getConnection().getId());
                VM vm = new VM(cmd.getData().getAsJsonObject().get("ref").getAsString(), false);
                for (Console c : vm.getConsoles()) {
                    if (c.getProtocol() == Console.Protocol.RFB) {
                        try {
                            conn.console = c;
                            URI uri = new URI(c.getLocation());
                            conn.uri = uri;
                            InetSocketAddress isa = new InetSocketAddress(uri.getHost(), 80);
                            conn.waitForAddress = isa;
                            conn.lastWriteTime = System.currentTimeMillis();
                            connections.put(conn.getReference(), conn);
                            cm.addConnection(isa);
                            break;
                        } catch (URISyntaxException ex) {
                            Logger.getLogger(getClass()).error("Failed to parse URI", ex);
                        } catch (IOException | InterruptedException ex) {
                            Logger.getLogger(getClass()).error("Failed to create connection", ex);
                        }
                    }
                }

                return conn.getReference();
            case "write":
                Arguments data = Arguments.fromJson(cmd.getData());
                if (!connections.containsKey(data.ref)) {
                    return new CommandException("Tried to write to unexisting connection", data.ref);
                }
                Connection c = connections.get(data.ref);
                c.lastWriteTime = System.currentTimeMillis();
                byte[] bytes = Base64.decodeBase64(data.data);
                cm.write(c.connection, ByteBuffer.wrap(bytes));
                break;
            case "closeConnection":
                Arguments close = Arguments.fromJson(cmd.getData());
                Connection ci = connections.get(close.ref);

                if (ci != null) {
                    // Indicate that we've initiated the connection close
                    ci.lastWriteTime = -1;
                    cm.close(ci.connection);
                }
                break;
            case "connectionHeartbeat":
                Arguments heartbeat = Arguments.fromJson(cmd.getData());
                if (!connections.containsKey(heartbeat.ref)) {
                    return new CommandException("Tried to write to unexisting connection", heartbeat.ref);
                }
                Connection ch = connections.get(heartbeat.ref);
                ch.lastHeartbeat = System.currentTimeMillis();
                break;
            }
        } catch (IOException | IllegalArgumentException ex) {
            Logger.getLogger(getClass()).error("Command failed : " + cmd.getName(), ex);
        }

        return null;
    }

    protected static int allocateConnection() {
        if (available != -1) {
            int temp = available;
            available = -1;
            return temp;
        }

        if (connectionSlots != Integer.MAX_VALUE) {
            return ++connectionSlots;
        }

        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            if (!connections.containsKey("ConnectionRef:" + i)) {
                return i;
            }
        }

        throw new Error("Maxed out at " + Integer.MAX_VALUE + " active VNC connections. Something's wrong");
    }

    protected static class Connection {

        public UUID clientId;
        public int connection;
        protected String reference;
        public InetSocketAddress waitForAddress;
        public long lastWriteTime;
        public long lastHeartbeat;
        public URI uri;
        public boolean dismissedHttpOK;
        public Console console;

        public Connection(UUID client) {
            this.reference = "ConnectionRef:" + allocateConnection();
            this.clientId = client;
            this.lastHeartbeat = System.currentTimeMillis();
        }

        public String getReference() {
            return reference;
        }
    }

    protected static class Arguments {

        public String data;
        public String ref;

        public Arguments() {
        }

        public static Arguments fromJson(JsonElement data) {
            Arguments na = gson.fromJson(data, Arguments.class);
            if (na == null || na.ref == null || !na.ref.startsWith("ConnectionRef:")) {
                throw new IllegalArgumentException("Illegal reference, is not a ConnectionRef");
            }
            return na;
        }

        public Arguments(String data, String ref) {
            this.data = data;
            this.ref = ref;
        }
    }

    protected static class AL implements ConnectionMultiplexer.ActivityListener {

        @Override
        public void dataReceived(ByteBuffer buffer, int connection, ConnectionMultiplexer cm) {
            Connection conn = null;
            for (Entry<String, Connection> entry : connections.entrySet()) {
                if (entry.getValue().connection == connection) {
                    conn = entry.getValue();
                    break;
                }
            }

            if (conn == null) {
                Logger.getLogger(getClass())
                        .warn("Received data on inactive connection " + connection + ". Closing ...");
                try {
                    cm.close(connection);
                } catch (IOException ex) {
                    Logger.getLogger(getClass()).error("Failed to close connection " + connection, ex);
                }
                return;
            }

            byte[] data = buffer.array();

            if (!conn.dismissedHttpOK) {
                String content = new String(data);
                // RFB xxx.xxx denotes start of VNC handshake
                if (content.contains("RFB")) {
                    conn.dismissedHttpOK = true;
                    data = content.substring(content.indexOf("RFB")).getBytes();
                } else {
                    return;
                }
            }

            if (data.length < 1) {
                return;
            }

            Arguments vncData = new Arguments();
            vncData.ref = conn.getReference();
            vncData.data = Base64.encodeBase64String(data).replace("\r\n", "");
            Command cmd = new Command("vnc", "updateScreen", vncData);
            ArrayList<UUID> ids = new ArrayList<>();
            ids.add(conn.clientId);
            Scope scope = new Scope(ids);
            Commander.get().commandeer(cmd, scope);
        }

        @Override
        public void connectionClosed(int connection) {
            for (Iterator<Entry<String, Connection>> it = connections.entrySet().iterator(); it.hasNext();) {
                Entry<String, Connection> entry = it.next();
                if (entry.getValue().connection == connection) {
                    Connection conn = entry.getValue();

                    // Check if this disconnect was initiated by a user
                    if (conn.lastWriteTime == -1) {
                        Command cmd = new Command("vnc", "connectionClosed", new Arguments("", entry.getKey()));
                        Commander.get().commandeer(cmd, new Scope(Scope.Target.ALL));
                        it.remove();
                    } else {
                        try {
                            // Try to reconnect
                            URI uri = new URI(conn.console.getLocation());
                            conn.uri = uri;
                            InetSocketAddress isa = new InetSocketAddress(uri.getHost(), 80);
                            conn.waitForAddress = isa;
                            conn.lastWriteTime = System.currentTimeMillis();
                            cm.addConnection(isa);
                        } catch (URISyntaxException | IOException | InterruptedException ex) {
                            Logger.getLogger(getClass()).error("Failed to reinitiate connection", ex);
                        }
                    }
                    break;
                }
            }
        }

        @Override
        public void connectionEstablished(int connection, Socket socket) {
            Connection conn = null;
            InetSocketAddress isa = null;
            for (Entry<String, Connection> entry : connections.entrySet()) {
                isa = entry.getValue().waitForAddress;
                if (isa != null && isa.equals(socket.getRemoteSocketAddress())) {
                    conn = entry.getValue();
                    conn.waitForAddress = null;
                    break;
                }
            }

            if (conn == null) {
                Logger.getLogger(getClass()).warn("Unknown connection established to "
                        + ((InetSocketAddress) socket.getRemoteSocketAddress()).getHostString());
                return;
            }

            conn.connection = connection;
            cm.write(conn.connection, ByteBuffer.wrap(buildHttpConnect(conn.uri).getBytes()));

            Command cmd = new Command("vnc", "connectionEstablished", new Arguments("", conn.getReference()));
            ArrayList<UUID> ids = new ArrayList<>();
            ids.add(conn.clientId);
            Scope scope = new Scope(ids);
            Commander.get().commandeer(cmd, scope);
        }
    }

    protected static void setupInfrastructure() {
        cm = new ConnectionMultiplexer();
        cm.addActivityListener(new AL());
        cm.start();
        connections = new ConcurrentHashMap<>();
        GlobalExecutorService.get().scheduleAtFixedRate(new Reaper(), 0, 10, TimeUnit.SECONDS);

    }

    protected static class Reaper implements Runnable {

        @Override
        public void run() {
            // Send a heartbeat
            Command cmd = new Command("vnc", "connectionHeartbeat", new Arguments());
            Commander.get().commandeer(cmd, new Scope(Scope.Target.ALL));

            for (Iterator<Entry<String, Connection>> it = connections.entrySet().iterator(); it.hasNext();) {
                Entry<String, Connection> entry = it.next();
                // Skipped 2 hearbeats, is probably dead.
                if (System.currentTimeMillis() - entry.getValue().lastHeartbeat > 1000 * 20) {
                    try {
                        Logger.getLogger(getClass())
                                .info("Reaper closing inactive connection " + entry.getValue().connection);
                        cm.close(entry.getValue().connection);
                        it.remove();
                    } catch (IOException ex) {
                        Logger.getLogger(getClass()).error("Failed to close connection", ex);
                    }
                }
            }
        }
    }
}