Fork me on GitHub

随笔分类 - guacamole

guacamole实现剪切复制

主要功能是实现把堡垒机的内容复制到浏览器端,把浏览器端的文本复制到堡垒机上。 借助一个中间的文本框,现将堡垒机内容复制到一个文本框,然后把文本框内容复制出来。或者将需要传递到堡垒机的内容先复制到文本框,然后在传递到堡垒机上。 ``` //监听堡垒机端往剪切板复制事件,然后写入文本框中 client.onclipboard = function(stream, mimetype){ if (/^text\//.exec(mimetype)) { var stringReader = new Guacamole.StringReader(stream); var json = ""; stringReader.ontext = function ontext(text) { json += text } stringReader.onend = function() { var clipboardElement = document.getElementById("clipboard"); clipboardElement.value = ''; clipboardElement.value = json; } } } //将内容传送到往堡垒机,data是获取到的文本框中的内容 function setClipboard(data) { var stream = client.createClipboardStream("text/plain"); var writer = new Guacamole.StringWriter(stream); for (var i=0; i<data.length; i += 4096){ writer.sendText(data.substring(i, i+4096)); } writer.sendEnd(); } ```

centos7.3配置guacamole

[TOC] # 1 安装guacamole所需要的依赖库 必需安装的库有 ``` yum install -y cairo-devel libjpeg-turbo-devel libpng-devel uuid-devel ``` 可选择安装的库 ``` yum install -y freerdp-devel pango-devel libssh2-devel libvncserver-devel pulseaudio-libs-devel openssl-devel libvorbis-devel libwebp-devel ``` # 2 安装配置tomcat,架设服务 ## 2.1 下载tomcat ``` yum -y install tomcat ``` ## 2.2 配置环境变量,使tomcat可以找到guacamole客户端配置 ``` vim /etc/tomcat/tomcat.conf ``` 加入下面一句 ``` GUACAMOLE_HOME=/etc/guacamole ``` 其中GUACAMOLE_HOME文件夹在后面创建。 注意: ## 2.3 安装guacamole ### 2.3.1 编译安装guacamole-server 1.下载服务端压缩包 http://guacamole.incubator.apache.org/releases/0.9.13-incubating/ 地址中下载guacamole-server-0.9.13-incubating.tar.gz 使用wget命令或者本地下载后使用ftp工具上传到服务器 2.解压 ``` tar -xzf guacamole-server-0.9.13-incubating.tar.gz ``` 3.编译安装 ``` cd guacamole-server-0.9.13-incubating/ sudo ./configure --with-init-dir=/etc/init.d make make install ldconfig ``` ### 2.3.2 安装guacamole-client 1.下载客户端包 http://guacamole.incubator.apache.org/releases/0.9.13-incubating/ 地址中下载guacamole-0.9.13-incubating.war 使用wget命令或者本地下载后使用ftp工具上传到服务器 2.将客户端包部署到Tomcat ``` cp guacamole-0.9.13.war /var/lib/tomcat/webapps/guacamole.war ``` # 3 配置guacamole 创建配置文件夹 ``` mkdir -p /etc/guacamole/ ``` 配置用户映射文件 ``` vim /etc/guacamole/guacamole.properties ``` 将文件内容改为下面的 `basic-user-mapping: /etc/guacamole/user-mapping.xml` 编写用户映射配置文件 ``` vim /etc/guacamole/user-mapping.xml ``` 在配置文件内,按下面的格式输入信息: ``` <user-mapping> <authorize username="admin" password="123456"> <!-- First authorized connection --> <connection name="ssh"> <protocol>ssh</protocol> <param name="hostname">123.206.xx.xx</param> <param name="port">22</param> </connection> <!-- Second authorized connection --> <connection name="otherhost"> <protocol>vnc</protocol> <param name="hostname">otherhost</param> <param name="port">5900</param> <param name="password">VNCPASS</param> </connection> </authorize> </user-mapping> ``` 以上只是极简配置,该文件更改后即时生效,具体参数配置文档: http://guacamole.incubator.apache.org/doc/gug/configuring-guacamole.html # 4 重启tomcat,并启动guacd服务 ``` service tomcat start /etc/init.d/guacd start ``` 在浏览器地址栏输入 http://xxx.xxx.xxx.xxx:8080/guacamole/ ,可以看到登入界面(第一次加载比较慢) 使用admin和密码123456登陆 点击ssh进入服务器123.206.xx.xx的登陆认证 登陆成功 # 参考资料 http://www.jianshu.com/p/aa63006b2edb

guacamole实现RDP的下载

# 1. 配置说明 ## 1.1 主要特别配置以下三项 **enable-drive** 默认情况下禁用文件传输,但启用文件传输后,RDP用户可以将文件传输到持久存在于Guacamole服务器上的虚拟驱动器。通过将此参数设置为“true”来启用文件传输支持。文件将存储在由“ drive-path”参数指定的目录中,如果启用了文件传输,则该参数是必需的。 **drive-path|Guacamole** 服务器上应存储传输文件的目录。该目录必须对guacd可访问,并且对运行guacd的用户可读写。此参数不指RDP服务器上的目录。如果文件传输未启用,则此参数将被忽略。 **create-drive-path** 如果设置为“true”,并且启用了文件传输,drive-path指定的目录将自动创建。将只创建路径中的最终目录 - 如果路径之前的其他目录不存在,则自动创建将失败,并会记录错误。默认情况下,该drive-path参数指定的目录 将不会自动创建,并且尝试将文件传输到不存在的目录将被记录为错误。 如果文件传输未启用,则此参数将被忽略。 - 参考配置 ``` enable-drive = true drive-path = '/yourpath' create-drive-path = true ``` 在浏览器登录到堡垒机,可以看到设备和驱动里面多了一个guacamole RDP驱动 以下是截图 ![](http://markdown.archerwong.cn/2018-12-18-07-52-20_clipboard.png) 进入驱动下 ![](http://markdown.archerwong.cn/2018-12-18-07-52-41_clipboard.png) 理解: 实际上在使用RDP协议的时候,有下面的关系。 ``` 浏览器<->guacamole服务器<->真实服务器 ``` 我们看到的guacamole RDP,指向的是guacamole服务器的 /yourpath文件夹,你把文件拖动到这个驱动下,就把文件上传到guacamole服务器的 /yourpath目录下了,接下来我们可以通过浏览器下载guacamole服务器的文件,这个需要自己去实现。特别注意,发现有个download文件夹,默认存在并且不能删除掉,作用就是你在堡垒机上操作把文件拖进去,会向浏览器发送file命令(如下图所示),然后我们可以在cilent端进行监听,然后实现拖动自动下载。 ![](http://markdown.archerwong.cn/2018-12-18-07-52-57_clipboard.png) # 2. 源码分析实现 ## 2.1 把指令转换成相应的client操作 ``` /** * Handlers for all instruction opcodes receivable by a Guacamole protocol * client. * @private */ var instructionHandlers = { ...其它指令 "file": function(parameters) { //处理参数 var stream_index = parseInt(parameters[0]); var mimetype = parameters[1]; var filename = parameters[2]; // Create stream if (guac_client.onfile) { //这里根据index得到了输入流的抽象,注意下InputStream方法 var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); //创建完流之后,调用了client的onfile方法,并且把参数传递过去,我们需要在client的onfile方法里面处理输入的流。 guac_client.onfile(stream, mimetype, filename); } // Otherwise, unsupported else guac_client.sendAck(stream_index, "File transfer unsupported", 0x0100); }, ...其它指令 } ``` 以下是InputStream的代码,实际上是进行了一些属性的初始化工作 ``` /** * An input stream abstraction used by the Guacamole client to facilitate * transfer of files or other binary data. * * @constructor * @param {Guacamole.Client} client The client owning this stream. * @param {Number} index The index of this stream. */ Guacamole.InputStream = function(client, index) { /** * Reference to this stream. * @private */ var guac_stream = this; /** * The index of this stream. * @type {Number} */ this.index = index; /** * Called when a blob of data is received. * * @event * @param {String} data The received base64 data. */ this.onblob = null; /** * Called when this stream is closed. * * @event */ this.onend = null; /** * Acknowledges the receipt of a blob. * * @param {String} message A human-readable message describing the error * or status. * @param {Number} code The error code, if any, or 0 for success. */ this.sendAck = function(message, code) { client.sendAck(guac_stream.index, message, code); }; }; ``` ## 2.2 client的onfile方法 这个自己实现,作用就是监听上面的onfile事件,并进一步处理 ``` client.onfile = function(stream, mimetype, filename){ //通知服务端,已经收到了stream stream.sendAck('OK', Guacamole.Status.Code.SUCCESS); //开始处理输入流,这里封装了一个downloadFile方法 downloadFile(stream, mimetype, filename); } ``` ## 2.3 处理输入流的逻辑 ``` downloadFile = (stream, mimetype, filename) => { //拿到的流不能直接使用,先实例化一个处理器,使用blob reader处理数据 var blob_builder; if (window.BlobBuilder) blob_builder = new BlobBuilder(); else if (window.WebKitBlobBuilder) blob_builder = new WebKitBlobBuilder(); else if (window.MozBlobBuilder) blob_builder = new MozBlobBuilder(); else blob_builder = new (function() { var blobs = []; /** @ignore */ this.append = function(data) { blobs.push(new Blob([data], {"type": mimetype})); }; /** @ignore */ this.getBlob = function() { return new Blob(blobs, {"type": mimetype}); }; })(); // Append received blobs stream.onblob = function(data) { // Convert to ArrayBuffer var binary = window.atob(data); var arrayBuffer = new ArrayBuffer(binary.length); var bufferView = new Uint8Array(arrayBuffer); for (var i=0; i<binary.length; i++) bufferView[i] = binary.charCodeAt(i); //收到后就交给blob_builder blob_builder.append(arrayBuffer); length += arrayBuffer.byteLength; // Send success response stream.sendAck("OK", 0x0000); }; stream.onend = function(){ //结束的时候,获取blob_builder里面的可用数据 var blob_data = blob_builder.getBlob(); //数据传输完成后进行下载等处理 if(mimetype.indexOf('stream-index+json') != -1){ //如果是文件夹,使用filereader读取blob数据,可以获得该文件夹下的文件和目录的名称和类型,是一个json形式 var blob_reader = new FileReader(); blob_reader.addEventListener("loadend", function() { let folder_content = JSON.parse(blob_reader.result) //重新组织当前文件目录,appendFileItem是自己封装的文件系统动态展示 appendFileItem(folder_content) $("#header_title").text(filename); }); blob_reader.readAsBinaryString(blob_data); } else { //如果是文件,直接下载,但是需要解决个问题,就是如何下载blob数据 //借鉴了https://github.com/eligrey/FileSaver.js这个库 var file_arr = filename.split("/"); var download_file_name = file_arr[file_arr.length - 1]; saveAs(blob_data, download_file_name); } } } ```

guacamole实现上传下载

[TOC] 分析的入手点,查看websocket连接的frame ![](http://markdown.archerwong.cn/2018-12-18-07-54-47_clipboard.png) 看到首先服务端向客户端发送了filesystem请求,紧接着浏览器向服务端发送了get请求,并且后面带有根目录标识(“/”)。 # 1. 源码解读 查看指令 ``` /** * Handlers for all instruction opcodes receivable by a Guacamole protocol * client. * @private */ var instructionHandlers = { ...其它指令 "filesystem" : function handleFilesystem(parameters) { var objectIndex = parseInt(parameters[0]); var name = parameters[1]; // Create object, if supported if (guac_client.onfilesystem) { //这里实例化一个object,并且传递给客户端监听的onfilesystem方法 var object = objects[objectIndex] = new Guacamole.Object(guac_client, objectIndex); guac_client.onfilesystem(object, name); } // If unsupported, simply ignore the availability of the filesystem }, ...其它指令 } ``` 查看实例化的object源码 ``` /** * An object used by the Guacamole client to house arbitrarily-many named * input and output streams. * * @constructor * @param {Guacamole.Client} client * The client owning this object. * * @param {Number} index * The index of this object. */ Guacamole.Object = function guacamoleObject(client, index) { /** * Reference to this Guacamole.Object. * * @private * @type {Guacamole.Object} */ var guacObject = this; /** * Map of stream name to corresponding queue of callbacks. The queue of * callbacks is guaranteed to be in order of request. * * @private * @type {Object.<String, Function[]>} */ var bodyCallbacks = {}; /** * Removes and returns the callback at the head of the callback queue for * the stream having the given name. If no such callbacks exist, null is * returned. * * @private * @param {String} name * The name of the stream to retrieve a callback for. * * @returns {Function} * The next callback associated with the stream having the given name, * or null if no such callback exists. */ var dequeueBodyCallback = function dequeueBodyCallback(name) { // If no callbacks defined, simply return null var callbacks = bodyCallbacks[name]; if (!callbacks) return null; // Otherwise, pull off first callback, deleting the queue if empty var callback = callbacks.shift(); if (callbacks.length === 0) delete bodyCallbacks[name]; // Return found callback return callback; }; /** * Adds the given callback to the tail of the callback queue for the stream * having the given name. * * @private * @param {String} name * The name of the stream to associate with the given callback. * * @param {Function} callback * The callback to add to the queue of the stream with the given name. */ var enqueueBodyCallback = function enqueueBodyCallback(name, callback) { // Get callback queue by name, creating first if necessary var callbacks = bodyCallbacks[name]; if (!callbacks) { callbacks = []; bodyCallbacks[name] = callbacks; } // Add callback to end of queue callbacks.push(callback); }; /** * The index of this object. * * @type {Number} */ this.index = index; /** * Called when this object receives the body of a requested input stream. * By default, all objects will invoke the callbacks provided to their * requestInputStream() functions based on the name of the stream * requested. This behavior can be overridden by specifying a different * handler here. * * @event * @param {Guacamole.InputStream} inputStream * The input stream of the received body. * * @param {String} mimetype * The mimetype of the data being received. * * @param {String} name * The name of the stream whose body has been received. */ this.onbody = function defaultBodyHandler(inputStream, mimetype, name) { // Call queued callback for the received body, if any var callback = dequeueBodyCallback(name); if (callback) callback(inputStream, mimetype); }; /** * Called when this object is being undefined. Once undefined, no further * communication involving this object may occur. * * @event */ this.onundefine = null; /** * Requests read access to the input stream having the given name. If * successful, a new input stream will be created. * * @param {String} name * The name of the input stream to request. * * @param {Function} [bodyCallback] * The callback to invoke when the body of the requested input stream * is received. This callback will be provided a Guacamole.InputStream * and its mimetype as its two only arguments. If the onbody handler of * this object is overridden, this callback will not be invoked. */ this.requestInputStream = function requestInputStream(name, bodyCallback) { // Queue body callback if provided if (bodyCallback) enqueueBodyCallback(name, bodyCallback); // Send request for input stream client.requestObjectInputStream(guacObject.index, name); }; /** * Creates a new output stream associated with this object and having the * given mimetype and name. The legality of a mimetype and name is dictated * by the object itself. * * @param {String} mimetype * The mimetype of the data which will be sent to the output stream. * * @param {String} name * The defined name of an output stream within this object. * * @returns {Guacamole.OutputStream} * An output stream which will write blobs to the named output stream * of this object. */ this.createOutputStream = function createOutputStream(mimetype, name) { return client.createObjectOutputStream(guacObject.index, mimetype, name); }; }; ``` 读取下官方的注释,关于此类的定义: ``` An object used by the Guacamole client to house arbitrarily-many named input and output streams. ``` 我们需要操作的应该就是input 和 output stream,下面我们进行下猜测 1> this.onbody对应的方法应该就是我们需要实际处理inputStream的地方, 2> this.requestInputStream后面调用了client.requestObjectInputStream(guacObject.index, name);方法,源码如下: ``` Guacamole.Client = function(tunnel) { ...其它内容 this.requestObjectInputStream = function requestObjectInputStream(index, name) { // Do not send requests if not connected if (!isConnected()) return; tunnel.sendMessage("get", index, name); }; ...其它内容 } ``` 可以看出这个方法就是向服务端方发送get请求。我们上面分析websocket请求的时候,提到过向客户端发送过这样一个请求,并且在一直监听的onbody方法中应该能收到服务器返回的响应。 3> this.createOutputStream应该是创建了一个通往guacamole服务器的stream,我们上传文件的时候可能会用到这个stream,调用了client.createObjectOutputStream(guacObject.index, mimetype, name);方法,其源码如下: ``` Guacamole.Client = function(tunnel) { ...其它内容 /** * Creates a new output stream associated with the given object and having * the given mimetype and name. The legality of a mimetype and name is * dictated by the object itself. The instruction necessary to create this * stream will automatically be sent. * * @param {Number} index * The index of the object for which the output stream is being * created. * * @param {String} mimetype * The mimetype of the data which will be sent to the output stream. * * @param {String} name * The defined name of an output stream within the given object. * * @returns {Guacamole.OutputStream} * An output stream which will write blobs to the named output stream * of the given object. */ this.createObjectOutputStream = function createObjectOutputStream(index, mimetype, name) { // 得到了stream,并向服务端发送了put请求 // Allocate and ssociate stream with object metadata var stream = guac_client.createOutputStream(); tunnel.sendMessage("put", index, stream.index, mimetype, name); return stream; }; ...其它内容 } ``` 继续往下追diamante, 这句var stream = guac_client.createOutputStream(); 源码如下: ``` Guacamole.Client = function(tunnel) { ...其它内容 /** * Allocates an available stream index and creates a new * Guacamole.OutputStream using that index, associating the resulting * stream with this Guacamole.Client. Note that this stream will not yet * exist as far as the other end of the Guacamole connection is concerned. * Streams exist within the Guacamole protocol only when referenced by an * instruction which creates the stream, such as a "clipboard", "file", or * "pipe" instruction. * * @returns {Guacamole.OutputStream} * A new Guacamole.OutputStream with a newly-allocated index and * associated with this Guacamole.Client. */ this.createOutputStream = function createOutputStream() { // Allocate index var index = stream_indices.next(); // Return new stream var stream = output_streams[index] = new Guacamole.OutputStream(guac_client, index); return stream; }; ...其它内容 } ``` 再继续,new Guacamole.OutputStream(guac_client, index);源码: ``` /** * Abstract stream which can receive data. * * @constructor * @param {Guacamole.Client} client The client owning this stream. * @param {Number} index The index of this stream. */ Guacamole.OutputStream = function(client, index) { /** * Reference to this stream. * @private */ var guac_stream = this; /** * The index of this stream. * @type {Number} */ this.index = index; /** * Fired whenever an acknowledgement is received from the server, indicating * that a stream operation has completed, or an error has occurred. * * @event * @param {Guacamole.Status} status The status of the operation. */ this.onack = null; /** * Writes the given base64-encoded data to this stream as a blob. * * @param {String} data The base64-encoded data to send. */ this.sendBlob = function(data) { //发送数据到服务端,并且数据格式应该为该base64-encoded data格式,分块传输过去的 client.sendBlob(guac_stream.index, data); }; /** * Closes this stream. */ this.sendEnd = function() { client.endStream(guac_stream.index); }; }; ``` 到此,我们可以知道this.createOutputStream是做的是事情就是建立了一个通往服务器的stream通道,并且,我们可以操作这个通道发送分块数据(stream.sendBlob方法)。 # 2. 上传下载的核心代码 关于文件系统和下载的代码 ``` var fileSystem; //初始化文件系统 client.onfilesystem = function(object){ fileSystem=object; //监听onbody事件,对返回值进行处理,返回内容可能有两种,一种是文件夹,一种是文件。 object.onbody = function(stream, mimetype, filename){ stream.sendAck('OK', Guacamole.Status.Code.SUCCESS); downloadFile(stream, mimetype, filename); } } //连接有滞后,初始化文件系统给个延迟 setTimeout(function(){ //从根目录开始,想服务端发送get请求 let path = '/'; fileSystem.requestInputStream(path); }, 5000); downloadFile = (stream, mimetype, filename) => { //使用blob reader处理数据 var blob_builder; if (window.BlobBuilder) blob_builder = new BlobBuilder(); else if (window.WebKitBlobBuilder) blob_builder = new WebKitBlobBuilder(); else if (window.MozBlobBuilder) blob_builder = new MozBlobBuilder(); else blob_builder = new (function() { var blobs = []; /** @ignore */ this.append = function(data) { blobs.push(new Blob([data], {"type": mimetype})); }; /** @ignore */ this.getBlob = function() { return new Blob(blobs, {"type": mimetype}); }; })(); // 收到blob的处理,因为收到的可能是一块一块的数据,需要把他们整合,这里用到了blob_builder stream.onblob = function(data) { // Convert to ArrayBuffer var binary = window.atob(data); var arrayBuffer = new ArrayBuffer(binary.length); var bufferView = new Uint8Array(arrayBuffer); for (var i=0; i<binary.length; i++) bufferView[i] = binary.charCodeAt(i); blob_builder.append(arrayBuffer); length += arrayBuffer.byteLength; // Send success response stream.sendAck("OK", 0x0000); }; // 结束后的操作 stream.onend = function(){ //获取整合后的数据 var blob_data = blob_builder.getBlob(); //数据传输完成后进行下载等处理 if(mimetype.indexOf('stream-index+json') != -1){ //如果是文件夹,需要解决如何将数据读出来,这里使用filereader读取blob数据,最后得到一个json格式数据 var blob_reader = new FileReader(); blob_reader.addEventListener("loadend", function() { let folder_content = JSON.parse(blob_reader.result) //这里加入自己代码,实现文件目录的ui,重新组织当前文件目录 }); blob_reader.readAsBinaryString(blob_data); } else { //如果是文件,直接下载,但是需要解决个问题,就是如何下载blob数据 //借鉴了https://github.com/eligrey/FileSaver.js这个库 var file_arr = filename.split("/"); var download_file_name = file_arr[file_arr.length - 1]; saveAs(blob_data, download_file_name); } } } ``` 感受下console.log(blob_data)和 console.log(folder_data)的内容如下 ![](http://markdown.archerwong.cn/2018-12-18-07-55-32_clipboard.png) 关于上传的代码 ``` const input = document.getElementById('file-input'); input.onchange = function() { const file = input.files[0]; //上传开始 uploadFile(fileSystem, file); }; uploadFile = (object, file) => { const _this = this; const fileUpload = {}; //需要读取文件内容,使用filereader const reader = new FileReader(); var current_path = $("#header_title").text(); //上传到堡垒机的目录,可以自己动态获取 var STREAM_BLOB_SIZE = 4096; reader.onloadend = function fileContentsLoaded() { //上面源码分析过,这里先创建一个连接服务端的数据通道 const stream = object.createOutputStream(file.type, current_path + '/' + file.name); const bytes = new Uint8Array(reader.result); let offset = 0; let progress = 0; fileUpload.name = file.name; fileUpload.mimetype = file.type; fileUpload.length = bytes.length; stream.onack = function ackReceived(status) { if (status.isError()) { //提示错误信息 //layer.msg(status.message); return false; } const slice = bytes.subarray(offset, offset + STREAM_BLOB_SIZE); const base64 = bufferToBase64(slice); // Write packet stream.sendBlob(base64); // Advance to next packet offset += STREAM_BLOB_SIZE; if (offset >= bytes.length) { stream.sendEnd(); } } }; reader.readAsArrayBuffer(file); return fileUpload; }; function bufferToBase64(buf) { var binstr = Array.prototype.map.call(buf, function (ch) { return String.fromCharCode(ch); }).join(''); return btoa(binstr); } ```

guacamole实现虚拟键盘

要做的事情比较简单,就是先实例化一个虚拟键盘,然后监听事件即可。 js代码 ``` //虚拟键盘数据 var a = {"language":"en_US","type":"qwerty","width":22,"keys":{"0":[{"title":"0","requires":[]},{"title":")","requires":["shift"]}],"1":[{"title":"1","requires":[]},{"title":"!","requires":["shift"]}],"2":[{"title":"2","requires":[]},{"title":"@","requires":["shift"]}],"3":[{"title":"3","requires":[]},{"title":"#","requires":["shift"]}],"4":[{"title":"4","requires":[]},{"title":"$","requires":["shift"]}],"5":[{"title":"5","requires":[]},{"title":"%","requires":["shift"]}],"6":[{"title":"6","requires":[]},{"title":"^","requires":["shift"]}],"7":[{"title":"7","requires":[]},{"title":"&","requires":["shift"]}],"8":[{"title":"8","requires":[]},{"title":"*","requires":["shift"]}],"9":[{"title":"9","requires":[]},{"title":"(","requires":["shift"]}],"Back":65288,"Tab":65289,"Enter":65293,"Esc":65307,"Home":65360,"PgUp":65365,"PgDn":65366,"End":65367,"Ins":65379,"F1":65470,"F2":65471,"F3":65472,"F4":65473,"F5":65474,"F6":65475,"F7":65476,"F8":65477,"F9":65478,"F10":65479,"F11":65480,"F12":65481,"Del":65535,"Space":" ","Left":[{"title":"←","keysym":65361}],"Up":[{"title":"↑","keysym":65362}],"Right":[{"title":"→","keysym":65363}],"Down":[{"title":"↓","keysym":65364}],"Menu":[{"title":"Menu","keysym":65383}],"LShift":[{"title":"Shift","modifier":"shift","keysym":65505}],"RShift":[{"title":"Shift","modifier":"shift","keysym":65506}],"LCtrl":[{"title":"Ctrl","modifier":"control","keysym":65507}],"RCtrl":[{"title":"Ctrl","modifier":"control","keysym":65508}],"Caps":[{"title":"Caps","modifier":"caps","keysym":65509}],"LAlt":[{"title":"Alt","modifier":"alt","keysym":65513}],"RAlt":[{"title":"Alt","modifier":"alt","keysym":65514}],"Super":[{"title":"Super","modifier":"super","keysym":65515}],"`":[{"title":"`","requires":[]},{"title":"~","requires":["shift"]}],"-":[{"title":"-","requires":[]},{"title":"_","requires":["shift"]}],"=":[{"title":"=","requires":[]},{"title":"+","requires":["shift"]}],",":[{"title":",","requires":[]},{"title":"<","requires":["shift"]}],".":[{"title":".","requires":[]},{"title":">","requires":["shift"]}],"/":[{"title":"/","requires":[]},{"title":"?","requires":["shift"]}],"[":[{"title":"[","requires":[]},{"title":"{","requires":["shift"]}],"]":[{"title":"]","requires":[]},{"title":"}","requires":["shift"]}],"\\":[{"title":"\\","requires":[]},{"title":"|","requires":["shift"]}],";":[{"title":";","requires":[]},{"title":":","requires":["shift"]}],"'":[{"title":"'","requires":[]},{"title":"\"","requires":["shift"]}],"q":[{"title":"q","requires":[]},{"title":"Q","requires":["caps"]},{"title":"Q","requires":["shift"]},{"title":"q","requires":["caps","shift"]}],"w":[{"title":"w","requires":[]},{"title":"W","requires":["caps"]},{"title":"W","requires":["shift"]},{"title":"w","requires":["caps","shift"]}],"e":[{"title":"e","requires":[]},{"title":"E","requires":["caps"]},{"title":"E","requires":["shift"]},{"title":"e","requires":["caps","shift"]}],"r":[{"title":"r","requires":[]},{"title":"R","requires":["caps"]},{"title":"R","requires":["shift"]},{"title":"r","requires":["caps","shift"]}],"t":[{"title":"t","requires":[]},{"title":"T","requires":["caps"]},{"title":"T","requires":["shift"]},{"title":"t","requires":["caps","shift"]}],"y":[{"title":"y","requires":[]},{"title":"Y","requires":["caps"]},{"title":"Y","requires":["shift"]},{"title":"y","requires":["caps","shift"]}],"u":[{"title":"u","requires":[]},{"title":"U","requires":["caps"]},{"title":"U","requires":["shift"]},{"title":"u","requires":["caps","shift"]}],"i":[{"title":"i","requires":[]},{"title":"I","requires":["caps"]},{"title":"I","requires":["shift"]},{"title":"i","requires":["caps","shift"]}],"o":[{"title":"o","requires":[]},{"title":"O","requires":["caps"]},{"title":"O","requires":["shift"]},{"title":"o","requires":["caps","shift"]}],"p":[{"title":"p","requires":[]},{"title":"P","requires":["caps"]},{"title":"P","requires":["shift"]},{"title":"p","requires":["caps","shift"]}],"a":[{"title":"a","requires":[]},{"title":"A","requires":["caps"]},{"title":"A","requires":["shift"]},{"title":"a","requires":["caps","shift"]}],"s":[{"title":"s","requires":[]},{"title":"S","requires":["caps"]},{"title":"S","requires":["shift"]},{"title":"s","requires":["caps","shift"]}],"d":[{"title":"d","requires":[]},{"title":"D","requires":["caps"]},{"title":"D","requires":["shift"]},{"title":"d","requires":["caps","shift"]}],"f":[{"title":"f","requires":[]},{"title":"F","requires":["caps"]},{"title":"F","requires":["shift"]},{"title":"f","requires":["caps","shift"]}],"g":[{"title":"g","requires":[]},{"title":"G","requires":["caps"]},{"title":"G","requires":["shift"]},{"title":"g","requires":["caps","shift"]}],"h":[{"title":"h","requires":[]},{"title":"H","requires":["caps"]},{"title":"H","requires":["shift"]},{"title":"h","requires":["caps","shift"]}],"j":[{"title":"j","requires":[]},{"title":"J","requires":["caps"]},{"title":"J","requires":["shift"]},{"title":"j","requires":["caps","shift"]}],"k":[{"title":"k","requires":[]},{"title":"K","requires":["caps"]},{"title":"K","requires":["shift"]},{"title":"k","requires":["caps","shift"]}],"l":[{"title":"l","requires":[]},{"title":"L","requires":["caps"]},{"title":"L","requires":["shift"]},{"title":"l","requires":["caps","shift"]}],"z":[{"title":"z","requires":[]},{"title":"Z","requires":["caps"]},{"title":"Z","requires":["shift"]},{"title":"z","requires":["caps","shift"]}],"x":[{"title":"x","requires":[]},{"title":"X","requires":["caps"]},{"title":"X","requires":["shift"]},{"title":"x","requires":["caps","shift"]}],"c":[{"title":"c","requires":[]},{"title":"C","requires":["caps"]},{"title":"C","requires":["shift"]},{"title":"c","requires":["caps","shift"]}],"v":[{"title":"v","requires":[]},{"title":"V","requires":["caps"]},{"title":"V","requires":["shift"]},{"title":"v","requires":["caps","shift"]}],"b":[{"title":"b","requires":[]},{"title":"B","requires":["caps"]},{"title":"B","requires":["shift"]},{"title":"b","requires":["caps","shift"]}],"n":[{"title":"n","requires":[]},{"title":"N","requires":["caps"]},{"title":"N","requires":["shift"]},{"title":"n","requires":["caps","shift"]}],"m":[{"title":"m","requires":[]},{"title":"M","requires":["caps"]},{"title":"M","requires":["shift"]},{"title":"m","requires":["caps","shift"]}]},"layout":[["Esc",0.7,"F1","F2","F3","F4",0.7,"F5","F6","F7","F8",0.7,"F9","F10","F11","F12"],[0.1],{"main":{"alpha":[["`","1","2","3","4","5","6","7","8","9","0","-","=","Back"],["Tab","q","w","e","r","t","y","u","i","o","p","[","]","\\"],["Caps","a","s","d","f","g","h","j","k","l",";","'","Enter"],["LShift","z","x","c","v","b","n","m",",",".","/","RShift"],["LCtrl","Super","LAlt","Space","RAlt","Menu","RCtrl"]],"movement":[["Ins","Home","PgUp"],["Del","End","PgDn"],[1],["Up"],["Left","Down","Right"]]}}],"keyWidths":{"Back":2,"Tab":1.5,"\\":1.5,"Caps":1.85,"Enter":2.25,"LShift":2.1,"RShift":3.1,"LCtrl":1.6,"Super":1.6,"LAlt":1.6,"Space":6.1,"RAlt":1.6,"Menu":1.6,"RCtrl":1.6,"Ins":1.6,"Home":1.6,"PgUp":1.6,"Del":1.6,"End":1.6,"PgDn":1.6}}; //虚拟键盘 var onScreenKeyboard = new Guacamole.OnScreenKeyboard(a); document.getElementById('osk').appendChild(onScreenKeyboard.getElement()); onScreenKeyboard.onkeydown = function(keysym) { client.sendKeyEvent(1, keysym); }; onScreenKeyboard.onkeyup = function(keysym) { // Do something ... client.sendKeyEvent(0, keysym); }; ``` 给键盘一个容器 ``` <div class="osk" id="osk"> </div> ``` 键盘的css样式 ``` .osk { position: relative; } .guac-keyboard { display: inline-block; width: 100%; margin: 0; padding: 0; cursor: default; text-align: left; vertical-align: middle; } .guac-keyboard, .guac-keyboard * { overflow: hidden; white-space: nowrap; } .guac-keyboard .guac-keyboard-key-container { display: inline-block; margin: 0.05em; position: relative; } .guac-keyboard .guac-keyboard-key { position: absolute; left: 0; right: 0; top: 0; bottom: 0; background: #444; border: 0.125em solid #666; -moz-border-radius: 0.25em; -webkit-border-radius: 0.25em; -khtml-border-radius: 0.25em; border-radius: 0.25em; color: white; font-size: 40%; font-weight: lighter; text-align: center; white-space: pre; text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25), 1px -1px 0 rgba(0, 0, 0, 0.25), -1px 1px 0 rgba(0, 0, 0, 0.25), -1px -1px 0 rgba(0, 0, 0, 0.25); } .guac-keyboard .guac-keyboard-key:hover { cursor: pointer; } .guac-keyboard .guac-keyboard-key.highlight { background: #666; border-color: #666; } /* Align some keys to the left */ .guac-keyboard .guac-keyboard-key-caps, .guac-keyboard .guac-keyboard-key-enter, .guac-keyboard .guac-keyboard-key-tab, .guac-keyboard .guac-keyboard-key-lalt, .guac-keyboard .guac-keyboard-key-ralt, .guac-keyboard .guac-keyboard-key-alt-gr, .guac-keyboard .guac-keyboard-key-lctrl, .guac-keyboard .guac-keyboard-key-rctrl, .guac-keyboard .guac-keyboard-key-lshift, .guac-keyboard .guac-keyboard-key-rshift { text-align: left; padding-left: 0.75em; } /* Active shift */ .guac-keyboard.guac-keyboard-modifier-shift .guac-keyboard-key-rshift, .guac-keyboard.guac-keyboard-modifier-shift .guac-keyboard-key-lshift, /* Active ctrl */ .guac-keyboard.guac-keyboard-modifier-control .guac-keyboard-key-rctrl, .guac-keyboard.guac-keyboard-modifier-control .guac-keyboard-key-lctrl, /* Active alt */ .guac-keyboard.guac-keyboard-modifier-alt .guac-keyboard-key-ralt, .guac-keyboard.guac-keyboard-modifier-alt .guac-keyboard-key-lalt, /* Active alt-gr */ .guac-keyboard.guac-keyboard-modifier-alt-gr .guac-keyboard-key-alt-gr, /* Active caps */ .guac-keyboard.guac-keyboard-modifier-caps .guac-keyboard-key-caps, /* Active super */ .guac-keyboard.guac-keyboard-modifier-super .guac-keyboard-key-super { background: #882; border-color: #DD4; } .guac-keyboard .guac-keyboard-key.guac-keyboard-pressed { background: #822; border-color: #D44; } .guac-keyboard .guac-keyboard-group { line-height: 0; } .guac-keyboard .guac-keyboard-group.guac-keyboard-alpha, .guac-keyboard .guac-keyboard-group.guac-keyboard-movement { display: inline-block; text-align: center; vertical-align: top; } .guac-keyboard .guac-keyboard-group.guac-keyboard-main { /* IE10 */ display: -ms-flexbox; -ms-flex-align: stretch; -ms-flex-direction: row; /* Ancient Mozilla */ display: -moz-box; -moz-box-align: stretch; -moz-box-orient: horizontal; /* Ancient WebKit */ display: -webkit-box; -webkit-box-align: stretch; -webkit-box-orient: horizontal; /* Old WebKit */ display: -webkit-flex; -webkit-align-items: stretch; -webkit-flex-direction: row; /* W3C */ display: flex; align-items: stretch; flex-direction: row; } .guac-keyboard .guac-keyboard-group.guac-keyboard-movement { -ms-flex: 1 1 auto; -moz-box-flex: 1; -webkit-box-flex: 1; -webkit-flex: 1 1 auto; flex: 1 1 auto; } .guac-keyboard .guac-keyboard-gap { display: inline-block; } /* Hide keycaps requiring modifiers which are NOT currently active. */ .guac-keyboard:not(.guac-keyboard-modifier-caps) .guac-keyboard-cap.guac-keyboard-requires-caps, .guac-keyboard:not(.guac-keyboard-modifier-shift) .guac-keyboard-cap.guac-keyboard-requires-shift, .guac-keyboard:not(.guac-keyboard-modifier-alt-gr) .guac-keyboard-cap.guac-keyboard-requires-alt-gr, /* Hide keycaps NOT requiring modifiers which ARE currently active, where that modifier is used to determine which cap is displayed for the current key. */ .guac-keyboard.guac-keyboard-modifier-shift .guac-keyboard-key.guac-keyboard-uses-shift .guac-keyboard-cap:not(.guac-keyboard-requires-shift), .guac-keyboard.guac-keyboard-modifier-caps .guac-keyboard-key.guac-keyboard-uses-caps .guac-keyboard-cap:not(.guac-keyboard-requires-caps), .guac-keyboard.guac-keyboard-modifier-alt-gr .guac-keyboard-key.guac-keyboard-uses-alt-gr .guac-keyboard-cap:not(.guac-keyboard-requires-alt-gr) { display: none; } /* Fade out keys which do not use AltGr if AltGr is active */ .guac-keyboard.guac-keyboard-modifier-alt-gr .guac-keyboard-key:not(.guac-keyboard-uses-alt-gr):not(.guac-keyboard-key-alt-gr) { opacity: 0.5; } ```