// 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 rdpclient;

import java.net.InetAddress;
import java.net.UnknownHostException;

import rdpclient.adapter.AwtRdpKeyboardAdapter;
import rdpclient.adapter.AwtRdpMouseAdapter;
import rdpclient.hyperv.ClientPreConnectionBlob;
import rdpclient.ntlmssp.ClientNtlmsspNegotiate;
import rdpclient.ntlmssp.ClientNtlmsspPubKeyAuth;
import rdpclient.ntlmssp.ClientNtlmsspUserCredentials;
import rdpclient.ntlmssp.NtlmState;
import rdpclient.ntlmssp.ServerNtlmsspChallenge;
import rdpclient.ntlmssp.ServerNtlmsspPubKeyPlus1;
import rdpclient.rdp.ClientConfirmActivePDU;
import rdpclient.rdp.ClientFastPathPDU;
import rdpclient.rdp.ClientInfoPDU;
import rdpclient.rdp.ClientMCSAttachUserRequest;
import rdpclient.rdp.ClientMCSChannelJoinRequestServerMCSChannelConfirmPDUs;
import rdpclient.rdp.ClientMCSConnectInitial;
import rdpclient.rdp.ClientMCSErectDomainRequest;
import rdpclient.rdp.ClientTpkt;
import rdpclient.rdp.ClientX224ConnectionRequestPDU;
import rdpclient.rdp.ClientX224DataPDU;
import rdpclient.rdp.RdpConstants;
import rdpclient.rdp.RdpState;
import rdpclient.rdp.ServerBitmapUpdate;
import rdpclient.rdp.ServerDemandActivePDU;
import rdpclient.rdp.ServerFastPath;
import rdpclient.rdp.ServerIOChannelRouter;
import rdpclient.rdp.ServerLicenseErrorPDUValidClient;
import rdpclient.rdp.ServerMCSAttachUserConfirmPDU;
import rdpclient.rdp.ServerMCSConnectResponse;
import rdpclient.rdp.ServerMCSPDU;
import rdpclient.rdp.ServerPaletteUpdate;
import rdpclient.rdp.ServerX224ConnectionConfirmPDU;
import rdpclient.rdp.ServerX224DataPdu;
import streamer.PipelineImpl;
import streamer.Queue;
import streamer.ssl.SSLState;
import streamer.ssl.UpgradeSocketToSSL;
import common.AwtKeyEventSource;
import common.AwtMouseEventSource;
import common.BufferedImageCanvas;
import common.ScreenDescription;
import common.adapter.AwtCanvasAdapter;

public class RdpClient extends PipelineImpl {

    /**
     * Name of last OneTimePacket in handshake sequence.
     */
    private static final String HANDSHAKE_END = "server_valid_client";

    /**
     * Create new RDP or HyperV cli
     *
     * @param id
     *          id of this element
     * @param userName
     *          user name
     * @param password
     *          password
     * @param pcb
     *          pre-connection blob for HyperV server or null/empty string to
     *          disable. Usually, HyperV VM ID, e.g.
     *          "39418F90-6D03-468E-B796-91C60DD6653A".
     * @param screen
     *          screen description to fill
     * @param canvas
     *          canvas to draw on
     * @param sslState
     */
    public RdpClient(String id, String serverHostName, String domain, String userName, String password, String pcb, ScreenDescription screen,
            BufferedImageCanvas canvas, SSLState sslState) {
        super(id);
        assembleRDPPipeline(serverHostName, domain, userName, password, pcb, screen, canvas, sslState);
    }

    // /* DEBUG */
//  @Override
//  protected HashMap<String, streamer.Element> initElementMap(String id) {
//    HashMap<String, streamer.Element> map = new HashMap<String, streamer.Element>();
//    map.put("IN", new ServerPacketSniffer("server <"));
//    map.put("OUT", new ClientPacketSniffer("> client"));
//    return map;
//  }

    /**
     * Assemble connection sequence and main pipeline.
     *
     * Connection sequence for RDP w/o NLA: cookie(TPKT) SSL x224(TPKT)
     * main(FastPath).
     *
     * Connection sequence for RDP w NLA: cookie(TPKT) SSL credssp x224(TPKT)
     * main(FastPath).
     *
     * Connection sequence for HyperV w NLA: pcb SSL credssp cookie(TPKT)
     * x224(TPKT) main(FastPath).
     */
    protected void assembleRDPPipeline(String serverHostName, String domain, String userName, String password, String pcb, ScreenDescription screen,
            BufferedImageCanvas canvas, SSLState sslState) {
        // If preconnection blob with VM ID is specified, then we are connecting to
        // HyperV server
        boolean hyperv = (pcb != null && !pcb.isEmpty());
        // HyperV server requires NLA (CredSSP/SPNEGO/NTLMSSP) to connect, because
        // it cannot display login screen
        boolean credssp = hyperv || (password != null && !password.isEmpty());

        String workstation;
        try {
            workstation = InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException e) {
            workstation = "workstation";
        }

        //
        // Handshake chain
        //

        RdpState state = new RdpState();
        NtlmState ntlmState = new NtlmState();

        int[] channelsToJoin = new int[] {RdpConstants.CHANNEL_IO,
                // RdpConstants.CHANNEL_RDPRDR, // RDPRDR channel is not used in current
                // version

                // RdpConstants .CHANNEL_CLIPRDR // Clipboard channel is refused to join :-/
        };

        // Add elements

        // If pre-connection blob is specified, then add element to send it as
        // first packet
        if (hyperv) {
            add(new ClientPreConnectionBlob("pcb", pcb));
        }

        // If password is specified, then use CredSSP/NTLM (NTLMSSP)
        int protocol = RdpConstants.RDP_NEG_REQ_PROTOCOL_SSL;
        if (credssp) {
            protocol = RdpConstants.RDP_NEG_REQ_PROTOCOL_HYBRID;

            add(
                    new ClientNtlmsspNegotiate("client_ntlmssp_nego", ntlmState),

                    new ServerNtlmsspChallenge("server_ntlmssp_challenge", ntlmState),

                    new ClientNtlmsspPubKeyAuth("client_ntlmssp_auth", ntlmState, sslState, serverHostName, domain, workstation, userName, password),

                    new ServerNtlmsspPubKeyPlus1("server_ntlmssp_confirm", ntlmState),

                    new ClientNtlmsspUserCredentials("client_ntlmssp_finish", ntlmState)

                    );
        }

        add(new ClientX224ConnectionRequestPDU("client_connection_req", userName, protocol), new ServerX224ConnectionConfirmPDU("server_connection_conf"),
                new UpgradeSocketToSSL("upgrade_to_ssl"),

                new ClientMCSConnectInitial("client_initial_conference_create"), new ServerMCSConnectResponse("server_initial_conference_create"),

                new ClientMCSErectDomainRequest("client_erect_domain"),

                new ClientMCSAttachUserRequest("client_atach_user"), new ServerMCSAttachUserConfirmPDU("server_atach_user_confirm", state),

                new ClientMCSChannelJoinRequestServerMCSChannelConfirmPDUs("client_channel_join_rdprdr", channelsToJoin, state),

                new ClientInfoPDU("client_info_req", userName),

                new ServerLicenseErrorPDUValidClient("server_valid_client"),

                new ServerFastPath("server_fastpath"),

                // new ServerTpkt("server_tpkt"),

                new ServerX224DataPdu("server_x224_data"),

                // These TPKT and X224 wrappers are connected directly to OUT for
                // handshake sequence
                new ClientTpkt("client_tpkt_ot"),

                new ClientX224DataPDU("client_x224_data_ot")

                );

        // If HyperV VM ID is set, then insert element which will send VM ID as
        // first packet of connection, before other packets
        if (hyperv) {

            // HyperV: pcb SSL credssp cookie x224 main.

            link("IN",

                    // Pre Connection Blob
                    "pcb",

                    // Main (will be used after connection seq) or tpkt (to X224)
                    "server_fastpath >tpkt",

                    // SSL
                    "upgrade_to_ssl",

                    // CredSSP
                    "client_ntlmssp_nego", "server_ntlmssp_challenge", "client_ntlmssp_auth", "server_ntlmssp_confirm", "client_ntlmssp_finish",

                    // Cookie
                    "client_connection_req", "server_connection_conf",

                    // X224
                    "client_initial_conference_create");

            for (String element : new String[] {"pcb", "client_ntlmssp_nego", "server_ntlmssp_challenge", "client_ntlmssp_auth", "server_ntlmssp_confirm",
            "client_ntlmssp_finish"}) {
                link(element + " >otout", element + "< OUT");

            }

        } else {

            // RDP: cookie SSL (credssp) x224 main.

            link("IN",

                    // Main or tpkt
                    "server_fastpath >tpkt",

                    // Cookie
                    "client_connection_req", "server_connection_conf",

                    // SSL
                    "upgrade_to_ssl");

            if (credssp) {
                // SSL
                link("upgrade_to_ssl",

                        // CredSSP
                        "client_ntlmssp_nego", "server_ntlmssp_challenge", "client_ntlmssp_auth", "server_ntlmssp_confirm", "client_ntlmssp_finish",

                        // X224
                        "client_initial_conference_create");

                for (String element : new String[] {"client_ntlmssp_nego", "server_ntlmssp_challenge", "client_ntlmssp_auth", "server_ntlmssp_confirm",
                "client_ntlmssp_finish"}) {
                    link(element + " >otout", element + "< OUT");

                }

            } else {

                link(
                        // SSL
                        "upgrade_to_ssl",

                        // X224
                        "client_initial_conference_create");
            }
        }

        link(
                // X224
                "client_initial_conference_create", "server_initial_conference_create",

                "client_erect_domain",

                "server_x224_data",

                "client_atach_user", "server_atach_user_confirm",

                "client_channel_join_rdprdr",

                "client_info_req",

                "server_valid_client"

                );

        // Chain for direct handshake responses (without involving of queue)
        link("client_x224_data_ot", "client_tpkt_ot", "client_tpkt_ot< OUT");

        // Connect one time outputs to client TPKT input
        String tpkt_peers[] = new String[] {"client_connection_req", "server_connection_conf", "upgrade_to_ssl", "client_x224_data_ot"};
        for (String element : tpkt_peers) {
            link(element + " >otout", element + "< client_tpkt_ot");
        }

        // Connect one time outputs to client X224 input
        String x224_peers[] = new String[] {"client_initial_conference_create", "server_initial_conference_create", "client_erect_domain", "client_atach_user",
                "server_atach_user_confirm", "client_channel_join_rdprdr", "client_info_req", "server_valid_client"};
        for (String element : x224_peers) {
            link(element + " >otout", element + "< client_x224_data_ot");
        }

        //
        // Transition
        //

        add(
                // To transfer packets between input threads and output thread.
                new Queue("queue"),

                // Slow path: MultiChannel Support
                new ServerMCSPDU("server_mcs")

                );

        // Last element of handshake sequence will wake up queue and and socket
        // output pull loop, which will switch links, between socket output and
        // queue, from push mode to pull mode.
        link(HANDSHAKE_END + " >queue", "queue", "OUT");

        // Transition from handshake sequence for slow path packets
        link(HANDSHAKE_END, "server_mcs");

        //
        // Main network
        //

        AwtMouseEventSource mouseEventSource = new AwtMouseEventSource("mouse");
        AwtKeyEventSource keyEventSource = new AwtKeyEventSource("keyboard");

        // Subscribe packet sender to various events
        canvas.addMouseListener(mouseEventSource);
        canvas.addMouseMotionListener(mouseEventSource);
        canvas.addKeyListener(keyEventSource);

        // Add elements
        add(

                new ServerIOChannelRouter("server_io_channel", state),

                new ServerDemandActivePDU("server_demand_active", screen, state),

                new ClientConfirmActivePDU("client_confirm_active", screen, state),

                new ServerBitmapUpdate("server_bitmap_update"),

                new AwtCanvasAdapter("canvas_adapter", canvas, screen),

                new ServerPaletteUpdate("server_palette", screen),

                keyEventSource, new AwtRdpKeyboardAdapter("keyboard_adapter"),

                mouseEventSource, new AwtRdpMouseAdapter("mouse_adapter"),

                // These FastPath, TPKT, and X224 wrappers are connected to queue
                new ClientTpkt("client_tpkt_queue"),

                new ClientX224DataPDU("client_x224_data_queue"),

                new ClientFastPathPDU("client_fastpath_queue"));

        // Server packet handlers
        link("server_mcs >channel_1003", "server_io_channel");
        link("server_fastpath >bitmap", "fastpath< server_bitmap_update", "server_bitmap_update< canvas_adapter");
        link("server_io_channel >bitmap", "slowpath< server_bitmap_update");

        link("server_fastpath >palette", "fastpath< server_palette");
        link("server_io_channel >palette", "slowpath< server_palette");

        link("server_io_channel >demand_active", "slowpath< server_demand_active");
        // link("server_demand_active >confirm_active", "client_confirm_active",
        // "confirm_active< client_channel_1003");
        link("server_demand_active >confirm_active", "client_confirm_active", "confirm_active< client_x224_data_queue");

        // Link mouse and keyboard to socket via adapters and send them using
        // FastPath protocol
        link(mouseEventSource.getId(), "mouse_adapter", "mouse_adapter< client_fastpath_queue");
        link(keyEventSource.getId(), "keyboard_adapter", "keyboard_adapter< client_fastpath_queue");

        // Link packet wrappers to outgoing queue
        link("client_fastpath_queue", "client_fastpath_queue< queue");
        link("client_x224_data_queue", "client_tpkt_queue", "client_tpkt_queue< queue");

    }
}
