精华内容
下载资源
问答
  • guacamole协议 协议组成 Guacamole 协议由若干指令组成。每条指令是一个逗号分隔的列表,最后以分号终止,其中列表中的第一个元素是指令操作码,其后的元素是该指令的参数: OPCODE,ARG1,ARG2,ARG3,……; 指令列表...

    guacamole协议

    协议组成

    Guacamole 协议由若干指令组成。每条指令是一个逗号分隔的列表,最后以分号终止,其中列表中的第一个元素是指令操作码,其后的元素是该指令的参数:

    OPCODE,ARG1,ARG2,ARG3,……;
    

    指令列表中的每个元素都是由一个正的十进制整数前缀和一个具体的元素值组成,其中前缀由一个英文句点( . )分隔。整数前缀表示具体的元素值的Unicode字符的数量,字符由UTF-8编码:

    LENGTH.VALUE
    

    若干条完整的指令组成一条消息,该消息从客户端发送到服务器,或者从服务器发送到客户端。客户端到服务器的指令通常是控制指令(用于建立连接或断开连接)和事件(鼠标事件和键盘事件)。服务器到客户端的指令通常是将客户端用作远程显示器的绘制指令(缓存,剪切,绘制图像)。

    例如,将显示尺寸设置为1024*768的完整有效的指令是:

    4.size,1.0,4.1024,3.768;
    

    对于这个指令,会被服务器解析为四个元素:“size” ,作为size指令的操作码,“0”,是图像默认层的索引,“1024”,为所需的像素宽度,“768”,为所需的像素高度。

    正因为Guacamole协议的设计方式,使得它可以流式传输协议,同时也可以很容易地被JavaScript解析。

    JS确实原生支持类似于XML、JSON这样格式的信息,但是这类格式的信息都不能以流的形式传输。JS在解析这类格式的信息前必须接收到完整的XML或者JSON的包,而guacamole协议的信息,却可以一边接收一边解析。它的指令内每个元素的长度前缀使得解析器不用遍历每个字符就可以完成指令之间的跳转。

    握手阶段

    新建连接

    握手的过程是guacamole协议建立连接的过程。当客户端发送“select”指令时,握手阶段就开始了,select指令告诉服务器要加载哪个协议:

    6.select,3.vnc;
    

    收到“select”指令后,服务器会加载对应的客户端组件,并且回复一个“args”指令,这个指令指明了服务器端需要的参数列表

    4.args,13.VERSION_1_1_0,8.hostname,4.port,8.password,13.swap-red-blue,9.read-only;
    

    其中的协议版本用于协商客户端和服务器的不同版本之间的兼容性,从而允许双方协商最高支持的版本并启用或禁用与该版本关联的功能。不支持该指令的旧版本的Guacamole客户端将无提示地将其忽略为空连接参数。有效协议版本如下:

    版本号对应参数值描述
    1.0.0VERSION_1_0_0这是默认版本,适用于1.1.0之前的任何版本。协议的版本1.0.0不支持协议协商,并且要求握手指令以特定顺序传递,并且存在(即使为空)。
    1.1.0VERSION_1_1_0协议版本1.1.0引入了对协议版本协商,握手指令的任意顺序的支持,并支持在握手期间传递时区指令。

    客户端接收到服务端可接受的参数列表后,需要回复给服务器,自己支持的音频(audio),视屏(video),图像(image),最佳屏幕尺寸(size)及时区(timezone),并在最后回复所有的服务器要求的参数的值(connect),即使是空,也要回复。任意要求没有满足,连接都将被关闭。客户端回复给服务端的消息如下:

    4.size,4.1024,3.768,2.96;
    5.audio,9.audio/ogg;
    5.video;
    5.image,9.image/png,10.image/jpeg;
    8.timezone,16.America/New_York;
    7.connect,13.VERSION_1_1_0,9.localhost,4.5900,0.,0.,0.;
    

    如上,在客户端回复服务端的参数中,回复了三个0.给服务端,意味着客户端这三个参数为空,没有值,所以留空,长度为0,回复0.恰好。

    在实际协议中,指令之间是紧挨着的不存在换行符,如果一条指令之后除了新指令的开始之外还有其他内容,则连接将关闭。

    以下是握手过程中的指令的说明:

    指令名称描述
    audio客户端支持的音频编码解码器。在上面的例子中指定了audio/ogg作为支持的编码解码器
    connect这是握手阶段的最后一个指令,它表明握手阶段已经结束,并且连接正常建立,可以继续进行。这条指令后续跟着服务器中args指令发送的连接参数的参数值。在上面的例子中,参数指定了在5900端口与localhost进行连接,后续三个参数值为空。
    iamge客户端支持的图像格式,按首选项顺序。上例中的客户端同时支持PNG和JPEG。
    timezone客户端的时区,采用IANA区域密钥格式。上例的时区是美国纽约
    video客户端支持的视频编码解码器。上例的客户端是不支持任何视频编解码器。

    客户端在握手中发送的指令的顺序是任意的,除了最后一条指令connect将结束握手并尝试开始连接。

    客户端发送完这些指令后,服务器将尝试使用接收到的参数初始化连接,如果成功,则以“ready”指令进行响应。这条指令中包含新客户端连接的ID,并标记交互阶段的开始。这个ID是一个任意字符串,但是保证这个ID在所有活动链接以及受支持协议中是唯一的:

    5.ready,37.$260d01da-779b-4ee9-afc1-c16bae885cc7;
    

    当服务器发送ready后,真正的交互阶段就开始了。客户端和服务器端之间相互传递绘图和事件指令,直到关闭连接。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OmgcnZVV-1610937685314)(/Users/wengcy/Library/Application Support/typora-user-images/image-20201230202507183.png)]

    加入现有连接

    握手阶段完成后,如果通过“select”指令提供了ID而不是协议名称,则这个连接将被认为是活跃的并且是能被加入的。

    6.select,37.$260d01da-779b-4ee9-afc1-c16bae885cc7;
    

    加入现有连接的其余阶段与握手的阶段是相同的。与新连接一样,这次连接的其他参数由握手期间提供的参数值决定。

    流与对象

    guacamole支持传输剪切板内容,音频内容,视频内容和图像数据,以及文件和任意的命名管道。

    特殊语义的指令将会通过新分配的流传送。例如,用于播放媒体文件的“audio”或“video”指令。用于传输文件的“file”指令,用于在客户端和服务端传输任意数据的“pipe”指令。在某些情况下,将通过已命名的流传送的结构化集合对象的方式来显示指明流的能力范围和语义。

    流一旦被创建,将通过“blob”指令一块一块地传送数据,通过“ack”来确认已收到的消息,流的结束通过一个“end”指令来标识。

    常用协议指令解析

    命令解析参考官方文档与客户端源码,由一次真实的链接作为样例参考

    样例

    在一次访问中遇到的,除了建立连接的指令外的指令

    read请求:

    4.size,1.0,4.1364,3.768;4.size,2.-1,2.11,2.16;3.img,1.3,2.12,2.-1,9.image/png,1.0,1.0;4.blob,1.3,232.iVBORw0KGgoAAAANSUhEUgAAAAsAAAAQCAYAAADAvYV+AAAABmJLR0QA/wD/AP+gvaeTAAAAYklEQVQokY2RQQ4AIQgDW+L/v9y9qCEsIJ4QZggoJAnDYwAwFQwASI4EO8FEMH95CRYTnfCDOyGFK6GEM6GFo7AqKI4sSSsCJH1X+roFkKdjueABX/On77lz2uGtr6pj9okfTeJQAYVaxnMAAAAASUVORK5CYII=;3.end,1.3;6.cursor,1.0,1.0,2.-1,1.0,1.0,2.11,2.16;4.size,2.-1,2.32,2.32;0.;

    3.img,1.3,2.14,4.-885,9.image/png,1.0,1.0;4.blob,1.3,152.iVBORw0KGgoAAAANSUhEUgAAAI8AAACfAQMAAAD6exhLAAAABlBMVEUAAAA4bKCQgYqkAAAAJUlEQVRIie3IMQ0AAAgDsDnBv0ngxwFJezY1R0cppZRSSin1oBaBVZeq9v/dCwAAAABJRU5ErkJggg==;3.end,1.3;4.copy,4.-885,1.3,1.0,3.140,3.159,2.14,1.0,3.971,3.257;4.sync,11.14688328152;

    4.rect,1.0,3.994,3.263,2.42,2.12;5.cfill,2.14,1.0,1.8,2.36,3.104,3.255;

    5.error,18.Aborted. See logs.,3.520;7.dispose,3.-46;0.;

    7.dispose,3.-28;7.dispose,3.-45;7.dispose,3.-37;7.dispose,2.-1;7.dispose,1.0;10.disconnect;

    write请求:

    3.ack,1.3,2.OK,1.0;

    4.sync,11.14685868962;

    3.nop;

    5.mouse,3.702,2.16,1.0;

    3.key,3.115,1.1;

    说明:read请求中出现的命令为guacd向客户端发送的命令,write请求中出现的命令为客户端向guacd发送的命令。read请求,write请求均为客户端发出,指令分别出现在write请求的请求头的requestPayload部分和read请求的response部分。

    指令解析

    size

    设置指定图层的大小,拥有三个参数

    layer:指定图层下标

    width:指定图层宽度

    height:指定图层高度

    完整指令顺序:

    size,layer,width,height;

    样例:

    4.size,1.0,4.1364,3.768;

    作用:将图层0的大小设置为1364*768

    img

    分配一个新的流,并将其与图像更新的元数据相关联,包括图像类型,目标图层和目标坐标。图像的具体内容将由其后的Blob指令指定,因此img通常与Blob绑定使用。

    img指令有六个参数

    stream:要分配的流的索引

    mask:绘制图像时要应用的通道蒙版(具体见最后通道蒙版表)

    layer:图像生成的目标图层

    mimetype:发送的图像的格式

    x:目标图像左上角在目标图层中的X坐标

    y:目标图像左上角在目标图层中的Y坐标

    完整指令顺序:

    img,stream,mask,layer,mimetype,x,y;

    样例:

    3.img,1.3,2.12,2.-1,9.image/png,1.0,1.0;

    作用:通知客户端将要使用索引为3的流传输图像,绘图时使用12号通道蒙版,图像生成在-1号图层(负数图层在客户端中作为缓冲图册,存储各种缓冲图像),图像格式为

    image/png,坐标为(0,0)

    blob

    通过给定的流发送一组数据。数据长度任意,采用base64编码,该数据仅当通过指定流传输时才有意义。

    blob指令有两个参数

    stream:指定流的索引

    data:要发送的base64编码数据

    完整指令顺序:

    blob,stream,data;

    样例:

    4.blob,1.3,232.iVBORw0KGgoAA…………(具体数据省略)

    作用:通过索引为3的流传输数据

    end

    结束指定流,体现在客户端源码中为将指定索引流删除。

    end指令有一个参数

    stream:指定流的索引

    完整指令顺序:

    end,stream;

    样例:

    3.end,1.3;

    作用:将索引为3的流关闭

    cursor

    将客户端的光标设置为具有指定热点的图层的指定矩形中的图像数据。有七个参数

    X:光标热点的X坐标

    Y:光标热点的Y坐标

    srclayer:要复制图像的图层的索引

    srcX:源图层内源矩形左上角的X坐标

    srcY:源图层内源矩形左上角的Y坐标

    srcWidth:源图层内源矩形的宽度

    srcHeight:源图层内源矩形的高度

    完整指令顺序:

    cursor,X,Y,srcLayer,srcX,srcY,srcWidth,srcHeight;

    样例:

    6.cursor,1.0,1.0,2.-1,1.0,1.0,2.11,2.16;

    作用:从-1图层(0,0)位置拷贝一个11*16的矩形作为光标图像生成在(0,0)位置

    copy

    将图像数据从指定图层或缓冲区的指定矩形复制到另一个指定图层或缓冲区的其他位置。

    copy指令有九个参数:

    srclayer:源图层的索引

    srcx:源图层内源矩形左上角的X坐标

    srcy:源图层内源矩形左上角的Y坐标

    srcwidth:源图层内源矩形的宽度

    srcheight:源图层内源矩形的高度

    mask:在目标图层上绘制图像数据时要应用的通道蒙版

    dstlayer:要绘制图像的目标图层的索引

    dstx:目标图层内目标图像的左上角的X坐标

    dsty:目标图层内目标图像的左上角的Y坐标

    完整指令顺序:

    copy,srclayer,srcx,srcy,srcwidth,srcheight,mask,dstlayer,dstx,dsty;

    样例:

    4.copy,4.-885,1.3,1.0,3.140,3.159,2.14,1.0,3.971,3.257;

    作用:

    从索引为-885的图层中复制(3,0)位置的140*159大小的矩形到索引为0的图层的(971,257)位置,绘图时采用14号通道蒙版

    sync

    服务器指示给定的时间戳是所有先前操作的当前时间戳。客户端必须响应收到的每个sync指令。

    客户端和服务器均应偶尔发送sync指令以报告当前操作执行状态。如果客户端没有响应服务器的sync指令,服务器可能会停止发送更新,直到客户端赶上来。

    sync指令只有一个参数:
    timestamp:有效的服务器相对时间戳

    完整指令顺序:

    sync,timestamp;

    样例:

    4.sync,11.14688328152;

    作用:指示当前时间戳为14688328152

    rect

    将一个矩形路径添加到指定的图层

    rect指令文档上有6个参数

    mask:绘制图像数据时要应用的通道蒙版

    layer:目标层

    X:要绘制的矩形的左上角的X坐标

    Y:要绘制的矩形的左上角的Y坐标

    width:要绘制的矩形的宽度

    height:要绘制的矩形的高度

    客户端源码与网页请求数据中只有5个参数:

    layer:目标层

    X:要绘制的矩形的左上角的X坐标

    Y:要绘制的矩形的左上角的Y坐标

    width:要绘制的矩形的宽度

    height:要绘制的矩形的高度

    按照客户端源码与网页请求数据为准

    指令完整顺序:

    rect,layer,x,y,width,height;

    样例:

    4.rect,1.0,3.994,3.263,2.42,2.12;

    作用:

    在0号图层(994,263)位置绘制一个42*12的矩形

    cfill

    用指定的颜色填充当前路径。

    cfill指令有六个参数

    mask:在指定层中填充当前路径时要应用的通道蒙版

    layer:要填充路径所在的图层

    r:用于填充路径的颜色的红色分量

    g:用于填充路径的颜色的绿色分量

    b:用于填充路径的颜色的蓝色分量

    a:用于填充路径的颜色的alpha分量(不透明度)

    完整指令顺序:

    cfill,mask,layer,r,g,b,a;

    样例:

    5.cfill,2.14,1.0,1.8,2.36,3.104,3.255;

    作用:

    使用14号蒙版填充0号图层中当前存在的路径,rgb颜色分量为(8,36,104),不透明度为255

    error

    通知客户端由于指定的错误即将关闭连接。服务器可以在任何阶段发送此消息。

    error指令有两个参数

    test:描述错误的消息

    status:描述错误的guaca协议代码(具体见最后状态码表)

    完整指令顺序:

    error,test,status;

    样例:

    5.error,18.Aborted. See logs.,3.520;

    作用:通知客户端发送520异常,具体原因查询日志。

    dispose

    删除指定的图层

    dispose指令只有一个参数

    layer:要删除的图层

    完整指令顺序:

    dispose, layer;

    样例:

    7.dispose,2.-1;

    作用:
    删除-1图层

    disconnect

    通知客户端服务器即将关闭连接。该指令不带任何参数。

    ack

    ack指令确认接收到的数据blob,并提供状态代码和消息,说明该blob关联的操作是成功还是失败。

    ack指令有三个参数:

    stream:接收相应blob的流的索引

    message:提示消息

    status:表示成功或失败的状态码

    完整指令顺序:

    ack,stream,message,status;

    样例:

    3.ack,1.3,2.OK,1.0;

    作用:索引为3的流中的blob接收成功

    nop

    客户端“ nop”指令不执行任何操作,没有任何参数,并且被Guacamole服务器忽略。它的主要用途是作为保持活动信号,并且在没有活动可确保套接字由于超时而没有关闭的情况下,可由Guacamole 客户端发送。

    mouse(客户端指令)

    发送指定的鼠标移动或按钮按下或释放事件

    mouse指令有三个参数

    X:鼠标指针的当前X坐标。

    Y:鼠标指针的当前Y坐标。

    mask:按钮掩码,代表每个鼠标按钮的按下或释放状态

    按钮掩码:

    0-未按下鼠标

    1-按下鼠标左键

    2-按下鼠标中键

    4-按下鼠标右键

    8-鼠标滚轮向上

    16-鼠标滚轮向下

    完整指令顺序:

    mouse,x,y,mask;

    样例:

    5.mouse,3.702,2.16,1.0;

    作用:

    鼠标移动到坐标(702,16)位置,未按下任何键

    key(客户端指令)

    发送指定的按键按下或释放事件

    keysym:按下或释放的键对应的X11值

    pressed:如果未按下该键,则为0;如果按下该键,则为1。

    完整指令顺序:

    key,keysym,pressed;

    样例:

    3.key,3.115,1.1;

    作用:

    按下115对应的键(S键)

    图像绘制

    图像数据

    guacamole协议通过img指令建立一个流,用于以PNG,JPEG或者WebP的格式传送的图像数据。根据使用的格式的不同,通过这种方式传送的图像更新数据可能以RGB或者RGBA(A代表透明度)编码,如果通过libguac传送的话,还会被自动调色。

    图像数据还可以被发送到任意指定的矩形、图层或者缓存。将图像数据发送到图层意味着立刻可见,将图像数据发送到缓存意味着可以在将来被重用。

    图像数据可以在图层或者缓存之间拷贝,这在屏幕滚动的时候很有用(屏幕滚动的时候,更新的图像经常与之前的图像完全一样),在缓存某个部分的图像的时候,也很有用。

    guacamole吸收了RDP和VNC中拷贝屏幕区域数据的概念,并将之进一步发展,将屏幕可见(图层)与屏幕不可见(缓存)的存储统一起来。使用copy指令可以拷贝一个矩形的图像数据,并且可以将其放置到其他任意图层包括缓存中。

    缓存与图层

    guacamole里的每个绘制操作都会作用到一个具体的图层,每个图层都有一个唯一的编号来标识它自身。图层编号是负数的时候,这个图层是不可见的,可以用于存储或者缓存图像数据。此时,图层通过编码被引用,且等同于文档中所谓的缓存,当通过某个指令引用图层的时候,图层会被自动创建。

    0号图层会被当作默认图层,调整这个图层大小的时候就会调整整个远端屏幕大小。其他图层创建时候的初始大小与默认图层的大小一致。缓存的初始大小为0x0,并且会自动调整大小来适配装入的内容。

    非缓存图层可以在其他图层中被移动或者嵌套。通过这种方式,提供了一种简单的硬件加速合成图像的方式。如果你需要一个窗口浮现在另外的窗口之上,或者你想要移除一些对象,又或者你想要自动保存一些对象之下的图像数据,图层是实现这些需求较好的方式。如果一个图层嵌套在其他图层里,它的位置是相对于父图层的。当父图层被移动或者重排序(调整图层之间的顺序)的时候,子图层会随之移动以及重排序。如果子图层超出了父图层的边界,字图层将被裁切。

    绘制过程

    当客户端与guacd成功建立连接后,客户端将开始进行图像绘制,图像刷新主要以img指令配合blob指令为主。guacamole协议通过img指令建立一个流,随后blob指令沿流开始传输数据(base64格式),客户端收到数据解析后就将按照img指令指定的绘制方式(通道蒙版样式)将图形绘制在指定的图层上。在视频播放(存疑,实际与官方文档有出入)时,同理guacd不断向客户端沿流传输img指令+blob指令的组合,客户端收到指令后进行解析绘制,只要流中一直传输img指令+blob指令,视频就将播放下去。guaca采用局部刷新的方式,

    copy与cfill指令也参与图像刷新,copy指令通常是从缓冲图层获取数据,常见于屏幕滚动, 光标变化时刷新;cfill指令则配合rect指令负责填充指定矩形。

    附录

    状态码表

    CodeNameDescription
    0SUCCESS操作成功。OK
    256UNSUPPORTED请求的操作不受支持
    512SERVER_ERROR发生内部错误,无法执行操作
    513SERVER_BUSY由于服务器繁忙,无法执行该操作
    514UPSTREAM_TIMEOUT上游服务器没有响应。在大多数情况下,上游服务器是远程桌面服务器。
    515UPSTREAM_ERROR上游服务器遇到错误。
    516RESOURCE_NOT_FOUND找不到关联的资源,例如文件或流,因此操作失败。
    517RESOURCE_CONFLICT资源已被使用或锁定,从而阻止了所请求的操作。
    518RESOURCE_CLOSED请求的操作无法继续,因为相关的资源已关闭。
    519UPSTREAM_NOT_FOUND上游服务器似乎不存在,或者无法通过网络访问。
    520UPSTREAM_UNAVAILABLE上游服务器拒绝服务连接。
    521SESSION_CONFLICT上游服务器内的会话已结束,因为它与另一个会话发生冲突。
    522SESSION_TIMEOUT上游服务器内的会话已结束,因为它似乎处于非活动状态。
    523SESSION_CLOSED上游服务器中的会话已被强制关闭。
    768CLIENT_BAD_REQUEST请求的参数无效或无效。
    769CLIENT_UNAUTHORIZED权限被拒绝,因为该用户尚未登录。
    771CLIENT_FORBIDDEN权限被拒绝,登录将无法解决问题。
    776CLIENT_TIMEOUT客户端花费的时间太长而无法反映
    781CLIENT_OVERRUN客户端发送的数据超出了协议允许的范围。
    783CLIENT_BAD_TYPE客户端发送了意外或非法类型的数据。
    797CLIENT_TOO_MANY客户端已经使用了太多资源。在允许进一步的请求之前,必须释放现有资源。

    通道蒙版表

    CodeNameDescription
    0x00CLear在目标图层中清空所有已经存在的图像数据。
    0x01B in A在源图层不透明的地方绘制目标图层,清空所有的源图层透明以及目标图层透明的地方。
    0x02B out A清除目标图层中源图层不透明的部分,而且不会绘制任何东西。可用于遮盖。
    0x03B不做任何操作
    0x04A in B在目标图层不透明的地方绘制源图层,清空所有的源图层透明以及目标图层透明的地方。
    0x05A xnor B将目标图层与源图层不透明的地方相加起来。清空所有目标图层与源图层透明的地方。这与A+B相似,除了透明的地方要清空以外。
    0x06A atop B在目标图层中填充源图层中不透明的部分。
    0x07(A+B)atop B在目标图层不透明的地方绘制源图层,并保留目标图层余下的地方。
    0x08A out B在目标图层透明的地方画源图层,清空所有的源图层不透明以及目标图层不透明的地方。
    0x09B atop A在源图层不透明的地方填充目标图层。
    0x0AA xor B与逻辑上的xor操作一样。但是这是图像合成操作,不是位运算,实质含义是在目标图层的透明部分绘制源图层,在源图层的透明部分绘制目标图层。
    0x0BB over A与你期望的常规绘制相反。源图层出现在目标图层透明的地方。如同你将目标图层绘制在源图层上,而不是反过来。
    0x0CA填充源图层,忽略目标图层。
    0x0D(A+B)atop A在源图层不透明的地方绘制目标图层,并且复制源图层余下的地方。
    0x0EA over B最常见的合成操作,在目标图层绘制全部的源图层,除了源图层透明的地方。
    0x0FA + B将源图层与目标图层相加起来,并将结果填充到空白的画布上。
    展开全文
  • guacamole协议 协议组成 Guacamole 协议由若干指令组成。每条指令是一个逗号分隔的列表,最后以分号终止,其中列表中的第一个元素是指令操作码,其后的元素是该指令的参数: OPCODE,ARG1,ARG2,ARG3,……; 指令列表...

    guacamole协议

    协议组成

    Guacamole 协议由若干指令组成。每条指令是一个逗号分隔的列表,最后以分号终止,其中列表中的第一个元素是指令操作码,其后的元素是该指令的参数:

    OPCODE,ARG1,ARG2,ARG3,……;
    

    指令列表中的每个元素都是由一个正的十进制整数前缀和一个具体的元素值组成,其中前缀由一个英文句点( . )分隔。整数前缀表示具体的元素值的Unicode字符的数量,字符由UTF-8编码:

    LENGTH.VALUE
    

    若干条完整的指令组成一条消息,该消息从客户端发送到服务器,或者从服务器发送到客户端。客户端到服务器的指令通常是控制指令(用于建立连接或断开连接)和事件(鼠标事件和键盘事件)。服务器到客户端的指令通常是将客户端用作远程显示器的绘制指令(缓存,剪切,绘制图像)。

    例如,将显示尺寸设置为1024*768的完整有效的指令是:

    4.size,1.0,4.1024,3.768;
    

    对于这个指令,会被服务器解析为四个元素:“size” ,作为size指令的操作码,“0”,是图像默认层的索引,“1024”,为所需的像素宽度,“768”,为所需的像素高度。

    正因为Guacamole协议的设计方式,使得它可以流式传输协议,同时也可以很容易地被JavaScript解析。

    JS确实原生支持类似于XML、JSON这样格式的信息,但是这类格式的信息都不能以流的形式传输。JS在解析这类格式的信息前必须接收到完整的XML或者JSON的包,而guacamole协议的信息,却可以一边接收一边解析。它的指令内每个元素的长度前缀使得解析器不用遍历每个字符就可以完成指令之间的跳转。

    握手阶段

    新建连接

    握手的过程是guacamole协议建立连接的过程。当客户端发送“select”指令时,握手阶段就开始了,select指令告诉服务器要加载哪个协议:

    6.select,3.vnc;
    

    收到“select”指令后,服务器会加载对应的客户端组件,并且回复一个“args”指令,这个指令指明了服务器端需要的参数列表

    4.args,13.VERSION_1_1_0,8.hostname,4.port,8.password,13.swap-red-blue,9.read-only;
    

    其中的协议版本用于协商客户端和服务器的不同版本之间的兼容性,从而允许双方协商最高支持的版本并启用或禁用与该版本关联的功能。不支持该指令的旧版本的Guacamole客户端将无提示地将其忽略为空连接参数。有效协议版本如下:

    版本号对应参数值描述
    1.0.0VERSION_1_0_0这是默认版本,适用于1.1.0之前的任何版本。协议的版本1.0.0不支持协议协商,并且要求握手指令以特定顺序传递,并且存在(即使为空)。
    1.1.0VERSION_1_1_0协议版本1.1.0引入了对协议版本协商,握手指令的任意顺序的支持,并支持在握手期间传递时区指令。

    客户端接收到服务端可接受的参数列表后,需要回复给服务器,自己支持的音频(audio),视屏(video),图像(image),最佳屏幕尺寸(size)及时区(timezone),并在最后回复所有的服务器要求的参数的值(connect),即使是空,也要回复。任意要求没有满足,连接都将被关闭。客户端回复给服务端的消息如下:

    4.size,4.1024,3.768,2.96;
    5.audio,9.audio/ogg;
    5.video;
    5.image,9.image/png,10.image/jpeg;
    8.timezone,16.America/New_York;
    7.connect,13.VERSION_1_1_0,9.localhost,4.5900,0.,0.,0.;
    

    如上,在客户端回复服务端的参数中,回复了三个0.给服务端,意味着客户端这三个参数为空,没有值,所以留空,长度为0,回复0.恰好。

    在实际协议中,指令之间是紧挨着的不存在换行符,如果一条指令之后除了新指令的开始之外还有其他内容,则连接将关闭。

    以下是握手过程中的指令的说明:

    指令名称描述
    audio客户端支持的音频编码解码器。在上面的例子中指定了audio/ogg作为支持的编码解码器
    connect这是握手阶段的最后一个指令,它表明握手阶段已经结束,并且连接正常建立,可以继续进行。这条指令后续跟着服务器中args指令发送的连接参数的参数值。在上面的例子中,参数指定了在5900端口与localhost进行连接,后续三个参数值为空。
    iamge客户端支持的图像格式,按首选项顺序。上例中的客户端同时支持PNG和JPEG。
    timezone客户端的时区,采用IANA区域密钥格式。上例的时区是美国纽约
    video客户端支持的视频编码解码器。上例的客户端是不支持任何视频编解码器。

    客户端在握手中发送的指令的顺序是任意的,除了最后一条指令connect将结束握手并尝试开始连接。

    客户端发送完这些指令后,服务器将尝试使用接收到的参数初始化连接,如果成功,则以“ready”指令进行响应。这条指令中包含新客户端连接的ID,并标记交互阶段的开始。这个ID是一个任意字符串,但是保证这个ID在所有活动链接以及受支持协议中是唯一的:

    5.ready,37.$260d01da-779b-4ee9-afc1-c16bae885cc7;
    

    当服务器发送’ready‘后,真正的交互阶段就开始了。客户端和服务器端之间相互传递绘图和事件指令,直到关闭连接。

    加入现有连接

    握手阶段完成后,如果通过“select”指令提供了ID而不是协议名称,则这个连接将被认为是活跃的并且是能被加入的。

    6.select,37.$260d01da-779b-4ee9-afc1-c16bae885cc7;
    

    加入现有连接的其余阶段与握手的阶段是相同的。与新连接一样,这次连接的其他参数由握手期间提供的参数值决定。

    流与对象

    guacamole支持传输剪切板内容,音频内容,视频内容和图像数据,以及文件和任意的命名管道。

    特殊语义的指令将会通过新分配的流传送。例如,用于播放媒体文件的“audio”或“video”指令。用于传输文件的“file”指令,用于在客户端和服务端传输任意数据的“pipe”指令。在某些情况下,将通过已命名的流传送的结构化集合对象的方式来显示指明流的能力范围和语义。

    流一旦被创建,将通过“blob”指令一块一块地传送数据,通过“ack”来确认已收到的消息,流的结束通过一个“end”指令来标识。

    客户端源码解析

    在这里插入图片描述

    guacaclient目录结构 如上图所示 为官方guacamole-common完整项目

    因个人能力有限 所以源码不会一一解读 只看几个我看的懂的。

    接下来将按包目录展开

    IO

    一看名字就知道是负责客户端读取输出的包,包中共有两个接口,两个实现类。

    GuacamoleReader

    是一个接口,注释说明这个接口是提供对guacamole指令流的抽象和原始字符的读取访问。

    接口中一共提供了三个了方法

    public boolean available() throws GuacamoleException;
    

    available()方法返回指令信息是否可以继续读取,返回true表示可以继续读取,false则不行。具体含义结合实现类看比较容易懂。

    public char[] read() throws GuacamoleException;
    

    read()方法负责读取流中的指令。此方法一次至少读取一个完整的guacamole指令,返回的缓存中包括一个或多个完整指令,但不会包含不完整指令。

    public GuacamoleInstruction readInstruction() 
    

    readInstruction()方法读取一个完整的指令并返回完整解析的指令。

    看到这可能觉得read(),readInstruction() 方法类似,看完了实现类会发现两者区别还是挺大的,一个是从流中截取一段段的guacamole指令,一个是将指令的前缀数值去掉直接获取指令的opcode和args。

    ReaderGuacamoleReader

    ReaderGuacamoleReader是GuacamoleReader的实现类

    注释说明了这个类 使用了标准的java的Reader对guacamole的指令流进行读取。

    这个类一共有四个成员变量,实现了GuacamoleReader的所有方法。

    四个成员变量:

    private Reader input;
    public ReaderGuacamoleReader(Reader input){this.input = input}
    

    这个input对象作为Reader类的对象将负责整个类的数据读取,在后续的read()方法中有出现;input在构造函数中进行了初始化:

    private int parseStart;
    

    parseStart变量用来记录当前解析到的数据在数据堆(后续的char类型数组)中的位置, 防止重复或超前解析。在read()方法中主要做一个记录的作用

    private cahr[] buffer = new char[20480];
    

    buffer数组用来接受存放所有未解析的数据,也就是说通过input读取的数据都会放置在这个数组中。

    private int usedLength = 0;
    

    记录buffer数组中的实际字符数,也就是记录当前buffer数组的实际使用长度。

    实现的三个方法:

    available()方法

        public boolean available() throws GuacamoleException {
            try {
                return input.ready() || usedLength != 0;
            }
            catch (IOException e) {
                throw new GuacamoleServerException(e);
            }
        }
    

    在input准备就绪和usedLength不为0时返回true;当usedLength!=0意味着buffer数组中还存在指令信息,可继续进行解析处理。

    read()方法

    public char[] read() throws GuacamoleException {
    
            try {
    
                // 当方法阻塞时,input会读取新数据,循环一直进行直到input读到最后的数据
                for (;;) {
    
                    // 记录指令长度,也是协议 数值.值 中的数值部分
                    int elementLength = 0;
    
                    // 设置i的位置从parseStart开始,因为parseStart的作用就是记录解析到的位置
                    int i = parseStart;
    
                    // 从i位置开始解析直到usedLength处,因为usedLength记录了buffer数组实际使用的长度
                    while (i < usedLength) {
    
                        // 先读取一个字符
                        char readChar = buffer[i++];
    
                        // 如果是数字则更新数值
                        if (readChar >= '0' && readChar <= '9'){
                            elementLength = elementLength * 10 + readChar - '0';
    										}
                        //如果是 . 说明数值部分已经结束 检查当前元素块结尾是 分号(;)还是(,)号,如果都不是则报错
                        else if (readChar == '.') {
    
                            // 检查当前元素是否都在buffer数组中
                            if (i + elementLength < usedLength) {
    
                                // 获取终结符
                                char terminator = buffer[i + elementLength];
    
                                // 将i向后移动指令长度+1,后续直接将指令长度的字符全部读取
                                i += elementLength + 1;
    
                                // 重制指令长度,方便下次读取
                                elementLength = 0;
    
                                // 重新记录解析位置为i
                                parseStart = i;
    
                                // 如果终结符是‘;’,说明我们已经读取完了一个完整的指令
                                if (terminator == ';') {
    
                                    // 复制全部指令
                                    char[] instruction = new char[i];
                                    System.arraycopy(buffer, 0, instruction, 0, i);
    
                                    // 更新buffer实际使用长度,要减去 i
                                    因为现在i之前的字符都已经解析过了
                                    usedLength -= i;
                                    parseStart = 0;
                                    System.arraycopy(buffer, i, buffer, 0, usedLength);
    
                                    return instruction;
    
                                }
    
                                // 如果终结符不是‘,‘说明传过来的协议数据不符合约定的协议格式,抛出异常
                                else if (terminator != ',')
                                    throw new GuacamoleServerException("Element terminator of instruction was not ';' nor ','");
    
                            }
    
                            // 当前元素内容没有全在buffer数组中则停止解析,等待input读取后续内容
                            else
                                break;
    
                        }
    
                        // 在数值部分存在非数值字符,抛出异常
                        else
                            throw new GuacamoleServerException("Non-numeric character in element length.");
    
                    }
    
                    // 如果当前实际使用长度超过了buffer数组长度的一半则进行扩容,直接扩充一倍
                    if (usedLength > buffer.length/2) {
                        char[] biggerBuffer = new char[buffer.length*2];
                        System.arraycopy(buffer, 0, biggerBuffer, 0, usedLength);
                        buffer = biggerBuffer;
                    }
    
                    // 因为上面进行了解析或者扩容,buffer数组肯定有空余位置了,让input继续读取新的指令信息
                    int numRead = input.read(buffer, usedLength, buffer.length - usedLength);
                    if (numRead == -1)
                        return null;
    
                    //更新buffer数组的实际使用长度 
                    usedLength += numRead;
    
                }//至此 循环结束 
    
            }
            catch (SocketTimeoutException e) {
                throw new GuacamoleUpstreamTimeoutException("Connection to guacd timed out.", e);
            }
            catch (SocketException e) {
                throw new GuacamoleConnectionClosedException("Connection to guacd is closed.", e);
            }
            catch (IOException e) {
                throw new GuacamoleServerException(e);
            }
    
        }
    

    read()方法的整体逻辑也是从解析guacamole协议出发的因为协议规定协议中每个元素的组成格式为 LENGTH.VALUE 所以read方法从读取LENGTH开始,当读取完length后判断后续的value是否都已经在buffer数组中了,如果是 则开始下一个元素的解析直到读取到 ‘ ; ’ 说明一条指令读取完毕 将整条指令返回。中间方法也进行了格式完整性的判断。这个方法也体现了guacamole协议设计的便利性,可以进行流式传输,一条指令可以在前后两次数据包中传输。

    readInstruction()

        public GuacamoleInstruction readInstruction() throws GuacamoleException {
    
            // 通过read()方法直接获得一组指令
            char[] instructionBuffer = read();
    
            // 如果read方法返回为null说明已经解析结束,直接返回null
            if (instructionBuffer == null)
                return null;
    
            // 记录当前解析到的指令位置
            int elementStart = 0;
    
            // 创建一个双端队列存储指令
            Deque<String> elements = new LinkedList<String>();
            while (elementStart < instructionBuffer.length) {
    
                // 记录LENGTH的结尾位置,初始化为 -1
                int lengthEnd = -1;
                //寻找LENGTH的结尾位置
                for (int i=elementStart; i<instructionBuffer.length; i++) {
                    if (instructionBuffer[i] == '.') {
                        lengthEnd = i;
                        break;
                    }
                }
    
                // 如果遍历指令结束都没有找到‘.’ 说明返回的指令是不完整的,抛出异常
                if (lengthEnd == -1)
                    throw new GuacamoleServerException("Read returned incomplete instruction.");
    
                // 找到了一个完整的LENGTH,将char类型的数值转化为int
                int length = Integer.parseInt(new String(
                        instructionBuffer,
                        elementStart,
                        lengthEnd - elementStart
                ));
    
                // 去掉一个‘.’的位置 通过length获取一个元素
                elementStart = lengthEnd + 1;
                String element = new String(
                        instructionBuffer,
                        elementStart,
                        length
                );
    
                // 将获取的元素加到队列中
                elements.addLast(element);
    
                // 读取当前元素后的结束符,只可能是‘,’或‘.’
                elementStart += length;
                char terminator = instructionBuffer[elementStart];
    
                // 继续读取下一个元素
                elementStart++;
    
                // 当读取到‘;’时 结束循环
                if (terminator == ';')
                    break;
    
            }
    
            // 从队列中弹出第一个加入队列的元素 按照协议格式 第一个元素为操作码
            String opcode = elements.removeFirst();
    
            // 创建GuacamoleInstruction类对象,将读取的协议传入,构建一个实体对象
            GuacamoleInstruction instruction = new GuacamoleInstruction(
                    opcode,
                    elements.toArray(new String[elements.size()])
            );
    
            // 返回解析完成的协议对象
            return instruction;
    
        }
    

    readInstruction()方法的实现逻辑很简单,先从read()处获得规定格式的协议源码,再通过LENGTH.VALUE格式 逐步获取协议的操作码和后续参数,最后构建对象,返回结果。

    展开全文
  • 堡垒机xrdp/guacamole协议代理

    千次阅读 2019-12-23 10:51:32
    堡垒机的底层开发是基于ssh/rdp/vnc/x11/sftp/telnet等协议的研究,目前有开源项目,像guacamole/xrdp/freerdp都是非常优秀的c语言开源项目,本人研究改造源码有两年了。 xrdp的代码架构很优秀,能代理rdp/vnc/xorg...

    堡垒机,就是运维审计系统,它和防火墙几乎是一个时期的东西,最近两年等级保护政策让它又值钱了,包括腾讯云、华为云以及阿里云都在做。堡垒机的底层开发是基于ssh/rdp/vnc/x11/sftp/telnet等协议的研究,目前有开源项目,像guacamole/xrdp/freerdp都是非常优秀的c语言开源项目,本人研究改造源码有两年了。

    xrdp的代码架构很优秀,能代理rdp/vnc/xorg,但是没有数据记录和控制,需要自己添加代码,包括用户信息代填,录像,实时监控,剪切板控制,共享磁盘,权限控制等。xrdp支持多线程和多进程,在此忠告,不要使用多线程模式。xrdp不支持x11中转,如果需要支持xorg,需要自己代理x11客户端去连接xdmcp服务器。

    guacamole代码前端是java封装的war包,后端即guacamole-server使用c语言编写,支持ssh/vnc/rdp,支持监控和录像。但ssh协议的字符审计不支持,需要自己根据业务做更改。

    freerdp是关于rdp协议封装好的c语言接口,同时也提供client工具,如果基于它做堡垒机,工作量是比较大的,首先要了解rdp协议本身,你可能要抓包。teamview开发团队就是基于freerdp。通过freerdp做rdp的代理,比较废时,但却能做多个业务。

    协议代理的业务短时间说不完,有时间再具体分析一下吧。

     

    展开全文
  • guacamole

    2021-03-25 17:55:26
    Apache Guacamole是无客户端远程桌面网关,它支持VNC,RDP和SSH等标准协议。 我们称其为“无客户端”,因为不需要插件或客户端软件。 将Guacamole安装在服务器上后,只需访问Web浏览器即可访问桌面。...

空空如也

空空如也

1 2 3 4 5 ... 16
收藏数 313
精华内容 125
关键字:

guacamole支持的协议