From 589e19142c51ab7af0bd2c2faa3ea09b7d5dca8c Mon Sep 17 00:00:00 2001 From: Hector Sanjuan Date: Mon, 30 Jan 2012 12:16:14 +0100 Subject: [PATCH] Feature #1069: Support secure-websocket-based VNC session in Sunstone. This commit adds support for using wss capabilities of websockify: * Add configuration option to Sunstone and saving/restore in user template support * Add new options to sunstone server configuration file * VNC session is started according to user setting * The code related to VNC proxy launch has been outsourced to OpenNebulaVNC.rb, so it can be mantained more easily and reused by, for example, SelfService. * Install novnc script has been corrected to point to "websockify" full path. Note: this commit changes vnc-related sunstone-server.conf keys and breaks vnc support in former versions of the configuration file. Update if necessary. (cherry picked from commit 00cf42e6b685a5ab26ca02d0eb142c311f56f6ee) --- install.sh | 3 +- share/install_novnc.sh | 3 +- src/sunstone/OpenNebulaVNC.rb | 90 ++++++++++++++++++++ src/sunstone/etc/sunstone-server.conf | 12 ++- src/sunstone/models/SunstoneServer.rb | 89 ++++++------------- src/sunstone/public/js/plugins/config-tab.js | 40 +++++++++ src/sunstone/public/js/plugins/vms-tab.js | 2 +- src/sunstone/sunstone-server.rb | 35 +++++++- 8 files changed, 205 insertions(+), 69 deletions(-) create mode 100644 src/sunstone/OpenNebulaVNC.rb diff --git a/install.sh b/install.sh index a9dee20015..ef321d1dd6 100755 --- a/install.sh +++ b/install.sh @@ -1077,7 +1077,8 @@ ETC_CLIENT_FILES="src/cli/etc/group.default" #----------------------------------------------------------------------------- SUNSTONE_FILES="src/sunstone/config.ru \ - src/sunstone/sunstone-server.rb" + src/sunstone/sunstone-server.rb \ + src/sunstone/OpenNebulaVNC.rb" SUNSTONE_BIN_FILES="src/sunstone/bin/sunstone-server" diff --git a/share/install_novnc.sh b/share/install_novnc.sh index 5dc90d6763..52dbf373ef 100755 --- a/share/install_novnc.sh +++ b/share/install_novnc.sh @@ -1,6 +1,7 @@ #!/bin/bash NOVNC_TMP=/tmp/one/novnc-$(date "+%Y%m%d%H%M%S") +PROXY_PATH=noVNC/utils/websockify if [ -z "$ONE_LOCATION" ]; then ONE_SHARE=/usr/share/one @@ -34,7 +35,7 @@ mv $ONE_SHARE/$dir $ONE_SHARE/noVNC mkdir -p $ONE_PUBLIC_SUNSTONE/vendor/noVNC mv $ONE_SHARE/noVNC/include/ $ONE_PUBLIC_SUNSTONE/vendor/noVNC/ -sed -i.bck "s%^\(:novnc_path: \).*$%\1$ONE_SHARE/noVNC%" $SUNSTONE_CONF +sed -i.bck "s%^\(:vnc_proxy_path: \).*$%\1$ONE_SHARE/$PROXY_PATH%" $SUNSTONE_CONF #Update file permissions chmod +x $ONE_SHARE/noVNC/utils/launch.sh diff --git a/src/sunstone/OpenNebulaVNC.rb b/src/sunstone/OpenNebulaVNC.rb new file mode 100644 index 0000000000..ecaac733b0 --- /dev/null +++ b/src/sunstone/OpenNebulaVNC.rb @@ -0,0 +1,90 @@ +# -------------------------------------------------------------------------- # +# Copyright 2002-2012, OpenNebula Project Leads (OpenNebula.org) # +# # +# 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. # +#--------------------------------------------------------------------------- # + + +#This file provides support for launching and stopping a websockify proxy + +require 'json' + +class OpenNebulaVNC + def initialize(config,opt={:json_errors => true}) + @proxy_path = config[:vnc_proxy_path] + @proxy_base_port = config[:vnc_proxy_base_port].to_i + @wss = config[:vnc_proxy_support_wss] + $stderr.puts "wss #{@wss}" + @enable_wss = (@wss == "yes") || (@wss == "only") + @cert = @enable_wss? config[:vnc_proxy_cert] : nil + @key = @enable_wss? config[:vnc_proxy_key] : nil + @options=opt + end + + def error(code, msg) + if @options[:json_errors] + return [code,OpenNebula::Error.new(msg).to_json] + else + return [code,msg] + end + end + + def start(vm_resource) + if vm_resource['LCM_STATE'] != "3" + return error(403,"VM is not running") + end + + if vm_resource['TEMPLATE/GRAPHICS/TYPE'] != "vnc" + return error(403,"VM has no VNC configured") + end + + # The VM host and its VNC port + host = vm_resource['/VM/HISTORY_RECORDS/HISTORY[last()]/HOSTNAME'] + vnc_port = vm_resource['TEMPLATE/GRAPHICS/PORT'] + # The port on which the proxy will listen + proxy_port = @proxy_base_port + vnc_port.to_i + + if !@proxy_path || @proxy_path.size == 0 + return error(403,"VNC proxy not configured") + end + + proxy_options = "" + + if @enable_wss + proxy_options += " --cert #{@cert}" + proxy_options += " --key #{@key}" if @key && @key.size > 0 + proxy_options += " --ssl-only" if @wss == "only" + end + + proxy_cmd = "#{@proxy_path} #{proxy_options} #{proxy_port} #{host}:#{vnc_port}" + + begin + $stderr.puts("Starting vnc proxy: #{proxy_cmd}") + pipe = IO.popen(proxy_cmd) + rescue Exception => e + error = Error.new(e.message) + return [500, error.to_json] + end + + vnc_pw = vm_resource['TEMPLATE/GRAPHICS/PASSWD'] + + info = {:pipe => pipe, :port => proxy_port, :password => vnc_pw} + return [200, info] + end + + #handle exceptions outside + def self.stop(pipe) + Process.kill('KILL',pipe.pid) + pipe.close + end +end diff --git a/src/sunstone/etc/sunstone-server.conf b/src/sunstone/etc/sunstone-server.conf index 1927bfb949..349fa484c1 100644 --- a/src/sunstone/etc/sunstone-server.conf +++ b/src/sunstone/etc/sunstone-server.conf @@ -16,8 +16,18 @@ :core_auth: cipher # VNC Configuration +# base_port: base_port + vnc_port of the VM is the port where the +# proxy will listen for VNC session connections to that VM. +# vnc_proxy_path: path to the websockets proxy (set by install_novnc.sh) +# support_wss: "no", "yes", "only". For yes and only, provide path to +# cert and key. Note value must be a quoted string. +# (key is only necessary if not included in cert). :vnc_proxy_base_port: 29876 -:novnc_path: +:vnc_proxy_path: +:vnc_proxy_support_wss: "no" +:vnc_proxy_cert: +:vnc_proxy_key: + # Default language setting :lang: en_US diff --git a/src/sunstone/models/SunstoneServer.rb b/src/sunstone/models/SunstoneServer.rb index a8113bcf65..359a24267e 100644 --- a/src/sunstone/models/SunstoneServer.rb +++ b/src/sunstone/models/SunstoneServer.rb @@ -18,6 +18,7 @@ require 'OpenNebulaJSON' include OpenNebulaJSON require 'acct/watch_client' +require 'OpenNebulaVNC' class SunstoneServer # FLAG that will filter the elements retrieved from the Pools @@ -147,35 +148,35 @@ class SunstoneServer end ############################################################################ - # + # Unused ############################################################################ - def get_configuration(user_id) - if user_id != "0" - return [401, ""] - end + # def get_configuration(user_id) + # if user_id != "0" + # return [401, ""] + # end - one_config = VAR_LOCATION + "/config" - config = Hash.new + # one_config = VAR_LOCATION + "/config" + # config = Hash.new - begin - cfg = File.read(one_config) - rescue Exception => e - error = Error.new("Error reading config: #{e.inspect}") - return [500, error.to_json] - end + # begin + # cfg = File.read(one_config) + # rescue Exception => e + # error = Error.new("Error reading config: #{e.inspect}") + # return [500, error.to_json] + # end - cfg.lines do |line| - m=line.match(/^([^=]+)=(.*)$/) + # cfg.lines do |line| + # m=line.match(/^([^=]+)=(.*)$/) - if m - name=m[1].strip.upcase - value=m[2].strip - config[name]=value - end - end + # if m + # name=m[1].strip.upcase + # value=m[2].strip + # config[name]=value + # end + # end - return [200, config.to_json] - end + # return [200, config.to_json] + # end ############################################################################ # @@ -211,50 +212,16 @@ class SunstoneServer return [404, resource.to_json] end - if resource['LCM_STATE'] != "3" - error = OpenNebula::Error.new("VM is not running") - return [403, error.to_json] - end - - if resource['TEMPLATE/GRAPHICS/TYPE'] != "vnc" - error = OpenNebula::Error.new("VM has no VNC configured") - return [403, error.to_json] - end - - # The VM host and its VNC port - host = resource['/VM/HISTORY_RECORDS/HISTORY[last()]/HOSTNAME'] - vnc_port = resource['TEMPLATE/GRAPHICS/PORT'] - # The noVNC proxy_port - proxy_port = config[:vnc_proxy_base_port].to_i + vnc_port.to_i - - begin - novnc_cmd = "#{config[:novnc_path]}/utils/wsproxy.py" - novnc_exec = "#{novnc_cmd} #{proxy_port} #{host}:#{vnc_port}" - $stderr.puts("Starting vnc proxy: #{novnc_exec}") - pipe = IO.popen(novnc_exec) - rescue Exception => e - error = Error.new(e.message) - return [500, error.to_json] - end - - vnc_pw = resource['TEMPLATE/GRAPHICS/PASSWD'] - - info = {:pipe => pipe, :port => proxy_port, :password => vnc_pw} - return [200, info] + vnc_proxy = OpenNebulaVNC.new(config) + return vnc_proxy.start(resource) end ############################################################################ # ############################################################################ - def stopvnc(id,pipe) - resource = retrieve_resource("vm", id) - if OpenNebula.is_error?(resource) - return [404, resource.to_json] - end - + def stopvnc(pipe) begin - Process.kill('KILL',pipe.pid) - pipe.close + OpenNebulaVNC.stop(pipe) rescue Exception => e error = Error.new(e.message) return [500, error.to_json] diff --git a/src/sunstone/public/js/plugins/config-tab.js b/src/sunstone/public/js/plugins/config-tab.js index 6f64a8bab4..126c509fcd 100644 --- a/src/sunstone/public/js/plugins/config-tab.js +++ b/src/sunstone/public/js/plugins/config-tab.js @@ -33,6 +33,12 @@ var config_tab_content = \ \ \ + \ + ' + tr("Secure websockets connection") + '\ + \ + \ + \ + \ \ \ \ @@ -48,6 +54,34 @@ var config_tab = { Sunstone.addMainTab('config_tab',config_tab); +function updateWss(){ + var user_info_req = { + data : { + id: uid, + }, + success: function(req,user_json) { + var template = user_json.USER.TEMPLATE; + var template_str=""; + template['VNC_WSS']= + $('#config_table #wss_checkbox').is(':checked') ? "yes" : "no"; + //convert json to ONE template format - simple conversion + $.each(template,function(key,value){ + template_str += (key + '=' + '"' + value + '"\n'); + }); + + var request = { + data: { + id: uid, + extra_param: template_str + }, + error: onError + }; + OpenNebula.User.update(request); + }, + }; + OpenNebula.User.show(user_info_req); +}; + $(document).ready(function(){ if (lang) $('table#config_table #lang_sel option[value="'+lang+'"]').attr('selected','selected'); @@ -55,4 +89,10 @@ $(document).ready(function(){ setLang($(this).val()); }); + $('table#config_table #wss_checkbox').change(updateWss); + + $.get('config/wss',function(response){ + if (response != "no") + $('table#config_table input#wss_checkbox').attr('checked','checked'); + }); }); \ No newline at end of file diff --git a/src/sunstone/public/js/plugins/vms-tab.js b/src/sunstone/public/js/plugins/vms-tab.js index fa69971dd6..cfb477c9c7 100644 --- a/src/sunstone/public/js/plugins/vms-tab.js +++ b/src/sunstone/public/js/plugins/vms-tab.js @@ -1227,7 +1227,7 @@ function setupVNC(){ function vncCallback(request,response){ rfb = new RFB({'target': $D('VNC_canvas'), - 'encrypt': false, + 'encrypt': $('#config_table #wss_checkbox').is(':checked'), 'true_color': true, 'local_cursor': true, 'shared': true, diff --git a/src/sunstone/sunstone-server.rb b/src/sunstone/sunstone-server.rb index 2ece360837..2e22b2aa3f 100755 --- a/src/sunstone/sunstone-server.rb +++ b/src/sunstone/sunstone-server.rb @@ -39,6 +39,7 @@ SUNSTONE_ROOT_DIR = File.dirname(__FILE__) $: << RUBY_LIB_LOCATION $: << RUBY_LIB_LOCATION+'/cloud' +$: << SUNSTONE_ROOT_DIR $: << SUNSTONE_ROOT_DIR+'/models' ############################################################################## @@ -115,12 +116,27 @@ helpers do session[:ip] = request.ip session[:remember] = params[:remember] + #User IU options initialization + #Load options either from user settings or default config. + # - LANG + # - WSS CONECTION + if user['TEMPLATE/LANG'] session[:lang] = user['TEMPLATE/LANG'] else session[:lang] = settings.config[:lang] end + if user['TEMPLATE/VNC_WSS'] + session[:wss] = user['TEMPLATE/VNC_WSS'] + else + session[:wss] = settings.config[:vnc_proxy_support_wss] + #limit to yes,no options + session[:wss] = (session[:wss] != "no" ? "yes" : "no") + end + + #end user options + if params[:remember] env['rack.session.options'][:expire_after] = 30*60*60*24 end @@ -212,8 +228,16 @@ end ############################################################################## # Config and Logs ############################################################################## -get '/config' do - @SunstoneServer.get_configuration(session[:user_id]) +#get '/config' do +# @SunstoneServer.get_configuration(session[:user_id]) +#end + +get '/config/:opt' do + case params[:opt] + when "lang" then session[:lang] + when "wss" then session[:wss] + else "unknown" + end end post '/config' do @@ -226,6 +250,7 @@ post '/config' do body.each do | key,value | case key when "lang" then session[:lang]=value + when "wss" then session[:wss]=value end end end @@ -301,7 +326,8 @@ post '/vm/:id/stopvnc' do return [403, OpenNebula::Error.new(msg).to_json] end - rc = @SunstoneServer.stopvnc(vm_id, vnc_hash[vm_id][:pipe]) + rc = @SunstoneServer.stopvnc(vnc_hash[vm_id][:pipe]) + if rc[0] == 200 session['vnc'].delete(vm_id) end @@ -327,7 +353,8 @@ post '/vm/:id/startvnc' do return [200, info.to_json] end - rc = @SunstoneServer.startvnc(vm_id, settings.config) + rc = @SunstoneServer.startvnc(vm_id,settings.config) + if rc[0] == 200 info = rc[1] session['vnc'][vm_id] = info.clone