From b9e9b5384d8cae0a1b06c90bcb8ae60b8002de6d Mon Sep 17 00:00:00 2001 From: Andrew Lytvynov Date: Mon, 23 Aug 2021 21:03:52 +0000 Subject: [PATCH] RDP client implementation (#7824) * RDP client implementation This is a minimal RDP client on top of the rdp-rs Rust crate. The crate is wrapped with an FFI Rust adapter, statically compiled and called from Go via CGO. This PR also has toy web client to test the RDP code, and just enough of the desktop wire protocol implementation to make it work. I excluded the RDP client from the build via tags for now to avoid bloating the teleport binaries. Many more things missing that will come in later PRs, to keep this one reviewable. * flag tweaks and updated readme for macos * Switch to C-style strings between Go and Rust * Use regular top-level C function instead ofthe cgo jump function * Fix bitmap buffer memory leak * Consistent naming for CGO types * Pass rust object reference to Go * Clean up FFI error string management * Extract a thin C wrapper for read_rdp_output * Use the log crate in rust * Small rust cleanups * Fix shellcheck nit in run.sh * Fix RDP client memory release * Allow Rust code to compile on any unix * Force Alpha channel to 100% always For some reason, on Windows 10 with bitmap decompression the Alpha always ends up as 0. This makes everything transparent. * Implement screen size and credential negotiation * desktop protocol: remove password prompt It was decided that supporting passwords natively is a bad product decision. We will only support certificate-based authn and only in ActiveDirectory environments. Until we implement certificate support, passwords can be injected via an environment variable for testing. It will be removed before the beta release. * Address review feedback Co-authored-by: Isaiah Becker-Mayer --- lib/srv/desktop/deskproto/proto.go | 273 ++++++ lib/srv/desktop/deskproto/proto_test.go | 60 ++ lib/srv/desktop/rdp/rdpclient/.gitignore | 2 + lib/srv/desktop/rdp/rdpclient/Cargo.lock | 918 ++++++++++++++++++ lib/srv/desktop/rdp/rdpclient/Cargo.toml | 14 + lib/srv/desktop/rdp/rdpclient/client.go | 370 +++++++ lib/srv/desktop/rdp/rdpclient/librdprs.h | 66 ++ lib/srv/desktop/rdp/rdpclient/run.sh | 10 + lib/srv/desktop/rdp/rdpclient/src/lib.rs | 406 ++++++++ .../rdp/rdpclient/testclient/README.md | 20 + .../rdp/rdpclient/testclient/index.html | 16 + .../desktop/rdp/rdpclient/testclient/index.js | 322 ++++++ .../desktop/rdp/rdpclient/testclient/main.go | 82 ++ rfd/0037-desktop-access-protocol.md | 29 +- 14 files changed, 2569 insertions(+), 19 deletions(-) create mode 100644 lib/srv/desktop/deskproto/proto.go create mode 100644 lib/srv/desktop/deskproto/proto_test.go create mode 100644 lib/srv/desktop/rdp/rdpclient/.gitignore create mode 100644 lib/srv/desktop/rdp/rdpclient/Cargo.lock create mode 100644 lib/srv/desktop/rdp/rdpclient/Cargo.toml create mode 100644 lib/srv/desktop/rdp/rdpclient/client.go create mode 100644 lib/srv/desktop/rdp/rdpclient/librdprs.h create mode 100755 lib/srv/desktop/rdp/rdpclient/run.sh create mode 100644 lib/srv/desktop/rdp/rdpclient/src/lib.rs create mode 100644 lib/srv/desktop/rdp/rdpclient/testclient/README.md create mode 100644 lib/srv/desktop/rdp/rdpclient/testclient/index.html create mode 100644 lib/srv/desktop/rdp/rdpclient/testclient/index.js create mode 100644 lib/srv/desktop/rdp/rdpclient/testclient/main.go diff --git a/lib/srv/desktop/deskproto/proto.go b/lib/srv/desktop/deskproto/proto.go new file mode 100644 index 0000000000000..e5f9f6afe07da --- /dev/null +++ b/lib/srv/desktop/deskproto/proto.go @@ -0,0 +1,273 @@ +/* +Copyright 2021 Gravitational, Inc. + +Licensed 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 deskproto implements the desktop protocol encoder/decoder. +// See https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md +// +// TODO(awly): complete the implementation of all messages, even if we don't +// use them yet. +package deskproto + +import ( + "bytes" + "encoding/binary" + "image" + "image/png" + "io" + + "github.com/gravitational/trace" +) + +// MessageType identifies the type of the message. +type MessageType byte + +// For descriptions of each message type see: +// https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md#message-types +const ( + TypeClientScreenSpec = MessageType(1) + TypePNGFrame = MessageType(2) + TypeMouseMove = MessageType(3) + TypeMouseButton = MessageType(4) + TypeKeyboardButton = MessageType(5) + TypeClipboardData = MessageType(6) + TypeClientUsername = MessageType(7) +) + +// Message is a Go representation of a desktop protocol message. +type Message interface { + Encode() ([]byte, error) +} + +// Decode decodes the wire representation of a message. +func Decode(buf []byte) (Message, error) { + if len(buf) == 0 { + return nil, trace.BadParameter("input desktop protocol message is empty") + } + switch MessageType(buf[0]) { + case TypeClientScreenSpec: + return decodeClientScreenSpec(buf) + case TypePNGFrame: + return decodePNGFrame(buf) + case TypeMouseMove: + return decodeMouseMove(buf) + case TypeMouseButton: + return decodeMouseButton(buf) + case TypeKeyboardButton: + return decodeKeyboardButton(buf) + case TypeClientUsername: + return decodeClientUsername(buf) + default: + return nil, trace.BadParameter("unsupported desktop protocol message type %d", buf[0]) + } +} + +// PNGFrame is the PNG frame message +// https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md#2---png-frame +type PNGFrame struct { + Img image.Image +} + +func (f PNGFrame) Encode() ([]byte, error) { + type header struct { + Type byte + Left, Top uint32 + Right, Bottom uint32 + } + + buf := new(bytes.Buffer) + if err := binary.Write(buf, binary.BigEndian, header{ + Type: byte(TypePNGFrame), + Left: uint32(f.Img.Bounds().Min.X), + Top: uint32(f.Img.Bounds().Min.Y), + Right: uint32(f.Img.Bounds().Max.X), + Bottom: uint32(f.Img.Bounds().Max.Y), + }); err != nil { + return nil, trace.Wrap(err) + } + if err := png.Encode(buf, f.Img); err != nil { + return nil, trace.Wrap(err) + } + return buf.Bytes(), nil +} + +func decodePNGFrame(buf []byte) (PNGFrame, error) { + var header struct { + Left, Top uint32 + Right, Bottom uint32 + } + r := bytes.NewReader(buf[1:]) + if err := binary.Read(r, binary.BigEndian, &header); err != nil { + return PNGFrame{}, trace.Wrap(err) + } + img, err := png.Decode(r) + if err != nil { + return PNGFrame{}, trace.Wrap(err) + } + // PNG encoding does not preserve offset image bounds. + // Opportunistically restore them based on the header. + switch img := img.(type) { + case *image.RGBA: + img.Rect = image.Rect(int(header.Left), int(header.Top), int(header.Right), int(header.Bottom)) + case *image.NRGBA: + img.Rect = image.Rect(int(header.Left), int(header.Top), int(header.Right), int(header.Bottom)) + } + return PNGFrame{Img: img}, nil +} + +// MouseMove is the mouse movement message. +// https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md#3---mouse-move +type MouseMove struct { + X, Y uint32 +} + +func (m MouseMove) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte(byte(TypeMouseMove)) + if err := binary.Write(buf, binary.BigEndian, m); err != nil { + return nil, trace.Wrap(err) + } + return buf.Bytes(), nil +} + +func decodeMouseMove(buf []byte) (MouseMove, error) { + var m MouseMove + err := binary.Read(bytes.NewReader(buf[1:]), binary.BigEndian, &m) + return m, trace.Wrap(err) +} + +// MouseButtonType identifies a specific button on the mouse. +type MouseButtonType byte + +const ( + LeftMouseButton = MouseButtonType(0) + MiddleMouseButton = MouseButtonType(1) + RightMouseButton = MouseButtonType(2) +) + +// ButtonState is the press state of a keyboard or mouse button. +type ButtonState byte + +const ( + ButtonNotPressed = ButtonState(0) + ButtonPressed = ButtonState(1) +) + +// MouseButton is the mouse button press message. +// https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md#4---mouse-button +type MouseButton struct { + Button MouseButtonType + State ButtonState +} + +func (m MouseButton) Encode() ([]byte, error) { + return []byte{byte(TypeMouseButton), byte(m.Button), byte(m.State)}, nil +} + +func decodeMouseButton(buf []byte) (MouseButton, error) { + var m MouseButton + err := binary.Read(bytes.NewReader(buf[1:]), binary.BigEndian, &m) + return m, trace.Wrap(err) +} + +// KeyboardButton is the keyboard button press message. +// https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md#4---keyboard-input +type KeyboardButton struct { + KeyCode uint32 + State ButtonState +} + +func (k KeyboardButton) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte(byte(TypeKeyboardButton)) + if err := binary.Write(buf, binary.BigEndian, k); err != nil { + return nil, trace.Wrap(err) + } + return buf.Bytes(), nil +} + +func decodeKeyboardButton(buf []byte) (KeyboardButton, error) { + var k KeyboardButton + err := binary.Read(bytes.NewReader(buf[1:]), binary.BigEndian, &k) + return k, trace.Wrap(err) +} + +// ClientScreenSpec is the client screen specification. +// https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md#1---client-screen-spec +type ClientScreenSpec struct { + Width uint32 + Height uint32 +} + +func (s ClientScreenSpec) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte(byte(TypeClientScreenSpec)) + if err := binary.Write(buf, binary.BigEndian, s); err != nil { + return nil, trace.Wrap(err) + } + return buf.Bytes(), nil +} + +func decodeClientScreenSpec(buf []byte) (ClientScreenSpec, error) { + var s ClientScreenSpec + err := binary.Read(bytes.NewReader(buf[1:]), binary.BigEndian, &s) + return s, trace.Wrap(err) +} + +// ClientUsername is the client username. +// https://github.com/gravitational/teleport/blob/master/rfd/0037-desktop-access-protocol.md#7---client-username +type ClientUsername struct { + Username string +} + +func (r ClientUsername) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte(byte(TypeClientUsername)) + if err := encodeString(buf, r.Username); err != nil { + return nil, trace.Wrap(err) + } + return buf.Bytes(), nil +} + +func decodeClientUsername(buf []byte) (ClientUsername, error) { + r := bytes.NewReader(buf[1:]) + username, err := decodeString(r) + if err != nil { + return ClientUsername{}, trace.Wrap(err) + } + return ClientUsername{Username: username}, nil +} + +func encodeString(w io.Writer, s string) error { + if err := binary.Write(w, binary.BigEndian, uint32(len(s))); err != nil { + return trace.Wrap(err) + } + if _, err := w.Write([]byte(s)); err != nil { + return trace.Wrap(err) + } + return nil +} + +func decodeString(r io.Reader) (string, error) { + var length uint32 + if err := binary.Read(r, binary.BigEndian, &length); err != nil { + return "", trace.Wrap(err) + } + s := make([]byte, int(length)) + if _, err := io.ReadFull(r, s); err != nil { + return "", trace.Wrap(err) + } + return string(s), nil +} diff --git a/lib/srv/desktop/deskproto/proto_test.go b/lib/srv/desktop/deskproto/proto_test.go new file mode 100644 index 0000000000000..8a5102c279c1e --- /dev/null +++ b/lib/srv/desktop/deskproto/proto_test.go @@ -0,0 +1,60 @@ +/* +Copyright 2021 Gravitational, Inc. + +Licensed 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 deskproto + +import ( + "image" + "image/color" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" +) + +func TestEncodeDecode(t *testing.T) { + for _, m := range []Message{ + MouseMove{X: 1, Y: 2}, + MouseButton{Button: MiddleMouseButton, State: ButtonPressed}, + KeyboardButton{KeyCode: 1, State: ButtonPressed}, + func() Message { + img := image.NewNRGBA(image.Rect(5, 5, 10, 10)) + for x := img.Rect.Min.X; x < img.Rect.Max.X; x++ { + for y := img.Rect.Min.Y; y < img.Rect.Max.Y; y++ { + img.Set(x, y, color.NRGBA{1, 2, 3, 4}) + } + } + return PNGFrame{Img: img} + }(), + ClientScreenSpec{Width: 123, Height: 456}, + ClientUsername{Username: "admin"}, + } { + + buf, err := m.Encode() + require.NoError(t, err) + + out, err := Decode(buf) + require.NoError(t, err) + + require.Empty(t, cmp.Diff(m, out)) + } +} + +func TestBadDecode(t *testing.T) { + // 254 is an unknown message type. + _, err := Decode([]byte{254}) + require.Error(t, err) +} diff --git a/lib/srv/desktop/rdp/rdpclient/.gitignore b/lib/srv/desktop/rdp/rdpclient/.gitignore new file mode 100644 index 0000000000000..b17385cdee91d --- /dev/null +++ b/lib/srv/desktop/rdp/rdpclient/.gitignore @@ -0,0 +1,2 @@ +target/ +main diff --git a/lib/srv/desktop/rdp/rdpclient/Cargo.lock b/lib/srv/desktop/rdp/rdpclient/Cargo.lock new file mode 100644 index 0000000000000..514e079587d56 --- /dev/null +++ b/lib/srv/desktop/rdp/rdpclient/Cargo.lock @@ -0,0 +1,918 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] +name = "bufstream" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "core-foundation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" + +[[package]] +name = "crypto-mac" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "der-parser" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51f64dcdf1cdc550d21d73dc959726c7dbeeab4a01481d08084a7736956464e" +dependencies = [ + "nom", + "num-bigint", + "rusticata-macros", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hmac" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dcb5e64cda4c23119ab41ba960d1e170a774c8e4b9d9e6a9bc18aabf5e59695" +dependencies = [ + "crypto-mac", + "digest", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if", + "ryu", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "md-5" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18af3dcaf2b0219366cdb4e2af65a6101457b415c3d1a5c71dd9c2b7c77b9c8" +dependencies = [ + "block-buffer", + "digest", + "opaque-debug", +] + +[[package]] +name = "md4" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4030c65cf2aab7ada769cae7d1e7159f8d034d6ded4f39afba037f094bfd9a1" +dependencies = [ + "block-buffer", + "digest", + "fake-simd", + "opaque-debug", +] + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "native-tls" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "lexical-core", + "memchr", + "version_check", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca565a7df06f3d4b485494f25ba05da1435950f4dc263440eda7a6fa9b8e36e4" +dependencies = [ + "derivative", + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffa5a33ddddfee04c0283a7653987d634e880347e96b5b2ed64de07efb59db9d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "openssl" +version = "0.10.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" + +[[package]] +name = "openssl-sys" +version = "0.9.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" +dependencies = [ + "libc", + "rand 0.4.6", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.3", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "rdp-client" +version = "0.1.0" +dependencies = [ + "env_logger", + "libc", + "log", + "rdp-rs", +] + +[[package]] +name = "rdp-rs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "992c4d207f9bc333843cef355a3a837cca72720aaa3f29bd2646282f9213c905" +dependencies = [ + "bufstream", + "byteorder", + "hmac", + "indexmap", + "md-5", + "md4", + "native-tls", + "num-bigint", + "num_enum", + "rand 0.7.3", + "rust-crypto", + "x509-parser", + "yasna", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rust-crypto" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" +dependencies = [ + "gcc", + "libc", + "rand 0.3.23", + "rustc-serialize", + "time", +] + +[[package]] +name = "rustc-serialize" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" + +[[package]] +name = "rusticata-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a9050636e8a1b487ba1fbe99114021cd7594dde3ce6ed95bfc1691e5b5367b" +dependencies = [ + "nom", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + +[[package]] +name = "security-framework" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" + +[[package]] +name = "syn" +version = "1.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if", + "libc", + "rand 0.8.4", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "typenum" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "x509-parser" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99bbe736dd2b422d66e4830f4a06f34387c9814c027efcbda5c2f86463e8e5b0" +dependencies = [ + "base64", + "der-parser", + "nom", + "num-bigint", + "rusticata-macros", + "time", +] + +[[package]] +name = "yasna" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de7bff972b4f2a06c85f6d8454b09df153af7e3a4ec2aac81db1b105b684ddb" diff --git a/lib/srv/desktop/rdp/rdpclient/Cargo.toml b/lib/srv/desktop/rdp/rdpclient/Cargo.toml new file mode 100644 index 0000000000000..ffb084dc24bee --- /dev/null +++ b/lib/srv/desktop/rdp/rdpclient/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "rdp-client" +version = "0.1.0" +authors = ["Andrew Lytvynov "] +edition = "2018" + +[lib] +crate-type = ["staticlib"] + +[dependencies] +env_logger = "0.9.0" +libc = "0.2.98" +log = "0.4.14" +rdp-rs = "0.1.0" diff --git a/lib/srv/desktop/rdp/rdpclient/client.go b/lib/srv/desktop/rdp/rdpclient/client.go new file mode 100644 index 0000000000000..d9eac3c799f72 --- /dev/null +++ b/lib/srv/desktop/rdp/rdpclient/client.go @@ -0,0 +1,370 @@ +//+build desktop_access_beta + +/* +Copyright 2021 Gravitational, Inc. + +Licensed 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 implements an RDP client. +package rdpclient + +// Some implementation details that don't belong in the public godoc: +// This package wraps a Rust library based on https://crates.io/crates/rdp-rs. +// +// The Rust library is statically-compiled and called via CGO. +// The Go code sends and receives the CGO versions of Rust RDP events +// https://docs.rs/rdp-rs/0.1.0/rdp/core/event/index.html and translates them +// to the desktop protocol versions. +// +// The flow is roughly this: +// Go Rust +// ============================================== +// rdpclient.New -----------------> connect_rdp +// *connected* +// +// *register output callback* +// -----------------> read_rdp_output +// handleBitmap <---------------- +// handleBitmap <---------------- +// handleBitmap <---------------- +// *output streaming continues...* +// +// *user input messages* +// InputMessage(MouseMove) ------> write_rdp_pointer +// InputMessage(MouseButton) ----> write_rdp_pointer +// InputMessage(KeyboardButton) -> write_rdp_keyboard +// *user input continues...* +// +// *connection closed (client or server side)* +// Wait -----------------> close_rdp +// + +/* +// Flags to include the static Rust library. +#cgo linux LDFLAGS: -L${SRCDIR}/target/debug -l:librdp_client.a -lpthread -lcrypto -ldl -lssl -lm +#cgo darwin LDFLAGS: -framework CoreFoundation -framework Security -L${SRCDIR}/target/debug -lrdp_client -lpthread -lcrypto -ldl -lssl -lm +#include +*/ +import "C" +import ( + "errors" + "fmt" + "image" + "os" + "sync" + "unsafe" + + "github.com/gravitational/trace" + "github.com/sirupsen/logrus" + + "github.com/gravitational/teleport/lib/srv/desktop/deskproto" +) + +func init() { + C.init() +} + +// Config for creating a new Client. +type Config struct { + // Addr is the network address of the RDP server, in the form host:port. + Addr string + // InputMessage is called to receive a message from the client for the RDP + // server. This function should block until there is a message. + InputMessage func() (deskproto.Message, error) + // OutputMessage is called to send a message from RDP server to the client. + OutputMessage func(deskproto.Message) error + // Log is the logger for status messages. + Log logrus.FieldLogger +} + +func (c *Config) checkAndSetDefaults() error { + if c.Addr == "" { + return trace.BadParameter("missing Addr in rdpclient.Config") + } + if c.InputMessage == nil { + return trace.BadParameter("missing InputMessage in rdpclient.Config") + } + if c.OutputMessage == nil { + return trace.BadParameter("missing OutputMessage in rdpclient.Config") + } + if c.Log == nil { + c.Log = logrus.New() + } + c.Log = c.Log.WithField("rdp-addr", c.Addr) + return nil +} + +// Client is the RDP client. +type Client struct { + cfg Config + + clientWidth, clientHeight uint16 + username string + rustClient *C.Client + wg sync.WaitGroup + closeOnce sync.Once +} + +// New creates and connects a new Client based on cfg. +func New(cfg Config) (*Client, error) { + if err := cfg.checkAndSetDefaults(); err != nil { + return nil, err + } + c := &Client{cfg: cfg} + + if err := c.readClientUsername(); err != nil { + return nil, trace.Wrap(err) + } + if err := c.readClientSize(); err != nil { + return nil, trace.Wrap(err) + } + if err := c.connect(); err != nil { + return nil, trace.Wrap(err) + } + c.start() + return c, nil +} + +func (c *Client) readClientUsername() error { + for { + msg, err := c.cfg.InputMessage() + if err != nil { + return trace.Wrap(err) + } + u, ok := msg.(deskproto.ClientUsername) + if !ok { + c.cfg.Log.Debugf("Expected ClientUsername message, got %T", msg) + continue + } + c.username = u.Username + return nil + } +} + +func (c *Client) readClientSize() error { + for { + msg, err := c.cfg.InputMessage() + if err != nil { + return trace.Wrap(err) + } + s, ok := msg.(deskproto.ClientScreenSpec) + if !ok { + c.cfg.Log.Debugf("Expected ClientScreenSpec message, got %T", msg) + continue + } + c.clientWidth = uint16(s.Width) + c.clientHeight = uint16(s.Height) + return nil + } +} + +func (c *Client) connect() error { + addr := C.CString(c.cfg.Addr) + defer C.free(unsafe.Pointer(addr)) + username := C.CString(c.username) + defer C.free(unsafe.Pointer(username)) + + // *Temporary* hack for injecting passwords until we implement cert-based + // authentication. + // TODO(awly): remove this after certificates are implemented. + passwordStr := os.Getenv("TELEPORT_DEV_RDP_PASSWORD") + if passwordStr == "" { + return trace.BadParameter("missing TELEPORT_DEV_RDP_PASSWORD env var and certificate authentication is not implemented yet") + } + password := C.CString(passwordStr) + defer C.free(unsafe.Pointer(password)) + + res := C.connect_rdp( + addr, + username, + password, + C.uint16_t(c.clientWidth), + C.uint16_t(c.clientHeight), + ) + if err := cgoError(res.err); err != nil { + return trace.Wrap(err) + } + c.rustClient = res.client + return nil +} + +func (c *Client) start() { + // Video output streaming worker goroutine. + c.wg.Add(1) + go func() { + defer c.wg.Done() + defer c.closeConn() + defer c.cfg.Log.Info("RDP output streaming finished") + + clientRef := registerClient(c) + defer unregisterClient(clientRef) + + if err := cgoError(C.read_rdp_output(c.rustClient, C.int64_t(clientRef))); err != nil { + c.cfg.Log.Warningf("Failed reading RDP output frame: %v", err) + } + }() + + // User input streaming worker goroutine. + c.wg.Add(1) + go func() { + defer c.wg.Done() + defer c.closeConn() + defer c.cfg.Log.Info("RDP input streaming finished") + var mouseX, mouseY uint32 + for { + msg, err := c.cfg.InputMessage() + if err != nil { + c.cfg.Log.Warningf("Failed reading RDP input message: %v", err) + return + } + switch m := msg.(type) { + case deskproto.MouseMove: + mouseX, mouseY = m.X, m.Y + if err := cgoError(C.write_rdp_pointer( + c.rustClient, + C.CGOPointer{ + x: C.uint16_t(m.X), + y: C.uint16_t(m.Y), + button: C.PointerButtonNone, + }, + )); err != nil { + c.cfg.Log.Warningf("Failed forwarding RDP input message: %v", err) + return + } + case deskproto.MouseButton: + var button C.CGOPointerButton + switch m.Button { + case deskproto.LeftMouseButton: + button = C.PointerButtonLeft + case deskproto.RightMouseButton: + button = C.PointerButtonRight + case deskproto.MiddleMouseButton: + button = C.PointerButtonMiddle + default: + button = C.PointerButtonNone + } + if err := cgoError(C.write_rdp_pointer( + c.rustClient, + C.CGOPointer{ + x: C.uint16_t(mouseX), + y: C.uint16_t(mouseY), + button: uint32(button), + down: m.State == deskproto.ButtonPressed, + }, + )); err != nil { + c.cfg.Log.Warningf("Failed forwarding RDP input message: %v", err) + return + } + case deskproto.KeyboardButton: + if err := cgoError(C.write_rdp_keyboard( + c.rustClient, + C.CGOKey{ + code: C.uint16_t(m.KeyCode), + down: m.State == deskproto.ButtonPressed, + }, + )); err != nil { + c.cfg.Log.Warningf("Failed forwarding RDP input message: %v", err) + return + } + default: + c.cfg.Log.Warningf("Skipping unimplemented desktop protocol message type %T", msg) + } + } + }() +} + +//export handle_bitmap +func handle_bitmap(ci C.int64_t, cb C.CGOBitmap) C.CGOError { + return findClient(int64(ci)).handleBitmap(cb) +} + +func (c *Client) handleBitmap(cb C.CGOBitmap) C.CGOError { + data := C.GoBytes(unsafe.Pointer(cb.data_ptr), C.int(cb.data_len)) + // Convert BGRA to RGBA. It's likely due to Windows using uint32 values for + // pixels (ARGB) and encoding them as big endian. The image.RGBA type uses + // a byte slice with 4-byte segments representing pixels (RGBA). + // + // Also, always force Alpha value to 100% (opaque). On some Windows + // versions it's sent as 0% after decompression for some reason. + for i := 0; i < len(data); i += 4 { + data[i], data[i+2], data[i+3] = data[i+2], data[i], 255 + } + img := image.NewNRGBA(image.Rectangle{ + Min: image.Pt(int(cb.dest_left), int(cb.dest_top)), + Max: image.Pt(int(cb.dest_right)+1, int(cb.dest_bottom)+1), + }) + copy(img.Pix, data) + + if err := c.cfg.OutputMessage(deskproto.PNGFrame{Img: img}); err != nil { + return C.CString(fmt.Sprintf("failed to send PNG frame %v: %v", img.Rect, err)) + } + return nil +} + +// Wait blocks until the client disconnects and runs the cleanup. +func (c *Client) Wait() error { + c.wg.Wait() + C.free_rdp(c.rustClient) + return nil +} + +func (c *Client) closeConn() { + c.closeOnce.Do(func() { + if err := cgoError(C.close_rdp(c.rustClient)); err != nil { + c.cfg.Log.Warningf("Error closing RDP connection: %v", err) + } + }) +} + +func cgoError(s C.CGOError) error { + if s == nil { + return nil + } + gs := C.GoString(s) + C.free_rust_string(s) + return errors.New(gs) +} + +//export free_go_string +func free_go_string(s *C.char) { + C.free(unsafe.Pointer(s)) +} + +// Global registry of active clients. This allows Rust to reference a specific +// client without sending actual objects around. +var ( + clientsMu = &sync.RWMutex{} + clients = make(map[int64]*Client) + clientsIndex = int64(-1) +) + +func registerClient(c *Client) int64 { + clientsMu.Lock() + defer clientsMu.Unlock() + clientsIndex++ + clients[clientsIndex] = c + return clientsIndex +} + +func unregisterClient(i int64) { + clientsMu.Lock() + defer clientsMu.Unlock() + delete(clients, i) +} + +func findClient(i int64) *Client { + clientsMu.RLock() + defer clientsMu.RUnlock() + return clients[i] +} diff --git a/lib/srv/desktop/rdp/rdpclient/librdprs.h b/lib/srv/desktop/rdp/rdpclient/librdprs.h new file mode 100644 index 0000000000000..75409749f17b9 --- /dev/null +++ b/lib/srv/desktop/rdp/rdpclient/librdprs.h @@ -0,0 +1,66 @@ +#include +#include +#include +#include + +typedef enum CGOPointerButton { + PointerButtonNone, + PointerButtonLeft, + PointerButtonRight, + PointerButtonMiddle, +} CGOPointerButton; + +typedef struct Client Client; + +typedef char *CGOError; + +typedef struct ClientOrError { + struct Client *client; + CGOError err; +} ClientOrError; + +typedef struct CGOPointer { + uint16_t x; + uint16_t y; + enum CGOPointerButton button; + bool down; +} CGOPointer; + +typedef struct CGOKey { + uint16_t code; + bool down; +} CGOKey; + +typedef struct CGOBitmap { + uint16_t dest_left; + uint16_t dest_top; + uint16_t dest_right; + uint16_t dest_bottom; + uint8_t *data_ptr; + uintptr_t data_len; + uintptr_t data_cap; +} CGOBitmap; + +void init(void); + +struct ClientOrError connect_rdp(char *go_addr, + char *go_username, + char *go_password, + uint16_t screen_width, + uint16_t screen_height); + +CGOError read_rdp_output(struct Client *client_ptr, int64_t client_ref); + +CGOError write_rdp_pointer(struct Client *client_ptr, struct CGOPointer pointer); + +CGOError write_rdp_keyboard(struct Client *client_ptr, struct CGOKey key); + +CGOError close_rdp(struct Client *client_ptr); + +void free_rdp(struct Client *client_ptr); + +void free_rust_string(char *s); + +extern void free_go_string(char *s); + +extern CGOError handle_bitmap(int64_t client_ref, struct CGOBitmap b); diff --git a/lib/srv/desktop/rdp/rdpclient/run.sh b/lib/srv/desktop/rdp/rdpclient/run.sh new file mode 100755 index 0000000000000..d8fcfb761b597 --- /dev/null +++ b/lib/srv/desktop/rdp/rdpclient/run.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail + +cargo build +cargo install cbindgen +cbindgen --crate rdp-client --output librdprs.h --lang c + +export RUST_BACKTRACE=1 +go build -tags desktop_access_beta testclient/main.go +./main "$@" diff --git a/lib/srv/desktop/rdp/rdpclient/src/lib.rs b/lib/srv/desktop/rdp/rdpclient/src/lib.rs new file mode 100644 index 0000000000000..6df1eba697a44 --- /dev/null +++ b/lib/srv/desktop/rdp/rdpclient/src/lib.rs @@ -0,0 +1,406 @@ +#[macro_use] +extern crate log; + +use libc::{fd_set, select, FD_SET}; +use rdp::core::client::{Connector, RdpClient}; +use rdp::core::event::*; +use rdp::model::error::Error as RdpError; +use std::convert::TryFrom; +use std::ffi::{CStr, CString}; +use std::io::Error as IoError; +use std::mem; +use std::net::TcpStream; +use std::os::raw::c_char; +use std::os::unix::io::AsRawFd; +use std::ptr; +use std::sync::{Arc, Mutex}; + +#[no_mangle] +pub extern "C" fn init() { + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .init(); +} + +// Client has an unusual lifecycle: +// - connect_rdp creates it on the heap, grabs a raw pointer and returns in to Go +// - most other exported rdp functions take the raw pointer, convert it to a reference for use +// without dropping the Client +// - close_rdp takes the raw pointer and drops it +// +// All of the exported rdp functions could run concurrently, so the rdp_client is synchronized. +// tcp_fd is only set in connect_rdp and used as read-only afterwards, so it does not need +// synchronization. +pub struct Client { + rdp_client: Arc>>, + tcp_fd: usize, +} + +impl Client { + fn into_raw(self: Box) -> *mut Self { + Box::into_raw(self) + } + unsafe fn from_ptr<'a>(ptr: *const Self) -> Option<&'a Client> { + ptr.as_ref() + } + unsafe fn from_raw(ptr: *mut Self) -> Box { + Box::from_raw(ptr) + } +} + +#[repr(C)] +pub struct ClientOrError { + client: *mut Client, + err: CGOError, +} + +impl From> for ClientOrError { + fn from(r: Result) -> ClientOrError { + match r { + Ok(client) => ClientOrError { + client: Box::new(client).into_raw(), + err: CGO_OK, + }, + Err(e) => ClientOrError { + client: ptr::null_mut(), + err: to_cgo_error(format!("{:?}", e)), + }, + } + } +} + +// connect_rdp establishes an RDP connection to go_addr with the provided credentials and screen +// size. If succeeded, the client is internally registered under client_ref. When done with the +// connection, the caller must call close_rdp. +#[no_mangle] +pub extern "C" fn connect_rdp( + go_addr: *mut c_char, + go_username: *mut c_char, + go_password: *mut c_char, + screen_width: u16, + screen_height: u16, +) -> ClientOrError { + // Convert from C to Rust types. + let addr = from_go_string(go_addr); + let username = from_go_string(go_username); + let password = from_go_string(go_password); + + connect_rdp_inner(&addr, &username, &password, screen_width, screen_height).into() +} + +#[derive(Debug)] +enum ConnectError { + TCP(IoError), + RDP(RdpError), +} + +impl From for ConnectError { + fn from(e: IoError) -> ConnectError { + ConnectError::TCP(e) + } +} + +impl From for ConnectError { + fn from(e: RdpError) -> ConnectError { + ConnectError::RDP(e) + } +} + +fn connect_rdp_inner( + addr: &str, + username: &str, + password: &str, + screen_width: u16, + screen_height: u16, +) -> Result { + // Connect and authenticate. + let tcp = TcpStream::connect(addr)?; + let tcp_fd = tcp.as_raw_fd() as usize; + let mut connector = Connector::new() + .screen(screen_width, screen_height) + .credentials(".".to_string(), username.to_string(), password.to_string()); + let client = connector.connect(tcp)?; + + Ok(Client { + rdp_client: Arc::new(Mutex::new(client)), + tcp_fd: tcp_fd, + }) +} + +#[repr(C)] +pub struct CGOBitmap { + pub dest_left: u16, + pub dest_top: u16, + pub dest_right: u16, + pub dest_bottom: u16, + // Memory is freed on the Rust side. + pub data_ptr: *mut u8, + pub data_len: usize, + pub data_cap: usize, +} + +impl TryFrom for CGOBitmap { + type Error = RdpError; + + fn try_from(e: BitmapEvent) -> Result { + let mut res = CGOBitmap { + dest_left: e.dest_left, + dest_top: e.dest_top, + dest_right: e.dest_right, + dest_bottom: e.dest_bottom, + data_ptr: ptr::null_mut(), + data_len: 0, + data_cap: 0, + }; + + // e.decompress consumes e, so we need to call it separately, after populating the fields + // above. + let mut data = if e.is_compress { + e.decompress()? + } else { + e.data + }; + res.data_ptr = data.as_mut_ptr(); + res.data_len = data.len(); + res.data_cap = data.capacity(); + mem::forget(data); + + Ok(res) + } +} + +impl Drop for CGOBitmap { + fn drop(&mut self) { + // Reconstruct into Vec to drop the allocated buffer. + unsafe { + let _ = Vec::from_raw_parts(self.data_ptr, self.data_len, self.data_cap); + } + } +} + +#[cfg(unix)] +fn wait_for_fd(fd: usize) -> bool { + unsafe { + let mut raw_fds: fd_set = mem::zeroed(); + + FD_SET(fd as i32, &mut raw_fds); + + let result = select( + fd as i32 + 1, + &mut raw_fds, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ); + result == 1 + } +} + +// read_rdp_output reads incoming RDP bitmap frames from client at client_ref and forwards them to +// handle_bitmap. handle_bitmap *must not* free the memory of CGOBitmap. +#[no_mangle] +pub extern "C" fn read_rdp_output(client_ptr: *mut Client, client_ref: i64) -> CGOError { + let client = unsafe { Client::from_ptr(client_ptr) }; + let client = match client { + Some(client) => client, + None => { + return to_cgo_error("invalid Rust client pointer".to_string()); + } + }; + if let Some(err) = read_rdp_output_inner(client, client_ref) { + to_cgo_error(err) + } else { + CGO_OK + } +} + +fn read_rdp_output_inner(client: &Client, client_ref: i64) -> Option { + let tcp_fd = client.tcp_fd; + // Read incoming events. + while wait_for_fd(tcp_fd as usize) { + let mut err = CGO_OK; + let res = client + .rdp_client + .lock() + .unwrap() + .read(|rdp_event| match rdp_event { + RdpEvent::Bitmap(bitmap) => { + let cbitmap = match CGOBitmap::try_from(bitmap) { + Ok(cb) => cb, + Err(e) => { + error!( + "failed to convert RDP bitmap to CGO representation: {:?}", + e + ); + return; + } + }; + unsafe { + err = handle_bitmap(client_ref, cbitmap) as CGOError; + }; + } + // These should never really be sent by the server to us. + RdpEvent::Pointer(_) => { + debug!("got unexpected pointer event from RDP server, ignoring"); + } + RdpEvent::Key(_) => { + debug!("got unexpected keyboard event from RDP server, ignoring"); + } + }); + if let Err(e) = res { + return Some(format!("failed forwarding RDP bitmap frame: {:?}", e)); + }; + if err != CGO_OK { + let err_str = from_cgo_error(err); + return Some(format!("failed forwarding RDP bitmap frame: {}", err_str)); + } + } + None +} + +// A CGO-compatible copy of PointerEvent. +#[repr(C)] +#[derive(Copy, Clone)] +pub struct CGOPointer { + pub x: u16, + pub y: u16, + pub button: CGOPointerButton, + pub down: bool, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub enum CGOPointerButton { + PointerButtonNone, + PointerButtonLeft, + PointerButtonRight, + PointerButtonMiddle, +} + +impl From for PointerEvent { + fn from(p: CGOPointer) -> PointerEvent { + PointerEvent { + x: p.x, + y: p.y, + button: match p.button { + CGOPointerButton::PointerButtonNone => PointerButton::None, + CGOPointerButton::PointerButtonLeft => PointerButton::Left, + CGOPointerButton::PointerButtonRight => PointerButton::Right, + CGOPointerButton::PointerButtonMiddle => PointerButton::Middle, + }, + down: p.down, + } + } +} + +#[no_mangle] +pub extern "C" fn write_rdp_pointer(client_ptr: *mut Client, pointer: CGOPointer) -> CGOError { + let client = unsafe { Client::from_ptr(client_ptr) }; + let client = match client { + Some(client) => client, + None => { + return to_cgo_error("invalid Rust client pointer".to_string()); + } + }; + let res = client + .rdp_client + .lock() + .unwrap() + .write(RdpEvent::Pointer(pointer.into())); + if let Err(e) = res { + to_cgo_error(format!("failed writing RDP pointer event: {:?}", e)) + } else { + CGO_OK + } +} + +// A CGO-compatible copy of KeyboardEvent. +#[repr(C)] +#[derive(Copy, Clone)] +pub struct CGOKey { + pub code: u16, + pub down: bool, +} + +impl From for KeyboardEvent { + fn from(k: CGOKey) -> KeyboardEvent { + KeyboardEvent { + code: k.code, + down: k.down, + } + } +} + +#[no_mangle] +pub extern "C" fn write_rdp_keyboard(client_ptr: *mut Client, key: CGOKey) -> CGOError { + let client = unsafe { Client::from_ptr(client_ptr) }; + let client = match client { + Some(client) => client, + None => { + return to_cgo_error("invalid Rust client pointer".to_string()); + } + }; + let res = client + .rdp_client + .lock() + .unwrap() + .write(RdpEvent::Key(key.into())); + if let Err(e) = res { + to_cgo_error(format!("failed writing RDP keyboard event: {:?}", e)) + } else { + CGO_OK + } +} + +#[no_mangle] +pub extern "C" fn close_rdp(client_ptr: *mut Client) -> CGOError { + let client = unsafe { Client::from_ptr(client_ptr) }; + let client = match client { + Some(client) => client, + None => { + return to_cgo_error("invalid Rust client pointer".to_string()); + } + }; + if let Err(e) = client.rdp_client.lock().unwrap().shutdown() { + to_cgo_error(format!("failed writing RDP keyboard event: {:?}", e)) + } else { + CGO_OK + } +} + +#[no_mangle] +pub extern "C" fn free_rdp(client_ptr: *mut Client) { + unsafe { drop(Client::from_raw(client_ptr)) } +} + +#[no_mangle] +pub unsafe extern "C" fn free_rust_string(s: *mut c_char) { + let _ = CString::from_raw(s); +} + +fn from_go_string(s: *mut c_char) -> String { + unsafe { CStr::from_ptr(s).to_string_lossy().into_owned().clone() } +} + +// CGOError is an alias for a C string pointer, for C API clarity. +pub type CGOError = *mut c_char; + +const CGO_OK: CGOError = ptr::null_mut(); + +fn to_cgo_error(s: String) -> CGOError { + CString::new(s).expect("CString::new failed").into_raw() +} + +// from_cgo_error copies CGOError into a String and frees the underlying Go memory. +fn from_cgo_error(e: CGOError) -> String { + let s = from_go_string(e); + unsafe { + free_go_string(e); + } + s +} + +extern "C" { + fn free_go_string(s: *mut c_char); + fn handle_bitmap(client_ref: i64, b: CGOBitmap) -> CGOError; +} diff --git a/lib/srv/desktop/rdp/rdpclient/testclient/README.md b/lib/srv/desktop/rdp/rdpclient/testclient/README.md new file mode 100644 index 0000000000000..073deb7229ef9 --- /dev/null +++ b/lib/srv/desktop/rdp/rdpclient/testclient/README.md @@ -0,0 +1,20 @@ +## Install + +#### MacOS + +First ensure that `libcrypto` and `libssl` are in your include path. If you use `brew`, this can be done by running + +``` +brew install openssl@1.1 +sudo cp /usr/local/Cellar/openssl@1.1/1.1.1k/lib/libcrypto.* /usr/local/Cellar/openssl@1.1/1.1.1k/lib/libssl.* /usr/local/lib/ +``` + +## Run + +To run the test client, from the `rdpclient` directory execute: + +```sh +./run.sh :3389 +``` + +After it starts, open http://localhost:8080 and click `connect`. diff --git a/lib/srv/desktop/rdp/rdpclient/testclient/index.html b/lib/srv/desktop/rdp/rdpclient/testclient/index.html new file mode 100644 index 0000000000000..f8d1ab0acfe18 --- /dev/null +++ b/lib/srv/desktop/rdp/rdpclient/testclient/index.html @@ -0,0 +1,16 @@ + + + + + + Desktop Access + + + + +
+ +
+ + + diff --git a/lib/srv/desktop/rdp/rdpclient/testclient/index.js b/lib/srv/desktop/rdp/rdpclient/testclient/index.js new file mode 100644 index 0000000000000..5a8d2f903de89 --- /dev/null +++ b/lib/srv/desktop/rdp/rdpclient/testclient/index.js @@ -0,0 +1,322 @@ +var stream; +var canvas; + +function connectStream() { + if (stream) { stream.close(); } + + var ctx = canvas.getContext("2d"); + + stream = new WebSocket("ws://"+document.location.host+"/connect"); + stream.onerror = function(event) { console.log("ws error:", event); + } + stream.onopen = function(event) { + console.log("ws opened"); + sendScreenSize(); + } + stream.onclose = function(event) { + console.log("ws closed"); + } + stream.onmessage = function(event) { + processUpdateImage(ctx, event.data); + } +} + +function sendScreenSize(e) { + if (stream === undefined) { + return; + } + const buffer = new ArrayBuffer(9); + const view = new DataView(buffer); + view.setUint8(0, 1); + view.setUint32(1, canvas.width); + view.setUint32(5, canvas.height); + console.log("canvas size: ", canvas.width, canvas.height); + stream.send(buffer); +} + +function processUpdateImage(ctx, data) { + var prefix = data.slice(0, 1); + var rest = data.slice(1); + prefix.arrayBuffer().then( buf => { + const updateType = new DataView(buf).getUint8(0); + if (updateType === 2) { + processFrameImage(ctx, rest); + } else { + console.log("unknown image update type", updateType); + } + }); +} + +function processFrameImage(ctx, blob) { + var dimensions = blob.slice(0, 16); + var pngData = blob.slice(16); + dimensions.arrayBuffer().then( buf => { + var dv = new DataView(buf); + const left = dv.getUint32(0); + const top = dv.getUint32(4); + const right = dv.getUint32(8); + const bottom = dv.getUint32(12); + + createImageBitmap(pngData).then( bitmap => { + ctx.drawImage(bitmap, left, top); + }); + }) +} + +export function handleMouseEnter(e) { + // Grab focus on canvas to catch keyboard events. + canvas.focus(); +} + +export function handleMouseLeave(e) { + canvas.blur(); +} + +export function handleContextMenu(e) { + // Prevent native context menu to not obscure remote context menu. + return false; +} + +export function handleMouseMove(e) { + if (stream === undefined) { + return; + } + var rect = canvas.getBoundingClientRect(); + const x = (e.clientX - rect.left); + const y = (e.clientY - rect.top); + const buffer = new ArrayBuffer(9); + const view = new DataView(buffer); + view.setUint8(0, 3); + view.setUint32(1, x); + view.setUint32(5, y); + stream.send(buffer); +} + +export function handleMouseDown(e) { + handleMouseButton(e, true); +} + +export function handleMouseUp(e) { + handleMouseButton(e, false); +} + +function handleMouseButton(e, down) { + if (stream === undefined) { + return; + } + const buffer = new ArrayBuffer(3); + const view = new DataView(buffer); + view.setUint8(0, 4); + view.setUint8(1, e.button); + if (down) { + view.setUint8(2, 1); + } else { + view.setUint8(2, 0); + } + stream.send(buffer); +} + +export function handleKeyDown(e) { + handleKeyboardButton(e, true); + return false; +} + +export function handleKeyUp(e) { + handleKeyboardButton(e, false); + return false; +} + +function handleKeyboardButton(e, down) { + if (stream === undefined) { + return; + } + const scanCode = keyScancodes[e.code]; + if (!scanCode) { + return; + } + const buffer = new ArrayBuffer(6); + const view = new DataView(buffer); + view.setUint8(0, 5); + view.setUint32(1, scanCode); + if (down) { + view.setUint8(5, 1); + } else { + view.setUint8(5, 0); + } + stream.send(buffer); +} + +window.onload = function() { + canvas = document.getElementById("canvas"); + // Resize to match viewport size. + canvas.width = document.body.clientWidth; + canvas.height = document.documentElement.clientHeight - 50; + // Allow catching of keyboard events. + canvas.tabIndex = 1000; + canvas.onmouseenter = handleMouseEnter; + canvas.onmouseleave = handleMouseLeave; + canvas.oncontextmenu = handleContextMenu; + canvas.onmousemove = handleMouseMove; + canvas.onmousedown = handleMouseDown; + canvas.onmouseup = handleMouseUp; + canvas.onkeydown = handleKeyDown; + canvas.onkeyup = handleKeyUp; + + document.getElementById("button-start").addEventListener('click', connectStream); +} + +// TODO: make these cross-browser, some key codes depend on the browser: +// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code/code_values +const keyScancodes = { + "Unidentified": 0x0000, + "": 0x0000, + "Escape": 0x0001, + "Digit0": 0x0002, + "Digit1": 0x0003, + "Digit2": 0x0004, + "Digit3": 0x0005, + "Digit4": 0x0006, + "Digit5": 0x0007, + "Digit6": 0x0008, + "Digit7": 0x0009, + "Digit8": 0x000A, + "Digit9": 0x000B, + "Minus": 0x000C, + "Equal": 0x000D, + "Backspace": 0x000E, + "Tab": 0x000F, + "KeyQ": 0x0010, + "KeyW": 0x0011, + "KeyE": 0x0012, + "KeyR": 0x0013, + "KeyT": 0x0014, + "KeyY": 0x0015, + "KeyU": 0x0016, + "KeyI": 0x0017, + "KeyO": 0x0018, + "KeyP": 0x0019, + "BracketLeft": 0x001A, + "BracketRight": 0x001B, + "Enter": 0x001C, + "ControlLeft": 0x001D, + "KeyA": 0x001E, + "KeyS": 0x001F, + "KeyD": 0x0020, + "KeyF": 0x0021, + "KeyG": 0x0022, + "KeyH": 0x0023, + "KeyJ": 0x0024, + "KeyK": 0x0025, + "KeyL": 0x0026, + "Semicolon": 0x0027, + "Quote": 0x0028, + "Backquote": 0x0029, + "ShiftLeft": 0x002A, + "Backslash": 0x002B, + "KeyZ": 0x002C, + "KeyX": 0x002D, + "KeyC": 0x002E, + "KeyV": 0x002F, + "KeyB": 0x0030, + "KeyN": 0x0031, + "KeyM": 0x0032, + "Comma": 0x0033, + "Period": 0x0034, + "Slash": 0x0035, + "ShiftRight": 0x0036, + "NumpadMultiply": 0x0037, + "AltLeft": 0x0038, + "Space": 0x0039, + "CapsLock": 0x003A, + "F1": 0x003B, + "F2": 0x003C, + "F3": 0x003D, + "F4": 0x003E, + "F5": 0x003F, + "F6": 0x0040, + "F7": 0x0041, + "F8": 0x0042, + "F9": 0x0043, + "F10": 0x0044, + "Pause": 0x0045, + "ScrollLock": 0x0046, + "Numpad7": 0x0047, + "Numpad8": 0x0048, + "Numpad9": 0x0049, + "NumpadSubtract": 0x004A, + "Numpad4": 0x004B, + "Numpad5": 0x004C, + "Numpad6": 0x004D, + "NumpadAdd": 0x004E, + "Numpad1": 0x004F, + "Numpad2": 0x0050, + "Numpad3": 0x0051, + "Numpad0": 0x0052, + "NumpadDecimal": 0x0053, + "PrintScreen": 0x0054, + "IntlBackslash": 0x0056, + "F11": 0x0057, + "F12": 0x0058, + "NumpadEqual": 0x0059, + "F13": 0x0064, + "F14": 0x0065, + "F15": 0x0066, + "F16": 0x0067, + "F17": 0x0068, + "F18": 0x0069, + "F19": 0x006A, + "F20": 0x006B, + "F21": 0x006C, + "F22": 0x006D, + "F23": 0x006E, + "KanaMode": 0x0070, + "Lang2": 0x0071, + "Lang1": 0x0072, + "IntlRo": 0x0073, + "F24": 0x0076, + "Convert": 0x0079, + "NonConvert": 0x007B, + "IntlYen": 0x007D, + "NumpadComma": 0x007E, + "MediaTrackPrevious": 0xE010, + "MediaTrackNext": 0xE019, + "NumpadEnter": 0xE01C, + "ControlRight": 0xE01D, + "AudioVolumeMute": 0xE020, + "LaunchApp2": 0xE021, + "MediaPlayPause": 0xE022, + "MediaStop": 0xE024, + "AudioVolumeDown": 0xE02E, + "AudioVolumeUp": 0xE030, + "BrowserHome": 0xE032, + "NumpadDivide": 0xE035, + "PrintScreen": 0xE037, + "AltRight": 0xE038, + "NumLock": 0xE045, + "Pause": 0xE046, + "Home": 0xE047, + "ArrowUp": 0xE048, + "PageUp": 0xE049, + "ArrowLeft": 0xE04B, + "ArrowRight": 0xE04D, + "End": 0xE04F, + "ArrowDown": 0xE050, + "PageDown": 0xE051, + "Insert": 0xE052, + "Delete": 0xE053, + "MetaLeft": 0xE05B, + "MetaRight": 0xE05C, + "ContextMenu": 0xE05D, + "Power": 0xE05E, + "BrowserSearch": 0xE065, + "BrowserFavorites": 0xE066, + "BrowserRefresh": 0xE067, + "BrowserStop": 0xE068, + "BrowserForward": 0xE069, + "BrowserBack": 0xE06A, + "LaunchApp1": 0xE06B, + "LaunchMail": 0xE06C, + "LaunchMediaPlayer": 0xE06D, + "Lang2": 0xE0F1, + "Lang1": 0xE0F2, +} diff --git a/lib/srv/desktop/rdp/rdpclient/testclient/main.go b/lib/srv/desktop/rdp/rdpclient/testclient/main.go new file mode 100644 index 0000000000000..6db9112a52b5d --- /dev/null +++ b/lib/srv/desktop/rdp/rdpclient/testclient/main.go @@ -0,0 +1,82 @@ +//+build desktop_access_beta + +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "path/filepath" + + "golang.org/x/net/websocket" + + "github.com/gravitational/teleport/lib/srv/desktop/deskproto" + "github.com/gravitational/teleport/lib/srv/desktop/rdp/rdpclient" +) + +func main() { + if len(os.Args) < 3 { + log.Fatalf("usage: TELEPORT_DEV_RDP_PASSWORD=password %s host:port user", os.Args[0]) + } + addr := os.Args[1] + username := os.Args[2] + if os.Getenv("TELEPORT_DEV_RDP_PASSWORD") == "" { + log.Fatal("missing TELEPORT_DEV_RDP_PASSWORD env var") + } + + assetPath := filepath.Join(exeDir(), "testclient") + log.Printf("serving assets from %q", assetPath) + http.Handle("/", http.FileServer(http.Dir(assetPath))) + http.Handle("/connect", handleConnect(addr, username)) + + log.Println("listening on http://localhost:8080") + if err := http.ListenAndServe(":8080", nil); err != nil { + log.Fatal("ListenAndServe failed:", err) + } +} + +func handleConnect(addr, username string) http.Handler { + return websocket.Handler(func(conn *websocket.Conn) { + usernameSent := false + c, err := rdpclient.New(rdpclient.Config{ + Addr: addr, + OutputMessage: func(m deskproto.Message) error { + data, err := m.Encode() + if err != nil { + return fmt.Errorf("failed to encode output message: %w", err) + } + return websocket.Message.Send(conn, data) + }, + InputMessage: func() (deskproto.Message, error) { + // Inject username as the first message. + if !usernameSent { + usernameSent = true + return deskproto.ClientUsername{Username: username}, nil + } + var buf []byte + if err := websocket.Message.Receive(conn, &buf); err != nil { + return nil, fmt.Errorf("failed to read input message: %w", err) + } + return deskproto.Decode(buf) + }, + }) + if err != nil { + log.Printf("failed to create rdpclient: %v", err) + return + } + if err := c.Wait(); err != nil { + log.Printf("failed to wait for rdpclient to finish: %v", err) + return + } + }) +} + +func exeDir() string { + exe, err := os.Executable() + if err != nil { + log.Println("failed to find executable path:", err) + return "." + } + return filepath.Dir(exe) +} diff --git a/rfd/0037-desktop-access-protocol.md b/rfd/0037-desktop-access-protocol.md index 938b824896075..02e4334f22253 100644 --- a/rfd/0037-desktop-access-protocol.md +++ b/rfd/0037-desktop-access-protocol.md @@ -46,11 +46,9 @@ Typical sequence of messages in a desktop session: +--------+ +--------+ | client | | server | +--------+ +--------+ - | 1 - client screen spec | + | 7 - client username | |--------------------------------------->| - | 7 - username/password required | - |<---------------------------------------| - | 8 - username/password response | + | 1 - client screen spec | |--------------------------------------->| | 2 - PNG frame | |<---------------------------------------| @@ -69,6 +67,10 @@ Typical sequence of messages in a desktop session: | .... | ``` +Note that `client username` and `client screen spec` **must** be the first two +messages sent by the client, in that order. Any other incoming messages will be +discarded until those two are received. + ### Message encoding Each message consists of a sequence of fields. @@ -170,22 +172,11 @@ This message contains clipboard data. Sent in either direction. When this message is sent from server to client, it's a "copy" action. When this message is sent from client to server, it's a "paste" action. -#### 7 - username/password required - -``` -| message type (7) | -``` - -This message indicates that automatic authentication at the transport layer -failed and the server requests the client to enter their username/password. -Sent from server to client, usually when RDP server does not accept Teleport's -certificate authentication. - -#### 8 - username/password response +#### 7 - client username ``` -| message type (8) | user_length uint32 | username []byte | pass_length uint32 | password []byte +| message type (7) | username_length uint32 | username []byte ``` -This message is a response to message 7, carrying user-provided username and -password. Sent from client to server. +This is the first message of the protocol and contains the username to login as +on the remote desktop.